首先,需要实现UINavigationController
的delegate
方法。
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?
该代理方法提供一个UIViewControllerAnimatedTransitioning
对象,该对象负责在自定义转场的源视图和目标视图的动画。
为了给view controller
之间的转场提供用户交互,我们必须实现另外一个代理方法。
func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning?
该方法也会用于处理view controller
的侧滑返回。
首先,我们创建一个遵循UIViewControllerAnimatedTransitioning
协议的对象。
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
}
该协议包含如上两个required
方法。该协议继承NSObjectProtocol
协议,因此遵循该协议的对象可继承自NSObject
来满足。
另外,在遵循该协议的对象中,需要知道操作是Push
还是Pop
操作。因此,你可以在对象中声明一个标志位来区分当前操作,或者将Push
和Pop
操作的转场分离到两个对象中。
对于复杂的转场动画,你可以在对象中存储动画所需的任何值。
这里的示例是创建视图滑动动画,以下代码片段是用于Push
操作:
class SlidePushAnimator: NSObject, UIViewControllerAnimatedTransitioning {
var direction = TransitionType.Direction.up
init(direction: TransitionType.Direction) {
self.direction = direction
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
// TimeInterval(UINavigationController.hideShowBarDuration)
0.5
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// 1.
guard let fromView = transitionContext.view(forKey: .from),
let toView = transitionContext.view(forKey: .to) else { return }
// 2.
transitionContext.containerView.addSubview(toView)
// 3.
let size = transitionContext.containerView.bounds.size
switch direction {
case .up:
toView.frame = CGRect(x: 0, y: size.height, width: size.width, height: size.height)
case .down:
toView.frame = CGRect(x: 0, y: -size.height, width: size.width, height: size.height)
case .left:
toView.frame = CGRect(x: size.width, y: 0, width: size.width, height: size.height)
case .right:
toView.frame = CGRect(x: -size.width, y: 0, width: size.width, height: size.height)
}
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: .curveEaseInOut) {
toView.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
switch self.direction {
case .up:
fromView.frame = CGRect(x: 0, y: -size.height, width: size.width, height: size.height)
case .down:
fromView.frame = CGRect(x: 0, y: size.height, width: size.width, height: size.height)
case .left:
fromView.frame = CGRect(x: -size.width, y: 0, width: size.width, height: size.height)
case .right:
fromView.frame = CGRect(x: size.width, y: 0, width: size.width, height: size.height)
}
} completion: { finished in
// 4.
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
}
滑动动画可能会是不同的方向,因此这里存储了关于方向的属性。
UIViewControllerContextTransitioning
的func view(forKey key: UITransitionContextViewKey) -> UIView?
方法获取动画所需的controller
视图containerView
,其作为转场中涉及视图的父视图。依赖于view controller
的是Push
还是Pop
,可能是将目标视图加载到containerView
(Push: containerView.addSubview(toView)
),或者是将目标视图添加到源视图fromView
底部(Pop: containerView.insertSubview(toView, belowSubview: fromView)
)。UIViewControllerContextTransitioning
的方法func completeTransition(_ didComplete: Bool)
转场相关已经完成,现在就考虑如何将自定义转场附加到navigation controller
上。
这里定义TransitionCoordinator
来遵循UINavigationControllerDelegate
协议。
class TransitionCoordinator: NSObject, UINavigationControllerDelegate {
// 1.
var interactionController: UIPercentDrivenInteractiveTransition?
// 2.
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let transitionType = navigationController.transition?[operation]
return transitionType?.transitionAnimator()
}
// 3.
func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactionController
}
}
Push
还是Pop
,返回对应的转场对象interactionController
以处理交互式转场。最后,我们通过UINavigationController
的扩展将自定义转场附加到UINavigationController
上。
extension UINavigationController {
enum TransitionKey {
static var coordinator: Void?
static var transition: Void?
}
private static let disposeBag = DisposeBag()
var transitionCoordinatorHelper: TransitionCoordinator? {
get {
return objc_getAssociatedObject(self, &TransitionKey.coordinator) as? TransitionCoordinator
}
set {
objc_setAssociatedObject(self, &TransitionKey.coordinator, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
var transition: [UINavigationController.Operation: TransitionType]? {
get {
return objc_getAssociatedObject(self, &TransitionKey.transition) as? [UINavigationController.Operation: TransitionType]
}
set {
return objc_setAssociatedObject(self, &TransitionKey.transition, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
func addTransition(_ transitionType: TransitionType) {
// 1.
transitionCoordinatorHelper = TransitionCoordinator()
delegate = transitionCoordinatorHelper
// 2.
let operation = transitionType.operation()
if var transition = transition {
transition[operation] = transitionType
self.transition = transition
} else {
transition = [operation: transitionType]
}
// 3.
addEdgePan()
}
private func addEdgePan() {
let swipeGestureRecognizer = UIScreenEdgePanGestureRecognizer()
swipeGestureRecognizer.edges = .left
swipeGestureRecognizer.rx.event.bind(onNext: { [weak self] recognizer in
guard let self = self, let view = recognizer.view else {
self?.transitionCoordinatorHelper?.interactionController = nil
return
}
let percent = recognizer.translation(in: view).x / view.bounds.width
if recognizer.state == .began {
self.transitionCoordinatorHelper?.interactionController = UIPercentDrivenInteractiveTransition()
self.popViewController(animated: true)
} else if recognizer.state == .changed {
self.transitionCoordinatorHelper?.interactionController?.update(percent)
} else if recognizer.state == .ended || recognizer.state == .cancelled {
if percent > 0.5 {
self.transitionCoordinatorHelper?.interactionController?.finish()
} else {
self.transitionCoordinatorHelper?.interactionController?.cancel()
}
self.transitionCoordinatorHelper?.interactionController = nil
}
}).disposed(by: UINavigationController.disposeBag)
view.addGestureRecognizer(swipeGestureRecognizer)
}
}
TransitionCoordinator
实例,赋值给UINavigationController
的关联对象transitionCoordinatorHelper
,同时将UINavationController
的代理设置为TransitionCoordinator
实例UIScreenEdgePanGestureRecognizer
以处理页面侧滑返回转场类型根据项目需要自己设定,如下:
enum TransitionType {
enum Direction {
case up, down, left, right
}
case slide(direction: Direction, operation: UINavigationController.Operation)
func operation() -> UINavigationController.Operation {
switch self {
case .slide(_, let operation):
return operation
}
}
}
extension TransitionType {
func transitionAnimator() -> UIViewControllerAnimatedTransitioning? {
TransitionAnimator.create(with: self)
}
}
class TransitionAnimator: NSObject {
static func create(with transitionType: TransitionType) -> UIViewControllerAnimatedTransitioning? {
switch transitionType {
case .slide(let direction, let operation):
switch operation {
case .push:
return SlidePushAnimator(direction: direction)
case .pop:
return SlidePopAnimator(direction: direction)
default:
return nil
}
}
}
}
在业务代码中,可如下使用:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// navigationController?.delegate = self
navigationController?.addTransition(.slide(direction: .down, operation: .pop))
}
代码仓库见:自定义Push转场