How to animate Tab bar tab switch with a CrossDissolve slide transition?

后端 未结 5 773
既然无缘
既然无缘 2020-12-04 11:20

I\'m trying to create a transition effect on a UITabBarController somewhat similar to the Facebook app. I managed to get a \"scrolling effect\" working on tab s

相关标签:
5条回答
  • 2020-12-04 12:00

    If you want to use UIViewControllerAnimatedTransitioning to do something more custom than UIView.transition, take a look at this gist.

    // MyTabController.swift
    
    import UIKit
    
    class MyTabBarController: UITabBarController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            delegate = self
        }
    }
    
    extension MyTabBarController: UITabBarControllerDelegate {
    
        func tabBarController(_ tabBarController: UITabBarController, animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            return MyTransition(viewControllers: tabBarController.viewControllers)
        }
    }
    
    class MyTransition: NSObject, UIViewControllerAnimatedTransitioning {
    
        let viewControllers: [UIViewController]?
        let transitionDuration: Double = 1
    
        init(viewControllers: [UIViewController]?) {
            self.viewControllers = viewControllers
        }
    
        func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
            return TimeInterval(transitionDuration)
        }
    
        func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
    
            guard
                let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
                let fromView = fromVC.view,
                let fromIndex = getIndex(forViewController: fromVC),
                let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to),
                let toView = toVC.view,
                let toIndex = getIndex(forViewController: toVC)
                else {
                    transitionContext.completeTransition(false)
                    return
            }
    
            let frame = transitionContext.initialFrame(for: fromVC)
            var fromFrameEnd = frame
            var toFrameStart = frame
            fromFrameEnd.origin.x = toIndex > fromIndex ? frame.origin.x - frame.width : frame.origin.x + frame.width
            toFrameStart.origin.x = toIndex > fromIndex ? frame.origin.x + frame.width : frame.origin.x - frame.width
            toView.frame = toFrameStart
    
            DispatchQueue.main.async {
                transitionContext.containerView.addSubview(toView)
                UIView.animate(withDuration: self.transitionDuration, animations: {
                    fromView.frame = fromFrameEnd
                    toView.frame = frame
                }, completion: {success in
                    fromView.removeFromSuperview()
                    transitionContext.completeTransition(success)
                })
            }
        }
    
        func getIndex(forViewController vc: UIViewController) -> Int? {
            guard let vcs = self.viewControllers else { return nil }
            for (index, thisVC) in vcs.enumerated() {
                if thisVC == vc { return index }
            }
            return nil
        }
    }
    
    0 讨论(0)
  • 2020-12-04 12:02

    So, a few years later and more experienced, after revisiting my own question for the same behaviour, I improved a little bit upon Derek's answer.

    I inherited most of his code (as it seems like the best solution).

    What I changed

    • I added a crossDissolve animation (as I originally wanted) to the slide animation by adding a toCoverView and fromCoverView, these are snapshotviews of the other view which will be used to fade in/out at the same time.
    • Changed the frame width to already start at 75% instead of having to translate the full 100% width, it's only translating 25% now which makes it feel snappier.
    • Added SpringWithDamping and initialSpringVelocity settings.

    These changes made it feel just about as close as I could get it to Facebook's implementation and I'm personally quite happy with it.

    Here's the adapted answer (most of the credit goes to Derek so be sure to upvote him):

    class MyTabBarController: UITabBarController {
    
        override func viewDidLoad() {
            super.viewDidLoad()
            delegate = self
        }
    }
    
    extension MyTabBarController: UITabBarControllerDelegate {
    
        func tabBarController(_ tabBarController: UITabBarController, animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            return MyTransition(viewControllers: tabBarController.viewControllers)
        }
    }
    
    class MyTransition: NSObject, UIViewControllerAnimatedTransitioning {
    
        let viewControllers: [UIViewController]?
        let transitionDuration: Double = 0.2
    
        init(viewControllers: [UIViewController]?) {
            self.viewControllers = viewControllers
        }
    
        func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
            return TimeInterval(transitionDuration)
        }
    
        func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
    
            guard
                let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
                let fromView = fromVC.view,
                let fromIndex = getIndex(forViewController: fromVC),
                let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to),
                let toView = toVC.view,
                let toIndex = getIndex(forViewController: toVC)
                else {
                    transitionContext.completeTransition(false)
                    return
            }
    
            let frame = transitionContext.initialFrame(for: fromVC)
            var fromFrameEnd = frame
            var toFrameStart = frame
            let quarterFrame = frame.width * 0.25
            fromFrameEnd.origin.x = toIndex > fromIndex ? frame.origin.x - quarterFrame : frame.origin.x + quarterFrame
            toFrameStart.origin.x = toIndex > fromIndex ? frame.origin.x + quarterFrame : frame.origin.x - quarterFrame
            toView.frame = toFrameStart
    
            let toCoverView = fromView.snapshotView(afterScreenUpdates: false)
            if let toCoverView = toCoverView {
                toView.addSubview(toCoverView)
            }
            let fromCoverView = toView.snapshotView(afterScreenUpdates: false)
            if let fromCoverView = fromCoverView {
                fromView.addSubview(fromCoverView)
            }
    
            DispatchQueue.main.async {
                transitionContext.containerView.addSubview(toView)
                UIView.animate(withDuration: self.transitionDuration, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.8, options: [.curveEaseOut], animations: {
                    fromView.frame = fromFrameEnd
                    toView.frame = frame
                    toCoverView?.alpha = 0
                    fromCoverView?.alpha = 1
                }) { (success) in
                    fromCoverView?.removeFromSuperview()
                    toCoverView?.removeFromSuperview()
                    fromView.removeFromSuperview()
                    transitionContext.completeTransition(success)
                }
            }
        }
    
        func getIndex(forViewController vc: UIViewController) -> Int? {
            guard let vcs = self.viewControllers else { return nil }
            for (index, thisVC) in vcs.enumerated() {
                if thisVC == vc { return index }
            }
            return nil
        }
    }
    

    The only thing I've yet to figure out is how to make it "interruptible" like Facebook does. I know there's a interruptibleAnimator function for this but I haven't been able to make it work yet.

    0 讨论(0)
  • 2020-12-04 12:06

    I was struggling with the tab bar animation both from a user tap and programmatically calling selectedIndex = X since the accepted solution didn't work for me when setting the selected tab programatically.

    In the end I managed to solve it by a UITabBarControllerDelegate and a custom UIViewControllerAnimatedTransitioning as follows:

    extension MainController: UITabBarControllerDelegate {
    
        public func tabBarController(
                _ tabBarController: UITabBarController,
                animationControllerForTransitionFrom fromVC: UIViewController,
                to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            return FadePushAnimator()
        }
    }
    

    Where the FadePushAnimator looks like this:

    class FadePushAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    
        func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
            return 0.3
        }
    
        func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
            guard
                    let toViewController = transitionContext.viewController(forKey: .to)
                    else {
                return
            }
    
            transitionContext.containerView.addSubview(toViewController.view)
            toViewController.view.alpha = 0
    
            let duration = self.transitionDuration(using: transitionContext)
            UIView.animate(withDuration: duration, animations: {
                toViewController.view.alpha = 1
            }, completion: { _ in
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            })
    
        }
    }
    

    This approach supports any sort of custom animation and works both on user tap and setting the selected tab programatically. Tested on Swift 5.

    0 讨论(0)
  • 2020-12-04 12:11

    There is a simpler way to doing this. Add the following code in the tabbar delegate:

    Working on Swift 2, 3 and 4

    class MySubclassedTabBarController: UITabBarController {
    
        override func viewDidLoad() {
          super.viewDidLoad()
          delegate = self
        }
    }
    
    extension MySubclassedTabBarController: UITabBarControllerDelegate  {
        func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
    
            guard let fromView = selectedViewController?.view, let toView = viewController.view else {
              return false // Make sure you want this as false
            }
    
            if fromView != toView {
              UIView.transition(from: fromView, to: toView, duration: 0.3, options: [.transitionCrossDissolve], completion: nil)
            }
    
            return true
        }
    }
    

    EDIT (4/23/18) Since this answer is getting popular, I updated the code to remove the force unwraps, which is a bad practice, and added the guard statement.

    EDIT (7/11/18) @AlbertoGarcía is right. If you tap the tabbar icon twice you get a blank screen. So I added an extra check

    0 讨论(0)
  • 2020-12-04 12:18

    To expand on @gmogames answer: https://stackoverflow.com/a/45362914/1993937

    I couldn't get this to animate when selecting the tab bar index via code, as calling:

    tabBarController.setSeletedIndex(0)
    

    Doesn't seem to go through the same call heirarchy, and it skips the method:

    tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController)
    

    entirely, resulting in no animation.

    In my code I wanted to have an animation transition for a user tapping a tab bar item in addition to me setting the tab bar item in-code manually under certain circumstances.

    Here is my addition to the solution above which adds a different method to set the selected index via code that will animate the transition:

    import Foundation
    import UIKit
    
    @objc class CustomTabBarController: UITabBarController {
        override func viewDidLoad() {
            super.viewDidLoad()
            delegate = self
        }
    
        @objc func set(selectedIndex index : Int) {
            _ = self.tabBarController(self, shouldSelect: self.viewControllers![index])
        }
    }
    
    @objc extension CustomTabBarController: UITabBarControllerDelegate  {
        @objc func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
    
            guard let fromView = selectedViewController?.view, let toView = viewController.view else {
                return false // Make sure you want this as false
            }
    
            if fromView != toView {
    
                UIView.transition(from: fromView, to: toView, duration: 0.3, options: [.transitionCrossDissolve], completion: { (true) in
    
                })
    
                self.selectedViewController = viewController
            }
    
            return true
        }
    }
    

    Now just call

    tabBarController.setSelectedWithIndex(1)   
    

    for an in-code animated transition!

    I still think it is unfortunate that to get this done we have to override a method that isn't a setter and manipulate data within it. It doesn't make the tab bar controller as extensible as it should be if this is the method that we need to override to get this done.

    0 讨论(0)
提交回复
热议问题