© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
A. NekrasovSwift Recipes for iOS Developershttps://doi.org/10.1007/978-1-4842-8098-0_7

7. UI Animation and Effects

Alexander Nekrasov1  
(1)
Moscow, Russia
 

Animations and effects are important elements/aspects of modern mobile apps. Each app has dozens of similar apps available in the stores/gallery; you need to make yours stand out so customers will download yours. Beautiful animated UI helps it stand out.

We will review the most popular animation types :
  • Moving UIView from one point to another, (e.g., when the keyboard appears/disappears)

  • Animating properties like color or transparency

  • Parallax effect with UIScrollView, UITableView, or UICollectionView

  • “Hero” animation – moving the item from one view to another

  • Overriding default transition between UIViewControllers

Animating Views

UIView and its subclasses have several properties that can be animated. We will split them into two groups:
  • Animating own properties of views, like color, transparency, or scale

  • Animating layout by changing constraints

Animating UIView’s Own Properties

The most popular animation of UIView is “fade”. “Fade in” makes UIView appear; “fade out” makes it disappear. What is appearing and disappearing? It’s changing alpha property from 0.0 to 1.0 and back. Why do we change alpha instead of isHidden? It’s a Boolean value. Boolean values cannot gradually change. They’re always either true or false. Will this animation work anyway? Well, try it as an exercise. We’ll stick to alpha here, as it’s a gradually changeable property.

Animation can be done using UIView type animation (withDuration:animations:). It’s a type method, so you call it directly on type UIView, not on instance.

Let’s see how it looks in code. Make UIView or any subclass an outlet fadeView. Be careful, don’t name it just view because it’s reserved for root view (Recipe 7-1).
class FadeInViewController: UIViewController {
    @IBOutlet weak var fadeView: UIView!
    func fadeIn() {
        fadeView.isHidden = false
        fadeView.alpha = 1.0
        UIView.animate(withDuration: 0.3) {
            self.fadeView.alpha = 0.0
        }
    }
}
Recipe 7-1

Fade-in Effect

Before performing an animation, we set initial parameters. It’s not compulsory, especially if you want to animate it from its current state. Adjust it for your needs.

You can also see a magic number in this recipe – 0.3. Magic numbers are unnamed numeric constants. It’s a bad practice because the developer working on this code after you (or even you in several months or years) may not understand why this constant is there and what it means.

We’re talking about animation, and this code is part of the book, so it’s acceptable to give an explanation here. 0.3 seconds is a common time interval for animations. It’s slow enough to let the user see it, but fast enough not to delay them.

If you use this code, adjust this time interval for your needs and define a named constant for it.

In Recipe 7-2 we have another version of animate method:
func animate(with Duration:animations:completion:)
class FadeOutViewController: UIViewController {
    @IBOutlet weak var fadeView: UIView!
    func fadeOut() {
        fadeView.alpha = 0.0
        fadeView.isHidden = false
        UIView.animate(withDuration: 0.3) {
            self.fadeView.alpha = 1.0
        } completion: { _ in
            self.fadeView.isHidden = true
        }
    }
}
Recipe 7-2

Fade-Out Effect

UIView object can animate the following properties:
  • Frame

  • Bounds

  • Center

  • Transform

  • Alpha

  • BackgroundColor

Subclasses of UIView may have other properties to animate; you can refer to documentation to see a full list for each class.

Animating Layout by Changing Constraints

When iPhone just appeared , it had only one resolution. Later, when new models appeared, screen became taller. On iPad, the opposite, it became wider. Now we have many aspect ratios. The way to survive in all this variety is to use constraints. If you’re reading this, you probably already know what are constraints. If not, you can always find documentation, books, articles, or tutorials explaining how they work.

If you have constraints, you shouldn’t animate frame and other position-related properties. You should animate changes in constraints.

To animate a constraint, you should make an outlet first. Let’s say, we have some UIView, and we need to move it up. It has a constraint defining distance with an object on top. By default, it’s 50, but we want it to be 20. Why? Maybe the keyboard appeared, or something else happened that we need to shrink our spaces.
@IBOutlet weak var distanceConstraint: NSLayoutConstraint!
Note

Up to the present day, constraints are objects of class NSLayoutConstraint; it starts with NS, and it doesn’t have an alternative.

Here’s the trick. Layout changes aren’t performed when you change the constraint properties. They’re performed when view is getting laid out. We need to prepare new layout before calling animate(withDuration:animations:). And in this method, we only call layoutIfNeeded of a parent or root UIView. You can see how it's done in Recipe 7-3.
class AnimatedConstraintsViewController: UIViewController {
    @IBOutlet weak var distanceConstraint: NSLayoutConstraint!
    func changeDistanceAnimated(_ newDistance: CGFloat) {
        distanceConstraint.constant = newDistance
        UIView.animate(withDuration: 0.3) {
            self.view.layoutIfNeeded()
        }
    }
}
Recipe 7-3

Animating Constraints

Restrictions

Animations in iOS have their own restrictions . We already discussed that you can’t animate values that can’t be gradually changed.

Another restriction is simultaneous animations. You can animate several properties or different objects at the same time, but they must be wrapped into the same block.

If you have complex animations, you have two solutions:
  • Split them into small blocks, each of them with a set of changes ending at the same moment. When one block ends, start the next block. It may need some calculating.

  • Make a timer to animate manually. The UIView.animate(withDuration:animations☺ method is not the only way.

Animating Layout When Keyboard Appears

In the “Keyboard Handling” section in Chapter 6, we discussed changes in keyboard layout. We changed a constraint without animation:
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size { bottomConstraint.constant = keyboardSize.height
    view.layoutIfNeeded()}
In Recipe 7-4 we improve the code to make it look smoother.
class AnimatedKeyboardListenerViewController: UIViewController {
    @IBOutlet weak var bottomConstraint: NSLayoutConstraint!
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillShow(notification:)),
            name: UIResponder.keyboardWillShowNotification,
            object: nil
        )
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(keyboardWillHide),
            name: UIResponder.keyboardWillHideNotification,
            object: nil
        )
    }
    override func viewDidDisappear(_ animated: Bool) {
        NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
        super.viewWillDisappear(animated)
    }
    @objc func keyboardWillShow(notification: NSNotification) {
        if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size {
            bottomConstraint.constant = keyboardSize.height
            UIView.animate(withDuration: 0.3) {
                self.view.layoutIfNeeded()
            }
        }
    }
    @objc func keyboardWillHide() {
        bottomConstraint.constant = 0
        UIView.animate(withDuration: 0.3) {
            self.view.layoutIfNeeded()
        }
    }
}
Recipe 7-4

Animating Layout Changes When Keyboard Appears

Note

Recipe 7-4 is almost identical to Recipe 6-13. The only difference is animation. Compare them.

Parallax Effect

Parallax effect in general is creating several layers moving with different speeds. This effect is often used in 2D platformer games or runners. Clouds behind the player move much slower than the player themselves. At the same time, the front layer covering the character moves faster.

In mobile apps, the parallax effect is often used as a screen header when content is scrollable. It can be a food ordering app, showing the restaurant photo above the menu. When the user scrolls the menu, the header should become smaller, leaving the back button and restaurant name, possibly with the restaurant logo or a card icon.

The parallax scrolling effect is not only moving from point A to point B, but it should also be responsive. The header should dynamically change when the user scrolls up and down (Figure 7-1).

A set of four images represents the parallax effect in the user interface scroll view. The first image has the label as the title on a grey background and labels for line 1 to line 16. The consecutive images also have labels for line.

Figure 7-1

Parallax effect

Parallax Header with UIScrollView

A big part of this feature is the user interface, so let’s prepare it first.

Preparing User Interface

In this example, we’ll use a header containing the following objects:
  • UIImageView on the background. Content mode should be set to Aspect Fill. This way, the background image will scale automatically, leaving only part of the picture visible, but it will always fill the whole screen.

  • UIView shading. Background color will be black, but transparency (alpha) will change from 0.3 to 0.8.

  • UIButton will be a static back button, always located in the top-left corner of the header.

  • UILabel will display a title. Large and centered when the header is expanded and small when the header is shrunk.

When the user scrolls UIScrollView, the header will change height from 180 to 40. Scrolling back will reverse the process. In the full version of code, all numbers will be declared as constants.

The UIScrollView object should be located behind the header; otherwise, it will overlap the entire header. At the same time, we can’t put it below. This needs a detailed explanation.

If our scrollable area is located below the header, its position and size will change every time the header height is changed. As the user’s finger won’t move, it will change its position in a scrollable area system of coordinates. It may look good, but usually it creates undesired shaking of scrolling offset.

Finally, we need to add some content to the scrollable area. Don’t forget that it must have the margin from the top border, at least 180; otherwise, it will be hidden behind the header. If you want to scroll the bar, you should also add an inset in the Indicator Insets section in the storyboard editor. If not, hide it.

If you’re done with storyboard editing, create the following outlets:
// Views
@IBOutlet weak var scrollView: UIScrollView!
@IBOutlet weak var headerView: UIView!
@IBOutlet weak var shadeView: UIView!
@IBOutlet weak var titleLabel: UILabel!
// Layout constraints
@IBOutlet weak var headerHeightConstraint: NSLayoutConstraint!
@IBOutlet weak var titleLeadingConstraint: NSLayoutConstraint!
Starting values of constraints, alpha, font, and other parameters should be the following:
  • shadeView.alpha is 0.3.

  • titleLabel.font is UIFont.systemFont(ofSize: 32).

  • headerHeightConstraint.constaint is 180.

  • titleLeadingConstraint.constaint is 16.

If the layout is too complicated, and you can’t build it using this instruction, you can find an example in the GitHub repository .

Parallax Functionality

We need to set a delegate of UIScrollView . It will be our UIViewController. This method is called for every time the scroll position is changed:
func scrollViewDidScroll(_ scrollView: UIScrollView)
The current scroll position is
let scrollPosition = scrollView.contentOffset.y

To calculate all the values, we need to know current progress, which is a value from 0.0 to 1.0, where 0.0 is fully expanded and 1.0 is fully shrunk. How do we calculate shrink progress from scrollPosition?

We need to decide the speed. The header range is 180 - 40 = 140. We could change from 0.0 to 1.0 when the user scrolls it by 140 pixels, but we’re building parallax effect, not another section in UIScrollView. Let’s make it scroll half as slow. To make it happen, we need to divide scrollPosition by 280. Recipe 7-5 shows the full version of parallax effect code.
class ParallaxViewController: UIViewController {
    // Views
    @IBOutlet weak var scrollView: UIScrollView!
    @IBOutlet weak var headerView: UIView!
    @IBOutlet weak var shadeView: UIView!
    @IBOutlet weak var titleLabel: UILabel!
    // Layout constraints
    @IBOutlet weak var headerHeightConstraint: NSLayoutConstraint!
    @IBOutlet weak var titleLeadingConstraint: NSLayoutConstraint!
    func setHeaderShrinkProgress(_ progress: CGFloat) {
        let progressClamped = max(min(progress, 1.0), 0.0)
        headerHeightConstraint.constant =
            ParallaxViewController.maxHeaderHeight * (1.0 - progressClamped) +
            ParallaxViewController.minHeaderHeight * progressClamped
        titleLeadingConstraint.constant =
            ParallaxViewController.maxLabelOffset * progressClamped +
            ParallaxViewController.minLabelOffset * (1.0 - progressClamped)
        titleLabel.font = UIFont.systemFont(
            ofSize: ParallaxViewController.minTitleFontSize * progressClamped +
            ParallaxViewController.maxTitleFontSize * (1.0 - progressClamped))
        shadeView.alpha =
            ParallaxViewController.minShadeAlpha * (1.0 - progressClamped) +
            ParallaxViewController.maxShadeAlpha * progressClamped
        headerView.layoutIfNeeded()
    }
    static let minHeaderHeight = CGFloat(40)
    static let maxHeaderHeight = CGFloat(180)
    static let minTitleFontSize = CGFloat(14)
    static let maxTitleFontSize = CGFloat(32)
    static let minLabelOffset = CGFloat(16)
    static let maxLabelOffset = CGFloat(56)
    static let minShadeAlpha = CGFloat(0.3)
    static let maxShadeAlpha = CGFloat(0.8)
}
extension ParallaxViewController: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let scrollPosition = scrollView.contentOffset.y
        let progress = scrollPosition / 280.0
        setHeaderShrinkProgress(progress)
    }
}
Recipe 7-5

Parallax Effect

Congratulations! This is our working parallax header. Method setHeaderShrinkProgress is just doing math using the same formula:
finalValue = expandedValue * (1.0 - progress) + shrinkedValue * progress

To make sure all our values stay in range, we clamp progress, limiting its values from 0.0 to 1.0 in the first line.

Parallax Header with UITableView and UICollectionView

Having code for parallax effect with UIScrollView, we can easily make parallax effect with UITableView or UICollectionView. The thing is that both of these views are subclasses of UIScrollView, so when you set a delegate, it automatically sets UIScrollViewDelegate.

Still, we need to make some changes both in the code and in the storyboard.

First, change UIScrollView to UITableView. Add indicator inset like you did before and create a cell prototype to make some demo content. In the following recipe, it’s just one UILabel with tag property equal to 1.

Then we need to add content inset:
tableView.contentInset = UIEdgeInsets(top: ParallaxTableViewController.maxHeaderHeight, left: 0, bottom: 0, right: 0)
Progress calculation is also changed, as now it counts from -180, not from 0.
func scrollViewDidScroll(_ scrollView: UIScrollView)
{ let scrollPosition = scrollView.contentOffset.y
    let progress = (scrollPosition + ParallaxTableViewController.maxHeaderHeight) / 280.0
    setHeaderShrinkProgress(progress)}
And finally, we need to implement UITableViewDataSource. Don’t forget to set both delegate and dataSource. Recipe 7-6 has the final code:
class ParallaxTableViewController: UIViewController {
    // Views
    @IBOutlet weak var tableView: UIScrollView!
    @IBOutlet weak var headerView: UIView!
    @IBOutlet weak var shadeView: UIView!
    @IBOutlet weak var titleLabel: UILabel!
    // Layout constraints
    @IBOutlet weak var headerHeightConstraint: NSLayoutConstraint!
    @IBOutlet weak var titleLeadingConstraint: NSLayoutConstraint!
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.contentInset = UIEdgeInsets(
            top: ParallaxTableViewController.maxHeaderHeight, left: 0,
            bottom: 0, right: 0)
    }
    func setHeaderShrinkProgress(_ progress: CGFloat) {
        let progressClamped = max(min(progress, 1.0), 0.0)
        headerHeightConstraint.constant =
            ParallaxTableViewController.maxHeaderHeight * (1.0 - progressClamped) +
            ParallaxTableViewController.minHeaderHeight * progressClamped
        titleLeadingConstraint.constant =
            ParallaxTableViewController.maxLabelOffset * progressClamped +
            ParallaxTableViewController.minLabelOffset * (1.0 - progressClamped)
        titleLabel.font = UIFont.systemFont(
            ofSize: ParallaxTableViewController.minTitleFontSize * progressClamped +
            ParallaxTableViewController.maxTitleFontSize * (1.0 - progressClamped))
        shadeView.alpha =
            ParallaxTableViewController.minShadeAlpha * (1.0 - progressClamped) +
            ParallaxTableViewController.maxShadeAlpha * progressClamped
        headerView.layoutIfNeeded()
    }
    static let minHeaderHeight = CGFloat(40)
    static let maxHeaderHeight = CGFloat(180)
    static let minTitleFontSize = CGFloat(14)
    static let maxTitleFontSize = CGFloat(32)
    static let minLabelOffset = CGFloat(16)
    static let maxLabelOffset = CGFloat(56)
    static let minShadeAlpha = CGFloat(0.3)
    static let maxShadeAlpha = CGFloat(0.8)
}
extension ParallaxTableViewController: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let scrollPosition = scrollView.contentOffset.y
        let progress = (scrollPosition + ParallaxTableViewController.maxHeaderHeight) / 280.0
        setHeaderShrinkProgress(progress)
    }
}
extension ParallaxTableViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        50
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "DemoCell", for: indexPath)
        if let label = cell.viewWithTag(1) as? UILabel {
            label.text = "Line (indexPath.row + 1)"
        }
        return cell
    }
}
Recipe 7-6

Parallax Effect with UITableView

Similarly, you can do it for UICollectionView.

Feel free to customize header effects. Add more elements; make movements nonlinear or whatever comes to mind.

Hero Animation

“Hero” animation is a view, “flying” from screen to screen, visually resembling a superhero (Figure 7-2). When you tap a photo, or a dish in a menu, it increases its size and changes its position, while other views change in background.

A set of four screenshots to represent the hero animation. The first image has a relatively small colored square in the middle and the text, 'fly, Hero' at the bottom. In the consecutive screenshots, the area of the square increases and seems to move to the top of the phone screen.

Figure 7-2

Hero animation

Hero Animation Within the Same UIViewController

Now, if we know how to make regular animations, we can create hero animation. To do it, we need to perform the following steps:
  • Create and hide UIView in its destination point.

  • Calculate the difference in position and size between source and destination points.

  • Apply transformation to this UIView to make it look exactly like source UIView.

  • Hide source UIView and show destination UIView.

  • Animate destination UIView to return it to destination.

Assuming you don’t rotate it, there are two transformations:
  • Translation Difference in position

  • Scale Difference in size

Let’s build the entire algorithm, step by step; then wrap it up in a recipe.

Create and Show UIView

Hero animation is always the movement of an element , which looks the same but becomes larger. Original (source) UIView is smaller, so upscaling it will lead to quality loss. We need to create another UIView. Possibly, it’s already created. In this case, we need to set it up. If you choose audio tracks with thumbnails, and the user selects one, you need to apply image and track name to the destination UIView.

At some point, we should have two UIViews (or subclasses), which look identical, but in different locations and with different sizes.
var destinationView: UIView
var sourceView: UIView

If you create it, make it hidden. If you have it in storyboard, it must be marked as hidden there.

Calculate Transformation

First, let’s calculate the location in root view . If your sourceView and destinationView are both children of root UIView, you can skip this step.
let sourceCenter = view.convert(sourceView.center, to: nil)
let destinationCenter = view.convert(destinationView.center, to: nil)
Then we need to calculate the distance between scales, horizontal and vertical.
let scaleX = sourceView.frame.width / destinationView.frame.width
let scaleY = sourceView.frame.height / destinationView.frame.height

Apply Transformation

On this step, we only apply a calculated transformation .
destinationView.transform = CGAffineTransform(scaleX: scaleX, y: scaleY)
destinationView.center = sourceCenter

Hide Source UIView and Show Destination UIView

This is pretty straightforward. Until this step, destination UIView must be hidden .
destinationView.isHidden = false
sourceView.isHidden = true

Animate

Finally, we apply an animation the same way we reviewed earlier in the chapter.
UIView.animate(withDuration: 1.0)
    {destinationView.transform = CGAffineTransform.identity
    destinationView.center = destinationCenter}

That’s it! Your hero can be a simple UIImage or a complex layout inside UIView.

Final Code

In Recipe 7-7, we simply have a UIView with background color. If you need a more complex structure , you’ll have to duplicate it manually. There’s no guaranteed way of copying a view structure in UIKit.
class HeroViewController: UIViewController {
    @IBOutlet weak var sourceView: UIView!
    @IBOutlet weak var targetView: UIView!
    @IBAction func animateHero() {
        let destinationView = UIView(frame: targetView.frame)
        destinationView.backgroundColor = sourceView.backgroundColor
        view.addSubview(destinationView)
        let sourceCenter = view.convert(sourceView.center, to: nil)
        let destinationCenter = view.convert(destinationView.center, to: nil)
        let scaleX = sourceView.frame.width / destinationView.frame.width
        let scaleY = sourceView.frame.height / destinationView.frame.height
        destinationView.transform = CGAffineTransform(scaleX: scaleX, y: scaleY)
        destinationView.center = sourceCenter
        UIView.animate(withDuration: 1.0) {
            destinationView.transform = CGAffineTransform.identity
            destinationView.center = destinationCenter
        }
    }
}
Recipe 7-7

Hero Animation

Also, we use another UIViewtargetView. It’s an invisible instance of UIView, allowing us to set the geometry of destination in a storyboard.

Hero Flying to a New UIViewController

The difference between animation within the same UIViewController and two different UIViewControllers is that we can’t use destination UIView from a storyboard.

The algorithm will be a little different:
  • Run a segue or push destination UIViewController. Hide destination UIView on start.

  • Create a transition UIView matching destination UIView.

  • Calculate source and destination positions and scales.

  • Add transition UIView to UIWindow and apply the source transformation.

  • Hide source UIView.

  • Apply animation of transition UIView’s transformation. Transition between screens; the animation of UIView should be synchronous.

  • When the animation ends, hide the transition UIView and show the destination UIView.

  • Show the source UIView to make it visible when the user goes back.

Looks rather complicated, but it looks clearer in code. (Please find the currentWindow extension function in Recipe 4-3 or use the full version of Recipe 7-8 on GitHub.
// In projects without scenes get window using this code instead: (UIApplication.shared.delegate as? AppDelegate)?.window
class Hero1ViewController: ViewController {
    @IBOutlet weak var sourceView: UIView!
    @IBAction func animateHero() {
        performSegue(withIdentifier: "Hero", sender: nil)
    }
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if let destinationVC = segue.destination as? Hero2ViewController {
            guard let window = UIApplication.shared.currentWindow else {
                return
            }
            destinationVC.loadViewIfNeeded()
            destinationVC.view.layoutSubviews()
            let heroView = UIView(frame: destinationVC.destinationView.frame)
            heroView.backgroundColor = self.sourceView.backgroundColor
            destinationVC.destinationView.backgroundColor = self.sourceView.backgroundColor
            window.addSubview(heroView)
            self.sourceView.isHidden = true
            let sourceCenter = window.convert(self.sourceView.center, to: nil)
            var destinationCenter = window.convert(destinationVC.destinationView.center, to: nil)
            destinationCenter.y += self.view.safeAreaInsets.top
            let scaleX = self.sourceView.frame.width / heroView.frame.width
            let scaleY = self.sourceView.frame.height / heroView.frame.height
            heroView.transform = CGAffineTransform(scaleX: scaleX, y: scaleY)
            heroView.center = sourceCenter
            destinationVC.destinationView.isHidden = true
            UIView.animate(withDuration: 1.0) {
                heroView.transform = CGAffineTransform.identity
                heroView.center = destinationCenter
            } completion: { _ in
                destinationVC.destinationView.isHidden = false
                self.sourceView.isHidden = false
                heroView.removeFromSuperview()
            }
        }
    }
}
class Hero2ViewController: ViewController {
    @IBOutlet weak var destinationView: UIView!
}
Recipe 7-8

Hero Animation Between Screens

Getting and Using UIWindow

In this recipe, we assume that the project uses Scenes. currentWindow computed property is defined in Recipe 4-3. If you don’t use Scenes in your project, use Recipe 4-4 instead.

It may sound strange, but UIWindow is a subclass of UIView. That means we can use any UIView method on UIWindow. This includes addSubview and converse.

Details

Several more explanations to make it clear.

When destination UIViewController is created, view is not loaded. We need to do it manually:
destinationVC.loadViewIfNeeded()
We also need to layout views manually. Otherwise, they’ll be in the same positions as in your storyboard editor. And it’s not always the same after calculation. They match only when the device is the same as in your storyboard settings.
destinationVC.view.layoutSubviews
Another detail is that at the moment of calculation, the screen doesn’t have any bars on top of the screen because the screen is not visible yet. So, the “y” coordinate of your destination UIView will be calculated incorrectly. Assuming that the top insets are the same on both screens, we make the following correction:
destinationCenter.y += self.view.safeAreaInsets.top

The rest of the code is basically the same as in the previous section with minor adjustments.

Transition Between Screens

By default, there are two types of animation:
  • Appearing from right to left inside UINavigationController

  • Appearing from bottom to top when you present a modal UIViewController

You can change animation in the “storyboard editor” or in code, but there’s very limited choice. Luckily, iOS allows us to create a custom transition animation.

Standard Transitions

Depending on navigation type, there are two cases :
  • If it happens inside UINavigationController, there is only one animation. The second screen covers the first one from the right side (or left side for right-to-left languages).

  • When you present new UIViewController modally, there are several default options. We can choose the animation type in storyboard or in code before presenting.

If you use storyboards and segues, you need to choose the type of segue (Show for pushing and Present Modally for... presenting modally). Others are also used, but less often, and they have less customization options, so we’ll set them aside for now.

The most interesting, from a transition point of view, is modal presentation. If you choose this type, you’ll have two additional options:
  • Presentation defines how the result looks. By default, it covers about 90% of the screen, leaving a piece of the previous screen on top.

  • Transition defines the effect.

These options are accessible from code as shown in Recipe 7-9. You can instantiate the view controller from code or override parameters from segue.
extension UIViewController {
    func openViewControllerDefinedWithStyle(destinationViewController: UIViewController) {
        destinationViewController.modalPresentationStyle = .fullScreen
        destinationViewController.modalTransitionStyle = .crossDissolve
    }
}
Recipe 7-9

Changing Presentation and Transition

Creating a Custom Transition

If you’re not happy with standard animation, you can create your own custom transition.

When we’re talking about custom transitions , we need to review several protocols:
  • UIViewControllerTransitioningDelegate is a protocol you need to implement to make custom transitions. Implementation of this protocol defines animation and interaction controllers for presenting and dismissing.

  • Implementation of the UIViewControllerAnimatedTransitioning protocol defines the duration of transitions and the transitions themselves and handles some callbacks.

  • UIViewControllerContextTransitioning is a context. Implementation provides information about transitioning views and controllers.

When you implement the first two protocols, you need to set destinationViewController.transitioningDelegate. Let’s see how it works, step by step.
  1. 1.

    When you trigger transition (using segue or manually), iOS checks if transitioningDelegate is set. If no, it uses one standard transition, the one you set or the default one.

     
  2. 2.

    If transitioningDelegate is set, it calls animationController(forPresented:presenting:source) of your transitioningDelegate. This function is optional; it’s valid to return nil. If it does, the transition will be standard, like if transitioningDelegate wasn’t set.

     
  3. 3.

    At this point, iOS concludes that custom animation must be used and creates a context.

     
  4. 4.

    transitionDuration(using:) is called to define transition time. Time should return in seconds. Usually, it’s around 0.3 seconds. Transitions longer than one second will probably be uncomfortable for users.

     
  5. 5.

    Next, animateTransition(using:) is called. With this method, you can apply animations. Having context (in using argument), you can apply any modifications to both screens. You can change colors, positions, transparency – whatever you like.

     
  6. 6.

    Finally, you need to call completeTransition(_:). This will mark transitions as over and make the destination view controller live. If you don’t call it, it will remain inactive .

     
Some things are easier shown than explained, so let’s have a look at Recipe 7-10 to see how it looks in action. We’ll make one of the simplest transitions: the first screen will fade out, the second, fade in. In the middle of the transition, there will be black screen because both screens will be invisible. Animation duration will be one second, enough to see the entire process.
class FadeThroughBlackPresentAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 1.0
    }
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let fromView = transitionContext.viewController(forKey: .from)?.view,
              let toView = transitionContext.viewController(forKey: .to)?.view
        else { return }
        toView.isHidden = true
        transitionContext.containerView.addSubview(toView)
        UIView.animate(withDuration: 0.5) {
            fromView.alpha = 0.0
        } completion: { _ in
            fromView.isHidden = true
            toView.alpha = 0.0
            toView.isHidden = false
            UIView.animate(withDuration: 0.5) {
                toView.alpha = 1.0
            } completion: { _ in
                fromView.isHidden = false
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            }
        }
    }
}
class FromViewController: UIViewController, UIViewControllerTransitioningDelegate {
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        FadeThroughBlackPresentAnimationController()
    }
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        FadeThroughBlackPresentAnimationController()
    }
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        segue.destination.modalPresentationStyle = .fullScreen
        segue.destination.transitioningDelegate = self
    }
}
class ToViewController: UIViewController {
    @IBAction func goBack() {
        dismiss(animated: true, completion: nil)
    }
}
Recipe 7-10

Fade Through Black Transition

This code includes backward transition, identical to the forward one.

Transition Libraries

If you want to create something new , look around – maybe someone created it already. There are many libraries offering transitions. Let’s have a look at some of them:

All these libraries are free, open source, and available via the Swift Package Manager and CocoaPods. You can use them free of charge in your apps, but don’t forget to donate if you like them.

Summary

Modern mobile apps should use animations to look more user-friendly. UIKit offers native solutions for simple UI animations. In this chapter we discussed fade animations and animated layout changes. We talked about popular in mobile apps parallax effect, hero animation as a part of transition between screens. Finally, we reviewed different ways of transition itself. This wraps up UIKit topic, but UIKit is not the only way to create UI in iOS apps. Since iOS 13 Apple introduces SwiftUI, which is already actively used in both iOS and macOS UI development. In the next chapter we’ll have a fast look at SwiftUI.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.224.38.43