如何在基于导航的应用程序中更改“推入和弹出”动画


221

我有一个基于导航的应用程序,我想更改推送和弹出动画的动画。我该怎么办?

编辑2018

这个问题有很多答案,而且已经有一段时间了,我重新选择了我认为最相关的答案。如果有人认为不是,请在评论中让我知道


25
从iOS 7开始,有针对此的官方API。请参见UINavigationControllerDelegate的自定义过渡动画支持。与此相关的还有WWDC 2013视频
Jesse Rusak 2013年

我为在Swift中执行此操作添加了一个答案(如下)-碰到了这个问题,它询问了Swift的实现,因此我想在后续的实现中加入其中。
djbp

1
有关官方(iOS 7+)API的很好教程,请参阅:bradbambara.wordpress.com/2014/04/11/…–
nikolovski

1
@JesseRusak更新了WWDC 2013视频的链接:developer.apple.com/videos/play/wwdc2013-218
Wojciech Rutkowski

1
更改了我接受的答案n gals。希望这可以帮助!GLHF
Jab

Answers:


35

如何在基于导航的应用程序中更改“推入和弹出”动画...

对于2019年,“最终答案!”

前言:

假设您是iOS开发的新手。令人困惑的是,Apple提供了两个易于使用的过渡。它们是:“ crossfade”和“ flip”。

但是,当然,“交叉淡入淡出”和“翻转”是没有用的。他们从未使用过。没有人知道为什么苹果提供了这两个无用的过渡!

所以:

假设您要执行一个普通的,常见的过渡,例如“幻灯片”。在这种情况下,您必须做大量的工作!

这篇文章对此工作进行了解释。

只是重复一遍:

令人惊讶:与iOS,如果你想最简单,最普通的日常转换(如一个普通的幻灯片),你必须所有的工作实施的全定制转变

这是怎么做的...

1.您需要一个自定义 UIViewControllerAnimatedTransitioning

  1. 你需要一个自己喜欢的布尔popStyle。(它是弹出还是弹出?)

  2. 您必须包括transitionDuration(平凡的)和主要电话,animateTransition

  3. 实际上,您必须为inside编写两个不同的例程animateTransition。一种用于推动,另一种用于流行。也许他们的名字animatePushanimatePop。在内部animateTransition,只需popStyle转到两个例程

  4. 下面的示例做了一个简单的移动/移动

  5. 在你animatePushanimatePop例程。您必须获得“从视图”和“到视图”。(如何执行,如代码示例所示。)

  6. 并且您必须 addSubview使用新的“收件人”视图。

  7. 并且您必须completeTransition在动漫结尾处致电

所以..

  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)
            })
        }
    }

然后 ...

2.在视图控制器中使用它。

注意:奇怪的是,你 只需要在“第一个”视图控制器中执行此操作。(位于“下面”的那个。)

这样,您就弹出一个顶部,做都不。简单。

所以你的课...

class SomeScreen: UIViewController {
}

变成...

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
    }
}

而已。

完全正常推入并弹出,没有变化。推...

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

并弹出它,如果您愿意,可以在下一个屏幕上进行操作:

class NextScreen: TotallyOrdinaryUIViewController {
    
    @IBAction func userClickedBackOrDismissOrSomethingLikeThat() {
        
        navigationController?.popViewController(animated: true)
    }
}

ew


3.还要享受此页面上的其他答案,这些答案解释了如何覆盖AnimatedTransitioning

滚动到@AlanZeino和@elias答案,以了解有关AnimatedTransitioning这些天如何在iOS应用程序中进行更多讨论!


优秀的!如果我希望导航后退手势也支持相同的AnimatedTransitioning。任何想法?
sam chi wen

感谢@samchiwen -事实上,这正是animatePushanimatePop是..两个不同的方向!
Fattie

268

我做了以下工作,它工作正常..而且简单易懂..

CATransition* transition = [CATransition animation];
transition.duration = 0.5;
transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
transition.type = kCATransitionFade; //kCATransitionMoveIn; //, kCATransitionPush, kCATransitionReveal, kCATransitionFade
//transition.subtype = kCATransitionFromTop; //kCATransitionFromLeft, kCATransitionFromRight, kCATransitionFromTop, kCATransitionFromBottom
[self.navigationController.view.layer addAnimation:transition forKey:nil];
[[self navigationController] popViewControllerAnimated:NO];

和推动同样的事情。


Swift 3.0版本:

let transition = CATransition()
transition.duration = 0.5
transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
transition.type = kCATransitionFade
self.navigationController?.view.layer.add(transition, forKey: nil)
_ = self.navigationController?.popToRootViewController(animated: false)

34
+1,这确实是最理智的解决方案。只是给未来的访客一个小小的注意事项:这一Animated:NO部分至关重要。如果YES通过,动画将混合并产生有趣的效果。
DarkDust 2012年

12
迄今为止最好的解决方案..对于初学者来说,不要忘了包括QuartCore(#import <QuartzCore / QuartzCore.h>)
nomann 2012年

4
我对这种解决方案唯一的问题是,在没有动画的情况下推送被推送的viewcontroller的viewDidAppear会立即被调用。有办法解决吗?
Pedro Mancheno

9
我的这段代码的问题是,当每个视图滑入或滑出时,它们都会闪烁为灰色或白色。
克里斯,

1
检查在iOS 7.1.2和iOS 8.3 -这个代码也工作正常为方法工作正常setViewControllers:
PROFF

256

这就是我一直设法完成此任务的方式。

对于推送:

MainView *nextView=[[MainView alloc] init];
[UIView  beginAnimations:nil context:NULL];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDuration:0.75];
[self.navigationController pushViewController:nextView animated:NO];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromRight forView:self.navigationController.view cache:NO];
[UIView commitAnimations];
[nextView release];

对于流行音乐:

[UIView  beginAnimations:nil context:NULL];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDuration:0.75];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft forView:self.navigationController.view cache:NO];
[UIView commitAnimations];

[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDelay:0.375];
[self.navigationController popViewControllerAnimated:NO];
[UIView commitAnimations];


我仍然从中得到很多反馈,因此我将继续进行更新,以使用动画块,这是Apple建议的无论如何制作动画的方法。

对于推送:

MainView *nextView = [[MainView alloc] init];
[UIView animateWithDuration:0.75
                         animations:^{
                             [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
                             [self.navigationController pushViewController:nextView animated:NO];
                             [UIView setAnimationTransition:UIViewAnimationTransitionFlipFromRight forView:self.navigationController.view cache:NO];
                         }];

对于流行音乐:

[UIView animateWithDuration:0.75
                         animations:^{
                             [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
                             [UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft forView:self.navigationController.view cache:NO];
                         }];
[self.navigationController popViewControllerAnimated:NO];

3
谢谢你 但是弹出是由UINavigationController自动完成的。您如何覆盖该行为,以便可以调用自定义弹出逻辑?
2013年

1
@stuckj实际上它确实有效!!!您只需替换superself.navigationController
holierthanthou84 2013年

有什么方法可以从左侧获取幻灯片,而不是从右侧获取默认幻灯片?
shim

第一个根本不显示新视图。第二个不显示动画。答案很差!iOS 7
Dmitry

2
为什么要给UIViewController子类一个不带“ ViewController”部分的名称。此名称更适合UIView。
user2159978 2014年

29

推动

CATransition *transition = [CATransition animation];
transition.duration = 0.3;
transition.type = kCATransitionFade;
//transition.subtype = kCATransitionFromTop;

[self.navigationController.view.layer addAnimation:transition forKey:kCATransition];
[self.navigationController pushViewController:ViewControllerYouWantToPush animated:NO];

流行

CATransition *transition = [CATransition animation];
transition.duration = 0.3;
transition.type = kCATransitionFade;
//transition.subtype = kCATransitionFromTop;

[self.navigationController.view.layer addAnimation:transition forKey:kCATransition];
[self.navigationController popViewControllerAnimated:NO];

19

@Magnus回答,仅适用于Swift(2.0)

    let transition = CATransition()
    transition.duration = 0.5
    transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
    transition.type = kCATransitionPush
    transition.subtype = kCATransitionFromTop
    self.navigationController!.view.layer.addAnimation(transition, forKey: nil)
    let writeView : WriteViewController = self.storyboard?.instantiateViewControllerWithIdentifier("WriteView") as! WriteViewController
    self.navigationController?.pushViewController(writeView, animated: false)

一些旁注:

您也可以使用Segue做到这一点,只需在prepareForSegue或中实现即可shouldPerformSegueWithIdentifier然而,这也将保留默认动画。要解决此问题,您必须转到情节提要,单击“ Segue”,然后取消选中“动画”框。但这将限制您的应用程序适用于IOS 9.0及更高版本(至少在Xcode 7中使用它时)。

在segue中执行时,最后两行应替换为:

self.navigationController?.popViewControllerAnimated(false)

即使我设置为false,它也会忽略它。


如何在动画结束时消除背景中的黑色。
Madhu

不适用于推送视图控制器动画,
无法

16

请记住,在Swift中扩展绝对是您的朋友!

public extension UINavigationController {

    /**
     Pop current view controller to previous view controller.

     - parameter type:     transition animation type.
     - parameter duration: transition animation duration.
     */
    func pop(transitionType type: String = kCATransitionFade, duration: CFTimeInterval = 0.3) {
        self.addTransition(transitionType: type, duration: duration)
        self.popViewControllerAnimated(false)
    }

    /**
     Push a new view controller on the view controllers's stack.

     - parameter vc:       view controller to push.
     - parameter type:     transition animation type.
     - parameter duration: transition animation duration.
     */
    func push(viewController vc: UIViewController, transitionType type: String = kCATransitionFade, duration: CFTimeInterval = 0.3) {
        self.addTransition(transitionType: type, duration: duration)
        self.pushViewController(vc, animated: false)
    }

    private func addTransition(transitionType type: String = kCATransitionFade, duration: CFTimeInterval = 0.3) {
        let transition = CATransition()
        transition.duration = duration
        transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        transition.type = type
        self.view.layer.addAnimation(transition, forKey: nil)
    }

}

11

使用私人电话是一个坏主意,因为Apple不再批准执行此操作的应用程序。也许您可以尝试以下方法:

//Init Animation
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration: 0.50];


[UIView setAnimationTransition:UIViewAnimationTransitionCurlUp forView:self.navigationController.view cache:YES];

//Create ViewController
MyViewController *myVC = [[MyViewController alloc] initWith...];

[self.navigationController pushViewController:myVC animated:NO];
[myVC release];

//Start Animation
[UIView commitAnimations];

它只能“一半”起作用-不能解决流行动画中的难题。
亚当

我更喜欢此解决方案,是的,它可以工作。使用私有方法肯定会导致您被拒绝。
本杰明·因塔尔

@nicktmro,这是私有api调用。我什么都没注意到。
富兰克林

@Franklin不久前在这里讨论了使用方法-pushViewController:transition:forceImmediate:,这将是一个坏主意。
nicktmro

9

由于这是Google的最佳结果,我想我会分享我认为最理智的方法;这将使用iOS 7+过渡API。我使用Swift 3在iOS 10上实现了此功能。

UINavigationController如果您创建一个子类UINavigationController并返回符合UIViewControllerAnimatedTransitioning协议的类的实例,则将其与两个视图控制器之间的动画处理方式结合起来非常简单。

例如,这是我的UINavigationController子类:

class NavigationController: UINavigationController {
    init() {
        super.init(nibName: nil, bundle: nil)

        delegate = self
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

extension NavigationController: UINavigationControllerDelegate {

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

}

您可以看到我将设置UINavigationControllerDelegate为本身,并且在子类的扩展中实现了该方法,该方法UINavigationControllerDelegate允许您返回自定义动画控制器(即NavigationControllerAnimation)。此自定义动画控制器将为您替换原始动画。

您可能想知道为什么我要NavigationControllerAnimation通过其初始化程序将操作传递给实例。我这样做是为了在中NavigationControllerAnimation执行UIViewControllerAnimatedTransitioning协议我知道操作是什么(即“ push”或“ pop”)。这有助于了解我应该做哪种动画。大多数情况下,您希望根据操作执行不同的动画。

其余的很标准。在UIViewControllerAnimatedTransitioning协议中实现两个必需的功能并设置动画,但是您喜欢:

class NavigationControllerAnimation: NSObject, UIViewControllerAnimatedTransitioning {

    let operation: UINavigationControllerOperation

    init(operation: UINavigationControllerOperation) {
        self.operation = operation

        super.init()
    }

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

    public func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
            let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) else { return }
        let containerView = transitionContext.containerView

        if operation == .push {
            // do your animation for push
        } else if operation == .pop {
            // do your animation for pop
        }
    }
}

重要的是要记住,对于每种不同类型的操作(即“推”或“弹出”),往返视图控制器将有所不同。当您进行推送操作时,to view控制器将被推送。当您处于弹出操作状态时,to视图控制器将是要转换到的控制器,而from视图控制器将是正在被弹出的控制器。

另外,to必须containerView在过渡上下文中将视图控制器添加为的子视图。

动画制作完成后,您必须致电transitionContext.completeTransition(true)。如果您要进行交互式过渡,则必须动态地将返回BoolcompleteTransition(didComplete: Bool),具体取决于过渡是否在动画结束时完成。

最后(可选阅读),您可能想看看我是如何进行过渡的。这段代码有点黑,我很快就写了,所以我不会说它是很棒的动画代码,但仍然显示了如何做动画部分。

我的过渡非常简单;我想模仿UINavigationController通常做的相同动画,但是我想模仿旧的视图控制器的1:1动画,而不是像UINavigationController那样做“顶部上方的下一页”动画。控制器出现。这具有使两个视图控制器看起来好像彼此固定的效果。

对于推送操作,首先需要toViewController在x轴外屏幕上设置的视图原点,将其添加为的子视图,然后将containerView其设置origin.x为零,从而将其动画显示在屏幕上。同时,fromViewController通过将其设置为origin.x不在屏幕上来动画化的视图:

toViewController.view.frame = containerView.bounds.offsetBy(dx: containerView.frame.size.width, dy: 0.0)

containerView.addSubview(toViewController.view)

UIView.animate(withDuration: transitionDuration(using: transitionContext),
               delay: 0,
               options: [ UIViewAnimationOptions.curveEaseOut ],
               animations: {
                toViewController.view.frame = containerView.bounds
                fromViewController.view.frame = containerView.bounds.offsetBy(dx: -containerView.frame.size.width, dy: 0)
},
               completion: { (finished) in
                transitionContext.completeTransition(true)
})

弹出操作基本上是相反的。将添加toViewController为的子视图containerView,并fromViewControllertoViewController从左侧向设置动画的同时向右移动:

containerView.addSubview(toViewController.view)

UIView.animate(withDuration: transitionDuration(using: transitionContext),
               delay: 0,
               options: [ UIViewAnimationOptions.curveEaseOut ],
               animations: {
                fromViewController.view.frame = containerView.bounds.offsetBy(dx: containerView.frame.width, dy: 0)
                toViewController.view.frame = containerView.bounds
},
               completion: { (finished) in
                transitionContext.completeTransition(true)
})

这是整个swift文件的要点:

https://gist.github.com/alanzeino/603293f9da5cd0b7f6b60dc20bc766be


大!。我想做的只是朝相反的方向进行动画处理。我检查了其他解决方案,但所有解决方案在左右屏幕上均闪烁。看起来隐式的alpha更改动画无法与它们一起删除。只有此解决方案可以解决此问题。
beshio

是的,这是唯一正确的现代解决方案。(我不介意,但这与我在下面键入的解决方案完全相同!:))
Fattie

@AlanZeino如果在同一个ViewController中需要对不同的按钮单击使用不同的动画怎么办?因此,对于button1,您需要一个分解动画,对于button2,您需要默认过渡。
jzeferino

7

有UINavigationControllerDelegate和UIViewControllerAnimatedTransitioning,您可以为所需的任何内容更改动画。

例如,这是VC的垂直弹出动画:

@objc class PopAnimator: NSObject, UIViewControllerAnimatedTransitioning {

func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
    return 0.5
}

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {

    let fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)!
    let toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)!
    let containerView = transitionContext.containerView()
    let bounds = UIScreen.mainScreen().bounds
    containerView!.insertSubview(toViewController.view, belowSubview: fromViewController.view)
    toViewController.view.alpha = 0.5

    let finalFrameForVC = fromViewController.view.frame

    UIView.animateWithDuration(transitionDuration(transitionContext), animations: {
        fromViewController.view.frame = CGRectOffset(finalFrameForVC, 0, bounds.height)
        toViewController.view.alpha = 1.0
        }, completion: {
            finished in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
    })
}

}

然后

func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    if operation == .Pop {
        return PopAnimator()
    }
    return nil;
}

有用的教程 https://www.objc.io/issues/5-ios7/view-controller-transitions/


6

基于 针对快速更新的答案 4jordanperry

推动 UIViewController

let yourVC = self.storyboard?.instantiateViewController(withIdentifier: "yourViewController") as! yourViewController
    UIView.animate(withDuration: 0.75, animations: {() -> Void in
    UIView.setAnimationCurve(.easeInOut)
    self.navigationController?.pushViewController(terms, animated: true)
    UIView.setAnimationTransition(.flipFromRight, for: (self.navigationController?.view)!, cache: false)
})

对于流行

UIView.animate(withDuration: 0.75, animations: {() -> Void in
    UIView.setAnimationCurve(.easeInOut)
    UIView.setAnimationTransition(.flipFromLeft, for: (self.navigationController?.view)!, cache: false)
})
navigationController?.popViewController(animated: false)

5

这是我在Swift中所做的相同操作:

对于推送:

    UIView.animateWithDuration(0.75, animations: { () -> Void in
        UIView.setAnimationCurve(UIViewAnimationCurve.EaseInOut)
        self.navigationController!.pushViewController(nextView, animated: false)
        UIView.setAnimationTransition(UIViewAnimationTransition.FlipFromRight, forView: self.navigationController!.view!, cache: false)
    })

对于流行音乐:

实际上,我的做法与上面的某些回答有所不同-但由于我是Swift开发的新手,所以可能不正确。我已覆盖viewWillDisappear:animated:并在其中添加了流行代码:

    UIView.animateWithDuration(0.75, animations: { () -> Void in
        UIView.setAnimationCurve(UIViewAnimationCurve.EaseInOut)
        UIView.setAnimationTransition(UIViewAnimationTransition.FlipFromLeft, forView: self.navigationController!.view, cache: false)
    })

    super.viewWillDisappear(animated)

5

@Luca Davanzo在Swift 4.2中的回答

public extension UINavigationController {

    /**
     Pop current view controller to previous view controller.

     - parameter type:     transition animation type.
     - parameter duration: transition animation duration.
     */
    func pop(transitionType type: CATransitionType = .fade, duration: CFTimeInterval = 0.3) {
        self.addTransition(transitionType: type, duration: duration)
        self.popViewController(animated: false)
    }

    /**
     Push a new view controller on the view controllers's stack.

     - parameter vc:       view controller to push.
     - parameter type:     transition animation type.
     - parameter duration: transition animation duration.
     */
    func push(viewController vc: UIViewController, transitionType type: CATransitionType = .fade, duration: CFTimeInterval = 0.3) {
        self.addTransition(transitionType: type, duration: duration)
        self.pushViewController(vc, animated: false)
    }

    private func addTransition(transitionType type: CATransitionType = .fade, duration: CFTimeInterval = 0.3) {
        let transition = CATransition()
        transition.duration = duration
        transition.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
        transition.type = type
        self.view.layer.add(transition, forKey: nil)
    }

}

4

我最近正在尝试做类似的事情。我决定不喜欢UINavigationController的滑动动画,但是我也不想做UIView给您的动画,例如curl或类似的东西。我想在推或弹出时在视图之间进行交叉淡入淡出。

那里的问题涉及到这样一个事实,即视图实际上是在删除视图或在当前视图的顶部弹出一个视图,因此淡入淡出不起作用。我涉及的解决方案涉及采用新视图并将其作为子视图添加到UIViewController堆栈的当前顶视图中。我将其alpha设置为0,然后进行淡入淡出。动画序列完成后,我将视图推入堆栈而不设置动​​画。然后,我回到旧的topView并清理已更改的内容。

它要复杂得多,因为您必须调整navigationItems才能使过渡看起来正确。另外,如果进行任何旋转,则必须在将视图添加为子视图时调整框架大小,以使其正确显示在屏幕上。这是我使用的一些代码。我将UINavigationController子类化,并覆盖了push和pop方法。

-(void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
      UIViewController *currentViewController = [self.viewControllers lastObject];
      //if we don't have a current controller, we just do a normal push
      if(currentViewController == nil)
      {
         [super pushViewController:viewController animated:animated];
         return;
      }
      //if no animation was requested, we can skip the cross fade
      if(!animation)
      {
         [super pushViewController:viewController animated:NO];
         return;
      }
      //start the cross fade.  This is a tricky thing.  We basically add the new view
//as a subview of the current view, and do a cross fade through alpha values.
//then we push the new view on the stack without animating it, so it seemlessly is there.
//Finally we remove the new view that was added as a subview to the current view.

viewController.view.alpha = 0.0;
//we need to hold onto this value, we'll be releasing it later
    NSString *title = [currentViewController.title retain];

//add the view as a subview of the current view
[currentViewController.view addSubview:viewController.view];
[currentViewController.view bringSubviewToFront:viewController.view];
UIBarButtonItem *rButtonItem = currentViewController.navigationItem.rightBarButtonItem;
UIBarButtonItem *lButtonItem = currentViewController.navigationItem.leftBarButtonItem;

NSArray *array = nil;

//if we have a right bar button, we need to add it to the array, if not, we will crash when we try and assign it
//so leave it out of the array we are creating to pass as the context.  I always have a left bar button, so I'm not checking to see if it is nil. Its a little sloppy, but you may want to be checking for the left BarButtonItem as well.
if(rButtonItem != nil)
    array = [[NSArray alloc] initWithObjects:currentViewController,viewController,title,lButtonItem,rButtonItem,nil];
else {
    array = [[NSArray alloc] initWithObjects:currentViewController,viewController,title,lButtonItem,nil];
}

//remove the right bar button for our transition
[currentViewController.navigationItem setRightBarButtonItem:nil animated:YES];
//remove the left bar button and create a backbarbutton looking item
//[currentViewController.navigationItem setLeftBarButtonItem:nil animated:NO];

//set the back button
UIBarButtonItem *backButton = [[UIBarButtonItem alloc] initWithTitle:title style:kButtonStyle target:self action:@selector(goBack)];
[currentViewController.navigationItem setLeftBarButtonItem:backButton animated:YES];
[viewController.navigationItem setLeftBarButtonItem:backButton animated:NO];
[backButton release];

[currentViewController setTitle:viewController.title];

[UIView beginAnimations:@"push view" context:array];
[UIView setAnimationDidStopSelector:@selector(animationForCrossFadePushDidStop:finished:context:)];
[UIView setAnimationDelegate:self];
[UIView setAnimationDuration:0.80];
[viewController.view setAlpha: 1.0];
[UIView commitAnimations];
}

-(void)animationForCrossFadePushDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context
{

UIViewController *c = [(NSArray*)context objectAtIndex:0];
UIViewController *n = [(NSArray*)context objectAtIndex:1];
NSString *title     = [(NSArray *)context objectAtIndex:2];
UIBarButtonItem *l = [(NSArray *)context objectAtIndex:3];
UIBarButtonItem *r = nil;
//not all views have a right bar button, if we look for it and it isn't in the context,
//we'll crash out and not complete the method, but the program won't crash.
//So, we need to check if it is there and skip it if it isn't.
if([(NSArray *)context count] == 5)
    r = [(NSArray *)context objectAtIndex:4];

//Take the new view away from being a subview of the current view so when we go back to it
//it won't be there anymore.
[[[c.view subviews] lastObject] removeFromSuperview];
[c setTitle:title];
[title release];
//set the search button
[c.navigationItem setLeftBarButtonItem:l animated:NO];
//set the next button
if(r != nil)
    [c.navigationItem setRightBarButtonItem:r animated:NO];


[super pushViewController:n animated:NO];

 }

正如我在代码中提到的那样,我总是有一个左键按钮项,因此在将其放入作为动画委托的上下文传递的数组之前,我不会检查它是否为nil。如果这样做,您可能需要进行检查。

我发现的问题是,如果您使委托方法完全崩溃,则不会使程序崩溃。它只是使委托无法完成,但您不会得到任何警告。
因此,由于我是在该委托例程中进行清理的,因此由于未完成清理而导致了一些奇怪的视觉行为。

我创建的后退按钮称为“ goBack”方法,该方法仅调用pop例程。

-(void)goBack
{ 
     [self popViewControllerAnimated:YES];
}

另外,这是我的弹出例程。

-(UIViewController *)popViewControllerAnimated:(BOOL)animated
{
    //get the count for the number of viewControllers on the stack
int viewCount = [[self viewControllers] count];
//get the top view controller on the stack
UIViewController *topViewController = [self.viewControllers objectAtIndex:viewCount - 1];
//get the next viewController after the top one (this will be the new top one)
UIViewController *newTopViewController = [self.viewControllers objectAtIndex:viewCount - 2];

//if no animation was requested, we can skip the cross fade
if(!animated)
{
    [super popViewControllerAnimated:NO];
            return topViewController;
}



//start of the cross fade pop.  A bit tricky.  We need to add the new top controller
//as a subview of the curent view controler with an alpha of 0.  We then do a cross fade.
//After that we pop the view controller off the stack without animating it.
//Then the cleanup happens: if the view that was popped is not released, then we
//need to remove the subview we added and change some titles back.
newTopViewController.view.alpha = 0.0;
[topViewController.view addSubview:newTopViewController.view];
[topViewController.view bringSubviewToFront:newTopViewController.view];
NSString *title = [topViewController.title retain];
UIBarButtonItem *lButtonItem = topViewController.navigationItem.leftBarButtonItem;
UIBarButtonItem *rButtonItem = topViewController.navigationItem.rightBarButtonItem;

//set the new buttons on top of the current controller from the new top controller
if(newTopViewController.navigationItem.leftBarButtonItem != nil)
{
    [topViewController.navigationItem setLeftBarButtonItem:newTopViewController.navigationItem.leftBarButtonItem animated:YES];
}
if(newTopViewController.navigationItem.rightBarButtonItem != nil)
{
    [topViewController.navigationItem setRightBarButtonItem:newTopViewController.navigationItem.rightBarButtonItem animated:YES];
}

[topViewController setTitle:newTopViewController.title];
//[topViewController.navigationItem.leftBarButtonItem setTitle:newTopViewController.navigationItem.leftBarButtonItem.title];

NSArray *array = nil;
if(rButtonItem != nil)
    array = [[NSArray alloc] initWithObjects:topViewController,title,lButtonItem,rButtonItem,nil];
else {
    array = [[NSArray alloc] initWithObjects:topViewController,title,lButtonItem,nil];
}


[UIView beginAnimations:@"pop view" context:array];
[UIView setAnimationDidStopSelector:@selector(animationForCrossFadePopDidStop:finished:context:)];
[UIView setAnimationDelegate:self];
[UIView setAnimationDuration:0.80];
[newTopViewController.view setAlpha: 1.0];
[UIView commitAnimations];
return topViewController;

 }

 -(void)animationForCrossFadePopDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context
 {

UIViewController *c = [(NSArray *)context objectAtIndex:0];
//UIViewController *n = [(NSArray *)context objectAtIndex:1];
NSString *title = [(NSArray *)context objectAtIndex:1];
UIBarButtonItem *l = [(NSArray *)context objectAtIndex:2];
UIBarButtonItem *r = nil;



//Not all views have a right bar button.  If we look for one that isn't there
// we'll crash out and not complete this method, but the program will continue.
//So we need to check if it is therea nd skip it if it isn't.
if([(NSArray *)context count] == 4)
    r = [(NSArray *)context objectAtIndex:3];

//pop the current view from the stack without animation
[super popViewControllerAnimated:NO];

//if what was the current veiw controller is not nil, then lets correct the changes
//we made to it.
if(c != nil)
{
    //remove the subview we added for the transition
    [[c.view.subviews lastObject] removeFromSuperview];
    //reset the title we changed
    c.title = title;
    [title release];
    //replace the left bar button that we changed
    [c.navigationItem setLeftBarButtonItem:l animated:NO];
    //if we were passed a right bar button item, replace that one as well
    if(r != nil)
        [c.navigationItem setRightBarButtonItem:r animated:NO];
    else {
        [c.navigationItem setRightBarButtonItem:nil animated:NO];
    }


 }
}

就是这样。如果要实现旋转,则需要一些其他代码。在显示为子视图之前,您需要设置要添加为子视图的视图的框架大小,否则会遇到方向为横向的问题,但是最后一次看到前一个视图是纵向的。因此,然后将其添加为子视图,然后将其淡入淡出,但它显示为纵向,然后当我们不使用动画弹出时,则使用相同的视图,但堆栈中的视图现在为横向。整个事情看起来有点时髦。每个人对轮换的实现都有点不同,因此我在这里没有包括我的代码。

希望它能对某些人有所帮助。我四处寻找这样的东西,找不到任何东西。我认为这不是一个完美的答案,但目前看来,它对我来说确实很好。


令人钦佩的是,坦白地说,这不是现在7年后的解决方案!
Fattie

你是对的。这个答案是从2011年开始的。当时它一直奏效,但此后发生了很大变化。=)
georryan

4

您现在可以使用UIView.transition。注意animated:false。这适用于任何过渡选项,弹出,推入或堆栈替换。

if let nav = self.navigationController
{
    UIView.transition(with:nav.view, duration:0.3, options:.transitionCrossDissolve, animations: {
        _ = nav.popViewController(animated:false)
    }, completion:nil)
}

1
@Fattie,这种特殊的方法只用任何标准的动画,如翻转和卷发中列出的工作developer.apple.com/documentation/uikit/uiviewanimationoptions
彼得迪威斯

3

以iJordan的答案为灵感,为什么不直接在UINavigationController上创建一个Category以便在整个应用程序中使用,而不是在整个地方复制/粘贴此动画代码?

UINavigationController + Animation.h

@interface UINavigationController (Animation)

- (void) pushViewControllerWithFlip:(UIViewController*) controller;

- (void) popViewControllerWithFlip;

@end

UINavigationController + Animation.m

@implementation UINavigationController (Animation)

- (void) pushViewControllerWithFlip:(UIViewController *) controller
{
    [UIView animateWithDuration:0.50
                     animations:^{
                         [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
                         [self pushViewController:controller animated:NO];
                         [UIView setAnimationTransition:UIViewAnimationTransitionFlipFromRight forView:self.view cache:NO];
                     }];
}

- (void) popViewControllerWithFlip
{
    [UIView animateWithDuration:0.5
                     animations:^{
                         [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
                         [UIView setAnimationTransition:UIViewAnimationTransitionFlipFromRight forView:self.view cache:NO];
                     }];

    [self popViewControllerAnimated:NO];
}

@end

然后只需导入UINavigationController + Animation.h文件并正常调用即可:

[self.navigationController pushViewControllerWithFlip:[[NewViewController alloc] init]];

[self.navigationController popViewControllerWithFlip];

聪明。但是,为什么不添加采用UIViewAnimationTransition参数而不是将硬代码转换为flipFromRight的push / pop方法呢?
杰夫

@Jef是方便的方法-这样,实现者无需记住要为每种特定的动画类型传递哪个UIViewAnimationTransition值,它们只需使用要完成的操作的“英语”名称来调用该方法。
DiscDev

@Jef同样,您的建议绝对有效-如果我仍在使用Objective-C并且需要支持许多过渡样式(绝对不建议这样做,因为许多不同的过渡样式会使用户感到困惑),我将有1个采用UIViewAnimationTransition类型的方法,然后几种方便的方法可以使开发更容易。
DiscDev

3

很简单

self.navigationController?.view.semanticContentAttribute = .forceRightToLeft

欢迎使用StackOverflow:如果您发布代码,XML或数据示例,请在文本编辑器中突出显示这些行,然后单击编辑器工具栏上的“代码示例”按钮({})或使用键盘上的Ctrl + K进行格式化和语法突出显示它!
WhatsThePoint

2

看看ADTransitionController,它是我们在Applidium上创建的具有自定义过渡动画(其API与UINavigationController的API匹配的API)的UINavigationController的替代品。

您可以对弹出动作使用不同的预定义动画,例如SwipeFadeCubeCarrouselZoom等。


2

虽然这里的所有答案都很不错,而且大多数方法效果很好,但是有一种稍微简单的方法可以达到相同的效果...

对于推送:

  NextViewController *nextViewController = [[NextViewController alloc] init];

  // Shift the view to take the status bar into account 
  CGRect frame = nextViewController.view.frame;
  frame.origin.y -= 20;
  frame.size.height += 20;
  nextViewController.view.frame = frame;

  [UIView transitionFromView:self.navigationController.topViewController.view toView:nextViewController.view duration:0.5 options:UIViewAnimationOptionTransitionFlipFromRight completion:^(BOOL finished) {
    [self.navigationController pushViewController:nextViewController animated:NO];
  }];

对于流行音乐:

  int numViewControllers = self.navigationController.viewControllers.count;
  UIView *nextView = [[self.navigationController.viewControllers objectAtIndex:numViewControllers - 2] view];

  [UIView transitionFromView:self.navigationController.topViewController.view toView:nextView duration:0.5 options:UIViewAnimationOptionTransitionFlipFromLeft completion:^(BOOL finished) {
    [self.navigationController popViewControllerAnimated:NO];
  }];}

弹出到根视图控制器时,这将崩溃。
阿卜杜拉·乌默

1

我不知道您可以公开更改过渡动画的任何方式。

如果不需要“后退”按钮,则应使用模式视图控制器转换“从底部推入” /“翻转” /“淡入” /(≥3.2)“页面卷曲”。


私有方面,该方法-pushViewController:animated:调用未记录的方法-pushViewController:transition:forceImmediate:,因此,例如,如果要从左向右翻转,可以使用

[navCtrler pushViewController:ctrler transition:10 forceImmediate:NO];

但是,您不能以这种方式更改“ pop”过渡。


1

请参阅我对这个问题的回答,寻求用更少的代码行来解决这个问题的方法。使用此方法,您可以按照自己喜欢的方式对新视图控制器的伪“推”进行动画处理,并且在完成动画后,就可以设置导航控制器,就像使用标准的Push方法一样。我的示例使您可以从左侧或右侧为插入动画设置动画。为了方便起见,这里重复了以下代码:

-(void) showVC:(UIViewController *) nextVC rightToLeft:(BOOL) rightToLeft {
    [self addChildViewController:neighbor];
    CGRect offscreenFrame = self.view.frame;
    if(rightToLeft) {
        offscreenFrame.origin.x = offscreenFrame.size.width * -1.0;
    } else if(direction == MyClimbDirectionRight) {
        offscreenFrame.origin.x = offscreenFrame.size.width;
    }
    [[neighbor view] setFrame:offscreenFrame];
    [self.view addSubview:[neighbor view]];
    [neighbor didMoveToParentViewController:self];
    [UIView animateWithDuration:0.5 animations:^{
        [[neighbor view] setFrame:self.view.frame];
    } completion:^(BOOL finished){
        [neighbor willMoveToParentViewController:nil];
        [neighbor.view removeFromSuperview];
        [neighbor removeFromParentViewController];
        [[self navigationController] pushViewController:neighbor animated:NO];
        NSMutableArray *newStack = [[[self navigationController] viewControllers] mutableCopy];
        [newStack removeObjectAtIndex:1]; //self, just below top
        [[self navigationController] setViewControllers:newStack];
    }];
}

0

从示例应用程序中,检查出这种变化。 https://github.com/mpospese/MPFoldTransition/

#pragma mark - UINavigationController(MPFoldTransition)

@implementation UINavigationController(MPFoldTransition)

//- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
- (void)pushViewController:(UIViewController *)viewController foldStyle:(MPFoldStyle)style
{
    [MPFoldTransition transitionFromViewController:[self visibleViewController] 
                                  toViewController:viewController 
                                          duration:[MPFoldTransition defaultDuration]  
                                             style:style 
                                        completion:^(BOOL finished) {
                                            [self pushViewController:viewController animated:NO];
                                        }
     ];
}

- (UIViewController *)popViewControllerWithFoldStyle:(MPFoldStyle)style
{
    UIViewController *toController = [[self viewControllers] objectAtIndex:[[self viewControllers] count] - 2];

    [MPFoldTransition transitionFromViewController:[self visibleViewController] 
                                  toViewController:toController 
                                          duration:[MPFoldTransition defaultDuration] 
                                             style:style
                                        completion:^(BOOL finished) {
                                            [self popViewControllerAnimated:NO];
                                        }
     ];

    return toController;
}

0

只需使用:

ViewController *viewController = [[ViewController alloc] init];

UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:viewController];
navController.navigationBarHidden = YES;

[self presentViewController:navController animated:YES completion: nil];
[viewController release];
[navController release];

0

意识到这是一个古老的问题。我仍然想发布此答案,因为我viewControllers在提出的答案中遇到了一些问题。我的解决方案是继承UINavigationController并覆盖所有pop和push方法。

FlippingNavigationController.h

@interface FlippingNavigationController : UINavigationController

@end

FlippingNavigationController.m:

#import "FlippingNavigationController.h"

#define FLIP_DURATION 0.5

@implementation FlippingNavigationController

- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    [UIView transitionWithView:self.view
                      duration:animated?FLIP_DURATION:0
                       options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionTransitionFlipFromRight
                    animations:^{ [super pushViewController:viewController
                                                   animated:NO]; }
                    completion:nil];
}

- (UIViewController *)popViewControllerAnimated:(BOOL)animated
{
    return [[self popToViewController:[self.viewControllers[self.viewControllers.count - 2]]
                             animated:animated] lastObject];
}

- (NSArray *)popToRootViewControllerAnimated:(BOOL)animated
{
    return [self popToViewController:[self.viewControllers firstObject]
                            animated:animated];
}

- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    __block NSArray* viewControllers = nil;

    [UIView transitionWithView:self.view
                      duration:animated?FLIP_DURATION:0
                       options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionTransitionFlipFromLeft
                    animations:^{ viewControllers = [super popToViewController:viewController animated:NO]; }
                    completion:nil];

    return viewControllers;
}

@end

0

我知道这个线程很旧,但是我想我会花两美分。您不需要制作自定义动画,这是一种简单的方法(也许很笨拙)。代替使用推,创建一个新的导航控制器,使新的视图控制器成为该导航控制器的根视图控制器,然后从原始导航控制器中显示该导航控制器。Present具有多种样式,可轻松自定义,而无需制作自定义动画。

例如:

UIViewcontroller viewControllerYouWantToPush = UIViewController()
UINavigationController newNavController = UINavigationController(root: viewControllerYouWantToView)
newNavController.navBarHidden = YES;
self.navigationController.present(newNavController)

并且您可以根据需要更改演示样式。


-1

我找到了一种适度递归的方法来完成此操作,该方法可以满足我的目的。我有一个实例变量BOOL,可用来阻止正常的弹出动画并替换自己的非动画弹出消息。该变量最初设置为NO。轻击后退按钮时,委托方法将其设置为YES,并将新的非动画弹出消息发送到导航栏,从而再次调用相同的委托方法,这次将变量设置为YES。将变量设置为YES时,委托方法将其设置为NO并返回YES,以允许非动画弹出发生。在第二次委托调用返回之后,我们回到第一个委托调用,返回NO,从而阻止了原始的动画弹出!实际上,它并不像听起来那样混乱。我的shouldPopItem方法看起来像这样:

- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item 
{
    if ([[navigationBar items] indexOfObject:item] == 1) 
    {
        [expandedStack restack];    
    }

    if (!progPop) 
    {
        progPop = YES;
        [navBar popNavigationItemAnimated:NO];
        return NO;
    }
    else 
    {
        progPop = NO;
        return YES;
    }
}

为我工作。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.