• 10 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 04/07/2019

Implement simple animations

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

Great, our app is pretty lively now. Let’s enhance it even more - with animations!

What's animation?

Animation is a progressive transition from one state into another. It’s an illusion of a smooth transition achieved by displaying discrete intermediate states. The more states we implement, the smoother the animation appears.

Animations can be described with various configurations:

  • The starting and ending states may be different, like moving an object from point A to Point B.

  • Or, the same, like enlarging an object and shrinking it back after reaching a certain scale.

  • Or, we can add a transformation in the process - like rotation while moving or scaling.

Animations are triggered by an action and then completed on their own, unless interrupted and then resumed, reversed, or canceled.

When can we use animations? o_O

Animations are used as an enhancement, they are now extremely popular and have become crucial to deliver good user experience. They are now an expected norm rather than a cool add-on.

We can apply them in various circumstances:

  • Smooth out discrete movement from A to B no matter how short the distance

  • Enhance interaction - e.g. a button click

  • Enhance replacement transition - e.g. image change 

  • Complete movement trajectory - e.g. swiping a view away

  • and many, many more...

And, of course, an app category that would be non-existent without animations is Games. :pirate: 

Animations in iOS

There are many ways to implement animations in iOS. Here are the key approaches:

  • UIView Animation - basic animations that allow animating view properties.

  • Core Animation - more sophisticated view animations.

  • SpriteKit - provides functionality to create 2D animations.

  • SceneKit - provides functionality to create 3D animations.

  • Dynamic Animation - for animations that incorporate physics such as gravity, collisions, etc.

There are a vast range of possibilities. The majority of designed animations for non-gaming apps can be implemented using the first two approaches. And UIView Animations can cover 80% of that, which is what we are going to focus on in this chapter.

UIView Animation, as its name indicates, allows to animate the properties of the views and in particular:

  • frame  - by animating this property, we can make a view change its size and position smoothly.

  • transform  - animating this this property makes various transformations appear smooth (including positioning and sizing).

  • alpha  - by adjusting this value with animation we can make a view change its opacity over a period of time. 

  • backgroundColor  - animating background color change provides a smooth color change appearance.

Let's take a look at our app. It's not bad, one actions is striking and not in a good way. Try transforming a photo and then click 'Start Over' button. The image resets to a placeholder and returns to its original frame. Suddenly, quickly. :'( It hurts my eyes. How about you? 

And that's a good thing - means we can practice using animations! We'll aim to animate a transformation of our image view back to the neutral state, this line of code:

self.creationImageView.transform = .identity

So, let's proceed!

Creating Animation

We are going to discover two class methods of UIView that can perform animations:

  • animate 

  • and transition

Utilizing 'animate' method

Animate method has several declarations, but the one that interests us for the moment is the following:

UIView.animate(withDuration: TimeInterval, animations: () -> Void, completion: ((Bool) -> Void)?)

This method takes a number of parameters:

  • Duration - TimeInterval type - a decimal number in seconds that describes how long the animation should be.

  • Animations - a set of instructions to animate.

  • and, Completion - a set of instructions to execute upon completion of animations.

And what are those sets of instructions? o_O

The sets of instructions - both parameters - animations and completion are closures. We briefly touched on them in earlier chapters of the course. Closures are functions that can be passed as parameters.

Completion closure has a boolean parameter that indicates whether the animation was successful or not. So that the implementation could look like this:

UIView.animate(withDuration: 1.0, 
    animations: {
        // instructions to animate
    }, 
    completion: { (success) in
        // instructions to execute upon completion
    })

Or, using syntax moving the completion block out:

UIView.animate(withDuration: 1.0, 
    animations: {
        // instructions to animate
    }) { (success) in
        // instructions to execute upon completion
    }

If we don't need to execute any code upon completion, we can pass nil to it (as it's optional) or, leave it out altogether:

UIView.animate(withDuration: TimeInterval, animations: () -> Void)

And here's how we can animate our creation view transformation back to neutral placing it right after reseting Creation data object: 

UIView.animate(withDuration: 0.4, animations: {
        self.creationImageView.transform = .identity
    }) { (success) in
        // do something
    }

Run the app, load a photo, transform it and reset. It works! A bit messy though :'( - the image change back to placeholder that's happening at the same time is striking. Let's make the rest of reset functionality happen  after our transformation animation is completed:

UIView.animate(withDuration: 0.4, animations: {
    self.creationImageView.transform = .identity
    }) { (success) in
        self.creationImageView.image = self.creation.image
        self.creationFrame.backgroundColor = self.creation.colorSwatch.color
        self.colorLabel.text = self.creation.colorSwatch.caption
    }

Better! 

Now, let's enhance this animation further. We can smoothen the 'arrival' of our view to the final state by applying bounce effect - or spring. In order to do that we simply need to use a different declaration of animate method and provide desired configuration:

UIView.animate(withDuration: TimeInterval, 
    delay: TimeInterval, 
    usingSpringWithDamping: CGFloat, 
    initialSpringVelocity: CGFloat, 
    options: [UIViewAnimationOption], 
    animations: () -> Void, 
    completion: (Bool) -> Void?)

There are a few more parameters that came in picture:

  • Delay - another TimeInterval - a decimal number in seconds that animation should wait until starting to animate. This is not related to the spring effect, however, is very handy in general. ;)

  • Damping - a decimal number describing spring damping. This value ranges from 0 to 1. The higher the value, the greater the bounce.

  • InitialSpringVelocity - a decimal number describing spring velocity. The higher the value, the greater the bounce.

  • Options - animation options. 

Well defined! But, what does bounce mean? :euh:

Let's visualize two versions of animations - without spring effect and with one:

Moving transition without and with sprint effect
Moving transition without and with sprint effect

We can now visually see the difference between the two implementations.

And, Options?

We won't need options just yet.

Let's modify our code to use the new implementation with spring:

UIView.animate(withDuration: 0.4, delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: [], animations: {
        self.creationImageView.transform = .identity
    }) { (success) in
        self.creationImageView.image = self.creation.image
        self.creationFrame.backgroundColor = self.creation.colorSwatch.color
        self.colorLabel.text = self.creation.colorSwatch.caption
    }

Cool? - Cool!

Where are those numbers come from?

As you may notice we used somewhat 'random' numbers as decimal parameters to this function - damping and velocity - those are recommended numbers for a mild spring. Feel free to play with variations.

We use 0.4 seconds for animation duration, also as a neutral mild period of time.

Here's another common declaration of animate method:

UIView.animate(withDuration: TimeInterval,
                       delay: TimeInterval,
                       options: UIViewAnimationOptions,
                       animations: () -> Void,
                       completion: (Bool) -> Void?)

For other simple animations options parameter can come handy. We'll use this parameter with the next method.

Utilizing 'transition' method

Our image view is now nicely bouncing. And then, suddenly, the image changes! I must note, same as when we pick a new image. :'( Let's do something about it too!

We can animate the transition of image property of UIImageView from one image to another using transition method. Like animate, it also has multiple declaration. We are going to use this one:

UIView.transition(with: UIView, duration: TimeInterval, options: UIViewAnimationOptions, animations: () -> Void, completion: (Bool) -> Void?)

And here's where options come handy!

We can use  .transitionCrossDissolve  option for example:

UIView.transition(with: self.creationImageView, duration: 0.4, options: .transitionCrossDissolve, animations: {
            self.creationImageView.image = self.creation.image
        }, completion: nil)

We need to place this code instead of the original version of reassigning the image value after returning the image in its original position in startOver function. However! We also need to perform the image change when user pick a new imager the creation. Looks like we could place this transition into a function, we'll call it  animateImageChange  :

func animateImageChange() {
    UIView.transition(with: self.creationImageView, duration: 0.4, options: .transitionCrossDissolve, animations: {
        self.creationImageView.image = self.creation.image
    }, completion: nil)
}

And now place it in both functions where we can make use of it. startOver:

@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
    }
}

And processPicked:

func processPicked(image: UIImage?) {
    if let newImage = image {
        creation.image = newImage
        animateImageChange()
    }
}

Let's put it all together for our 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) {
        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) {
        
        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()
    }
}

Test it out! Run the app, plat with image change, transformations and resets!

Nice work!  

After animating the image replacement, try experimenting with animations for frame background and label change. Remember, we need to do it in 2 places - when staring over and when user clicks on a swatch to apply the new color: :p

More experiments - more play!
More experiments - more play!

 Whatever your preferred variation ended up being, I trust it looks fantastic! :ange:

Let's Recap!

  • There're a number of ways to implement animations in iOS. The vast majority of animations for non-gaming apps can be covered using UIVIew Animations.

  • UIView Animations make it easy to animate some UIView properties like frame, alpha, transform and backgroundColor or subclass specific properties.

  • To create a UIView Animation, we can use two UIView class methods suitable for particular animations:

    • animate

    • transition

  • Spring capability allows creating a bounce effect at the destination point of animation.

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