有没有办法为 .fullscreen 模态呈现的视图控制器进行交互式模态解雇?

Is there a way to have interactive modal dismissal for a .fullscreen modally presented view controller?

我想启用交互式模式关闭,它会随着用户手指在全屏模式呈现的视图控制器上平移 .fullscreen。 我已经看到在内置它的 .pageSheet.formSheet 上这样做是相当微不足道的,但还没有看到全屏的清晰示例。 我猜我需要在我的 vc 代码主体中添加一个平移手势,然后自己调整状态,但想知道是否有人知道到底需要做什么/是否有更简单的方法这样做的方法,因为 .fullscreen 案例

似乎要复杂得多

可以通过创建自定义 UIPresentationControllerUIViewControllerTransitioningDelegate 来完成。假设我们有 TestViewController,我们想要呈现 SecondViewController,总 presentedHeight 为 1.0(全屏)。演示文稿将通过 @IBAction func buttonPressed 触发,并且可以通过向下拖动控制器来关闭(我们已经习惯了)。添加一些 backgroundEffect 以在用户向下滑动 SecondViewController 时逐渐改变(尤其是仅使用 0.6 的 presentedHeight 时)也很好。

  1. 首先我们定义了 OverlayViewController ,它将成为稍后呈现的 SecondViewController 的超类,并将包含 UIPanGestureRecognizer.

       class OverlayViewController: UIViewController {
    
       var hasSetPointOrigin = false
       var pointOrigin: CGPoint?
       var delegate: OverlayViewDelegate?
    
       override func viewDidLoad() {
        super.viewDidLoad()
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(panGestureRecognizerAction))
        view.addGestureRecognizer(panGesture)
    
       }
    
       override func viewDidLayoutSubviews() {
        if !hasSetPointOrigin {
         hasSetPointOrigin = true
         pointOrigin = self.view.frame.origin
        }
       }
       @objc func panGestureRecognizerAction(sender: UIPanGestureRecognizer) {
        let translation = sender.translation(in: view)
    
     // Not allowing the user to drag the view upward
     guard translation.y >= 0 else { return }
     let currentPosition = translation.y
     let originPos = self.pointOrigin
     delegate?.userDragged(draggedPercentage: translation.y/originPos!.y)
    
     // setting x as 0 because we don't want users to move the frame side ways!! Only want straight up or down
     view.frame.origin = CGPoint(x: 0, y: self.pointOrigin!.y + translation.y)
    
     if sender.state == .ended {
         let dragVelocity = sender.velocity(in: view)
         if dragVelocity.y >= 1100 {
             self.dismiss(animated: true, completion: nil)
         } else {
             // Set back to original position of the view controller
             UIView.animate(withDuration: 0.3) {
                 self.view.frame.origin = self.pointOrigin ?? CGPoint(x: 0, y: 400)
                 self.delegate?.animateBlurBack(seconds: 0.3)
             }
         }
       }
      }
     }
    
     protocol OverlayViewDelegate: AnyObject {
     func userDragged(draggedPercentage: CGFloat)
     func animateBlurBack(seconds: TimeInterval)
     }
    
  2. 接下来我们定义自定义PresentationController

       class PresentationController: UIPresentationController {
    
         private var backgroundEffectView: UIView?
         private var backgroundEffect: BackgroundEffect?
     private var viewHeight: CGFloat?
     private let maxDim:CGFloat = 0.6
     private var tapGestureRecognizer: UITapGestureRecognizer = UITapGestureRecognizer()
    
     convenience init(presentedViewController: UIViewController,
                      presenting presentingViewController: UIViewController?,
                      backgroundEffect: BackgroundEffect = .blur,
                      viewHeight: CGFloat = 0.6)
     {
    
         self.init(presentedViewController: presentedViewController, presenting: presentingViewController)
    
         self.backgroundEffect = backgroundEffect
         self.backgroundEffectView = returnCorrectEffectView(backgroundEffect)
         self.tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissController))
         self.backgroundEffectView?.autoresizingMask = [.flexibleWidth, .flexibleHeight]
         self.backgroundEffectView?.isUserInteractionEnabled = true
         self.backgroundEffectView?.addGestureRecognizer(tapGestureRecognizer)
         self.viewHeight = viewHeight
     }
    
     private override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
         super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
     }
    
     override var frameOfPresentedViewInContainerView: CGRect {
         CGRect(origin: CGPoint(x: 0, y: self.containerView!.frame.height * (1-viewHeight!)),
                size: CGSize(width: self.containerView!.frame.width, height: self.containerView!.frame.height *
                                 viewHeight!))
     }
    
     override func presentationTransitionWillBegin() {
         self.backgroundEffectView?.alpha = 0
         self.containerView?.addSubview(backgroundEffectView!)
         self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) in
             switch self.backgroundEffect! {
             case .blur:
                 self.backgroundEffectView?.alpha = 1
             case .dim:
                 self.backgroundEffectView?.alpha = self.maxDim
             case .none:
                 self.backgroundEffectView?.alpha = 0
             }
         }, completion: { (UIViewControllerTransitionCoordinatorContext) in })
     }
    
     override func dismissalTransitionWillBegin() {
         self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) in
             self.backgroundEffectView?.alpha = 0
         }, completion: { (UIViewControllerTransitionCoordinatorContext) in
             self.backgroundEffectView?.removeFromSuperview()
         })
     }
    
     override func containerViewWillLayoutSubviews() {
         super.containerViewWillLayoutSubviews()
     }
    
     override func containerViewDidLayoutSubviews() {
         super.containerViewDidLayoutSubviews()
         presentedView?.frame = frameOfPresentedViewInContainerView
         backgroundEffectView?.frame = containerView!.bounds
     }
    
     @objc func dismissController(){
         self.presentedViewController.dismiss(animated: true, completion: nil)
     }
    
     func graduallyChangeOpacity(withPercentage: CGFloat) {
         self.backgroundEffectView?.alpha = withPercentage
     }
    
     func returnCorrectEffectView(_ effect: BackgroundEffect) -> UIView {
         switch effect {
    
         case .blur:
             var blurEffect = UIBlurEffect(style: .dark)
             if self.traitCollection.userInterfaceStyle == .dark {
                 blurEffect = UIBlurEffect(style: .light)
             }
             return UIVisualEffectView(effect: blurEffect)
         case .dim:
             var dimView = UIView()
             dimView.backgroundColor = .black
             if self.traitCollection.userInterfaceStyle == .dark {
                 dimView.backgroundColor = .gray
             }
             dimView.alpha = maxDim
             return dimView
         case .none:
             let clearView = UIView()
             clearView.backgroundColor = .clear
             return clearView
         }
        }
       }
    
         extension PresentationController: OverlayViewDelegate {
         func userDragged(draggedPercentage: CGFloat) {
         graduallyChangeOpacity(withPercentage: 1-draggedPercentage)
    
         switch self.backgroundEffect! {
         case .blur:
             graduallyChangeOpacity(withPercentage: 1-draggedPercentage)
         case .dim:
             graduallyChangeOpacity(withPercentage: maxDim-draggedPercentage)
         case .none:
             self.backgroundEffectView?.alpha = 0
         }
     }
    
     func animateBlurBack(seconds: TimeInterval) {
         UIView.animate(withDuration: seconds) {
             switch self.backgroundEffect! {
             case .blur:
                 self.backgroundEffectView?.alpha = 1
             case .dim:
                 self.backgroundEffectView?.alpha = self.maxDim
             case .none:
                 self.backgroundEffectView?.alpha = 0
             }
    
         }
       }
      }
    
       enum BackgroundEffect {
        case blur
        case dim
        case none
       }
    
  3. 创建 SecondViewController 子类 OverlayViewController:

     class SecondViewController: OverlayViewController {
    
     override func viewDidLoad() {
         super.viewDidLoad()
         self.view.backgroundColor = .blue
         // Do any additional setup after loading the view.
     }
    
     override func viewDidLayoutSubviews() {
         super.viewDidLayoutSubviews()
         addSlider()
     }
    
     func addSlider() {
         let sliderWidth:CGFloat = 100
         let centerOfScreen = self.view.frame.size.width / 2
         let rect = CGRect(x: centerOfScreen - sliderWidth/2, y: 80, width: sliderWidth, height: 10)
         let slider = UIView(frame: rect)
         slider.backgroundColor = .black
         self.view.addSubview(slider)
     }
    
  4. 添加 showOverlay() 将在 buttonPressed 之后触发的功能,并使您的呈现 UIViewController (TestViewController) 符合 UIViewControllerTransitioningDelegate :

     class TestViewController: UIViewController {
    
     override func viewDidLoad() {
         super.viewDidLoad()
    
         // Do any additional setup after loading the view.
     }
    
     @IBAction func buttonPressed(_ sender: Any) {
         showOverlay()
     }
    
     func showOverlay() {
         let secondVC =  UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "secondVC") as! SecondViewController
         secondVC.modalPresentationStyle = .custom
         secondVC.transitioningDelegate = self
         self.present(secondVC, animated: true, completion: nil)
     }
    }
    
     extension TestViewController: UIViewControllerTransitioningDelegate {
     func presentationController(forPresented presented: UIViewController,
                                 presenting: UIViewController?,
                                 source: UIViewController) -> UIPresentationController?
     {
         let presentedHeight: CGFloat = 1.0
         let controller = PresentationController(presentedViewController: presented,
                                                 presenting: presenting,
                                                 backgroundEffect: .dim,
                                                 viewHeight: presentedHeight)
    
         if let vc = presented as? OverlayViewController {
             vc.delegate = controller
         }
         return controller
      }
     }
    
  5. 现在我们应该能够使用 showOverlay() 函数将 presentedHeight 设置为 1.0.dim 背景效果来呈现 SecondViewController .我们可以忽略 SecondViewController 类似于另一个模态演示。