使用向下平移关闭 UINavigationController 相对于顶部安全区域调整 UINavigationBar 的大小

Using pan down to dismiss a UINavigationController resizes the UINavigationBar with respect to the top safe area

我正在使用平移手势识别器关闭 UINavigationController。我正在使用 here 中的代码,并进行了修改以适合我的 NavigationController,我还将手势识别器添加到容器视图中的实际 VC。

一切正常,除了当我向下滑动时,只要 y 值 > 0.0,导航栏就会改变其位置。

我认为正在发生的事情是导航栏附加到顶部安全区域,当它离开顶部安全区域时,它会自动将自身调整到视图顶部。

我怎样才能简单地实现它,使导航栏的大小延伸到顶部安全区域的下方,或者完全忽略它,而不是限制它所在的视图?

图片如下:

正常:

向下滑动关闭:

这是我用于 UINavigationController 的代码

//
//  PannableViewController.swift
//

import UIKit

class PannableViewController: UINavigationController {
    public var minimumVelocityToHide = 1500 as CGFloat
    public var minimumScreenRatioToHide = 0.5 as CGFloat
    public var animationDuration = 0.2 as TimeInterval

    override func viewDidLoad() {
        super.viewDidLoad()

        // Listen for pan gesture
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(onPan(_:)))

        if let lastVC = self.childViewControllers.last {
            lastVC.view.addGestureRecognizer(panGesture)
        }

    }

    func slideViewVerticallyTo(_ y: CGFloat) {
        self.view.frame.origin = CGPoint(x: 0, y: y)
    }

    @objc func onPan(_ panGesture: UIPanGestureRecognizer) {
        switch panGesture.state {
        case .began, .changed:
            // If pan started or is ongoing then
            // slide the view to follow the finger
            let translation = panGesture.translation(in: view)
            let y = max(0, translation.y)
            self.slideViewVerticallyTo(y)
            break
        case .ended:
            // If pan ended, decide it we should close or reset the view
            // based on the final position and the speed of the gesture
            let translation = panGesture.translation(in: view)
            let velocity = panGesture.velocity(in: view)
            let closing = (translation.y > self.view.frame.size.height * minimumScreenRatioToHide) ||
                (velocity.y > minimumVelocityToHide)

            if closing {
                UIView.animate(withDuration: animationDuration, animations: {
                    // If closing, animate to the bottom of the view
                    self.slideViewVerticallyTo(self.view.frame.size.height)
                }, completion: { (isCompleted) in
                    if isCompleted {
                        // Dismiss the view when it dissapeared
                        self.dismiss(animated: false, completion: nil)
                    }
                })
            } else {
                // If not closing, reset the view to the top
                UIView.animate(withDuration: animationDuration, animations: {
                    self.slideViewVerticallyTo(0)
                })
            }
            break
        default:
            // If gesture state is undefined, reset the view to the top
            UIView.animate(withDuration: animationDuration, animations: {
                self.slideViewVerticallyTo(0)
            })
            break
        }
    }

    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)   {
        super.init(nibName: nil, bundle: nil)
        self.modalPresentationStyle = .overFullScreen;
        self.modalTransitionStyle = .coverVertical;
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.modalPresentationStyle = .overFullScreen;
        self.modalTransitionStyle = .coverVertical;
    }
}

这是我快速整理的东西,请大家根据需要进行优化和改进。

我已更新您的代码以使用转换委托。阅读 UIViewControllerTransitioningDelegate 和 UIViewControllerAnimatedTransitioning 以更好地理解 View Controller Transitions 的工作原理。

以下是更新的PannableViewController。改变了什么?

-添加了转换委托,它可以让您更好地控制 VC 之间的转换

-更新了平移手势处理程序

-更改了 minimumScreenRatioToHid(它太高了,如果需要,请保留 0.5)

class PannableViewController: UINavigationController {
    public var minimumVelocityToHide = 1500 as CGFloat
    public var minimumScreenRatioToHide = 0.3 as CGFloat
    public var animationDuration = 0.2 as TimeInterval

    private lazy var transitionDelegate: TransitionDelegate = TransitionDelegate()

    override func viewDidLoad() {
        super.viewDidLoad()

        self.transitioningDelegate = self.transitionDelegate

        // Listen for pan gesture
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(onPan(_:)))

        if let lastVC = self.childViewControllers.last {
            lastVC.view.addGestureRecognizer(panGesture)
        }
    }

    @objc func onPan(_ panGesture: UIPanGestureRecognizer) {

        let translation = panGesture.translation(in: self.view)
        let verticalMovement = translation.y / self.view.bounds.height
        let downwardMovement = fmaxf(Float(verticalMovement), 0.0)

        let downwardMovementPercent = fminf(downwardMovement, 1.0)
        let progress = CGFloat(downwardMovementPercent)

        let velocity = panGesture.velocity(in: self.view)
        let shouldFinish = progress > self.minimumScreenRatioToHide || velocity.y > self.minimumVelocityToHide

        switch panGesture.state {
        case .began:
            self.transitionDelegate.interactiveTransition.hasStarted = true
            self.dismiss(animated: true, completion: nil)
        case .changed:
            self.transitionDelegate.interactiveTransition.shouldFinish = shouldFinish
            self.transitionDelegate.interactiveTransition.update(progress)
        case .cancelled:
            self.transitionDelegate.interactiveTransition.hasStarted = false
            self.transitionDelegate.interactiveTransition.cancel()
        case .ended:
            self.transitionDelegate.interactiveTransition.hasStarted = false
            self.transitionDelegate.interactiveTransition.shouldFinish ? self.transitionDelegate.interactiveTransition.finish() : self.transitionDelegate.interactiveTransition.cancel()
        default:
            break
        }
    }

    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)   {
        super.init(nibName: nil, bundle: nil)
        self.modalPresentationStyle = .overFullScreen;
        self.modalTransitionStyle = .coverVertical;
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.modalPresentationStyle = .overFullScreen;
        self.modalTransitionStyle = .coverVertical;
    }
}

以下是 UIViewControllerTransitioningDelegate 和几个模型 类。

class InteractiveTransition: UIPercentDrivenInteractiveTransition {
        public var hasStarted: Bool = false
        public var shouldFinish: Bool = false
    }

class Transition {
    public var isPresenting: Bool = false
    public var presentDuration: TimeInterval = 0.5
    public var dismissDuration: TimeInterval = 0.5
}

class TransitionDelegate: NSObject, UIViewControllerTransitioningDelegate {

    lazy var transition: Transition = Transition()
    lazy var interactiveTransition: InteractiveTransition = InteractiveTransition()

    func animationController(forPresented presented: UIViewController,
                             presenting: UIViewController,
                             source: UIViewController) -> UIViewControllerAnimatedTransitioning? {

        self.transition.isPresenting = true
        return self
    }

    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        self.transition.isPresenting = false
        return self
    }

    func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        return self.interactiveTransition.hasStarted ? self.interactiveTransition : nil
    }
}


extension TransitionDelegate:  UIViewControllerAnimatedTransitioning {

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return self.transition.isPresenting ? self.transition.presentDuration : self.transition.dismissDuration
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

        guard let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
            let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) else {
                transitionContext.completeTransition(false)
                return
        }

        let containerView = transitionContext.containerView
        containerView.backgroundColor = UIColor.black.withAlphaComponent(0.5)

        if self.transition.isPresenting {

            let finalFrameForVC = transitionContext.finalFrame(for: toVC)
            toVC.view.frame = finalFrameForVC.offsetBy(dx: 0, dy: UIScreen.main.bounds.size.height)
            containerView.addSubview(toVC.view)

            // Additional ways to animate, Spring velocity & damping
            UIView.animate(withDuration: self.transition.presentDuration,
                           delay: 0.0,
                           options: .transitionCrossDissolve,
                           animations: {
                            toVC.view.frame = finalFrameForVC
            }, completion: { _ in
                transitionContext.completeTransition(true)
            })

        } else {

            var finalFrame = fromVC.view.frame
            finalFrame.origin.y += finalFrame.height

            // Additional ways to animate, Spring velocity & damping
            UIView.animate(withDuration: self.transition.dismissDuration,
                           delay: 0.0,
                           options: .curveEaseOut,
                           animations: {
                            fromVC.view.frame = finalFrame
                            toVC.view.alpha = 1.0
            },
                           completion: { _ in
                            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            })
        }
    }
}