iOS自定义Push转场

子车安和
2023-12-01

自定义Push转场

理论

首先,需要实现UINavigationControllerdelegate方法。

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操作。因此,你可以在对象中声明一个标志位来区分当前操作,或者将PushPop操作的转场分离到两个对象中。

对于复杂的转场动画,你可以在对象中存储动画所需的任何值。

这里的示例是创建视图滑动动画,以下代码片段是用于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)
        }
    }
}

滑动动画可能会是不同的方向,因此这里存储了关于方向的属性。

  1. 我们可通过UIViewControllerContextTransitioningfunc view(forKey key: UITransitionContextViewKey) -> UIView?方法获取动画所需的controller视图
  2. 获取containerView,其作为转场中涉及视图的父视图。依赖于view controller的是Push还是Pop,可能是将目标视图加载到containerViewPush: containerView.addSubview(toView)),或者是将目标视图添加到源视图fromView底部(Pop: containerView.insertSubview(toView, belowSubview: fromView))。
  3. 根据转场的设定,绘制相应的动画。
  4. 动画完成后,需调用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
    }
}
  1. 用于处理侧滑返回
  2. 根据当前是Push还是Pop,返回对应的转场对象
  3. 返回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)
    }
}
  1. 创建TransitionCoordinator实例,赋值给UINavigationController的关联对象transitionCoordinatorHelper,同时将UINavationController的代理设置为TransitionCoordinator实例
  2. 存储需要设置的转场类型
  3. 添加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转场

 类似资料: