Dive Deeper into Motion Design: Advanced iOS Transitions

Szymon Matysik | 9 Nov | 19 min read

Creativity brings with it a constant desire to impress the user. For centuries, man has tried to manipulate the means available to recreate the most natural interaction, taking nature as a fundamental example. 

When discovering the world, a person becomes more and more sensitive to the subtle details of the world around them, which allows them to instinctively distinguish artificiality from living beings. This line is blurred with the development of technology, where the software aims to create an environment in which its user describes his experience in an artificially created world as natural.

(Westworld, HBO, source)

Echoing nature in app’s design

This article will introduce the process of blurring the border using the example of shape transformation in interactive animation of everyday elements in most iOS applications. One way of imitating nature is through various transformations of an object’s position in time. Exemplary functions of animation time are presented below.

(Types of easings, source)

Combined with the right use of time by adding a geometric transformation, we can get an infinite number of effects. As a demonstration of the possibilities of today’s design and technology, the Motion Patterns application was created, which includes popular solutions, developed by Miquido. As I am not a writer, but a programmer, and nothing speaks better than live examples, I have no choice but to invite you to this wonderful world!

Samples to discover

(Westworld, HBO, source)

Let’s take a look at how motion design can transform a run-of-the-mill design into something exceptional! In the examples below, on the left side there is an application that uses only basic iOS animations, while on the right side there is a version of the same application with some improvements.

“Dive Deeper” effect 

This is a transition using transformation between two states of a view. Built on the basis of a collection, after selecting a specific cell, the transition to the details of an element takes place by transforming its individual elements *. An additional solution is the use of interactive transitions, which facilitate the use of the application.

*actually copying / mapping data elements on a temporary view taking part in the transition, between its start and its end … but I will explain this later in this article…

“Peek Over the Edge” effect 

Using the scroll view animation in its action transforms the image in the form of 3D for a cube effect. The main factor responsible for the effect is the offset of scroll view.

“Connect the Dots” effect

This is a transition between scenes that transforms the miniature object into the entire screen. The collections used for this purpose work concurrently, a change on one screen corresponds to a shift on the other. Additionally, when you enter the miniature in the background, a parallax effect appears when you swipe between scenes.

“Shift the Shape” effect 

The last type of animation is a simple one using the Lottie library. It is the most common use for animating icons. In this case, these are the icons on the tab bar. In addition, by changing the appropriate tabs, an animation of the transition in a specific direction was used to further intensify the interaction effect.

Dive even deeper

Now it’s time to get to the point… we need to go even deeper into the structure of the mechanisms that control these examples.

In this article, I will introduce you to the first motion design pattern, which we named ‘Dive Deeper’ with an abstract description of its use, without going into specific details. We plan to make the exact code and the entire repository available to everyone in the future, without any restrictions.

The architecture of the project and applied strict programming design patterns are not a priority at this time—we focus on animations and transition.

In this article, we will be using two sets of features that are provided to manage views during scene transitions. Thus, I would like to point out that this article is intended for people who are relatively familiar with UIKit and the Swift syntax.

https://developer.apple.com/documentation/uikit/uiviewcontrolleranimatedtransitioning

https://developer.apple.com/documentation/uikit/uipercentdriveninteractivetransition

https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/CustomizingtheTransitionAnimations.html

First the structure

For the basic version implementing a given solution, several helper classes will be needed, responsible for providing the necessary information about the views involved in the transition, and controlling the transition itself and the interactions.

The basic class responsible for managing the transition, the proxy, will be TransitionAnimation. It decides which way the transition will take place and covers the standard functions needed to perform the action provided by the Apple team.

/// This is a fundamental class for transitions which have different behavior on presenting and dismissing over specified duration.
open class TransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    
    /// Indicating if it's presenting or dismissing transition.
    var presenting: Bool = true
    
    /// Time interval in which whole transition takes place.
    private let duration: TimeInterval
    
    /// Default initializator of transition animator with default values.
    /// - Parameter duration: Time interval in which whole transition takes place.
    /// - Parameter presenting: Indicator if it's presenting or dismissing transition.
    public init(duration: TimeInterval = 0.5, presenting: Bool = true) {
        self.duration = duration
        self.presenting = presenting
        super.init()
    }
    
    /// Determines duration of transition.
    /// - Parameter transitionContext: Context of current transition.
    /// - Returns: Specified duration at initialization of animator.
    public func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return self.duration
    }
    
    /// Heart of the transition animator, in this function transition takes place.
    /// - Important: Overriding this function in more concrete type of transition is crucial to perform animations.
    /// - Parameter transitionContext: Context of transition.
    public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { }
    
}

Based on the TransitionAnimator, we create a TransformTransition file, the task of which will be to perform a specific transition, transition with transformation (parenting)

/// Implementation of transform transition.
open class TransformTransition: TransitionAnimator {
    
    /// View model which holds all needed specification of transition.
    private var viewModel: TransformViewModel
    
    /// Default initializator of transform transition.
    /// - Parameter viewModel: View model of transform transition.
    /// - Parameter duration: Duration of transition.
    init(viewModel: TransformViewModel, duration: TimeInterval) {
        self.viewModel = viewModel
        super.init(duration: duration, presenting: viewModel.presenting)
    }

The composition of the TransformTransition class includes the TransformViewModel, which, as the name suggests, informs the mechanism of which view models this transition will apply to.

/// View model of transform transition which holds basic information about it.
final class TransformViewModel {
    
    /// Indicates if transform transition is presenting or dismissing view.
    let presenting: Bool
    /// Array of models with specification about transform for each view.
    let models: [TransformModel]
    
    /// Default initializator of transform view model.
    /// - Parameter presenting: Indicates if it's presenting or dismissing transform transition.
    /// - Parameter models: Array of models with specification about transform for each view.
    init(presenting: Bool, models: [TransformModel]) {
        self.presenting = presenting
        self.models = models
    }
    
}

The transformation model is an auxiliary class that describes the specific elements of the views involved in the transition located in the parent, usually a controller’s views that can be transformed.

In the case of a transition, it is a necessary step because this transition consists in the operations of specific views between given states.

Second the implementation

We extend the view model from which we start the transition with Transformable, which forces us to implement a function that will prepare all the necessary elements. The size of this function can grow very quickly, so I suggest you break it down into smaller parts, for example per element.

/// Protocol for class which wants to perform transform transition.
protocol Transformable: ViewModel {
    
    /// Prepares models of views which are involved in transition.
    /// - Parameter fromView: The view from which transition starts
    /// - Parameter toView: The view to which transition goes.
    /// - Parameter presenting: Indicates if it's presenting or dismissing.
    /// - Returns: Array of structures which holds all needed information ready to transform transition for each view.
    func prepareTransitionModels(fromView: UIView, toView: UIView, presenting: Bool) -> [TransformModel]
    
}

The assumption is not to say how to search for the data of views participating in the transformation. In my example I used tags that represent a given view. You have a free hand in this part of implementation.

The models of specific view transformations (TransformModel) are the smallest model in the whole list. They consist of key transform information such as start view, transition view, start frame, end frame, start center, end center, concurrent animations, and end operation. Most of the parameters do not need to be used during the transformation, so they have their own default values. For minimal results, it is enough to use only those that are required.

    /// Default initializator of transform model with default values.
    /// - Parameter initialView: View from which the transition starts.
    /// - Parameter phantomView: View which is presented during transform transition.
    /// - Parameter initialFrame: Frame of view which starts transform transition.
    /// - Parameter finalFrame: Frame of view which will be presented at end of transform transition.
    /// - Parameter initialCenter: Needed when initial center point of view is different than initial view's center.
    /// - Parameter finalCenter: Needed when final center point of view is different than final view's center.
    /// - Parameter parallelAnimation: Additional animation of view performed during transform transition.
    /// - Parameter completion: Block of code triggered after the transform transition.
    /// - Note: Only initial view is needed to perform most minimalistic version of transform transition.
    init(initialView: UIView,
         phantomView: UIView = UIView(),
         initialFrame: CGRect = CGRect(),
         finalFrame: CGRect = CGRect(),
         initialCenter: CGPoint? = nil,
         finalCenter: CGPoint? = nil,
         parallelAnimation: (() -> Void)? = nil,
         completion: (() -> Void)? = nil) {
        self.initialView = initialView
        self.phantomView = phantomView
        self.initialFrame = initialFrame
        self.finalFrame = finalFrame
        self.parallelAnimation = parallelAnimation
        self.completion = completion
        self.initialCenter = initialCenter ?? CGPoint(x: initialFrame.midX, y: initialFrame.midY)
        self.finalCenter = finalCenter ?? CGPoint(x: finalFrame.midX, y: finalFrame.midY)
    }

Your attention may have been captured by phantom View. This is the moment when I will explain the workflow for iOS transitions. In the shortest possible form…

(Apple developer, source)

When the user wishes to move to the next scene, iOS prepares specific controllers by copying the start (blue) and target (green) controllers to memory. Next, a transition context is created through the transition coordinator which contains the container, a ‘stupid’ view that does not contain any special function, besides simulating the transition views between the two scenes.

The key principle of working with transitions is not to add any real view to the transition context, because at the end of the transition, all context is deallocated, along with the views added to the container. These are views that only exist during the transition and are then removed.

Therefore, the use of phantom views that are replicas of real views is an important solution to this transition.

(Westworld, HBO, source )

In this case, we have a transition that transforms one view into another by changing its shape and size. To do this, at the beginning of the transition, I create a PhantomView of the given element and add it to the container. FadeView is an auxiliary view to add softness to the overall transition.

 /// Heart of transform transition, where transition performs. Override of `TransitionAnimator.animateTransition(...)`.
    /// - Parameter transitionContext: The context of current transform transition.
    override open func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let toViewController = transitionContext.view(forKey: .to),
            let fromViewController = transitionContext.view(forKey: .from) else {
                return Log.unexpectedState()
        }
        let containerView = transitionContext.containerView
        let duration = transitionDuration(using: transitionContext)
        let fadeView = toViewController.makeFadeView(opacity: (!presenting).cgFloatValue)
        let models = viewModel.models
        let presentedView = presenting ? toViewController : fromViewController
        
        models.forEach { $0.initialView.isHidden = true }
        presentedView.isHidden = true
        containerView.addSubview(toViewController)
        if presenting {
            containerView.insertSubview(fadeView, belowSubview: toViewController)
        } else {
            containerView.addSubview(fadeView)
        }
        containerView.addSubviews(viewModel.models.map { $0.phantomView })

In the next step, I transform it to the target shape through transformations, and depending on whether it is a presentation or a recall, it performs additional operations to clean up specific views – this is the entire recipe for this transition.

let animations: () -> Void = { [weak self] in
            guard let self = self else { return Log.unexpectedState() }
            fadeView.alpha = self.presenting.cgFloatValue
            models.forEach {
                let center = self.presenting ? $0.finalCenter : $0.initialCenter
                let transform = self.presenting ? $0.presentTransform : $0.dismissTransform
                $0.phantomView.setTransformAndCenter(transform, center)
            }
            models.compactMap { $0.parallelAnimation }.forEach { $0() }
        }
        
        let completion: (Bool) -> Void = { _ in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            presentedView.isHidden = false
            models.compactMap { $0.completion }.forEach { $0() }
            models.forEach { $0.initialView.isHidden = false }
            if !self.presenting && transitionContext.transitionWasCancelled {
                toViewController.removeFromSuperview()
                fadeView.removeFromSuperview()
            }
        }
        
        UIView.animate(withDuration: duration,
                       delay: 0,
                       usingSpringWithDamping: 1,
                       initialSpringVelocity: 0.5,
                       options: .curveEaseOut,
                       animations: animations,
                       completion: completion)

Third the special ingredient

After putting together all the functions, classes and protocols, the result should look like this:

The final component of our transition will be its full interactivity. For this purpose we will use a Pan Gesture added in the controller view, TransitionInteractor… 

/// Mediator to handle interactive transition.
final class TransitionInteractor: UIPercentDrivenInteractiveTransition {
    
    /// Indicates if transition have started.
    var hasStarted = false
    /// Indicates if transition should finish.
    var shouldFinish = false

}

… which we also initialize in the controller body.

   /// Handles pan gesture on collection view items, and manages transition.
    @objc func handlePanGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
        let percentThreshold: CGFloat = 0.1
        let translation = gestureRecognizer.translation(in: view)
        let verticalMovement = translation.y / view.bounds.height
        let upwardMovement = fminf(Float(verticalMovement), 0.0)
        let upwardMovementPercent = fminf(abs(upwardMovement), 0.9)
        let progress = CGFloat(upwardMovementPercent)
        guard let interactor = interactionController else { return }
        switch gestureRecognizer.state {
        case .began:
            interactor.hasStarted = true
            let tapPosition = gestureRecognizer.location(in: collectionView)
            showDetailViewControllerFrom(location: tapPosition)
        case .changed:
            interactor.shouldFinish = progress > percentThreshold
            interactor.update(progress)
        case .cancelled:
            interactor.hasStarted = false
            interactor.cancel()
        case .ended:
            interactor.hasStarted = false
            interactor.shouldFinish
                ? interactor.finish()
                : interactor.cancel()
        default:
            break
        }
    }

The ready interaction should be as follows:

If everything went according to plan, our application will gain much more in the eyes of its users.

(Westworld, HBO, source)

Discovered only a peak of an iceberg

Next time, I will explain the implementation of related issues of motion design in the following editions.

The application, design and source code are the property of Miquido, and were created with passion by talented designers and programmers for the use of which we are not responsible for in our implementations. Detailed source code will be available in the future via our github account — we invite you to follow us!

Thank you for your attention and see you soon!