• 10 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

Ce cours est en vidéo.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

J'ai tout compris !

Mis à jour le 04/02/2019

Reach out! Using content sharing to promote your application

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

We’ve done such beautiful work, it would be a sin to keep it to ourselves.

In this chapter we’ll explore a content sharing functionality in iOS.

Like many other capabilities, content sharing has been provided for us by the Swift APIs. Apple dedicates a lot of effort making sure developers have great tools!

Let’s see how we can take advantage of it!

Understanding Activity View Controller

In iOS, sharing capability is often associated with this icon (or similar variations): 

It typically launches an Activity View that looks like this:

iOS Action Sheet
iOS Action Sheet

The components displayed (the apps available to share with) on the Activity View depend on device and user's settings.

You don't have to use this icon, you can use text instead or a custom variation of text & icon.

Activity View is managed Activity View Controller and  functions very similarly to Alert Controller. It's represented by a class  UIActivityViewController .

Let's prepare to code. We have a method share associated with our Share button action. It already has some code - saving the creation settings. To keep the code clean, let's create a function similar to displaying action sheet for image impact options and call it  displaySharingOptions() . And then call this function at the beginning of share function:

@IBAction func share(_ sender: Any) {
    displaySharingOptions()
        
    if let index = colorSwatches.index(where: {$0.caption == creation.colorSwatch.caption}) {
        savedColorSwatchIndex = index
    }
}

func displaySharingOptions() {
}

We'll be placing all the sharing code in displaySharingOptions method.

General syntax

All we need to do is prepare the content to share, create an activity view controller variable and present it:

// prepare content to share
...

// create activity view controller
let activityViewController = UIActivityViewController(activityItems: [Any], applicationActivities: [UIActivity]?)

// present activity view controller
present(activityViewController, animated: true, completion: nil)

The initializer of activity view controller takes 2 parameters:

  • activityItems  - content to share.

  • and  applicationActivities  - objects representing the custom services that an application supports. We'll have it as nil.

Let's do it!
Let's do it!

Let's FrameIT!

Defining content to share

 What data can we share? o_O

We can share typical sharing components:

  • Text - represented by  String  data type.

  • Images - represented by  UIImage  data type.

  • Links - represented by  NSURL  data type.

What's NSURL? :waw: - it's a data type to work will URLs. no panic, we won't use it just yet, but very soon you will become familiar with it!

activityItems is an array of Any elements, so we can list all the desired items there casting them to Any.

Let's generate some content to share:

  • Image - we'll use a placeholder image for the moment.

  • and Caption - something fun to announce our creation!

And pass it to create activity view controller:

// define content to share
let note = "I Framed IT!"
let image = UIImage(named: "FrameIT-placeholder")
let items = [image as Any, note as Any]
        
// create activity view controller
let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil)

// present the view controller
present(activityViewController, animated: true, completion: nil)
Preventative troubleshooting

We've got a complication here. Out app is supposed to be universal - i.e. developed for both, iPhone and iPad. Currently, displaying this view controller on iPad makes the device confused about which view has displayed the controller, which on iPad is displayed as a popover. o_O We're going to put a band-aid on it by adding this line of code: :soleil:

activityViewController.popoverPresentationController?.sourceView = view

So the full function looks like this:

func displaySharingOptions() {
        
    // define content to share
    let note = "I Framed IT!"
    let image = UIImage(named: "FrameIT-placeholder")
    let items = [image as Any, note as Any]
        
    // create activity view controller
    let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil)
    activityViewController.popoverPresentationController?.sourceView = view // fix for iPad

        // present the view controller
    present(activityViewController, animated: true, completion: nil)

}

We can improve this by associating the popover on iPad with Share button.

Alter the displaySharingOptions method to accept sender as a parameter - which we are receiving in the original share method. And then assign it as a popover source instead of the base view of the main screen.

I suppose you chose to do it!
I suppose you chose to do it!

Let's test it! Run the app, click 'Share' button. We can use both, simulator and device.

Depending whether you are testing on simulator or an iOS device, you will get different appearance:

Default Activity View Controller appearance - simulator vs iPhone
Default Activity View Controller appearance - simulator vs iPhone

Managing sharing channels - activities.

We may not force the user to enable access to certain activities, but we can prohibit using some of them. For example, if we are conscious about users sharing something on Facebook, we can say good-bye to it when presenting the activity view controller by setting the  excludedActivityTypes  property:

activityViewController.excludedActivityTypes = [UIActivityType.postToFacebook]

 What else can we exclude?

The types of built-in activities is defined in UIActivityType constants, here are a few examples:

  • UIActivityType.airDrop

  • UIActivityType.mail

  • UIActivityType.print

  • etc.

Almost done, last thing to do - prepare an image to represent our framing creation and pass it to share instead of the placeholder.

Preparing the Image

To implement this functionality we are going to learn a couple more things using UIGraphics functions to generate an image for sharing programmatically. We're going to take a screenshot of our creation. As usual, we'll keep our code organized and use a new function for it -  composeCreationImage :

func composeCreationImage() -> UIImage{
        
    UIGraphicsBeginImageContextWithOptions(creationFrame.bounds.size, false, 0)
    creationFrame.drawHierarchy(in: creationFrame.bounds, afterScreenUpdates: true)
    let screenshot = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()
        
    return screenshot
}

Let's review these magical lines of code:

  • UIGraphicsBeginImageContextWithOptions  - allows us to initialize an image creation of a given size.

  • drawHierarchy  method of a view (creationFrame in our case) ensures the view and subviews are  up to date.

  • UIGraphicsGetImageFromCurrentImageContext  - allows to us to simply snap the visuals of the screen area!

  • and, lastly,  UIGraphicsEndImageContext  - is closing the gate - we are done with image manipulations.

  • As a result the variable screenshot contains our image - ready to share - we're returning this value to our sharing function.

Last modification to the code - displaySharingOptions function: 

func displaySharingOptions() {
        // define content to share
    let note = "I Framed IT!"
    let image = composeCreationImage()
    let items = [image as Any, note as Any]
        
    // create activity view controller
    let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil)
    activityViewController.popoverPresentationController?.sourceView = view // so that iPads won't crash
        
    // present the view controller
    present(activityViewController, animated: true, completion: nil)
}

And the complete code for view controller:

//
//  ViewController.swift
//  FrameIT
//
//  Created by Olga Volkova on 2017-12-06.
//  Copyright © 2017 Olga Volkova OC. All rights reserved.
//

import UIKit
import Photos
import AVFoundation

class ViewController: UIViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate, UIGestureRecognizerDelegate {

    @IBOutlet weak var creationFrame: UIView!
    @IBOutlet weak var creationImageView: UIImageView!
    
    @IBOutlet weak var colorLabel: UILabel!
    @IBOutlet weak var colorsContainer: UIView!

    var creation = Creation.init()
    var localImages = [UIImage].init()
    let defaults = UserDefaults.standard
    var colorSwatches = [ColorSwatch].init()
    
    var initialImageViewOffset = CGPoint()

    let colorUserDefaultsKey = "ColorIndex"
    var savedColorSwatchIndex: Int {
        get {
            let savedIndex = defaults.value(forKey: colorUserDefaultsKey)
            if savedIndex == nil {
                defaults.set(colorSwatches.count - 1, forKey: colorUserDefaultsKey)
            }
            return defaults.integer(forKey: colorUserDefaultsKey)
        }
        set {
            if newValue >= 0 && newValue < colorSwatches.count {
                defaults.set(newValue, forKey: colorUserDefaultsKey)
            }
        }
    }

    @objc func changeImage(_ sender: UITapGestureRecognizer) {
        displayImagePickingOptions()
    }
    
    @IBAction func startOver(_ sender: Any) {
        creation.reset(colorSwatch: colorSwatches[savedColorSwatchIndex])
        
        UIView.animate(withDuration: 0.4, delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: [], animations: {
            self.creationImageView.transform = .identity
        }) { (success) in
            self.animateImageChange()
            self.creationFrame.backgroundColor = self.creation.colorSwatch.color
            self.colorLabel.text = self.creation.colorSwatch.caption
        }
    }
    
    @IBAction func applyColor(_ sender: UIButton) {
        //print("Applying color")
        if let index = colorsContainer.subviews.index(of: sender) {
            creation.colorSwatch = colorSwatches[index]
            creationFrame.backgroundColor = creation.colorSwatch.color
            colorLabel.text = creation.colorSwatch.caption
        }
    }

    @IBAction func share(_ sender: Any) {
        
        displaySharingOptions()
        
        if let index = colorSwatches.index(where: {$0.caption == creation.colorSwatch.caption}) {
            savedColorSwatchIndex = index
        }
    }

    func displayImagePickingOptions() {
        let alertController = UIAlertController(title: "Choose image", message: nil, preferredStyle: .actionSheet)

        // create camera action
        let cameraAction = UIAlertAction(title: "Take photo", style: .default) { (action) in
            self.displayCamera()
        }

        // add camera action to alert controller
        alertController.addAction(cameraAction)

        // create library action
        let libraryAction = UIAlertAction(title: "Library pick", style: .default) { (action) in
            self.displayLibrary()
        }

        // add library action to alert controller
        alertController.addAction(libraryAction)

        // create random action
        let randomAction = UIAlertAction(title: "Random", style: .default) { (action) in
            self.pickRandom()
        }

        // add random action to alert controller
        alertController.addAction(randomAction)

        // create cancel action
        let canceclAction = UIAlertAction(title: "Cancel", style: .cancel)

        // add cancel action to alert controller
        alertController.addAction(canceclAction)

        present(alertController, animated: true) {
            // code to execute after the controller finished presenting
        }
    }

    func displayCamera() {
        let sourceType = UIImagePickerControllerSourceType.camera

        if UIImagePickerController.isSourceTypeAvailable(sourceType) {

            let status = AVCaptureDevice.authorizationStatus(for: AVMediaType.video)

            let noPermissionMessage = "Looks like FrameIT have access to your camera:( Please use Setting app on your device to permit FrameIT accessing your camera"

            switch status {
            case .notDetermined:
                AVCaptureDevice.requestAccess(for: AVMediaType.video, completionHandler: { (granted) in
                    if granted {
                        self.presentImagePicker(sourceType: sourceType)
                    } else {
                        self.troubleAlert(message: noPermissionMessage)
                    }
                })
            case .authorized:
                self.presentImagePicker(sourceType: sourceType)
            case .denied, .restricted:
                self.troubleAlert(message: noPermissionMessage)
            }
        }
        else {
            troubleAlert(message: "Sincere apologies, it looks like we can't access your camera at this time")
        }
    }

    func displayLibrary() {
        let sourceType = UIImagePickerControllerSourceType.photoLibrary

        if UIImagePickerController.isSourceTypeAvailable(sourceType) {

            let status = PHPhotoLibrary.authorizationStatus()
            let noPermissionMessage = "Looks like FrameIT have access to your photos:( Please use Setting app on your device to permit FrameIT accessing your library"

            switch status {
            case .notDetermined:
                PHPhotoLibrary.requestAuthorization({ (newStatus) in
                    if newStatus == .authorized {
                        self.presentImagePicker(sourceType: sourceType)
                    } else {
                        self.troubleAlert(message: noPermissionMessage)
                    }
                })
            case .authorized:
                self.presentImagePicker(sourceType: sourceType)
            case .denied, .restricted:
                self.troubleAlert(message: noPermissionMessage)
            }
        }
        else {
            troubleAlert(message: "Sincere apologies, it looks like we can't access your photo library at this time")
        }
    }

    func presentImagePicker(sourceType: UIImagePickerControllerSourceType) {
        let imagePicker =  UIImagePickerController()
        imagePicker.delegate = self
        imagePicker.sourceType = sourceType
        present(imagePicker, animated: true, completion: nil)
    }

    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
        picker.dismiss(animated: true, completion: nil)
        let newImage = info[UIImagePickerControllerOriginalImage] as? UIImage
        processPicked(image: newImage)
    }

    func troubleAlert(message: String?) {
        let alertController = UIAlertController(title: "Oops...", message:message , preferredStyle: .alert)
        let OKAction = UIAlertAction(title: "Got it", style: .cancel)
        alertController.addAction(OKAction)
        present(alertController, animated: true)
    }

    func pickRandom() {
        processPicked(image: randomImage())
    }

    func randomImage() -> UIImage? {

        let currentImage = creation.image

        if localImages.count > 0 {
            while true {
                let randomIndex = Int(arc4random_uniform(UInt32(localImages.count)))
                let newImage = localImages[randomIndex]
                if newImage != currentImage {
                    return newImage
                }
            }
        }
        return nil
    }

    func collectLocalImageSet() {
        localImages.removeAll()
        let imageNames = ["Boats", "Car", "Crocodile", "Park", "TShirts"]

        for name in imageNames {
            if let image = UIImage.init(named: name) {
                localImages.append(image)
            }
        }
    }

    func collectColors() {
        colorSwatches = [
            ColorSwatch.init(caption: "Ocean", color: UIColor.init(red: 44/255, green: 151/255, blue: 222/255, alpha: 1)),
            ColorSwatch.init(caption: "Shamrock", color: UIColor.init(red: 28/255, green: 188/255, blue: 100/255, alpha: 1)),
            ColorSwatch.init(caption: "Candy", color: UIColor.init(red: 221/255, green: 51/255, blue: 27/255, alpha: 1)),
            ColorSwatch.init(caption: "Violet", color: UIColor.init(red: 136/255, green: 20/255, blue: 221/255, alpha: 1)),
            ColorSwatch.init(caption: "Sunshine", color: UIColor.init(red: 242/255, green: 197/255, blue: 0/255, alpha: 1))
        ]
        if colorSwatches.count == colorsContainer.subviews.count {
            for i in 0 ..< colorSwatches.count {
                colorsContainer.subviews[i].backgroundColor = colorSwatches[i].color
            }
        }
    }

    func processPicked(image: UIImage?) {
        if let newImage = image {
            creation.image = newImage
            //creationImageView.image = creation.image
            animateImageChange()
        }
    }
    
    func animateImageChange() {
        UIView.transition(with: self.creationImageView, duration: 0.4, options: .transitionCrossDissolve, animations: {
            self.creationImageView.image = self.creation.image
        }, completion: nil)
    }

    func configure() {
        // collect images
        collectLocalImageSet()

        // collect colors
        collectColors()

        // set creation data object
        creation.colorSwatch = colorSwatches[savedColorSwatchIndex]

        // apply creation data to the views
        creationImageView.image = creation.image
        creationFrame.backgroundColor = creation.colorSwatch.color
        colorLabel.text = creation.colorSwatch.caption
        
        // create tap gesture recognizer
        let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(changeImage(_:)))
        creationImageView.addGestureRecognizer(tapGestureRecognizer)
        
        let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(moveImageView(_:)))
        panGestureRecognizer.delegate = self
        creationImageView.addGestureRecognizer(panGestureRecognizer)
        
        let rotationGestureRecognizer = UIRotationGestureRecognizer(target: self, action: #selector(rotateImageView(_:)))
        rotationGestureRecognizer.delegate = self
        creationImageView.addGestureRecognizer(rotationGestureRecognizer)
        
        let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(scaleImageView(_:)))
        pinchGestureRecognizer.delegate = self
        creationImageView.addGestureRecognizer(pinchGestureRecognizer)
    }
    
    @objc func moveImageView(_ sender: UIPanGestureRecognizer) {
        let translation = sender.translation(in: creationImageView.superview)
        
        if sender.state == .began {
            initialImageViewOffset = creationImageView.frame.origin
        }
        
        let position = CGPoint(x: translation.x + initialImageViewOffset.x - creationImageView.frame.origin.x, y: translation.y + initialImageViewOffset.y - creationImageView.frame.origin.y)
        
        creationImageView.transform = creationImageView.transform.translatedBy(x: position.x, y: position.y)
    }
    
    @objc func rotateImageView(_ sender: UIRotationGestureRecognizer) {
        creationImageView.transform = creationImageView.transform.rotated(by: sender.rotation)
        sender.rotation = 0
    }
    
    @objc func scaleImageView(_ sender: UIPinchGestureRecognizer) {
        creationImageView.transform = creationImageView.transform.scaledBy(x: sender.scale, y: sender.scale)
        sender.scale = 1
    }
    
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
                           shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer)
        -> Bool {
            
            // simultaneous gesture recognition will only be supported for creationImageView
            if gestureRecognizer.view != creationImageView {
                return false
            }
            
            // neither of the recognized gestures should not be tap gesture
            if gestureRecognizer is UITapGestureRecognizer
                || otherGestureRecognizer is UITapGestureRecognizer
                || gestureRecognizer is UIPanGestureRecognizer
                || otherGestureRecognizer is UIPanGestureRecognizer {
                return false
            }
            
            return true
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        configure()
    }
    
    func displaySharingOptions() {
        // define content to share
        let note = "I Framed IT!"
        let image = composeCreationImage()
        let items = [image as Any, note as Any]
        
        // create activity view controller
        let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil)
        activityViewController.popoverPresentationController?.sourceView = view // so that iPads won't crash
        
        // present the view controller
        present(activityViewController, animated: true, completion: nil)
    }
    
    func composeCreationImage() -> UIImage{
        
        UIGraphicsBeginImageContextWithOptions(creationFrame.bounds.size, false, 0)
        creationFrame.drawHierarchy(in: creationFrame.bounds, afterScreenUpdates: true)
        let screenshot = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()
        
        return screenshot
    }
}

Let's test it:

Fantastic work!
Fantastic work!

Great work I must say! :zorro:

Improving User Experience

User experience is a key software component, especially for mobile apps. There are many apps available and more coming in every day. We must make sure we deliver the best apps we can. It helps to give some extra thought bound original requirements, what makes sense and can be achieved without compromising the delivery time.

How can we improve user experience in FrameIT? o_O

Let's challenge the functionality of FrameIT:

  • A user can transform an Image view, but does it make sense to do that if the image is a placeholder?

  • A user can share  a Creation, but does it make sense to do that if the image is a placeholder?

  • A user can reset the creation by starting over, but does it make sense to be able to do it of the image is a placeholder? 

As you can see, it's helpful to challenge the functionality, it may help us discover simple details that can be improved relatively quickly.

Try to implement a function manage the buttons availability depending on the state of an app. This is where having buttons connected as outlets in code will come handy! :pirate:

You can explore the following properties:

  • All views:  isUserInteractionEnabled

  • Button:  isEnabled

Keep Exploring!

There's almost always a way to customize and expand the basics. Stay curious and take initiative to explore advanced available options already available, or, CREATE YOUR OWN!

Many options in most programming languages can be expanded via subclassing.

When you are approached with something new, don't rush to define it impossible! Instead, consider the following:

  • It may be new TO YOU ;) - explore existing solutions that maybe already done by others.

  • Use this as an opportunity to learn something new and create what has never been created!

  • If you find it truly impossible to implement given the circumstances - technology, time, budget - come us with the closest alternative and get on it!

Nevertheless, Well Done! 

Let's Recap!

  • UIActivityViewController  class allows to take advantage of Swift API sharing capability.

  • User can define which apps are available as sharing channels, however, we can prohibit from using some of them.

  • Share content may include text, links and images.

  • UIGraphics  is used to work with images programmatically.

Exemple de certificat de réussite
Exemple de certificat de réussite