如何使用 CrossDissolve 幻灯片过渡动画标签栏选项卡切换?
How to animate Tab bar tab switch with a CrossDissolve slide transition?
我正在尝试在 UITabBarController
上创建一种类似于 Facebook 应用程序的过渡效果。我设法让 "scrolling effect" 在标签切换上工作,但我似乎无法弄清楚如何交叉溶解(或者它至少不起作用)。
这是我当前的代码:
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
})
但是,动画真的很卡,不能用:(
编辑 2: 对于尝试使用这些片段的人,不要忘记为 UITabBarController
连接委托,否则什么也不会发生。
编辑 3: 我找到了一个 Swift 库,它完全符合我的要求:
https://github.com/Interactive-Studio/TransitionableTab
有一种更简单的方法可以做到这一点。在标签栏委托中添加以下代码:
正在处理 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
}
}
编辑 (4/23/18)
由于这个答案越来越受欢迎,我更新了代码以删除强制展开,这是一种不好的做法,并添加了 guard 语句。
编辑 (7/11/18)
@AlbertoGarcía 是对的。如果你点击标签栏图标两次,你会得到一个空白屏幕。所以我添加了一个额外的检查
如果您想使用 UIViewControllerAnimatedTransitioning
做一些比 UIView.transition
更自定义的事情,请查看 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
}
}
扩展@gmogames 的回答:
当通过代码选择标签栏索引时,我无法让它动画化,因为调用:
tabBarController.setSeletedIndex(0)
似乎没有经过相同的调用层次结构,它跳过了方法:
tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController)
完全没有动画。
在我的代码中,除了我在某些情况下手动设置标签栏项目 in-code 之外,我还想为用户点击标签栏项目制作动画过渡。
这是我对上述解决方案的补充,它添加了一种不同的方法来通过动画转换的代码来设置选定的索引:
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)
in-code 动画过渡!
我仍然认为不幸的是,要完成此操作,我们必须重写一个不是 setter 的方法并在其中操作数据。如果这是我们需要覆盖以完成此操作的方法,它不会使选项卡栏控制器具有应有的可扩展性。
我一直在为用户点击和编程调用 selectedIndex = X
的选项卡栏动画而苦苦挣扎,因为在以编程方式设置所选选项卡时,接受的解决方案对我不起作用。
最后我设法通过 UITabBarControllerDelegate
和自定义 UIViewControllerAnimatedTransitioning
解决了这个问题,如下所示:
extension MainController: UITabBarControllerDelegate {
public func tabBarController(
_ tabBarController: UITabBarController,
animationControllerForTransitionFrom fromVC: UIViewController,
to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return FadePushAnimator()
}
}
FadePushAnimator 看起来像这样:
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)
})
}
}
此方法支持任何类型的自定义动画,并且在用户点击和以编程方式设置所选选项卡时均有效。 测试于 Swift 5.
所以,几年后,我更有经验了,在重新审视我自己针对相同行为的问题后,我对 Derek 的回答做了一点改进。
我继承了他的大部分代码(因为这似乎是最好的解决方案)。
我改变了什么
- 我通过添加
toCoverView
和 fromCoverView
向幻灯片动画添加了 crossDissolve 动画(正如我最初想要的),这些是将用于同时淡入淡出的其他视图 in/out。
- 将框架宽度更改为已经从 75% 开始,而不必平移整个 100% 的宽度,它现在只平移 25%,这让人感觉更流畅。
- 添加了 SpringWithDamping 和 initialSpringVelocity 设置。
这些变化让我感觉它与 Facebook 的实施非常接近,我个人对此非常满意。
这是改编后的答案(大部分功劳都归功于德里克,所以一定要给他投票):
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
}
}
我唯一还没有弄清楚的是如何让它像 Facebook 那样“可中断”。我知道有一个 interruptibleAnimator
功能,但我还没能让它工作。
我正在尝试在 UITabBarController
上创建一种类似于 Facebook 应用程序的过渡效果。我设法让 "scrolling effect" 在标签切换上工作,但我似乎无法弄清楚如何交叉溶解(或者它至少不起作用)。
这是我当前的代码:
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
})
但是,动画真的很卡,不能用:(
编辑 2: 对于尝试使用这些片段的人,不要忘记为 UITabBarController
连接委托,否则什么也不会发生。
编辑 3: 我找到了一个 Swift 库,它完全符合我的要求: https://github.com/Interactive-Studio/TransitionableTab
有一种更简单的方法可以做到这一点。在标签栏委托中添加以下代码:
正在处理 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
}
}
编辑 (4/23/18) 由于这个答案越来越受欢迎,我更新了代码以删除强制展开,这是一种不好的做法,并添加了 guard 语句。
编辑 (7/11/18) @AlbertoGarcía 是对的。如果你点击标签栏图标两次,你会得到一个空白屏幕。所以我添加了一个额外的检查
如果您想使用 UIViewControllerAnimatedTransitioning
做一些比 UIView.transition
更自定义的事情,请查看 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
}
}
扩展@gmogames 的回答:
当通过代码选择标签栏索引时,我无法让它动画化,因为调用:
tabBarController.setSeletedIndex(0)
似乎没有经过相同的调用层次结构,它跳过了方法:
tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController)
完全没有动画。
在我的代码中,除了我在某些情况下手动设置标签栏项目 in-code 之外,我还想为用户点击标签栏项目制作动画过渡。
这是我对上述解决方案的补充,它添加了一种不同的方法来通过动画转换的代码来设置选定的索引:
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)
in-code 动画过渡!
我仍然认为不幸的是,要完成此操作,我们必须重写一个不是 setter 的方法并在其中操作数据。如果这是我们需要覆盖以完成此操作的方法,它不会使选项卡栏控制器具有应有的可扩展性。
我一直在为用户点击和编程调用 selectedIndex = X
的选项卡栏动画而苦苦挣扎,因为在以编程方式设置所选选项卡时,接受的解决方案对我不起作用。
最后我设法通过 UITabBarControllerDelegate
和自定义 UIViewControllerAnimatedTransitioning
解决了这个问题,如下所示:
extension MainController: UITabBarControllerDelegate {
public func tabBarController(
_ tabBarController: UITabBarController,
animationControllerForTransitionFrom fromVC: UIViewController,
to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return FadePushAnimator()
}
}
FadePushAnimator 看起来像这样:
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)
})
}
}
此方法支持任何类型的自定义动画,并且在用户点击和以编程方式设置所选选项卡时均有效。 测试于 Swift 5.
所以,几年后,我更有经验了,在重新审视我自己针对相同行为的问题后,我对 Derek 的回答做了一点改进。
我继承了他的大部分代码(因为这似乎是最好的解决方案)。
我改变了什么
- 我通过添加
toCoverView
和fromCoverView
向幻灯片动画添加了 crossDissolve 动画(正如我最初想要的),这些是将用于同时淡入淡出的其他视图 in/out。 - 将框架宽度更改为已经从 75% 开始,而不必平移整个 100% 的宽度,它现在只平移 25%,这让人感觉更流畅。
- 添加了 SpringWithDamping 和 initialSpringVelocity 设置。
这些变化让我感觉它与 Facebook 的实施非常接近,我个人对此非常满意。
这是改编后的答案(大部分功劳都归功于德里克,所以一定要给他投票):
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
}
}
我唯一还没有弄清楚的是如何让它像 Facebook 那样“可中断”。我知道有一个 interruptibleAnimator
功能,但我还没能让它工作。