Как анимировать вкладку Tab tab с переходом слайда CrossDissolve?

Я пытаюсь создать эффект перехода на UITabBarController чем-то похожем на приложение Facebook. Мне удалось получить "эффект прокрутки", работающий на переключателе табуляции, но я не могу понять, как перекрестное растворение (или оно по крайней мере не работает).

Вот мой текущий код:

import UIKit

class ScrollingTabBarControllerDelegate: NSObject, UITabBarControllerDelegate {
    func tabBarController(_ tabBarController: UITabBarController, animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {

        return ScrollingTransitionAnimator(tabBarController: tabBarController, lastIndex: tabBarController.selectedIndex)
    }
}

class ScrollingTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    weak var transitionContext: UIViewControllerContextTransitioning?
    var tabBarController: UITabBarController!
    var lastIndex = 0

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

    init(tabBarController: UITabBarController, lastIndex: Int) {
        self.tabBarController = tabBarController
        self.lastIndex = lastIndex
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        self.transitionContext = transitionContext

        let containerView = transitionContext.containerView
        let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
        let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)

        containerView.addSubview(toViewController!.view)

        var viewWidth = toViewController!.view.bounds.width

        if tabBarController.selectedIndex < lastIndex {
            viewWidth = -viewWidth
        }

        toViewController!.view.transform = CGAffineTransform(translationX: viewWidth, y: 0)

        UIView.animate(withDuration: self.transitionDuration(using: (self.transitionContext)), delay: 0.0, usingSpringWithDamping: 1.2, initialSpringVelocity: 2.5, options: .transitionCrossDissolve, animations: {
            toViewController!.view.transform = CGAffineTransform.identity
            fromViewController!.view.transform = CGAffineTransform(translationX: -viewWidth, y: 0)
        }, completion: { _ in
            self.transitionContext?.completeTransition(!self.transitionContext!.transitionWasCancelled)
            fromViewController!.view.transform = CGAffineTransform.identity
        })
    }
}

Было бы замечательно, если бы кто-нибудь знал, как заставить это работать, пытаясь в течение многих дней без прогресса...:/

редактировать: я получил работу перекрестного растворения, заменив блок UIView.animate на:

UIView.transition(with: containerView, duration: 0.2, options: .transitionCrossDissolve, animations: {

    toViewController!.view.transform = CGAffineTransform.identity
    fromViewController!.view.transform = CGAffineTransform(translationX: -viewWidth, y: 0)

}, completion: { _ in

    self.transitionContext?.completeTransition(!self.transitionContext!.transitionWasCancelled)
    fromViewController!.view.transform = CGAffineTransform.identity

})

Тем не менее, анимация действительно запаздывает и не используется :(

edit 2: Для людей, пытающихся использовать эти фрагменты, не забудьте подключить делегат для UITabBarController, иначе ничего не произойдет.

редактировать 3: я нашел библиотеку Swift, которая делает именно то, что я искал: https://github.com/Interactive-Studio/TransitionableTab

Ответ 1

Есть более простой способ сделать это. Добавьте следующий код в делегат панели вкладок:

Работаем на Swift 2, 3 и 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
    }
}

РЕДАКТИРОВАТЬ (23/23/18) Так как этот ответ становится популярным, я обновил код, чтобы убрать принудительное развертывание, что является плохой практикой, и добавил защитное утверждение.

РЕДАКТИРОВАТЬ (7/11/18) @AlbertoGarcía прав. Если дважды коснуться значка панели вкладок, вы увидите пустой экран. Поэтому я добавил дополнительную проверку

Ответ 2

Если вы хотите использовать UIViewControllerAnimatedTransitioning чтобы сделать что-то более нестандартное, чем UIView.transition, взгляните на эту суть.

enter image description here

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

Ответ 3

Чтобы развернуть ответ @gmogames: fooobar.com/info/1268510/...

Я не смог получить анимацию при выборе индекса панели вкладок с помощью кода, например:

tabBarController.setSeletedIndex(0)

Похоже, не проходит через тот же heirarchy вызова, и он пропускает метод:

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

полностью, в результате чего нет анимации.

В моем коде я хотел сделать анимационный переход для пользователя, касающегося элемента панели вкладок, в дополнение к тому, что при определенных обстоятельствах я вручную настраивал код элемента панели вкладок.

Вот мое дополнение к решению выше, которое добавляет другой метод для установки выбранного индекса через код, который будет анимировать переход:

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

Теперь просто позвоните

tabBarController.setSelectedWithIndex(1)   

для анимационного перехода в коде!

Я все еще думаю, что к сожалению, чтобы сделать это, мы должны переопределить метод, который не является установщиком, и манипулировать данными внутри него. Это не делает контроллер панели вкладок таким расширяемым, каким он должен быть, если это метод, который нам нужно переопределить, чтобы сделать это.