ios - Swift:实例化 View Controller 以在当前导航堆栈中进行自定义转换

标签 ios swift uiviewcontroller uinavigationcontroller transition

介绍

我正在创建一个应用程序,在其 rootViewController 中有一个 UITableView 和一个 UIPanGestureRecognizer 附加到一个小的 UIView 作为“handle”,为名为“SlideOutViewController”的 UIViewController 启用自定义 View Controller 转换,以便从右侧平移。

问题

我注意到我的方法有两个问题。但实际的自定义转换按预期工作。

  1. 当创建 SlideOutViewController 时,我相信它没有附加到导航堆栈,因此它没有关联的导航栏。如果我使用 navigationController 将其推送到堆栈上,我将失去交互式转换。

  2. 旁注:我还没有找到将 handle 连接到交互拖出的 SlideOutViewController 的方法。所以 handle 的平移与SlideOutViewControllers的位置不一致。

问题

  • 如何将 SlideOutViewController 添加到导航堆栈?这样当我触发 UIPanGestureRecognizer 时,SlideOutViewController 会随着 navigationBar 转换?

我的代码

在 rootViewController 中。

class RootViewController: UIViewController {

    ...

    let slideControllerHandle = UIView()
    var interactionController : UIPercentDrivenInteractiveTransition?

    override func viewDidLoad() {
        super.viewDidLoad()

        ... // Setting up the table view etc...

        setupPanGForSlideOutController()
    }

    private func setupPanGForSlideOutController() {
         slideControllerHandle.translatesAutoresizingMaskIntoConstraints = false
         slideControllerHandle.layer.borderColor = UIColor.black.cgColor
         slideControllerHandle.layer.borderWidth = 1
         slideControllerHandle.layer.cornerRadius = 30
         view.addSubview(slideControllerHandle)
         slideControllerHandle.frame = CGRect(x: view.frame.width - 12.5, y: view.frame.height / 2, width: 25, height: 60)
         let panGestureForCalendar = UIPanGestureRecognizer(target: self, action: #selector(handlePanGestureForSlideOutViewController(_:)))
         slideControllerHandle.addGestureRecognizer(panGestureForCalendar)
    }

    @objc private func handlePanGestureForSlideOutViewController(_ gesture: UIPanGestureRecognizer) {

         let xPosition = gesture.location(in: view).x
         let percent = 1 - (xPosition / view.frame.size.width)

         switch gesture.state {
         case .began:
             guard let slideOutController = storyboard?.instantiateViewController(withIdentifier: "CNSlideOutViewControllerID") as? SlideOutViewController else { fatalError("Sigh...") }
             interactionController = UIPercentDrivenInteractiveTransition()
        slideOutController.customTransitionDelegate.interactionController = interactionController
             self.present(slideOutController, animated: true)
         case .changed:
             slideControllerHandle.center = CGPoint(x: xPosition, y: slideControllerHandle.center.y)
             interactionController?.update(percent)
         case .ended, .cancelled:
             let velocity = gesture.velocity(in: view)
             interactionController?.completionSpeed = 0.999
             if percent > 0.5 || velocity.x < 10 {
                 UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.5, options: .curveEaseInOut, animations: {
                     self.slideControllerHandle.center = CGPoint(x: self.view.frame.width, y: self.slideControllerHandle.center.y)
                 })
                 interactionController?.finish()
             } else {
                 UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.5, options: .curveEaseInOut, animations: {
                     self.slideControllerHandle.center = CGPoint(x: -25, y: self.slideControllerHandle.center.y)
                 })
                 interactionController?.cancel()
             }
             interactionController = nil
         default:
             break
         }
    }

SlideOutViewController

class SlideOutViewController: UIViewController {

    var interactionController : UIPercentDrivenInteractiveTransition?
    let customTransitionDelegate = TransitionDelegate()

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        modalPresentationStyle = .custom
        transitioningDelegate = customTransitionDelegate
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .red
        navigationItem.title = "Slide Controller"
        let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addNewData(_:)))
        navigationItem.setRightBarButton(addButton, animated: true)
    }

}

自定义转换代码。 Based on Rob's descriptive answer on this SO question

  1. 过渡代理

    class TransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {
    
        weak var interactionController : UIPercentDrivenInteractiveTransition?
        func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            return CNRightDragAnimationController(transitionType: .presenting)
        }
    
        func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            return CNRightDragAnimationController(transitionType: .dismissing)
        }
    
        func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
            return PresentationController(presentedViewController: presented, presenting: presenting)
        }
    
        func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
            return interactionController
        }    
    
        func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
            return interactionController
        }
    }
    
  2. 拖动动画过渡

    class CNRightDragAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
    
        enum TransitionType {
            case presenting
            case dismissing
        }
    
        let transitionType: TransitionType
    
        init(transitionType: TransitionType) {
            self.transitionType = transitionType
            super.init()
        }
    
        func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
            let inView   = transitionContext.containerView
            let toView   = transitionContext.view(forKey: .to)!
            let fromView = transitionContext.view(forKey: .from)!
    
            var frame = inView.bounds
    
            switch transitionType {
            case .presenting:
                frame.origin.x = frame.size.width
                toView.frame = frame
    
                inView.addSubview(toView)
                UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
                    toView.frame = inView.bounds
                }, completion: { finished in
                    transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
                })
            case .dismissing:
                toView.frame = frame
                inView.insertSubview(toView, belowSubview: fromView)
    
                UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
                    frame.origin.x = frame.size.width
                    fromView.frame = frame
                }, completion: { finished in
                    transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
                })
            }
        }
    
        func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
            return 0.5
        }
    }
    
  3. 展示 Controller

    class PresentationController: UIPresentationController {
    
        override var shouldRemovePresentersView: Bool { return true }
    }
    

感谢阅读我的问题。

最佳答案

您从中获取的动画代码用于自定义“呈现”(例如模态)过渡。但是,如果您希望在使用导航 Controller 时在推送/弹出时自定义导航,您可以为 UINavigationController 指定一个委托(delegate),然后在 navigationController(_:animationControllerFor:from:to:) 中返回适当的转换委托(delegate)。 .并实现 navigationController(_:interactionControllerFor:)并将您的交互 Controller 放回那里。


例如我会做类似的事情:

class FirstViewController: UIViewController {

    let navigationDelegate = CustomNavigationDelegate()

    override func viewDidLoad() {
        super.viewDidLoad()

        navigationController?.delegate = navigationDelegate
        navigationDelegate.addPushInteractionController(to: view)
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        navigationDelegate.pushDestination = { [weak self] in
            self?.storyboard?.instantiateViewController(withIdentifier: "Second")
        }
    }
}

地点:

class CustomNavigationDelegate: NSObject, UINavigationControllerDelegate {

    var interactionController: UIPercentDrivenInteractiveTransition?
    var current: UIViewController?
    var pushDestination: (() -> UIViewController?)?

    func navigationController(_ navigationController: UINavigationController,
                              animationControllerFor operation: UINavigationController.Operation,
                              from fromVC: UIViewController,
                              to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return CustomNavigationAnimator(transitionType: operation)
    }

    func navigationController(_ navigationController: UINavigationController,
                              interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        return interactionController
    }

    func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
        current = viewController
    }
}

// MARK: - Push

extension CustomNavigationDelegate {
    func addPushInteractionController(to view: UIView) {
        let swipe = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handlePushGesture(_:)))
        swipe.edges = [.right]
        view.addGestureRecognizer(swipe)
    }

    @objc private func handlePushGesture(_ gesture: UIScreenEdgePanGestureRecognizer) {
        guard let pushDestination = pushDestination else { return }

        let position = gesture.translation(in: gesture.view)
        let percentComplete = min(-position.x / gesture.view!.bounds.width, 1.0)

        switch gesture.state {
        case .began:
            interactionController = UIPercentDrivenInteractiveTransition()
            guard let controller = pushDestination() else { fatalError("No push destination") }
            current?.navigationController?.pushViewController(controller, animated: true)

        case .changed:
            interactionController?.update(percentComplete)

        case .ended, .cancelled:
            let speed = gesture.velocity(in: gesture.view)
            if speed.x < 0 || (speed.x == 0 && percentComplete > 0.5) {
                interactionController?.finish()
            } else {
                interactionController?.cancel()
            }
            interactionController = nil

        default:
            break
        }
    }
}

// MARK: - Pop

extension CustomNavigationDelegate {
    func addPopInteractionController(to view: UIView) {
        let swipe = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handlePopGesture(_:)))
        swipe.edges = [.left]
        view.addGestureRecognizer(swipe)
    }

    @objc private func handlePopGesture(_ gesture: UIScreenEdgePanGestureRecognizer) {
        let position = gesture.translation(in: gesture.view)
        let percentComplete = min(position.x / gesture.view!.bounds.width, 1)

        switch gesture.state {
        case .began:
            interactionController = UIPercentDrivenInteractiveTransition()
            current?.navigationController?.popViewController(animated: true)

        case .changed:
            interactionController?.update(percentComplete)

        case .ended, .cancelled:
            let speed = gesture.velocity(in: gesture.view)
            if speed.x > 0 || (speed.x == 0 && percentComplete > 0.5) {
                interactionController?.finish()
            } else {
                interactionController?.cancel()
            }
            interactionController = nil


        default:
            break
        }
    }
}

class CustomNavigationAnimator: NSObject, UIViewControllerAnimatedTransitioning {

    let transitionType: UINavigationController.Operation

    init(transitionType: UINavigationController.Operation) {
        self.transitionType = transitionType
        super.init()
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        let inView   = transitionContext.containerView
        let toView   = transitionContext.view(forKey: .to)!
        let fromView = transitionContext.view(forKey: .from)!

        var frame = inView.bounds

        switch transitionType {
        case .push:
            frame.origin.x = frame.size.width
            toView.frame = frame

            inView.addSubview(toView)
            UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
                toView.frame = inView.bounds
            }, completion: { finished in
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            })

        case .pop:
            toView.frame = frame
            inView.insertSubview(toView, belowSubview: fromView)

            UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
                frame.origin.x = frame.size.width
                fromView.frame = frame
            }, completion: { finished in
                transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            })

        case .none:
            break
        }
    }

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.5
    }
}

然后,如果第二个 View Controller 想要自定义交互式弹出窗口以及滑动到第三个 View Controller 的能力:

class SecondViewController: UIViewController {

    var navigationDelegate: CustomNavigationDelegate { return navigationController!.delegate as! CustomNavigationDelegate }

    override func viewDidLoad() {
        super.viewDidLoad()

        navigationDelegate.addPushInteractionController(to: view)
        navigationDelegate.addPopInteractionController(to: view)
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        navigationDelegate.pushDestination = { [weak self] in
            self?.storyboard?.instantiateViewController(withIdentifier: "Third")
        }
    }

}

但是如果最后一个 View Controller 不能推送任何东西,只能弹出:

class ThirdViewController: UIViewController {

    var navigationDelegate: CustomNavigationDelegate { return navigationController!.delegate as! CustomNavigationDelegate }

    override func viewDidLoad() {
        super.viewDidLoad()

        navigationDelegate.addPopInteractionController(to: view)
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        navigationDelegate.pushDestination = nil
    }

}

关于ios - Swift:实例化 View Controller 以在当前导航堆栈中进行自定义转换,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53858688/

相关文章:

ios - 在 NSUserDefaults 中存储字典并使用存储的数据。

swift - 嵌套和关联的枚举值 Swift

swift - 如何实时向音频流添加帧以拉伸(stretch)音频

ios - 在屏幕上停留特定秒数,然后转到下一个?

objective-c - Cocoa Touch 上不显示 UIButton 的 titleLabel.text

ios - 安全地使用 CFBridgingRelease

ios - Google 登录不在 UIVIewController 中

ios - UITableView 可滚动到 UIVIewController

ios - Facebook 登录 您已经授权 APP_NAME

ios - 我希望打印的值太长,无法打印 firebase 中的所有值