iPhone: 如何使用动画切换选项卡?

108

我正在使用UITabBarController.selectedIndex在一个选项卡驱动的应用程序中以编程方式切换选项卡。 我试图解决的问题是如何在视图之间进行过渡动画,即从当前选项卡的视图到所选选项卡的视图。

最初的想法是利用UITabBarControllerDelegate,但是当以编程方式切换选项卡时,似乎不会调用它。现在我考虑使用UITabBarDelegate.didSelectItem作为可能的挂钩来设置转换动画。

是否有人成功地实现了这些过渡动画?如果是,怎么实现的呢?


1
值得一提的是,许多这些得票最高的答案都早于Runo的回答Heberti的回答中概述的自定义转换。这些是解决这些自定义动画的正确方法。请参阅WWDC 2013视频使用视图控制器进行自定义转换 - Rob
20个回答

156

更新于 04/2016:只是想更新一下,感谢大家的所有投票。请注意,这篇文章最初是在ARC、约束之前写的...还有很多其他新技术!所以在决定是否使用这些技术时,请考虑这一点。可能会有更现代的方法。噢,如果你发现了一种更好的方法,请回复一下让大家都看到。谢谢!

随后的一段时间...

经过大量研究,我找到了两个可行的解决方案。这两个方案都可以实现选项卡之间的动画切换。

方案1:从视图中转换(简单)

这是最简单的方法,利用了预定义的UIView转换方法。使用此解决方案时,我们无需管理视图,因为该方法会为我们完成工作。

// Get views. controllerIndex is passed in as the controller we want to go to. 
UIView * fromView = tabBarController.selectedViewController.view;
UIView * toView = [[tabBarController.viewControllers objectAtIndex:controllerIndex] view];

// Transition using a page curl.
[UIView transitionFromView:fromView 
                    toView:toView 
                  duration:0.5 
                   options:(controllerIndex > tabBarController.selectedIndex ? UIViewAnimationOptionTransitionCurlUp : UIViewAnimationOptionTransitionCurlDown)
                completion:^(BOOL finished) {
                    if (finished) {
                        tabBarController.selectedIndex = controllerIndex;
                    }
                }];

解决方案2:滚动(更复杂)

这是一个更复杂的解决方案,但可以更好地控制动画。在这个示例中,我们使视图滑入和滑出。使用这种方法需要自己管理视图。

// Get the views.
UIView * fromView = tabBarController.selectedViewController.view;
UIView * toView = [[tabBarController.viewControllers objectAtIndex:controllerIndex] view];

// Get the size of the view area.
CGRect viewSize = fromView.frame;
BOOL scrollRight = controllerIndex > tabBarController.selectedIndex;

// Add the to view to the tab bar view.
[fromView.superview addSubview:toView];

// Position it off screen.
toView.frame = CGRectMake((scrollRight ? 320 : -320), viewSize.origin.y, 320, viewSize.size.height);

[UIView animateWithDuration:0.3 
                 animations: ^{

                     // Animate the views on and off the screen. This will appear to slide.
                     fromView.frame =CGRectMake((scrollRight ? -320 : 320), viewSize.origin.y, 320, viewSize.size.height);
                     toView.frame =CGRectMake(0, viewSize.origin.y, 320, viewSize.size.height);
                 }

                 completion:^(BOOL finished) {
                     if (finished) {

                         // Remove the old view from the tabbar view.
                         [fromView removeFromSuperview];
                         tabBarController.selectedIndex = controllerIndex;                
                     }
                 }];

这是Swift语言的解决方案:

extension TabViewController: UITabBarControllerDelegate {
      public func tabBarController(tabBarController: UITabBarController, shouldSelectViewController viewController: UIViewController) -> Bool {

           let fromView: UIView = tabBarController.selectedViewController!.view
           let toView  : UIView = viewController.view
           if fromView == toView {
                 return false
           }

           UIView.transitionFromView(fromView, toView: toView, duration: 0.3, options: UIViewAnimationOptions.TransitionCrossDissolve) { (finished:Bool) in

        }
        return true
   }
}

1
非常感谢您的回答,它确实非常有效。然而,我在两个解决方案中发现了一个错误,我不确定这是否会发生在每个人身上,但似乎当页面过渡时,导航栏和状态栏之间存在间隙,然后在动画完成后,间隙关闭。这使得动画结束有点抖动。您知道这是为什么吗? - Enrico Susatyo
2
@EmileCormier 把它放在TabBar代理的shouldSelectViewController方法中,并在那里返回NO。 - Jonas Sourlier
2
@drekka,这对我没有用,请问controllerIndex是从哪里来的?为什么在tabBarControllerDelegate方法中不直接使用[viewController view]作为“toView”呢?谢谢。 - shannoga
这是一个旧的答案,但仍然没有人遇到controllerIndex的问题吗?如果您在选项卡控制器中有许多视图控制器,则会出现一个UIMoreNavigationController。它的controllerIndex总是超出范围。由于它是一个私有类,我们无法使用[viewController isKindOfClass:]来检查它,因此我们必须检查索引:if(controllerIndex>tabBarController.viewControllers.count-1){...} - kientux
另外,如果您选择当前选定的视图控制器,则不会像正常情况下返回到根视图控制器。必须检查:if (controllerIndex == tabBarController.selectedIndex) { return YES; } - kientux
显示剩余7条评论

25

以下是我尝试将代码从drekka放入委托(UITabBarControllerDelegate)方法中的结果:

- (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController {

    NSArray *tabViewControllers = tabBarController.viewControllers;
    UIView * fromView = tabBarController.selectedViewController.view;
    UIView * toView = viewController.view;
    if (fromView == toView)
        return false;
    NSUInteger fromIndex = [tabViewControllers indexOfObject:tabBarController.selectedViewController];
    NSUInteger toIndex = [tabViewControllers indexOfObject:viewController];

    [UIView transitionFromView:fromView
                        toView:toView
                      duration:0.3
                       options: toIndex > fromIndex ? UIViewAnimationOptionTransitionFlipFromLeft : UIViewAnimationOptionTransitionFlipFromRight
                    completion:^(BOOL finished) {
                        if (finished) {
                            tabBarController.selectedIndex = toIndex;
                        }
                    }];
    return true;
}

2
根据方法声明,您应该返回一些值,但这种方法很好地运作了 +1。 - voromax
2
你可以在UITabController的实现文件中添加self.delegate = self;到你的viewDidLoad()函数中来设置代理。这将允许上述函数被调用。 - Chris Fremgen
如果您使用此代码,请记住,目标视图控制器直到其视图出现后才会收到viewWillAppear调用,当在完成处理程序中更改selectedIndex时。这可能会导致目标视图控制器中的奇怪行为。您可以在转换之前调用viewWillAppear,但那样它将被调用两次。 - Robin Daugherty

21

iOS7.0及以上版本的解决方案。

您可以在选项卡栏的代理中指定自定义动画控制器。

像这样实现一个动画控制器:

@interface TabSwitchAnimationController : NSObject <UIViewControllerAnimatedTransitioning>

@end

@implementation TabSwitchAnimationController

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

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
    UIViewController* fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController* toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView* toView = toVC.view;
    UIView* fromView = fromVC.view;

    UIView* containerView = [transitionContext containerView];
    [containerView addSubview:toView];
    toView.frame = [transitionContext finalFrameForViewController:toVC];

    // Animate by fading
    toView.alpha = 0.0;
    [UIView animateWithDuration:[self transitionDuration:transitionContext]
                          delay:0.0
                        options:UIViewAnimationOptionCurveEaseOut | UIViewAnimationOptionAllowUserInteraction
                     animations:^{
                         toView.alpha = 1.0;
                     }
                     completion:^(BOOL finished) {
                         toView.alpha = 1.0;
                         [fromView removeFromSuperview];
                         [transitionContext completeTransition:YES];
                     }];
}

@end

然后在你的UITabBarControllerDelegate中使用它:

- (id <UIViewControllerAnimatedTransitioning>)tabBarController:(UITabBarController *)tabBarController
            animationControllerForTransitionFromViewController:(UIViewController *)fromVC
                                              toViewController:(UIViewController *)toVC
{
    return [[TabSwitchAnimationController alloc] init];
}

2
请记得将您的代理连接到TabViewController的代理输出口。工作得非常好。这是最清晰的解决方案。 - Andrew Duncan
现在我正在研究IOS 10.x中的这个功能,是否可以通过Storyboard和Swift完成? - mobibob

18

使用tabBarController:shouldSelectViewController:不如实现tabBarController:animationControllerForTransitionFromViewController:toViewController:

TransitioningObject.swift

import UIKit

class TransitioningObject: NSObject, UIViewControllerAnimatedTransitioning {

    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        let fromView: UIView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
        let toView: UIView = transitionContext.viewForKey(UITransitionContextToViewKey)!

        transitionContext.containerView().addSubview(fromView)
        transitionContext.containerView().addSubview(toView)

        UIView.transitionFromView(fromView, toView: toView, duration: transitionDuration(transitionContext), options: UIViewAnimationOptions.TransitionCrossDissolve) { finished in
            transitionContext.completeTransition(true)
        }
    }

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

TabBarViewController.swift

import UIKit

    class TabBarViewController: UITabBarController, UITabBarControllerDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()

        self.delegate = self
    }

    // MARK: - Tabbar delegate

    func tabBarController(tabBarController: UITabBarController, animationControllerForTransitionFromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return TransitioningObject()
    }
}

4
这似乎是最好的答案。这个解决方案没有问题。 - sabiland

15

我认为你可以使用CATransition轻松实现UITabBarController的过渡效果;这也可以解决使用transitionFromView:toView:带来的任何副作用。

在扩展自UITabBarController的自定义TabBarController类中使用此方法。

- (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController (UIViewController*)viewController {

    CATransition *animation = [CATransition animation];
    [animation setType:kCATransitionFade];
    [animation setDuration:0.25];
    [animation setTimingFunction:[CAMediaTimingFunction functionWithName:
                              kCAMediaTimingFunctionEaseIn]];
    [self.view.window.layer addAnimation:animation forKey:@"fadeTransition"];
}

希望这可以帮到你 :)


1
我认为我们可以使用“shouldSelectViewController”而不是“didSelectViewController”。 - Ryan Wu

13
我在这里尝试了各种答案后,写了一篇文章
代码使用Swift编写,您可以通过调用animateToTab来以编程方式更改选项卡并带有动画效果。
func animateToTab(toIndex: Int) {
    let tabViewControllers = viewControllers!
    let fromView = selectedViewController!.view
    let toView = tabViewControllers[toIndex].view    
    let fromIndex = tabViewControllers.indexOf(selectedViewController!)

    guard fromIndex != toIndex else {return}

    // Add the toView to the tab bar view
    fromView.superview!.addSubview(toView)

    // Position toView off screen (to the left/right of fromView)
    let screenWidth = UIScreen.mainScreen().bounds.size.width;
    let scrollRight = toIndex > fromIndex;
    let offset = (scrollRight ? screenWidth : -screenWidth)
    toView.center = CGPoint(x: fromView.center.x + offset, y: toView.center.y)

    // Disable interaction during animation
    view.userInteractionEnabled = false

    UIView.animateWithDuration(0.5, delay: 0.0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: UIViewAnimationOptions.CurveEaseOut, animations: {

            // Slide the views by -offset
            fromView.center = CGPoint(x: fromView.center.x - offset, y: fromView.center.y);
            toView.center   = CGPoint(x: toView.center.x - offset, y: toView.center.y);

        }, completion: { finished in

            // Remove the old view from the tabbar view.
            fromView.removeFromSuperview()
            self.selectedIndex = toIndex
            self.view.userInteractionEnabled = true
        })
}

如果您希望所有选项卡更改都具有动画效果,则可以将其挂钩到UITabBarControllerDelegate,如下所示:
func tabBarController(tabBarController: UITabBarController, shouldSelectViewController viewController: UIViewController) -> Bool {
    let tabViewControllers = tabBarController.viewControllers!
    guard let toIndex = tabViewControllers.indexOf(viewController) else {
        return false
    }

    // Our method
    animateToTab(toIndex)

    return true
}

这非常干净且动画效果非常优美。 - Mohammad Zekrallah

9

我的Swift解决方案:

创建自定义TabBar类并将其设置在故事板的TabBar中。

class MainTabBarController: UITabBarController, UITabBarControllerDelegate {

override func viewDidLoad() {
    super.viewDidLoad()
    self.delegate = self
    // Do any additional setup after loading the view.
}

func tabBarController(tabBarController: UITabBarController, shouldSelectViewController viewController: UIViewController) -> Bool {


    let tabViewControllers = tabBarController.viewControllers!
    let fromView = tabBarController.selectedViewController!.view
    let toView = viewController.view

    if (fromView == toView) {
        return false
    }

    let fromIndex = tabViewControllers.indexOf(tabBarController.selectedViewController!)
    let toIndex = tabViewControllers.indexOf(viewController)

    let offScreenRight = CGAffineTransformMakeTranslation(toView.frame.width, 0)
    let offScreenLeft = CGAffineTransformMakeTranslation(-toView.frame.width, 0)

    // start the toView to the right of the screen


    if (toIndex < fromIndex) {
        toView.transform = offScreenLeft
        fromView.transform = offScreenRight
    } else {
        toView.transform = offScreenRight
        fromView.transform = offScreenLeft
    }

    fromView.tag = 124
    toView.addSubview(fromView)

    self.view.userInteractionEnabled = false
    UIView.animateWithDuration(0.5, delay: 0.0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: UIViewAnimationOptions.CurveEaseOut, animations: {

        toView.transform = CGAffineTransformIdentity

        }, completion: { finished in

            let subViews = toView.subviews
            for subview in subViews{
                if (subview.tag == 124) {
                    subview.removeFromSuperview()
                }
            }
            tabBarController.selectedIndex = toIndex!
            self.view.userInteractionEnabled = true

    })

    return true
 }

}

这在iOS9中不起作用 - 从find方法返回错误,即Downcast from '[UIViewController]?' to '[UIViewController]' only unwraps optionals; did you mean to use '!'? - lozflan
这里有个问题,虽然已经接近完美,但我遇到了一个无法运作的 bug (finished 将为 false)。我不知道发生了什么事情,但我认为它与 CA 变换有关,因为它认为“没有任何东西需要进行动画处理”。我转而使用帧来进行动画制作,这样就解决了问题。 - samwize

3

我使用了@Mofumofu的解决方案,并将其升级到Swift 1.2,还实现了上/下动画。意思是,如果新的视图控制器的索引大于旧的视图控制器的索引,则新的ViewController将出现并将旧的视图控制器向上推动。否则,方向是向下。

class TabScrollPageAnimationController: NSObject, UIViewControllerAnimatedTransitioning {

    let tabBarController: UITabBarController

    init(tabBarController: UITabBarController) {
        self.tabBarController = tabBarController
    }

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

    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        if let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey),
            let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) {
                let fromView = fromVC.view
                let toView = toVC.view

                let containerView = transitionContext.containerView()

                var directionUpwardMultiplier: CGFloat = 1.0
                if let vcs = tabBarController.viewControllers as? [UIViewController],
                    let fIndex = find(vcs, fromVC),
                    let tIndex = find(vcs, toVC) {
                        directionUpwardMultiplier = (fIndex < tIndex) ? +1.0 : -1.0
                }

                containerView.clipsToBounds = false
                containerView.addSubview(toView)

                var fromViewEndFrame = fromView.frame
                fromViewEndFrame.origin.y -= (containerView.frame.height * directionUpwardMultiplier)

                let toViewEndFrame = transitionContext.finalFrameForViewController(toVC)
                var toViewStartFrame = toViewEndFrame
                toViewStartFrame.origin.y += (containerView.frame.height * directionUpwardMultiplier)
                toView.frame = toViewStartFrame

                toView.alpha = 0.0
                UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.0, options: UIViewAnimationOptions.CurveEaseInOut, animations: { () -> Void in
                    toView.alpha = 1.0
                    toView.frame = toViewEndFrame
                    fromView.alpha = 0.0
                    fromView.frame = fromViewEndFrame
                }, completion: { (completed) -> Void in
                    toView.alpha = 1.0
                    fromView.removeFromSuperview()
                    transitionContext.completeTransition(completed)
                    containerView.clipsToBounds = true
                })

        }
    }

}

在容器视图控制器中:
extension XYViewController: UITabBarControllerDelegate {

    func tabBarController(tabBarController: UITabBarController, animationControllerForTransitionFromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return TabScrollPageAnimationController(tabBarController: tabBarController)
    }

}

3
这里是我的Swift 3解决方案:
我重写了UITabBarViewController的selectedIndex,代码如下:
override var selectedIndex: Int{
    get{
        return super.selectedIndex
    }
    set{
        animateToTab(toIndex: newValue)
        super.selectedIndex = newValue
    }
}

然后我使用了这个函数,它模拟了本地的推入/弹出动画:

func animateToTab(toIndex: Int) {
    guard let tabViewControllers = viewControllers, tabViewControllers.count > toIndex, let fromViewController = selectedViewController, let fromIndex = tabViewControllers.index(of: fromViewController), fromIndex != toIndex else {return}

    view.isUserInteractionEnabled = false

    let toViewController = tabViewControllers[toIndex]
    let push = toIndex > fromIndex
    let bounds = UIScreen.main.bounds

    let offScreenCenter = CGPoint(x: fromViewController.view.center.x + bounds.width, y: toViewController.view.center.y)
    let partiallyOffCenter = CGPoint(x: fromViewController.view.center.x - bounds.width*0.25, y: fromViewController.view.center.y)

    if push{
        fromViewController.view.superview?.addSubview(toViewController.view)
        toViewController.view.center = offScreenCenter
    }else{
        fromViewController.view.superview?.insertSubview(toViewController.view, belowSubview: fromViewController.view)
        toViewController.view.center = partiallyOffCenter
    }

    UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: .curveEaseIn, animations: {
        toViewController.view.center   = fromViewController.view.center
        fromViewController.view.center = push ? partiallyOffCenter : offScreenCenter
    }, completion: { finished in
        fromViewController.view.removeFromSuperview()
        self.view.isUserInteractionEnabled = true
    })
}

我希望这有所帮助 :)

2

您可以根据所选项进行动画 - 在此示例中,如果所选索引大于先前选择的索引,则从左侧翻转;如果所选索引小于先前选择的索引,则从右侧翻转。这是 Swift 4:实现 UITabBarControllerDelegate 方法。

func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {

    let fromView: UIView = tabBarController.selectedViewController!.view
    let toView: UIView = viewController.view

    if fromView == toView {
        return false
    }

    if let tappedIndex = tabBarController.viewControllers?.index(of: viewController) {
        if tappedIndex > tabBarController.selectedIndex {
            UIView.transition(from: fromView, to: toView, duration: 0.5, options: UIViewAnimationOptions.transitionFlipFromLeft, completion: nil)
        } else {
            UIView.transition(from: fromView, to: toView, duration: 0.5, options: UIViewAnimationOptions.transitionFlipFromRight, completion: nil)
        }
    }
    return true
}

@devedv,这个解决方案有什么问题吗?你是否将UITabBarControllerDelegate设置为你的ViewController了? - Teetz
是的,我在AppDelegate类中做了以下操作:AppDelegate: UIResponder,UIApplicationDelegate,UITabBarControllerDelegate {}。我是Swift的新手,你能详细解释一下你的答案中的步骤吗? - devedv
我尝试了,但不幸的是,它不起作用。@teetz在这里检查两个视图控制器和AppDelegate文件。codeshare.io/GAnL4r - devedv
我正在使用Swift 4.1 @Teetz。 - devedv
1
让我们在聊天中继续这个讨论 - Teetz
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接