Duc's Blog

Learn. Create. Contribute.



Create Zooming Image Transition - iOS


Download Starter Project  Join Total iOS Blueprint 2

When you look at the built-in Photos App, or Facebook, they use this same transition effect that whenever users tap on a photo to see the full size, the image zooms out nicely creating a really cool, intuitive and fun experience.

Let's recreate this same transition in this session. First, click the button below the video to download your starter project. Let's get started!!!

Detailed Code Instructions in the Video

If you'd like to have my detailed tutorial and explanation of this code, please watch the full video above. It will certainly help you understand a lot more than just reading and typing this code in.

First, let's create a new ZoomTransitioningDelegate object.

 

// 1
// this is to make sure that any fromViewController and toViewController must conform to this protocol and give us the imageView or anyother views we want to animate
@objc
protocol ZoomingViewController
{
    func zoomingImageView(for transition: ZoomTransitioningDelegate) -> UIImageView?
    func zoomingBackgroundView(for transition: ZoomTransitioningDelegate) -> UIView?
}

// 2 
// check on what state of the transition we're in
enum TransitionState {
    case initial
    case final
}

// 3 
// The Main transition object
class ZoomTransitioningDelegate: NSObject
{
    // 4 
    // What is the duration of the transition and operation can be push or pop for uinavigationcontroller
    var transitionDuration = 0.8
    var operation: UINavigationControllerOperation = .none
    
    // 5
    // how much do we want to zoom the imageView and how much do we want to shrink down our backgroundVC's views=
    private let zoomScale = CGFloat(15)
    private let backgroundScale = CGFloat(0.8)
  
}

Still in ZoomTransitioningDelegate.swift, we create a new extension for the class to conform to UINavigationControllerDelegate. This allows us to override the default transition of UINavigationController to use our ZoomTransitioningDelegate as the transition object. We first have to check and make sure that fromVC and toVC does conform to our ZoomingViewController protocol otherwise let the transition be the default one.

 

// 6
// This allows us to override the default transition of UINavigationController to use our ZoomTransitioningDelegate as the transition object.
// We first have to check and make sure that fromVC and toVC does conform to our ZoomingViewController protocol otherwise let the transition be the default one.
extension ZoomTransitioningDelegate : UINavigationControllerDelegate
{
    func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?
    {
        if fromVC is ZoomingViewController && toVC is ZoomingViewController {
            self.operation = operation
            return self
        } else {
            return nil
        }
    }
}

 

Awesome work so far! Now All we have to do is conform this class to UIViewControllerAnimatedTransitioning. This protocol allow us to customize our transition animation. Let's create a new extension for this:

// 7
// Implement this protocol to let UIViewController transition know what is the duration of the transition and how the transition looks like (our animation code)
extension ZoomTransitioningDelegate : UIViewControllerAnimatedTransitioning
{
    // 8 - our animation
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval
    {
        return transitionDuration
    }
    
    // 9 - Our animation method
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
    {
        // 10 - Get the duration, fromVC, toVC and containerView
        let duration = transitionDuration(using: transitionContext)
        let fromViewController = transitionContext.viewController(forKey: .from)!
        let toViewController = transitionContext.viewController(forKey: .to)!
        let containerView = transitionContext.containerView
        
        
        // 11 - check operation type
        var backgroundViewController = fromViewController
        var foregroundViewController = toViewController
        
        if operation == .pop {
            backgroundViewController = toViewController
            foregroundViewController = fromViewController
        }
        
        // 12 - get the imageview or any views to animate
        let maybeBackgroundImageView = (backgroundViewController as? ZoomingViewController)?.zoomingImageView(for: self)
        let maybeForegroundImageView = (foregroundViewController as? ZoomingViewController)?.zoomingImageView(for: self)
        
        assert(maybeBackgroundImageView != nil, "Cannot find image view in backgroundVC in ZoomingTransitioningDelegate")
        assert(maybeForegroundImageView != nil, "Cannot find image view in foregroundVC in ZoomingTransitioningDelegate")
        
        let backgroundImageView = maybeBackgroundImageView!
        let foregroundImageView = maybeForegroundImageView!
        
        // 13 - create some view snapshots
        let imageViewSnapshot = UIImageView(image: backgroundImageView.image)
        imageViewSnapshot.contentMode = .scaleAspectFill
        imageViewSnapshot.layer.masksToBounds = true
        
        // 14 - setup animation
        backgroundImageView.isHidden = true
        foregroundImageView.isHidden = true
        let foregroundViewBackgroundColor = foregroundViewController.view.backgroundColor
        foregroundViewController.view.backgroundColor = UIColor.clear
        containerView.backgroundColor = UIColor.white
        containerView.addSubview(backgroundViewController.view)
        containerView.addSubview(foregroundViewController.view)
        containerView.addSubview(imageViewSnapshot)
        
        // 15 - set up transition state - we check if it's pop or push. Then we use .final or .initial to use our helper method to set the backgroundVC's view and imageView initial state
        var preTransitionState = TransitionState.initial
        var postTransitionState = TransitionState.final
        
        if operation == .pop {
            preTransitionState = .final
            postTransitionState = .initial
        }
}

 

Great! We've done quite a lot to set up our initial state of the animation. Now let's go back to our ZoomTransitioningDelegate class and create a helper method to help us set the initial alpha, transform of the backgroundVC and imageView.

// 3 
// The Main transition object
class ZoomTransitioningDelegate: NSObject
{
   ....

    // - 16
    // what views do we want to animate in the transition
    typealias ZoomingViews = (otherView: UIView, imageView: UIView)
    
    // 16 - helper method to help us set the initial alpha, transform of the backgroundVC and imageView. It's the initial state of the transition.
    func configureViews(for state: TransitionState, containerView: UIView, backgroundViewController: UIViewController, viewsInBackground: ZoomingViews, viewsInForeground: ZoomingViews, snapshotViews: ZoomingViews)
    {
        switch state {
        case .initial:
            
            // set the initial state of the background view and its image view
            backgroundViewController.view.transform = CGAffineTransform.identity
            backgroundViewController.view.alpha = 1
            
            snapshotViews.imageView.frame = containerView.convert(viewsInBackground.imageView.frame, from: viewsInBackground.imageView.superview)
            
        case .final:
            // make the background view shrink down to backgroundScale
            backgroundViewController.view.transform = CGAffineTransform(scaleX: backgroundScale, y: backgroundScale)
            backgroundViewController.view.alpha = 0
            
            snapshotViews.imageView.frame = containerView.convert(viewsInForeground.imageView.frame, from: viewsInForeground.imageView.superview)
        }
    }
}

 

Awesome! Our work for ZoomTransitioningDelegate is almost finished! Let's go to our UIViewControllerAnimatedTransitioning extension > animateTransition method to finish our animation code.

// 9 - Our animation method
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
    {
        ....
        
        // 17 - Use our configureViews helper method to set the initial state of the transition.
        configureViews(for: preTransitionState, containerView: containerView, backgroundViewController: backgroundViewController, viewsInBackground: (backgroundImageView, backgroundImageView), viewsInForeground: (foregroundImageView, foregroundImageView), snapshotViews: (imageViewSnapshot, imageViewSnapshot))
        
        // 18 - during the transition, the device can be rotated or subviews can be changed so we call this to make sure everything is ok before we animate stuff.
        foregroundViewController.view.layoutIfNeeded()
        
        // 19 - We use UIView's animate method to animate from the initial state to the final state. We use the configureViews method again to help us with this.
        UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0, options: [], animations: { 
            
            self.configureViews(for: postTransitionState, containerView: containerView, backgroundViewController: backgroundViewController, viewsInBackground: (backgroundImageView, backgroundImageView), viewsInForeground: (foregroundImageView, foregroundImageView), snapshotViews: (imageViewSnapshot, imageViewSnapshot))
            
        }) { (finished) in
            
            backgroundViewController.view.transform = CGAffineTransform.identity
            imageViewSnapshot.removeFromSuperview()
            backgroundImageView.isHidden = false
            foregroundImageView.isHidden = false
            foregroundViewController.view.backgroundColor = foregroundViewBackgroundColor
            
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        }
    }

 Alright! We have successfully created our ZoomImageTransitioning object! Now let's make this guy work! Go to our PhotosCollectionViewController.swift, let's make sure that every time we tap on an item, we keep track of the selected index.

class PhotosCollectionViewController: UICollectionViewController
{
    var photoCategories = PhotoCategory.fetchPhotos()
    var selectedIndexPath: IndexPath!  // <<== INSERT THIS 
    
    ...

    // MARK: - UICollectionViewDelegate
    
    override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
    {
        let category = self.photoCategories[indexPath.section]
        let image = UIImage(named: category.imageNames[indexPath.item])
        
        self.selectedIndexPath = indexPath  // <<== INSERT THIS 
        self.performSegue(withIdentifier: Storyboard.showDetailSegue, sender: image)
    }
}

 Next, we need to make PhotosCollectionViewController conform to ZoomingViewController our little protocol in the ZoomTransitioningDelegate to make sure that this class gives the transition object its backgroundView and imageView.

extension PhotosCollectionViewController : ZoomingViewController
{
    func zoomingBackgroundView(for transition: ZoomTransitioningDelegate) -> UIView? {
        return nil
    }
    
    func zoomingImageView(for transition: ZoomTransitioningDelegate) -> UIImageView?
    {
        if let indexPath = selectedIndexPath {
            let cell = collectionView?.cellForItem(at: indexPath) as! PhotoCell
            return cell.photoImageView
        } else {
            return nil
        }
    }
}

 Almost done! In DetailViewController.swift, let's make this class conform to ZoomingViewController too. It goes like this:

extension DetailViewController : ZoomingViewController
{
    func zoomingBackgroundView(for transition: ZoomTransitioningDelegate) -> UIView? {
        return nil
    }
    
    func zoomingImageView(for transition: ZoomTransitioningDelegate) -> UIImageView? {
        return categoryImageView
    }
}

And finally, in the Storyboard, find the Navigation Controller and drag an Object from the Object Library to the navigation controller scene. Set the custom class of this to be ZoomTransitioningDelegate and set this object to be the navigation controller's delegate outlet.

This reads tricky in text but it's really the simplest part of our tutorial. Watch the video for my instruction to do this.

 

 Awesome! Run the project now and you'll see it works totally perfect!

Featured Free Trainings

Build Nike E-commerce Store

Get This Training
How to Build Facebook Newsfeed

Get This Training
How to Build Instagram

Get This Training