This article is a follow-up to the previous blog post and shows yet another way to implement animated view controller transitions using UIKit Dynamics.
Before iOS7, it wasn’t easy to create custom transitions between view controllers and, more often than not, developers limited themselves to using the classic push and modal presentations and their standard animations.
Apple introduced in iOS7 the UIViewControllerAnimatedTransitioning protocol, greatly simplifying the implementation of custom animations when navigating from one view controller to the next. In this article, I will not go in depth about this protocol and the custom transitions in general, I will leave this for a later post which will focus exclusively on the topic. Here my goal is to demonstrate the use of UIKit Dynamics animations in the context of custom transitions.
What are view controller animated transitions?
Even the most simple apps have several view controllers. At a given moment, there usually is only one view controller on screen and it can present another view controller either automatically or following a user action. When the new view controller is modal, we use the terms presenting and presented view controllers, and there are properties of the view control class exposing this bi-directional relationship (presentingViewController and presentedViewController).
When a view controller is replaced by another one on screen, the switch between their main views is animated. Apple provides a set of standard animations, the default one for a modal presentation being the slide-up, but the animated transitions allow us to implement custom shifting between the two views.
Limitations
Unsurprisingly, the custom view controller transitions have a very good support for the UIView based animations, but don’t work well with other kinds of animations, including UIKit Dynamics.
To be able to use dynamics, we have to improvise a little bit trying to get around some of these limitations.
Modal presentation with dynamic animations
UIKit Dynamics is a set of APIs made available in iOS7 allowing to easily implement physics based animations between views. If you need to overview some basics about these APIs, I encourage you to read an older blog post on the topic.
As usual, I will support the demonstration with a code sample available here for download. The app shows an example of modal presentation using interactive transitions with a small touch of dynamic animations:
Because we cannot implement the actual transition using UIKit Dynamics, we use it at the end of the transition to add an interesting bouncing effect.
Defining the transitioning animator
As specified above, the class that handles the animation between the view controllers conforms to the UIViewControllerAnimatedTransitioning protocol and implements the -transitionDuration: and -animateTransition: methods. The second method defines the actual animation replacing the presenting view with the presented view.
To be able to participate in a custom transition, the main view controller container conforms to the UIViewControllerTransitioningDelegate protocol and implements the -animationControllerForPresentedController:presentingController:sourceController: method returning an instance of the custom class which implements the transitioning animation.
Implementing the transition animation
To make things a bit more interesting, the transition is triggered by a pan gesture starting at the left edge of the main view controller. The gesture reveals the secondary (presented) view controller underneath with a continuous animation depending on the distance traveled by the finger on the screen. Playing the custom animation in sync with the user gesture is a feature of the interactive transitioning protocol, a nice addition to the view controller custom animated transitioning.
The main view controller is embedded in a container view controller in the storyboard. The container is the presenting view controller acting as the transitioning delegate and it also provides the area where the dynamic animations occur. As such, it contains most of the transitioning and dynamic animation logic.
We start by adding the UIScreenEdgePanGestureRecognizer to the main view controller:
1 2 3 4 5 6 7 8 9 10 |
- (void)viewDidLoad { [super viewDidLoad]; self.dynamicAnimator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view]; self.dynamicAnimator.delegate = self; UIScreenEdgePanGestureRecognizer *leftEdgeGestureRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(handleEdgeGesture:)]; leftEdgeGestureRecognizer.edges = UIRectEdgeLeft; [self.view addGestureRecognizer:leftEdgeGestureRecognizer]; } |
In the gesture handler, we enable each step of the animation depending on the state of the gesture:
- when the pan gesture starts, we instantiate the interaction controller from the standard UIPercentDrivenInteractiveTransition class and we present the secondary view controller
- we ask the transitioning animator linked to the interaction controller to update the screen using the custom animation which progresses in sync with the panning distance
- if the panning distance doesn’t exceed 40% of the screen width, we cancel the presentation and the main view controller slides back. Otherwise, the presentation is completed and the secondary view controller takes the full screen while the presenting view controller disappears through the right side.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
- (void)handleEdgeGesture:(UIScreenEdgePanGestureRecognizer *)edgeGestureRecognizer { CGPoint anchorPoint = [edgeGestureRecognizer locationInView:self.view]; // The anchor point X position is constant to prevent the pushed controller to move vertically anchorPoint.y = CGRectGetMidY(self.view.bounds); CGFloat translationPercent = [edgeGestureRecognizer translationInView:self.view].x / CGRectGetWidth(self.view.bounds); if (edgeGestureRecognizer.state == UIGestureRecognizerStateBegan) { self.interactionController = [[UIPercentDrivenInteractiveTransition alloc] init]; CCRSecondaryViewController *secondaryViewController = [[CCRSecondaryViewController alloc] init]; secondaryViewController.transitioningDelegate = self; [self presentViewController:secondaryViewController animated:YES completion:nil]; } else if (edgeGestureRecognizer.state == UIGestureRecognizerStateChanged) { // Move the view controller in sync with the gesture [self.interactionController updateInteractiveTransition:translationPercent]; } else if (edgeGestureRecognizer.state == UIGestureRecognizerStateEnded || edgeGestureRecognizer.state == UIGestureRecognizerStateCancelled) { self.wasPushed = NO; if (translationPercent > 0.4) { [self.dynamicAnimator removeAllBehaviors]; [self.interactionController finishInteractiveTransition]; } else { [self.interactionController cancelInteractiveTransition]; } self.interactionController = nil; } } |
Triggering the bouncing effect
The bouncing effect only applies to the main view controller either when the presentation is canceled (the user doesn’t slide the left edge far enough) or when the presented view controller is dismissed.
The effect is created using several dynamic behaviors:
- push behavior: triggered when the transition is finished, it gives an impulse so the main view moves to the right. This mimics an elastic collision with the left edge of the screen.
- gravity behavior: directed from right to left, it compensates the pushing force and makes the main view fall back to the left edge.
- collision behavior: preventing the main view from falling out of the screen under the gravity, by adding a boundary to the left edge.
The dynamic animator enters the scene when the transition completed or canceled. It takes over from the transitioning animation, adding the final effect. Because the transitioning animator has to trigger the bouncing effect when the transition is actually finished, it asks the main view controller to enable the dynamic animation through its delegate:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext { UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; UIView *containerViewBackground = [[UIView alloc] initWithFrame:transitionContext.containerView.bounds]; containerViewBackground.backgroundColor = [UIColor lightGrayColor]; if ([toViewController isKindOfClass:NSClassFromString(@"CCRSecondaryViewController")]) { [transitionContext.containerView insertSubview:toViewController.view belowSubview:fromViewController.view]; [transitionContext.containerView insertSubview:containerViewBackground belowSubview:toViewController.view]; toViewController.view.transform = CGAffineTransformMakeScale(0.9, 0.9); [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 options:UIViewAnimationOptionTransitionNone animations:^{ // Increase the size of the presented view toViewController.view.transform = CGAffineTransformIdentity; // Push the presenting view out of the screen through the left edge fromViewController.view.transform = CGAffineTransformMakeTranslation(toViewController.view.frame.size.width, 0); } completion:^(BOOL finished) { fromViewController.view.transform = CGAffineTransformIdentity; if ([transitionContext transitionWasCancelled]) { // Trigger the bouncing animation if the transition was cancelled [self.delegate didFinishTransition]; } [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; }]; } else if ([toViewController isKindOfClass:NSClassFromString(@"CCRContainerViewController")]) { [transitionContext.containerView insertSubview:toViewController.view aboveSubview:fromViewController.view]; toViewController.view.transform = CGAffineTransformMakeTranslation(toViewController.view.frame.size.width, 0); [transitionContext.containerView insertSubview:containerViewBackground belowSubview:fromViewController.view]; [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 options:UIViewAnimationOptionTransitionNone animations:^{ // Slide the presenting view back onto the screen toViewController.view.transform = CGAffineTransformIdentity; // Reduce the size of the presented view fromViewController.view.transform = CGAffineTransformMakeScale(0.9, 0.9); } completion:^(BOOL finished) { fromViewController.view.transform = CGAffineTransformIdentity; // Trigger the bouncing animation when the transition is finished [self.delegate didFinishTransition]; [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; }]; } } |
The delegate method simply adds the collision behavior to the dynamic animator. Then we use the collision delegate methods to activate the push behavior when the first collision is detected, and to replace it by the gravity behavior when the main view starts moving to the right under the force of the impulse:
1 2 3 4 5 6 7 8 9 10 11 12 |
// Transitioning animator delegate method - (void)didFinishTransition { UICollisionBehavior *collisionBehavior = [[UICollisionBehavior alloc] initWithItems:@[self.containerView]]; collisionBehavior.collisionDelegate = self; CGRect referenceViewBounds = self.dynamicAnimator.referenceView.bounds; [collisionBehavior addBoundaryWithIdentifier:@"edge boundary" fromPoint:referenceViewBounds.origin toPoint:CGPointMake(referenceViewBounds.origin.x, CGRectGetMaxY(referenceViewBounds))]; [self.dynamicAnimator addBehavior:collisionBehavior]; } |
Conclusion
The same kind of effect could be implemented using only the transitioning animator with a lot less code.
UIKit Dynamics are not a very good application for view controller transitions, but this was an interesting experiment which also let me introduce the UIViewControllerAnimatedTransitioning and UIViewControllerInteractiveTransitioning protocols. I will give special attention to these two protocols in a future article because they enable us to implement engaging animations between view controllers.