iOS 8 Custom transitions in Swift
<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.
}
*/
}