How can I have custom animation only for single UIViewController push/pop and default animation for other UIViewControllers in same UINavigationController?
I use UINavigationController and push/pop to navigate between different UIViewControllers. I created custom animation and set custom UINavigationControllerDelegate to UINavigationController.delegate. It animates fine but it changes animation style for all future push/pop in this UINavigationController.
I know I can change animation for single UIViewController using it's transitioningDelegate but it works only if it's displayed by present() and not push.
Is it possible to change animation only for single UIViewController in UINavigationController?
You can check for specific view controller from navigation controller like:
UINavigationcontroller.ViewControllers[number of that view controller]
and apply that animation on it.
Sub class UINavigationController and use CoreAnimation's transition to change push/pop animations like this.
enum TransitionType {
case fade
case movein
case push
case reveal
}
enum TransitionSubtype {
case right
case left
case top
case bottom
}
class MyNavigationController: UINavigationController {
fileprivate func getTransition(by type: TransitionType) -> String? {
var transition: String?
switch type {
case .fade:
transition = kCATransitionFade
break
case .push:
transition = kCATransitionPush
break
case .movein:
transition = kCATransitionMoveIn
break
case .reveal:
transition = kCATransitionReveal
break
default:
transition = nil
break
}
return transition
}
fileprivate func getSubTransition(by type: TransitionSubtype) -> String? {
var transition: String?
switch type {
case .right:
transition = kCATransitionFromRight
break
case .left:
transition = kCATransitionFromLeft
break
case .top:
transition = kCATransitionFromTop
break
case .bottom:
transition = kCATransitionFromBottom
break
default:
transition = nil
break
}
return transition
}
func display(viewController: UIViewController, animated: Bool, animationType: TransitionType = .push, animationSubtype: TransitionSubtype = .left) -> Bool {
guard let type = getTransition(by: animationType) else {return false}
guard let subtype = getSubTransition(by: animationSubtype) else {return false}
if animated {
let transition = CATransition()
transition.duration = 0.35
transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
transition.type = type
transition.subtype = subtype
self.view.layer.add(transition, forKey: nil)
}
_ = self.pushViewController(viewController, animated: false)
return true
}
func hide(animated: Bool, animationType: TransitionType = .push, animationSubtype: TransitionSubtype = .left) -> Bool {
guard let type = getTransition(by: animationType) else {return false}
guard let subtype = getSubTransition(by: animationSubtype) else {return false}
if animated {
let transition = CATransition()
transition.duration = 0.35
transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
transition.type = type
transition.subtype = subtype
self.view.layer.add(transition, forKey: nil)
}
_ = self.popViewController(animated: false)
return true
}
}
Related
I am trying to achieve some thing like following side menu open from tabbar item click.
I used the following class for Transition Animation ...
class SlideInTransition: NSObject, UIViewControllerAnimatedTransitioning {
var isPresenting = false
let dimmingView = UIView()
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 3
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toViewController = transitionContext.viewController(forKey: .to),
let fromViewController = transitionContext.viewController(forKey: .from) else { return }
let containerView = transitionContext.containerView
let finalWidth = toViewController.view.bounds.width * 0.3
let finalHeight = toViewController.view.bounds.height
if isPresenting {
// Add dimming view
dimmingView.backgroundColor = .black
dimmingView.alpha = 0.0
containerView.addSubview(dimmingView)
dimmingView.frame = containerView.bounds
// Add menu view controller to container
containerView.addSubview(toViewController.view)
// Init frame off the screen
toViewController.view.frame = CGRect(x: -finalWidth, y: 0, width: finalWidth, height: finalHeight)
}
// Move on screen
let transform = {
self.dimmingView.alpha = 0.5
toViewController.view.transform = CGAffineTransform(translationX: finalWidth, y: 0)
}
// Move back off screen
let identity = {
self.dimmingView.alpha = 0.0
fromViewController.view.transform = .identity
}
// Animation of the transition
let duration = transitionDuration(using: transitionContext)
let isCancelled = transitionContext.transitionWasCancelled
UIView.animate(withDuration: duration, animations: {
self.isPresenting ? transform() : identity()
}) { (_) in
transitionContext.completeTransition(!isCancelled)
}
}
}
and use it in my code as follow
guard let menuViewController = storyboard?.instantiateViewController(withIdentifier: "MenuVC") as? MenuVC else { return }
menuViewController.modalPresentationStyle = .overCurrentContext
menuViewController.transitioningDelegate = self as? UIViewControllerTransitioningDelegate
menuViewController.tabBarItem.image = UIImage(named: "ico_menu")
menuViewController.tabBarItem.selectedImage = UIImage(named: "ico_menu")
viewControllers = [orderVC,serverdVC,canceledVC,menuViewController]
extension TabbarVC: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transiton.isPresenting = true
return transiton
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transiton.isPresenting = false
return transiton
}
}
but animation doe't work at all ... I want to open it like side menu over current context ..
How can i achieve some thing like that ...
TabBar is not made to handle animate transition for just a single child View controller. If you apply a custom transition, it will be applied in all of its tabs (child view controllers). Plus last time i checked, airbnb's app doesn't behave like that when opening the user profile. :)
What you can do, though, is have a separate menu button at the top of your navigation view controller or wherever and call the slide in from there:
func slideInView() {
let vcToShow = MenuViewController()
vcToShow.modalPresentationStyle = .overCurrentContext
vcToShow.transitioningDelegate = self as? UIViewControllerTransitioningDelegate
present(vcToShow, animated: true, completion: nil)
}
Or if you insist on having the menu a part of the tabs, then you can do this.
Hope this helps. :)
I am making an onboarding screen, on the last screen I have a button that says "continue" and is supposed to dismiss the onboarding screen. The onboarding screen is a collection view controller with cells as each page. Please do not hesitate to ask for clarification I am don't know what else to add.
Thanks,
Edit
So I implemented user Francesco Deliro's answer, first problem was that I accidentally added the "delegate = self" into the viewDidLoad(). I fixed that but still the viewController does not dismiss.
My code is as follow in my viewController cell for item:
let loginCell = LoginCell()
loginCell.delegate = self
Here is the extension
extension TutorialViewController: LoginCellDelegate {
func didCompleteOnboarding() {
print("I should dimiss")
self.dismiss(animated: true, completion: nil)
}
Do I not need to call that function anywhere in the class just leave it outside the main class.
Edit 2
Here is how I connected my button action to the originial
#objc func continueTapped() {
...
continueButton.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
UIView.animate(withDuration: 1.0, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 1, options: .allowUserInteraction, animations: { [weak self] in
self?.continueButton.transform = .identity
let transition = CATransition()
transition.duration = 0.5
transition.type = CATransitionType.push
transition.subtype = CATransitionSubtype.fromRight
transition.timingFunction = CAMediaTimingFunction(name:CAMediaTimingFunctionName.easeInEaseOut)
self?.window!.layer.add(transition, forKey: kCATransition)
self?.delegate?.didCompleteOnboarding()
}, completion: { (success) in
token = true
defaults.set(token, forKey: "DidSee")
})
}
You can use delegation, for example:
protocol YourCellDelegate: class {
func didCompleteOnboarding()
}
Then in your cell:
class CustomCell: UICollectionViewCell {
weak var delegate: YourCellDelegate?
// in your button action
func dismissAction() {
delegate.didCompleteOnboarding()
}
}
Finally in your view controller set the cell delegate in the cellForItem function:
yourCell.delegate = self
And add:
extension YourViewController: YourCellDelegate {
func didCompleteOnboarding() {
// dismiss here
}
}
When adding a new controller to the navigation stack:
self.navigationController!.pushViewController(PushedViewController(), animated: true)
it appears from the right:
How can I change the direction of animation to make it appear from the left?
Swift 5.1: Segue from different directions
Here is a simple extension for different segue directions. (Tested in Swift 5)
It looks like you want to use segueFromLeft() I added some other examples as well.
extension CATransition {
//New viewController will appear from bottom of screen.
func segueFromBottom() -> CATransition {
self.duration = 0.375 //set the duration to whatever you'd like.
self.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
self.type = CATransitionType.moveIn
self.subtype = CATransitionSubtype.fromTop
return self
}
//New viewController will appear from top of screen.
func segueFromTop() -> CATransition {
self.duration = 0.375 //set the duration to whatever you'd like.
self.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
self.type = CATransitionType.moveIn
self.subtype = CATransitionSubtype.fromBottom
return self
}
//New viewController will appear from left side of screen.
func segueFromLeft() -> CATransition {
self.duration = 0.1 //set the duration to whatever you'd like.
self.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
self.type = CATransitionType.moveIn
self.subtype = CATransitionSubtype.fromLeft
return self
}
//New viewController will pop from right side of screen.
func popFromRight() -> CATransition {
self.duration = 0.1 //set the duration to whatever you'd like.
self.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
self.type = CATransitionType.reveal
self.subtype = CATransitionSubtype.fromRight
return self
}
//New viewController will appear from left side of screen.
func popFromLeft() -> CATransition {
self.duration = 0.1 //set the duration to whatever you'd like.
self.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
self.type = CATransitionType.reveal
self.subtype = CATransitionSubtype.fromLeft
return self
}
}
And here is how you implement the above extension:
let nav = self.navigationController //grab an instance of the current navigationController
DispatchQueue.main.async { //make sure all UI updates are on the main thread.
nav?.view.layer.add(CATransition().segueFromLeft(), forKey: nil)
nav?.pushViewController(YourViewController(), animated: false)
}
let obj = self.storyboard?.instantiateViewController(withIdentifier: "ViewController")as! ViewController
let transition:CATransition = CATransition()
transition.duration = 0.3
transition.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
transition.type = .push
transition.subtype = .fromLeft
self.navigationController?.view.layer.add(transition, forKey: kCATransition)
self.navigationController?.pushViewController(obj, animated: true)
Whene you use popToViewController that Time
transition.subtype = kCATransitionFromRight
This may help you
let nextVc = self.storyboard?.instantiateViewController(withIdentifier: "nextVc")
let transition = CATransition()
transition.duration = 0.5
transition.type = kCATransitionPush
transition.subtype = kCATransitionFromLeft
transition.timingFunction = CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseInEaseOut)
view.window!.layer.add(transition, forKey: kCATransition)
self.navigationController?.pushViewController(nextVc!, animated: false)
Ok, here's a drop-in solution for you. Add file named LeftToRightTransitionProxy.swift with the next content
import UIKit
final class LeftToRightTransitionProxy: NSObject {
func setup(with controller: UINavigationController) {
controller.delegate = self
}
}
extension LeftToRightTransitionProxy: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if operation == .push {
return AnimationController(direction: .forward)
} else {
return AnimationController(direction: .backward)
}
}
}
private final class AnimationController: NSObject, UIViewControllerAnimatedTransitioning {
enum Direction {
case forward, backward
}
let direction: Direction
init(direction: Direction) {
self.direction = direction
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.3
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toView = transitionContext.view(forKey: .to),
let fromView = transitionContext.view(forKey: .from) else {
return
}
let container = transitionContext.containerView
container.addSubview(toView)
let initialX: CGFloat
switch direction {
case .forward: initialX = -fromView.bounds.width
case .backward: initialX = fromView.bounds.width
}
toView.frame = CGRect(origin: CGPoint(x: initialX, y: 0), size: toView.bounds.size)
let animation: () -> Void = {
toView.frame = CGRect(origin: .zero, size: toView.bounds.size)
}
let completion: (Bool) -> Void = { _ in
let success = !transitionContext.transitionWasCancelled
if !success {
toView.removeFromSuperview()
}
transitionContext.completeTransition(success)
}
UIView.animate(
withDuration: transitionDuration(using: transitionContext),
animations: animation,
completion: completion
)
}
}
And here's how you can use it:
final class ViewController: UIViewController {
let animationProxy = LeftToRightTransitionProxy()
override func viewDidLoad() {
super.viewDidLoad()
animationProxy.setup(with: navigationController!)
}
}
This solution provides animation for both forward and backward (push and pop) directions.
This can be controlled in navigationController(_:animationControllerFor:from:to:) method of your LeftToRightTransitionProxy class (just return nil to remove animation).
If you need this behaviour for specific subclass of UIViewController put appropriate checks in navigationController(_:animationControllerFor:from:to:) method:
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if operation == .push && toVC is DetailViewController {
return AnimationController(direction: .forward)
} else if operation == .pop && toVC is ViewController {
return AnimationController(direction: .backward)
}
return nil
}
I used Hero as a solution.
import Hero
Then in that place where you’re going to show new UIViewController turn the default animation:
Hero.shared.defaultAnimation = HeroDefaultAnimationType.cover(direction: .right)
Also specify that your UINavigationController is going to use the Hero library:
self.navigationController?.hero.isEnabled = true
After that you’ll get the expected result even if you’re using the standard pushViewController function:
self.navigationController?.pushViewController(vc, animated: true)
You'll need to write your own transition procedure to achieve your needs.
DOCS from Apple:
https://developer.apple.com/documentation/uikit/uiviewcontrollercontexttransitioning
Article:
https://medium.com/#ludvigeriksson/custom-interactive-uinavigationcontroller-transition-animations-in-swift-4-a4b5e0cefb1e
If you want to learn how to do custom transitions (i.e. presenting from right to left) then this is a pretty good tutorial for setting them up.
The key things you need to do are set up a transitioning delegate, a custom presentation controller, and a custom animation controller.
you could use a third party library, you can search them in github.comor cocoacontrols.com as navigation Drawer
In my case I use this
https://github.com/CosmicMind/Material#NavigationDrawer
others
https://www.cocoacontrols.com/search?q=Drawer
https://github.com/dekatotoro/SlideMenuControllerSwift
https://github.com/jonkykong/SideMenu
:)
I want make some change to source code for 17-custom-presentation-controller to make custom transition animation,all my change is in class PopAnimator.
all my code is in here : https://github.com/hopy11/TransitionTest
This is my changes:
add a new instance variable in PopAnimator to save transitionContext:
var ctx:UIViewControllerContextTransitioning!
I rewrite the method:
animateTransition(using transitionContext: UIViewControllerContextTransitioning)
to this :
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
let toView = transitionContext.view(forKey: .to)!
//save transitionContext
ctx = transitionContext
containerView.addSubview(toView)
let animation = CATransition()
animation.duration = duration / 2
animation.type = "cube"
//use type kCATransitionReveal is not work too ...
//animation.type = kCATransitionReveal
animation.subtype = kCATransitionFromLeft
animation.delegate = self
containerView.layer.add(animation, forKey: nil)
}
3.Last I make class PopAnimator confirm CAAnimationDelegate delegate, and add the new method:
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if ctx != nil{
//make transition complete
ctx.completeTransition(true)
dismissCompletion?()
}
}
When run the App,the transition animation is look good when detailsVC dismiss,but nothing happened when presenting detailsVC!
You can see the demo above : when user tap the bottom left green view,it present detailsVC,but no animation happened!!! but when dismiss detailsVC,the animation work fine!
What’s wrong with it???
and how to fix it???
all my code is in here : https://github.com/hopy11/TransitionTest
thanks a lot! :)
What I Have
I am using UIViewControllerAnimatedTransitioning protocol with an attached UIViewPropertyAnimator to pan down to dismiss a View Controller
extension SecondViewController : UIViewControllerAnimatedTransitioning {
func interruptibleAnimator(using ctx: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
if self.animator != nil {
return self.animator!
}
let containerView = ctx.containerView
let toVC = ctx.viewController(forKey: .to) as! FirstViewController
let fromVC = ctx.viewController(forKey: .from) as! SecondViewController
containerView.insertSubview(toVC.view, belowSubview: fromVC.view)
self.animator = UIViewPropertyAnimator(duration: transitionDuration(using: ctx),
curve: .easeOut, animations: {
self.fromVC.view.transform = CGAffineTransform(scale: 0.5)
})
self.animator.isInterruptible = true
self.animator.isUserInteractionEnabled = true
self.animator.isManualHitTestingEnabled = true
self.animator.addCompletion { position in
switch position {
case .end:
break
case .current:
break
case .start:
break
}
let cancelled = ctx.transitionWasCancelled
if (cancelled) {
//..
} else {
//..
}
ctx.completeTransition(!cancelled)
}
self.animator = anim
return self.animator
}
func transitionDuration(using ctx: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5
}
func animateTransition(using ctx: UIViewControllerContextTransitioning) {
let animator = self.interruptibleAnimator(using: ctx)
self.animator.startAnimation()
}
func animationEnded(_ transitionCompleted: Bool) {
self.interactiveTransition = nil
self.animator = nil
}
}
Pan Gesture to handle the animation:
func handlePanGesture(gestureRecognizer: UIPanGestureRecognizer) {
let panTranslation = gestureRecognizer.translation(in: gestureRecognizer.view!)
var progress = panTranslation.y / (gestureRecognizer.view!.bounds.size.height * 0.5)
switch gestureRecognizer.state {
case .began:
self.interactiveTransition = UIPercentDrivenInteractiveTransition()
self.navigationController!.popViewController(animated: true)
case .changed:
self.interactiveTransition!.update(progress)
case .cancelled, .ended:
if progress > 0.5 {
//Complete Transition
let timingParameters = UICubicTimingParameters(animationCurve: .easeInOut)
self.animator!.continueAnimation!(withTimingParameters: timingParameters, durationFactor: progress)
self.animator?.addAnimations! {
//Completion Animations
}
self.interactiveTransition!.finish()
} else {
//Cancel Transition
self.animator!.isReversed = true
let timingParameters = UICubicTimingParameters(animationCurve: .easeInOut)
self.animator!.continueAnimation!(withTimingParameters: timingParameters, durationFactor: progress)
self.animator!.addAnimations!({
//Cancelling Animations
}, delayFactor: 0 )
self.interactiveTransition!.cancel()
}
default:
break
}
}
What Works
Swiping down to dismissal works perfectly. Swiping slightly down and lifting finger to cancel also works perfectly.
Issue
Swiping down and back up beyond starting point (where progress becomes negative) and lifting up the finger should cancel the transition with cancelling animation. This happens in iOS 10 but it first reverses the navigation controller transitions first, then snaps back. In iOS 11, cancelling animation happens, then I see navigation controller transition is reversed. If you wait, you can see navigation controller transition does try to correct it self in animation over 10 mins or so.
Issue with:
- self.interactiveTransition!.cancel()?
- self.interactiveTransition!.completionSpeed ??
I don't know if this is a bug or we're all just doing it wrong but to correct the behavior, add .completionSpeed = 0.999 to the interactionController in the .ended case of the pan gesture handler. It's a hack but at least it's only a single line.