Update: A fellow samaritan subscriber Sambaran Das took a different approach and found a bug in my approach. I have modified the article based on it.
Thanks Sambaran
I have seen many developers trying to achieve Custom transitions by adding child views instead of leveraging the API provided by Apple in iOS. I don’t think the ease of making custom transitions can be achieved by adding child views and animating them. I hope this article reaches the right people who need to change the way they understand transitions.
Custom transitions can be achieved in View Controllers getting presented or dismissed, Navigation Controllers Pushing or Popping, or even when a Tab Bar Controller switches its views. We do have a few built-in transitions which we can use. But indefinite ideas are evolving, and you might just need to make your own custom transition.
Let’s Get Started
Create an iOS App project and open the Main.storyboard
file. Next, Drag and Drop a button in your view controller, give it the title “Present Second VC”. Next, add another View Controller in your storyboard, and drag-drop a button in it, give it the title “Dismiss View”. Tweak the background colors of Buttons and Views if you like. Your Views should look like something shown in the below image.
Create a new file of type Cocoa Touch Class, subclassing UIViewController, name it SecondViewController
. Next, in the Main.storyboard
file, select the new view controller you added, then in Identity Inspector, add the Class name and Storyboard Id as shown in the image below.
We are almost done with setting up the project. Create the IBAction
for both the buttons in their respective classes, In ViewController.swift
file presentClicked(_:)
, and dismissClicked(_:)
in SecondViewController.swift
file.
Defining Transitions
There are two types of transition required, one presenting a view, second dismissing it. We will be recreating the transition style of pushing views on navigation controller without a navigation controller. Once you finish reading this article, you’ll be able to code your ideas of transition with ease.
Let’s start by creating two subclasses of NSObject
, name them PresentTransition
and DismissTransition
.
Open PresentTransition.swift
file, and conform it to a protocol UIViewControllerAnimatedTransitioning
, with this add a property
var animator: UIViewImplicitlyAnimating?
Now, how does it work? We need to define the duration of our transition, then define our animation, and then let it start. With the help of the UIViewControllerAnimatedTransitioning
protocol, all this is executed in separate functions. Let’s see how it’s done.
In your PresentTransition.swift
file, add the function given below, which defines the transition duration.
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.3
}
Next, we will add a function that defines our transition, basically moving the SecondViewController
right to left, and at the same time, moving out our current view. Once the transition is completed, we update the transitionContext
the status of completion. We will need another function that will fetch this declared animation and start the animation.
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let animator = self.interruptibleAnimator(using: transitionContext)
animator.startAnimation()
}
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
if self.animator != nil {
return self.animator!
}
let container = transitionContext.containerView
let fromVC = transitionContext.viewController(forKey: .from)!
let fromViewInitialFrame = transitionContext.initialFrame(for: fromVC)
var fromViewFinalFrame = fromViewInitialFrame
fromViewFinalFrame.origin.x = -fromViewFinalFrame.width
let fromView = fromVC.view!
let toView = transitionContext.view(forKey: .to)!
var toViewInitialFrame = fromViewInitialFrame
toViewInitialFrame.origin.x = toView.frame.size.width
toView.frame = toViewInitialFrame
container.addSubview(toView)
let animator = UIViewPropertyAnimator(duration: self.transitionDuration(using: transitionContext), curve: .easeInOut) {
toView.frame = fromViewInitialFrame
fromView.frame = fromViewFinalFrame
}
animator.addCompletion { _ in
transitionContext.completeTransition(true)
}
self.animator = animator
return animator
}
Once our animation is completed, we will reset our animator
property to nil in the protocol function animationEnded(_:)
.
func animationEnded(_ transitionCompleted: Bool) {
self.animator = nil
}
It’s not over yet. We have defined our animation for Presenting the View Controller. Next, we need to declare the animation for Dismissing the View Controller.
Don’t worry, just copy all the code we declared from PresentTransition.swift
to DismissTransition.swift
, including the global property animator. Next, replace the function interruptibleAnimator(using:) -> UIViewImplicitlyAnimating
with the code below.
func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
if self.animator != nil {
return self.animator!
}
let fromVC = transitionContext.viewController(forKey: .from)!
var fromViewInitialFrame = transitionContext.initialFrame(for: fromVC)
fromViewInitialFrame.origin.x = 0
var fromViewFinalFrame = fromViewInitialFrame
fromViewFinalFrame.origin.x = fromViewFinalFrame.width
let fromView = fromVC.view!
let toView = transitionContext.viewController(forKey: .to)!.view!
var toViewInitialFrame = fromViewInitialFrame
toViewInitialFrame.origin.x = -toView.frame.size.width
toView.frame = toViewInitialFrame
let animator = UIViewPropertyAnimator(duration: self.transitionDuration(using: transitionContext), curve: .easeInOut) {
toView.frame = fromViewInitialFrame
fromView.frame = fromViewFinalFrame
}
animator.addCompletion { _ in
transitionContext.completeTransition(true)
}
self.animator = animator
return animator
}
There are only a few things that changed here. It’s mostly about the x co-ordinate
.
With this, we have declared the style of our transition. Now, let’s just complete the final steps.
Almost there!
Open the ViewController.swift
file, and in the presentClicked(_:)
function you created, add the lines given below to initiate the transition.
let secondVC = self.storyboard?.instantiateViewController(withIdentifier: "SecondViewController") as! SecondViewController
secondVC.modalPresentationStyle = .custom
secondVC.transitioningDelegate = self
self.present(secondVC, animated: true, completion: nil)
As you can see, we have declared transitioningDelegate
as self
. So we’ll have to conform to its protocol to make our Custom Transitions work.
extension ViewController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return PresentTransition()
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return DismissTransition()
}
}
The above two functions defined in the protocol help us return them the type of transitions we are going to use.
There’s one last thing to do, Open your SecondViewController.swift
file, and in the dismissClicked(_:)
action you created, add the line mentioned below to dismiss the view.
self.dismiss(animated: true, completion: nil)
Run.
The smooth transition of navigation is achieved without the UINavigationController
. There’s so much more you can do with Custom Transitions.
What’s Next?
As I said, There’s so much more you can do with Custom Transitions. In the next few articles, I’ll be posting tutorials about some of the most useful Custom Transitions. So if you haven’t subscribed to my article yet, don’t wait. Let the transition begin.
I hope you guys liked the article. Please subscribe to keep me motivated 😉 and to receive weekly updates. I’ll be posting my articles every Tuesday or Wednesday. That will be the day you’ll have to open your Inbox for me. Also, feel free to share your experience with me on Twitter, maybe follow me there 🤗.