# CustomTransition **Repository Path**: caryaliu/custom-transition ## Basic Information - **Project Name**: CustomTransition - **Description**: iOS自定义Push转场 - **Primary Language**: Swift - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2021-10-22 - **Last Updated**: 2022-05-07 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README #自定义Push转场 ### 理论 首先,需要实现`UINavigationController`的`delegate`方法。 ```swift func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? ``` 该代理方法提供一个`UIViewControllerAnimatedTransitioning`对象,该对象负责在自定义转场的源视图和目标视图的动画。 为了给`view controller`之间的转场提供用户交互,我们必须实现另外一个代理方法。 ```swift func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? ``` 该方法也会用于处理`view controller`的侧滑返回。 ### 实践 首先,我们创建一个遵循`UIViewControllerAnimatedTransitioning`协议的对象。 ```swift func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { } ``` 该协议包含如上两个`required`方法。该协议继承`NSObjectProtocol`协议,因此遵循该协议的对象可继承自`NSObject`来满足。 另外,在遵循该协议的对象中,需要知道操作是`Push`还是`Pop`操作。因此,你可以在对象中声明一个标志位来区分当前操作,或者将`Push`和`Pop`操作的转场分离到两个对象中。 对于复杂的转场动画,你可以在对象中存储动画所需的任何值。 这里的示例是创建视图滑动动画,以下代码片段是用于`Push`操作: ```swift 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) } } } ``` 滑动动画可能会是不同的方向,因此这里存储了关于方向的属性。 1. 我们可通过`UIViewControllerContextTransitioning`的`func view(forKey key: UITransitionContextViewKey) -> UIView?`方法获取动画所需的`controller`视图 2. 获取`containerView`,其作为转场中涉及视图的父视图。依赖于`view controller`的是`Push`还是`Pop`,可能是将目标视图加载到`containerView`(`Push: containerView.addSubview(toView)`),或者是将目标视图添加到源视图`fromView`底部(`Pop: containerView.insertSubview(toView, belowSubview: fromView)`)。 3. 根据转场的设定,绘制相应的动画。 4. 动画完成后,需调用`UIViewControllerContextTransitioning`的方法`func completeTransition(_ didComplete: Bool)` 转场相关已经完成,现在就考虑如何将自定义转场附加到`navigation controller`上。 这里定义`TransitionCoordinator`来遵循`UINavigationControllerDelegate`协议。 ```swift 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 } } ``` 1. 用于处理侧滑返回 2. 根据当前是`Push`还是`Pop`,返回对应的转场对象 3. 返回`interactionController`以处理交互式转场。 最后,我们通过`UINavigationController`的扩展将自定义转场附加到`UINavigationController`上。 ```swift 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) } } ``` 1. 创建`TransitionCoordinator`实例,赋值给`UINavigationController`的关联对象`transitionCoordinatorHelper`,同时将`UINavationController`的代理设置为`TransitionCoordinator`实例 2. 存储需要设置的转场类型 3. 添加`UIScreenEdgePanGestureRecognizer`以处理页面侧滑返回 转场类型根据项目需要自己设定,如下: ```swift 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 } } } } ``` 在业务代码中,可如下使用: ```swift override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // navigationController?.delegate = self navigationController?.addTransition(.slide(direction: .down, operation: .pop)) } ```