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
You need a bool of your own like
popStyle
. (Is it popping on, or popping off?)You must include
transitionDuration
(trivial) and the main call,animateTransition
In fact you must write two different routines for inside
animateTransition
. One for the push, and one for the pop. Probably name themanimatePush
andanimatePop
. InsideanimateTransition
, just branch onpopStyle
to the two routinesThe example below does a simple move-over/move-off
In your
animatePush
andanimatePop
routines. You must get the "from view" and the "to view". (How to do that, is shown in the code example.)and you must
addSubview
for the new "to" view.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>
@endMake 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]];
}];
}
@endThat 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
Update Badge With Push Notification While App in Background
Disabling Auto-Play in Full Screen on Ios
How to Add Exception Breakpoint in Xcode
How to Set Up a Simple Delegate to Communicate Between Two View Controllers
Uicollectionview Self Sizing Cells With Auto Layout
How to Beta Test an Iphone App
Can You Build Dynamic Libraries For iOS and Load Them At Runtime
How Does a Delegate Work in Objective-C
How to Install iOS 6 Sdk on Xcode 5
Nsdictionary With Ordered Keys
Scrolling Issue on Position Fixed Element on Ios
Save Generated Gif to Camera Roll
How to Develop For Iphone Using a Windows Development Machine
Launch an App from Within Another (Iphone)
Undefined Symbols For Architecture Arm64
Uiview With Rounded Corners and Drop Shadow