How to Change the Push and Pop Animations in a Navigation Based App

How to change the Push and Pop animations in a navigation based app

How to change the Push and Pop animations in a navigation based app...

Preamble:

Say you are new to iOS development. Confusingly, Apple provide two transitions which can be used easily. These are: "crossfade" and "flip".

But of course, "crossfade" and "flip" are useless. They are never used. Nobody knows why Apple provided those two useless transitions!

So:

Say you want to do an ordinary, common, transition such as "slide". In that case, you have to do a HUGE amount of work!.

That work, is explained in this post.

Just to repeat:

Surprisingly: with iOS, if you want the simplest, most common, everyday transitions (such as an ordinary slide), you do have to all the work of implementing a full custom transition.

Here's how to do it ...

1. You need a custom UIViewControllerAnimatedTransitioning

  1. You need a bool of your own like popStyle . (Is it popping on, or popping off?)

  2. You must include transitionDuration (trivial) and the main call, animateTransition

  3. In fact you must write two different routines for inside animateTransition. One for the push, and one for the pop. Probably name them animatePush and animatePop. Inside animateTransition, just branch on popStyle to the two routines

  4. The example below does a simple move-over/move-off

  5. In your animatePush and animatePop routines. You must get the "from view" and the "to view". (How to do that, is shown in the code example.)

  6. and you must addSubview for the new "to" view.

  7. and you must call completeTransition at the end of your anime

So ..

  class SimpleOver: NSObject, UIViewControllerAnimatedTransitioning {

var popStyle: Bool = false

func transitionDuration(
using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.20
}

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

if popStyle {

animatePop(using: transitionContext)
return
}

let fz = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
let tz = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!

let f = transitionContext.finalFrame(for: tz)

let fOff = f.offsetBy(dx: f.width, dy: 55)
tz.view.frame = fOff

transitionContext.containerView.insertSubview(tz.view, aboveSubview: fz.view)

UIView.animate(
withDuration: transitionDuration(using: transitionContext),
animations: {
tz.view.frame = f
}, completion: {_ in
transitionContext.completeTransition(true)
})
}

func animatePop(using transitionContext: UIViewControllerContextTransitioning) {

let fz = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
let tz = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!

let f = transitionContext.initialFrame(for: fz)
let fOffPop = f.offsetBy(dx: f.width, dy: 55)

transitionContext.containerView.insertSubview(tz.view, belowSubview: fz.view)

UIView.animate(
withDuration: transitionDuration(using: transitionContext),
animations: {
fz.view.frame = fOffPop
}, completion: {_ in
transitionContext.completeTransition(true)
})
}
}

And then ...

2. Use it in your view controller.

Note: strangely, you only have to do this in the "first" view controller. (The one which is "underneath".)

With the one that you pop on top, do nothing. Easy.

So your class...

class SomeScreen: UIViewController {
}

becomes...

class FrontScreen: UIViewController,
UIViewControllerTransitioningDelegate, UINavigationControllerDelegate {

let simpleOver = SimpleOver()


override func viewDidLoad() {

super.viewDidLoad()
navigationController?.delegate = self
}

func navigationController(
_ navigationController: UINavigationController,
animationControllerFor operation: UINavigationControllerOperation,
from fromVC: UIViewController,
to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {

simpleOver.popStyle = (operation == .pop)
return simpleOver
}
}

That's it.

Push and pop exactly as normal, no change. To push ...

let n = UIStoryboard(name: "nextScreenStoryboardName", bundle: nil)
.instantiateViewController(withIdentifier: "nextScreenStoryboardID")
as! NextScreen
navigationController?.pushViewController(n, animated: true)

and to pop it, you can if you like just do that on the next screen:

class NextScreen: TotallyOrdinaryUIViewController {

@IBAction func userClickedBackOrDismissOrSomethingLikeThat() {

navigationController?.popViewController(animated: true)
}
}

Phew.



3. Also enjoy the other answers on this page which explain how to override AnimatedTransitioning !

Scroll to @AlanZeino and @elias answer for more discussion on how to AnimatedTransitioning in iOS apps these days!

How to apply custom transition animation in UINavigationControllerDelegate to specific push and pop events?

You Can Check the fromView and ToView controller in below delegate method for perform action

extension BaseViewController: UINavigationControllerDelegate {

func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
switch operation {
case .push:
if toVc is RandomViewController44{
return InAnimator()
}
return nil
case .pop:
if RandomViewController27 is fromVC{
return OutAnimator()
}
return nil
default:
return nil
}
}
}

You need to check from and toView controller when this method called as set condition as per your requirement

Navigation controller custom transition animation

To do a custom transition with navigation controller (UINavigationController), you should:

  • Define your view controller to conform to UINavigationControllerDelegate protocol. For example, you can have a private class extension in your view controller's .m file that specifies conformance to this protocol:

    @interface ViewController () <UINavigationControllerDelegate>

    @end
  • Make sure you actually specify your view controller as your navigation controller's delegate:

    - (void)viewDidLoad {
    [super viewDidLoad];

    self.navigationController.delegate = self;
    }
  • Implement animationControllerForOperation in your view controller:

    - (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
    animationControllerForOperation:(UINavigationControllerOperation)operation
    fromViewController:(UIViewController*)fromVC
    toViewController:(UIViewController*)toVC
    {
    if (operation == UINavigationControllerOperationPush)
    return [[PushAnimator alloc] init];

    if (operation == UINavigationControllerOperationPop)
    return [[PopAnimator alloc] init];

    return nil;
    }
  • Implement animators for push and pop animations, e.g.:

    @interface PushAnimator : NSObject <UIViewControllerAnimatedTransitioning>

    @end

    @interface PopAnimator : NSObject <UIViewControllerAnimatedTransitioning>

    @end

    @implementation PushAnimator

    - (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
    {
    return 0.5;
    }

    - (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
    {
    UIViewController* toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

    [[transitionContext containerView] addSubview:toViewController.view];

    toViewController.view.alpha = 0.0;

    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
    toViewController.view.alpha = 1.0;
    } completion:^(BOOL finished) {
    [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
    }];
    }

    @end

    @implementation PopAnimator

    - (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
    {
    return 0.5;
    }

    - (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
    {
    UIViewController* toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];

    [[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];

    [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
    fromViewController.view.alpha = 0.0;
    } completion:^(BOOL finished) {
    [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
    }];
    }

    @end

    That does fade transition, but you should feel free to customize the animation as you see fit.

  • If you want to handle interactive gestures (e.g. something like the native swipe left-to-right to pop), you have to implement an interaction controller:

    • Define a property for an interaction controller (an object that conforms to UIViewControllerInteractiveTransitioning):

      @property (nonatomic, strong) UIPercentDrivenInteractiveTransition *interactionController;

      This UIPercentDrivenInteractiveTransition is a nice object that does the heavy lifting of updating your custom animation based upon how complete the gesture is.

    • Add a gesture recognizer to your view. Here I'm just implementing the left gesture recognizer to simulate a pop:

      UIScreenEdgePanGestureRecognizer *edge = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(handleSwipeFromLeftEdge:)];
      edge.edges = UIRectEdgeLeft;
      [view addGestureRecognizer:edge];
    • Implement the gesture recognizer handler:

      /** Handle swipe from left edge
      *
      * This is the "action" selector that is called when a left screen edge gesture recognizer starts.
      *
      * This will instantiate a UIPercentDrivenInteractiveTransition when the gesture starts,
      * update it as the gesture is "changed", and will finish and release it when the gesture
      * ends.
      *
      * @param gesture The screen edge pan gesture recognizer.
      */

      - (void)handleSwipeFromLeftEdge:(UIScreenEdgePanGestureRecognizer *)gesture {
      CGPoint translate = [gesture translationInView:gesture.view];
      CGFloat percent = translate.x / gesture.view.bounds.size.width;

      if (gesture.state == UIGestureRecognizerStateBegan) {
      self.interactionController = [[UIPercentDrivenInteractiveTransition alloc] init];
      [self popViewControllerAnimated:TRUE];
      } else if (gesture.state == UIGestureRecognizerStateChanged) {
      [self.interactionController updateInteractiveTransition:percent];
      } else if (gesture.state == UIGestureRecognizerStateEnded) {
      CGPoint velocity = [gesture velocityInView:gesture.view];
      if (percent > 0.5 || velocity.x > 0) {
      [self.interactionController finishInteractiveTransition];
      } else {
      [self.interactionController cancelInteractiveTransition];
      }
      self.interactionController = nil;
      }
      }
    • In your navigation controller delegate, you also have to implement interactionControllerForAnimationController delegate method

      - (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
      interactionControllerForAnimationController:(id<UIViewControllerAnimatedTransitioning>)animationController {
      return self.interactionController;
      }

If you google "UINavigationController custom transition tutorial" and you'll get many hits. Or see WWDC 2013 Custom Transitions video.

Custom titleView changes location after push and pop new screen in stack

I have created a Title and Subtitle in the Navigation bar without the need for Interface Builder and constraint modification.

My Function to create the title view looks as follows:

class func setTitle(title:String, subtitle:String) -> UIView {
let titleLabel = UILabel(frame: CGRect(x: 0, y: -8, width: 0, height: 0))

titleLabel.backgroundColor = UIColor.clear
titleLabel.textColor = UIColor.white
titleLabel.font = UIFont.boldSystemFont(ofSize: 17)
titleLabel.text = title
titleLabel.sizeToFit()

let subtitleLabel = UILabel(frame: CGRect(x: 0, y: 12, width: 0, height: 0))
subtitleLabel.backgroundColor = UIColor.clear
subtitleLabel.textColor = UIColor.white
subtitleLabel.font = UIFont.systemFont(ofSize: 12)
subtitleLabel.text = subtitle
subtitleLabel.sizeToFit()

// Fix incorrect width bug
if (subtitleLabel.frame.size.width > titleLabel.frame.size.width) {
var titleFrame = titleLabel.frame
titleFrame.size.width = subtitleLabel.frame.size.width
titleLabel.frame = titleFrame
titleLabel.textAlignment = .center
}

let titleView = UIView(frame: CGRect(x: 0, y: 0, width: titleLabel.frame.size.width, height: titleLabel.frame.size.height))
titleView.addSubview(titleLabel)
titleView.addSubview(subtitleLabel)

let widthDiff = subtitleLabel.frame.size.width - titleLabel.frame.size.width

if widthDiff < 0 {
let newX = widthDiff / 2
subtitleLabel.frame.origin.x = abs(newX)
} else {
let newX = widthDiff / 2
titleLabel.frame.origin.x = newX
}

return titleView
}

Then to use the title view in any view controller I just call this line:

self.navigationItem.titleView = Functions.Views.setTitle(title: "Title String", subtitle: "Subtitle String")

Push Up View Controller from Bottom using Swift

Here is the code for pushing

let transition:CATransition = CATransition()
transition.duration = 0.5
transition.timingFunction = CAMediaTimingFunction(name:CAMediaTimingFunctionName.easeInEaseOut)
transition.type = CATransitionType.push
transition.subtype = CATransitionSubtype.fromBottom
self.hostNavController?.view.layer.add(transition, forKey: kCATransition)
self.hostNavController?.popViewController(animated: true)

and type of transition you can use are

kCATransitionFromLeft
kCATransitionFromBottom
kCATransitionFromRight
kCATransitionFromTop


Related Topics



Leave a reply



Submit