iOS 8 Custom transitions in Swift

March 15, 2015
Shrikar Archak

<Image alt="UITableView Customization" objectFit="contain" src="/static/images/Screen-Shot-2015-04-28-at-10.18.06-PM.png" height={350} width={1000} placeholder="blur" quality={100} />

iOS provides a few transitions like Modal, Push and a few more. But if you want to make your app looks different and want to have light weight transitions within your app then you are in for a treat with Custom transitions.In this tutorial we will learn how to build a custom transitions in Swift.

Lets try to implement the same in our How to make a photography inspiration appand Part 2 Swift iOS Tutorial: Taming UITableView Visual Blur and Autolayout.

<Image alt="UITableView Customization" objectFit="contain" src="/static/images/custom.gif" height={350} width={1000} placeholder="blur" quality={100} />

There are a few things which we need to make custom transitions work.

Our view controller which will begin the transition should implement the UIViewControllerTransitioningDelegate, UIViewControllerAnimatedTransitioning

Lets create a DetailViewController which has an imageview within that and setup the autolayout constraints with value 0 to superview on all the sides. If we want to go to a detailViewController on selecting a tableView cell we could implement our tableView didSelectRowAtIndexPath as below

override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    let dvc = DetailViewController(nibName: "DetailViewController", bundle: nil)
    dvc.photo = self.photos[indexPath.row]
    dvc.transitioningDelegate = self
    dvc.modalPresentationStyle = UIModalPresentationStyle.Custom
    let currRect = self.tableView.rectForRowAtIndexPath(indexPath)
    self.point = CGPointMake(currRect.midX, currRect.midY)
    self.presentViewController(dvc, animated: true, completion: nil)
}

The key thing to note here are the setting up of transitioningDelegate to self and the modalPresentationStyle to UIModalPresentationStyle.Custom. Once we have set this up we can present the view controller.

Next we need to implement the functions which will perform the actual transitions.

func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    isPresenting = true
    return self
}

func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    isPresenting = false;
    return self
}

Lets maintain a state variable like isPresenting which will be true if we are presenting and false if we are not. Then for both Presenting and Dismiss action we will set the variable appropriately and return ourself(self). The next two function deals with how much is the transition duration as well as an entry point for us to perform the transitions with animations.

func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
    return animationDuration
}

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    let containerView = transitionContext.containerView();
    let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)
    let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)
    toViewController!.view.frame = fromViewController!.view.frame
    if(self.isPresenting == true) {
        toViewController!.view.alpha = 0;
        toViewController!.view.transform = CGAffineTransformMakeScale(0, 0);

        UIView.animateWithDuration(animationDuration, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.3, options: nil, animations: { () -> Void in
            toViewController!.view.alpha = 1;
            toViewController!.view.transform = CGAffineTransformMakeScale(1, 1);
            containerView.addSubview(toViewController!.view)
        }, completion: { (completed) -> Void in
            transitionContext.completeTransition(completed)
        })

    } else {
        UIView.animateWithDuration(animationDuration, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.3, options: nil, animations: { () -> Void in
            fromViewController!.view.alpha = 0;
            fromViewController!.view.transform = CGAffineTransformMakeScale(0.001, 0.0001);
        }, completion: { (completed) -> Void in
            fromViewController?.view.removeFromSuperview()
            transitionContext.completeTransition(completed)

        })
    }
}

Now in the animateTransition we will get a transitionContext which has many necessary objects to help us with performing the transition. It has a containerView which will hold both the fromViewController and the toViewController. As well as the reference to both the controllers. We set the frame of the toViewController to that of the fromViewController.

In the presentiong mode we want the toViewController to fade in with the scaling effect. The code for the same is presented in the isPresenting block. Similarly we remove the view from the superview in the dismiss action.

One key important thing to note here is that in both the actions we need to call transitionContext.completeTransition(completed) otherwise the behaviour is unpredictable.

//
//  PhotoListController.swift
//  PicInspire
//
//  Created by Shrikar Archak on 3/14/15.
//  Copyright (c) 2015 Shrikar Archak. All rights reserved.
//

import UIKit

class PhotoListController: UITableViewController, UIViewControllerTransitioningDelegate, UIViewControllerAnimatedTransitioning {
    let manager : PhotoManager = PhotoManager()
    var photos: [Photo]!
    var isPresenting: Bool!
    var point: CGPoint!
    let animationDuration = 0.3
    override func viewDidLoad() {
        super.viewDidLoad()

        manager.getPhotos(["tag":"sunset","only":"Nature","image_size":"4","rpp":"100"], completion: { (photos, error) -> () in
            self.photos = photos
            self.tableView.reloadData()
            for photo in photos {
                println("(photo.name!)")
                println("(photo.lens)")
                println("(photo.shutterSpeed)")
                println("(photo.camera)")
                println("(photo.focalLength)")
                println("(photo.iso)")
            }

        })
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()

    }
    override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1;
    }

    override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if let tphotos = self.photos{
            return tphotos.count;
        }
        return 0;
    }

    override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("PhotoCell", forIndexPath: indexPath) as PhotoCell
        cell.posterImageView.image = nil;
        let photo = self.photos[indexPath.row]
        cell.camera.text = photo.camera
        cell.lens.text = photo.lens
        cell.shutterSpeed.text = photo.shutterSpeed
        cell.iso.text = photo.iso
        cell.focalLength.text = photo.focalLength
         /* AFNetworking extension for loading images async */
        cell.posterImageView.setImageWithURL(NSURL(string: photo.imageurl!));
        return cell
    }

    override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        let dvc = DetailViewController(nibName: "DetailViewController", bundle: nil)
        dvc.photo = self.photos[indexPath.row]
        dvc.transitioningDelegate = self
        dvc.modalPresentationStyle = UIModalPresentationStyle.Custom
        let currRect = self.tableView.rectForRowAtIndexPath(indexPath)
        self.point = CGPointMake(currRect.midX, currRect.midY)
        self.presentViewController(dvc, animated: true, completion: nil)
    }

    func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        isPresenting = true
        return self
    }

    func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        isPresenting = false;
        return self
    }

    func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
        return animationDuration
    }


    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        let containerView = transitionContext.containerView();
        let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)
        let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)
        toViewController!.view.frame = fromViewController!.view.frame
        if(self.isPresenting == true) {
            toViewController!.view.alpha = 0;
            toViewController!.view.transform = CGAffineTransformMakeScale(0, 0);

            UIView.animateWithDuration(animationDuration, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.3, options: nil, animations: { () -> Void in
                toViewController!.view.alpha = 1;
                toViewController!.view.transform = CGAffineTransformMakeScale(1, 1);
                containerView.addSubview(toViewController!.view)
            }, completion: { (completed) -> Void in
                transitionContext.completeTransition(completed)
            })

        } else {
            UIView.animateWithDuration(animationDuration, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.3, options: nil, animations: { () -> Void in
                fromViewController!.view.alpha = 0;
                fromViewController!.view.transform = CGAffineTransformMakeScale(0.001, 0.0001);
            }, completion: { (completed) -> Void in
                fromViewController?.view.removeFromSuperview()
                transitionContext.completeTransition(completed)

            })
        }
    }


}

Below is the code for DetailViewController

//
//  DetailViewController.swift
//  PicInspire
//
//  Created by Shrikar Archak on 3/14/15.
//  Copyright (c) 2015 Shrikar Archak. All rights reserved.
//

import UIKit

class DetailViewController: UIViewController {

    var photo: Photo!
    @IBOutlet weak var posterImageView: UIImageView!
    override func viewDidLoad() {
        super.viewDidLoad()
        self.posterImageView.setImageWithURL(NSURL(string: photo.imageurl!))

        self.posterImageView.contentMode = UIViewContentMode.ScaleAspectFill
        let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: Selector("handlePan:"))
        self.posterImageView.addGestureRecognizer(panGestureRecognizer)
    }

    func handlePan(sender: UIPanGestureRecognizer){
        if(sender.state == UIGestureRecognizerState.Ended){
            self.dismissViewControllerAnimated(true , completion: nil);
        }
    }
    override func viewWillAppear(animated: Bool) {

    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


    /*
    // MARK: - Navigation

    // In a storyboard-based application, you will often want to do a little preparation before navigation
    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        // Get the new view controller using segue.destinationViewController.
        // Pass the selected object to the new view controller.
    }
    */

}

Subscribe to the newsletter

Get notified when new content or topic is released.

You won't receive any spam! ✌️