拖动以关闭 UIPresentationController

Posted

技术标签:

【中文标题】拖动以关闭 UIPresentationController【英文标题】:Drag to dismiss a UIPresentationController 【发布时间】:2020-10-25 09:28:45 【问题描述】:

我制作了一个适合任何视图控制器的 UIPresentationController,并使用此 tutorial 显示在屏幕的一半上。现在我很想添加阻力来消除这一点。我试图让拖动感觉自然且响应灵敏,就像 Apple ios 13 股票应用程序上“热门故事”的拖动体验一样。我认为 iOS 13 模态拖动关闭会被延续,但它不会传递到这个控制器,但它不会。

我发现的每一段代码和教程都有糟糕的拖动体验。有谁知道如何做到这一点?过去一周我一直在尝试/寻找。提前谢谢你

这是我的演示控制器代码

class SlideUpPresentationController: UIPresentationController 
    // MARK: - Variables
    private var dimmingView: UIView!
    
    //MARK: - View functions
    override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) 
        super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
        setupDimmingView()
    
    
    override func containerViewWillLayoutSubviews() 
      presentedView?.frame = frameOfPresentedViewInContainerView
    
    
    override var frameOfPresentedViewInContainerView: CGRect 
        guard let container = containerView else  return super.frameOfPresentedViewInContainerView 
        let width = container.bounds.size.width
        let height : CGFloat = 300.0
        
        return CGRect(x: 0, y: container.bounds.size.height - height, width: width, height: height)
    
    
    override func presentationTransitionWillBegin() 
        guard let dimmingView = dimmingView else  return 
        
        containerView?.insertSubview(dimmingView, at: 0)
      
      NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "V:|[dimmingView]|",
                                                                 options: [],
                                                                 metrics: nil,
                                                                 views: ["dimmingView": dimmingView]))
      
      NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|[dimmingView]|",
                                                                 options: [],
                                                                 metrics: nil,
                                                                 views: ["dimmingView": dimmingView]))
      
      guard let coordinator = presentedViewController.transitionCoordinator else 
        dimmingView.alpha = 1.0
        return
      
      
      coordinator.animate(alongsideTransition:  _ in
        self.dimmingView.alpha = 1.0
      )
    
    
    override func dismissalTransitionWillBegin() 
      guard let coordinator = presentedViewController.transitionCoordinator else 
        dimmingView.alpha = 0.0
        return
      
      
      coordinator.animate(alongsideTransition:  _ in
        self.dimmingView.alpha = 0.0
      )
    
    
    func setupDimmingView() 
      dimmingView = UIView()
      dimmingView.translatesAutoresizingMaskIntoConstraints = false
      dimmingView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
      dimmingView.alpha = 0.0
      
      let recognizer = UITapGestureRecognizer(target: self,
                                              action: #selector(handleTap(recognizer:)))
      dimmingView.addGestureRecognizer(recognizer)
    
    
    @objc func handleTap(recognizer: UITapGestureRecognizer) 
      presentingViewController.dismiss(animated: true)
    

【问题讨论】:

不太清楚你想做什么,你能解释一下吗?谢谢 【参考方案1】:

由于您对您想要的拖动体验的描述不是很清楚,希望我没有误会您。

我正在尝试让拖动感觉自然且响应迅速,就像 Apple iOS 13 股票应用中“热门故事”的拖动体验一样。

我得到的是,您希望能够拖动呈现的视图,如果它达到某个点则将其关闭,否则返回其原始位置(因为您可以将视图带到您想要的任何位置)。 为此,我们可以在presentedViewController中添加一个UIPanGesture,然后

    根据手势移动presentedView

    关闭/移回呈现的视图

     class SlideUpPresentationController: UIPresentationController 
         // MARK: - Variables
         private var dimmingView: UIView!
         private var originalX: CGFloat = 0
    
         //MARK: - View functions
         override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) 
             super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
             setupDimmingView()
         
    
         override func containerViewWillLayoutSubviews() 
             presentedView?.frame = frameOfPresentedViewInContainerView
         
    
         override var frameOfPresentedViewInContainerView: CGRect 
             guard let container = containerView else  return super.frameOfPresentedViewInContainerView 
             let width = container.bounds.size.width
             let height : CGFloat = 300.0
    
             return CGRect(x: 0, y: container.bounds.size.height - height, width: width, height: height)
         
    
         override func presentationTransitionWillBegin() 
             guard let dimmingView = dimmingView else  return 
    
             containerView?.insertSubview(dimmingView, at: 0)
             // add PanGestureRecognizer for dragging the presented view controller
             let viewPan = UIPanGestureRecognizer(target: self, action: #selector(viewPanned(_:)))
             containerView?.addGestureRecognizer(viewPan)
    
             NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "V:|[dimmingView]|", options: [], metrics: nil, views: ["dimmingView": dimmingView]))
    
             NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|[dimmingView]|", options: [], metrics: nil, views: ["dimmingView": dimmingView]))
    
             guard let coordinator = presentedViewController.transitionCoordinator else 
                 dimmingView.alpha = 1.0
                 return
             
    
             coordinator.animate(alongsideTransition:  _ in
                 self.dimmingView.alpha = 1.0
             )
         
    
         @objc private func viewPanned(_ sender: UIPanGestureRecognizer) 
             // how far the pan gesture translated
             let translate = sender.translation(in: self.presentedView)
             switch sender.state 
                 case .began:
                     originalX = presentedViewController.view.frame.origin.x
                 case .changed:
                     // move the presentedView according to pan gesture
                     // prevent it from moving too far to the right
                     if originalX + translate.x < 0 
                         presentedViewController.view.frame.origin.x = originalX + translate.x
                     
                 case .ended:
                     let presentedViewWidth = presentedViewController.view.frame.width
                     let newX = presentedViewController.view.frame.origin.x
    
                     // if the presentedView move more than 0.75 of the presentedView's width, dimiss it, else bring it back to original position
                     if presentedViewWidth * 0.75 + newX > 0 
                         setBackToOriginalPosition()
                      else 
                         moveAndDismissPresentedView()
                     
                 default:
                     break
             
         
    
         private func setBackToOriginalPosition() 
             // ensure no pending layout change in presentedView
             presentedViewController.view.layoutIfNeeded()
             UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseIn, animations: 
                 self.presentedViewController.view.frame.origin.x = self.originalX
                 self.presentedViewController.view.layoutIfNeeded()
             , completion: nil)
         
    
         private func moveAndDismissPresentedView() 
             // ensure no pending layout change in presentedView
             presentedViewController.view.layoutIfNeeded()
             UIView.animate(withDuration: 0.25, delay: 0.0, options: .curveEaseIn, animations: 
                 self.presentedViewController.view.frame.origin.x = -self.presentedViewController.view.frame.width
                 self.presentedViewController.view.layoutIfNeeded()
             , completion:  _ in
                 // dimiss when the view is completely move outside the screen
                 self.presentingViewController.dismiss(animated: true, completion: nil)
             )
          
    
         override func dismissalTransitionWillBegin() 
             guard let coordinator = presentedViewController.transitionCoordinator else 
                 dimmingView.alpha = 0.0
                 return
             
    
             coordinator.animate(alongsideTransition:  _ in
                 self.dimmingView.alpha = 0.0
             )
         
    
         func setupDimmingView() 
             dimmingView = UIView()
             dimmingView.translatesAutoresizingMaskIntoConstraints = false
             dimmingView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
             dimmingView.alpha = 0.0
    
             let recognizer = UITapGestureRecognizer(target: self,
                                           action: #selector(handleTap(recognizer:)))
             dimmingView.addGestureRecognizer(recognizer)
         
    
        @objc func handleTap(recognizer: UITapGestureRecognizer) 
            presentingViewController.dismiss(animated: true)
        
    
     
    

以上代码只是基于您提供的代码的示例,但我希望能够解释您所谓的拖动体验背后发生的事情。希望这会有所帮助;)

这是示例结果:

via GIPHY

【讨论】:

很棒的体验!您可以将其调整为垂直阴暗(向下滑动以关闭)吗?我正在尝试调整代码,但遇到了问题

以上是关于拖动以关闭 UIPresentationController的主要内容,如果未能解决你的问题,请参考以下文章

如何检测弹出窗口中的图片已关闭(向下拖动以关闭)?

如何使用 ObjectC 向下拖动以关闭模式

如何实现“向右拖动以关闭”导航堆栈中的视图控制器?

如何让jqueryEasyUI里面的dialog弹出层跳出iframe框架在最外层显示以防止拖动时部门被遮住之后无法拖动

如何在IOS中绘制一个圆圈并移动它/拖动它? [关闭]

以编程方式更新桌面“拖动时显示窗口内容”设置