This post will show how to implement a transition where one item from “screen 1” flies into “screen 2”. More specific: we want to move an UIView from one UIViewController to another.

There are a some good introductions to UIViewController transitions, but I couldn’t find anything that fit my wants exacty, so I’m writing this post to fill the gap. Admittedly, I didn’t do a lot of research, as I planned to implement the transition myself.

End result

Here is the end result of what we will be making. As you see, the profile pictures of the list animates nicely (well..) into the second screen, the detail view.

You might notice that the dismissal animation is not nice, the reason for this is to keep the examples focused. To have a nice dismiss, we need to add some code to essentially run the animation in reverse.

That might come in another article.

Overview

So what is exactly happening here? Is the view actually transferred from one UIViewController to another UIViewController? No, it is not. It is all smoke and mirrors.

What really happens is that we create a fake profile picture, identical to the one in the list, that we add to a special view within the transition context. We then move that fake view into the position of where it should be in the second view. At the very last moment, we remove the fake view to reveal the identical view in the second view controller. The users should be none the wiser.

Here’s an approximation of how it looks when you tap a row in the table view.

Once we tap the row, we are given a sort of temporary space to add our views to. Here we have copied the UIImageView with the profile picture and added it to that space. Then we have retreived the intended frame in the second screen.

Not pictured is the actual animation we kick off.

So let’s see how this all works then. You might want to look at the introduction tutorial at ray wenderlich’s website, as it goes into more details of each step, why it is done.

Code

My assumption with this code is that you are kind of familar with the idea of transitions, maybe have implemented it from the tutorial linked above but didn’t find exactly what you wanted - just like me. So the code will show kind of the setup, but mainly the meat - getting that profile picture into the second view controller.

The parts we will touch are

  • ListViewController - The 1st screen, the initiator
  • DetailViewContrller - The 2nd screen, the target
  • ListToDetailAnimator - A class that will coordinate/start the animation, keep track of from/to where the animations will take place

ListViewController - Screen 1

So the very basic setup is something like this. We give our ListViewController the ListToDetailAnimator object.

class ListViewController : UIViewController {
    
    let transition = ListToDetailAnimator()
    ...
    ...
    
}

We then make it conform to the UIViewControllerTransitioningDelegate protocol, so that the app (?) knows that the VC is able to take care of the transitions from it. The method itself just returns the transition to the system, it doesn’t deal with the transitions like animations itself.

extension ListViewController : UIViewControllerTransitioningDelegate {
    
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return transition
    }
    
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return nil
    }
    
}

DetailViewController - Screen 2

The second screen only needs some way for the animator object to know the location of the profile picture, so that it can get the frame. For that, I just made the UIImageView public.

class DetailViewController: UIViewController {

    @IBOutlet weak var headerImageView: UIImageView!
    ...
    ...

}

ListToDetailAnimator - Basic overview

Now we get to the actial meat of the solution. This class is that literally makes the profile picture move “between” the screens.

For this class, let’s start with the base minimum and some explanations, then I will add the profile picture movement.

class ListToDetailAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    
    let duration = 0.5
    
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return duration
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        let toViewController = transitionContext.viewController(forKey: .to) as! DetailViewController
        toViewController.view.alpha = 0.0
        transitionContext.containerView.addSubview(toViewController.view)
        
        UIView.animate(withDuration: duration, animations: {
                toViewController.view.alpha = 1.0
            }, completion: { _ in
                transitionContext.completeTransition(true)
            })
    }
}

This should be fairly readable as it is. We can see that the transitionContext gives us access to the source VC and the destination VC. Here we add the destination into the context with alpha 0. This let’s us then animate it in with the normal UIView animation methods.

At the end, we tell the context that we’re done.

Something to note here is that the source VC’s view is automatically added into the context, so we could move it around if we wanted to. It is also automatically removed once we finish the transition. Again, the ray wenderlich link talks a bit more about this.

So that was the most simple thing we could do, fading it in. Now let’s see how we can get the profile picture animated.

ListToDetailAnimator - Actually moving the UIImageView

All of this code lives in the animateTransition method you saw above.

...

// 1. This removes the headerView in the target view
toViewController.headerView?.isHidden = true

// 2. Force autolayout to layout
toViewController.view.layoutIfNeeded()

// 3. Create a replica of the profile image to animate
let fakeImageView = UIImageView(frame: originFrame)
fakeImageView.backgroundColor = UIColor.purple
fakeImageView.layer.cornerRadius = 44
fakeImageView.image = self.image
fakeImageView.contentMode = .scaleAspectFill
fakeImageView.clipsToBounds = true
transitionContext.containerView.addSubview(fakeImageView)

UIView.animate(withDuration: duration, animations: {
                ...
                ...
    
                // 4. Find the target frame for the animating view
                let finalFrame = toViewController.headerView!.frame
                fakeImageView.frame = finalFrame
    
                // 5. We also need to animate the corner radius to match the target
                fakeImageView.layer.cornerRadius = 10
    
        }, completion: { _ in
            
                // 6. Show the header view in the destination VC
                toViewController.headerView?.isHidden = false
            
                // 7. Remove the fake one
                fakeImageView.removeFromSuperview()
            
                ...

Again the code should be fairly self explanatory here. But some parts may be less obvious.

For example, in step 1. and 6. we hide the header view inside the destination view controller, but why? This is to make sure that we don’t see the header being already in the view during the animation. The whole point if this animation is to make it look like the picture is being transferred into the new screen, if it is there already the illusion is broken.

This is actually done in the ListViewController as well. When we tap the row, we hide the small UIImageView in the UITableViewCell. Again this is to make it look like it is moving into the next screen.

Speaking of starting the animation..

ListViewController - Setting up the transition

There are a few more key parts that I haven’t touched on yet. The transition object needs some data injected to work properly. For example, it needs the image to add into the fake UIImageView, as well as a location to start from.

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    // Transition setup
    let cell = tableView.cellForRow(at: indexPath) as! ListCell
    transition.originFrame = cell.listCellImageView!.superview!.convert(cell.listCellImageView!.frame, to: nil)
    transition.image = cell.listCellImageView.image
    
    // Detail setup
    let storyboard = UIStoryboard.init(name: "DetailViewController", bundle: nil)
    let detail = storyboard.instantiateInitialViewController() as! DetailViewController
    detail.loadViewIfNeeded() // Ensure the imageView is loaded when assigning the image
    detail.transitioningDelegate = self
    detail.setImage(image: cell.listCellImageView.image)
    detail.setTitle(text: cell.listCellTitleLabel.text)
    show(detail, sender: nil)
}

Again this is fairly straight forward to read.