Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save yesleon/0e72ca8ff9bd9b684af22093d70bb751 to your computer and use it in GitHub Desktop.
Save yesleon/0e72ca8ff9bd9b684af22093d70bb751 to your computer and use it in GitHub Desktop.
//
// InteractivePushableNavigationController.swift
// InteractivePushGestureRecognizerExample
//
// Created by Li-Heng Hsu on 2020/3/11.
// Copyright © 2020 Li-Heng Hsu. All rights reserved.
//
import UIKit
class InteractivePushableNavigationController: UINavigationController {
// 因為官方沒有統合用來追蹤pop、push事件的API,所以只好自己實作。
enum TransitionState {
case popping([UIViewController]), pushing, setting, none
}
private(set) var transitionState = TransitionState.none
// 將被pop掉的VC儲存到一個stack裡。
var poppedViewControllerStack = [UIViewController]()
// 負責interactive push轉場的物件。
var interactivePushTransition: InteractivePushTransition?
// Interactive push手勢。
lazy var interactivePushGestureRecognizer: UIGestureRecognizer = {
let gesture = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(startPushGesture(_:)))
gesture.edges = .right
return gesture
}()
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
// 設定手勢。
self.view.addGestureRecognizer(interactivePushGestureRecognizer)
self.interactivePopGestureRecognizer?.delegate = self
self.interactivePushGestureRecognizer.delegate = self
}
// 負責處理interactive push手勢,轉給self.parallaxTransitionController。
@objc func startPushGesture(_ sender: UIScreenEdgePanGestureRecognizer) {
let percentage: CGFloat = (view.frame.width - sender.location(in: view).x) / view.frame.width
switch sender.state {
case .began:
guard let vc = poppedViewControllerStack.last else { return }
interactivePushTransition = .init()
super.pushViewController(vc, animated: true)
case .changed:
interactivePushTransition?.update(percentage)
case .ended:
if sender.velocity(in: view).x > 10 {
interactivePushTransition?.cancel()
} else if sender.velocity(in: view).x < -10 {
interactivePushTransition?.finish()
poppedViewControllerStack.removeLast()
} else {
if percentage >= 0.5 {
interactivePushTransition?.finish()
poppedViewControllerStack.removeLast()
} else {
interactivePushTransition?.cancel()
}
}
interactivePushTransition = nil
default:
break
}
}
// 將pop、push事件儲存成self.transitionState。
override func setViewControllers(_ viewControllers: [UIViewController], animated: Bool) {
super.setViewControllers(viewControllers, animated: animated)
self.transitionState = .setting
}
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
super.pushViewController(viewController, animated: animated)
self.transitionState = .pushing
}
override func popViewController(animated: Bool) -> UIViewController? {
guard let poppedVC = super.popViewController(animated: animated) else { return nil }
self.transitionState = .popping([poppedVC])
return poppedVC
}
override func popToViewController(_ viewController: UIViewController, animated: Bool) -> [UIViewController]? {
guard let poppedVCs = super.popToViewController(viewController, animated: animated) else { return nil }
self.transitionState = .popping(poppedVCs)
return poppedVCs
}
override func popToRootViewController(animated: Bool) -> [UIViewController]? {
guard let poppedVCs = super.popToRootViewController(animated: animated) else { return nil }
self.transitionState = .popping(poppedVCs)
return poppedVCs
}
}
extension InteractivePushableNavigationController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
switch gestureRecognizer {
case interactivePopGestureRecognizer:
return viewControllers.count > 1
case interactivePushGestureRecognizer:
return !poppedViewControllerStack.isEmpty
default:
return true
}
}
}
extension InteractivePushableNavigationController: UINavigationControllerDelegate {
// 只有在interactive push的時候,self.parallaxTransitionController才會存在,才會被調用。其它時候都是回傳nil用內建的轉場。
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
interactivePushTransition
}
func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
interactivePushTransition
}
// 獲取push與pop事件。
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
switch self.transitionState {
case .popping(let poppedVCs):
self.poppedViewControllerStack.append(contentsOf: poppedVCs.reversed())
case .pushing, .setting:
self.poppedViewControllerStack.removeAll()
case .none:
break
}
self.transitionState = .none
}
}
class InteractivePushTransition: UIPercentDrivenInteractiveTransition, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
0.5
}
// 模仿內建的parallax效果。
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromView = transitionContext.view(forKey: .from), let toView = transitionContext.view(forKey: .to) else { return }
let containerView = transitionContext.containerView
let dimmingView = UIView()
dimmingView.frame = fromView.frame
containerView.insertSubview(toView, aboveSubview: fromView)
containerView.insertSubview(dimmingView, belowSubview: toView)
dimmingView.addSubview(fromView)
fromView.frame = dimmingView.bounds
toView.frame = dimmingView.frame
toView.frame.origin.x += toView.frame.width
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: .curveLinear, animations: {
toView.frame = dimmingView.frame
dimmingView.alpha = 0.9
dimmingView.frame.origin.x -= dimmingView.frame.width * 0.3
}) { (finished) in
dimmingView.removeFromSuperview()
if transitionContext.transitionWasCancelled {
containerView.addSubview(fromView)
transitionContext.completeTransition(false)
} else {
transitionContext.completeTransition(true)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment