【问题标题】:Handling scroll views with (custom, interactive) view controller presentation and dismissal使用(自定义、交互式)视图控制器呈现和解除处理滚动视图
【发布时间】:2018-10-05 07:19:46
【问题描述】:

我一直在尝试自定义交互式视图控制器演示和关闭(使用 UIPresentationControllerUIPercentDrivenInteractiveTransitionUIViewControllerAnimatedTransitioningUIViewControllerTransitioningDelegate 的组合),并且大部分都可以很好地满足我的需求。

但是,在我阅读过的任何教程或文档中,我还没有找到一种常见的情况,这导致我提出以下问题:

...

当被解除的视图包含 UIScrollView(即 UITableView、UICollectionView、WKWebView 等)时,通过平移手势处理自定义交互式视图控制器解除的正确方法是什么?

...

基本上,我想要的是以下内容:

  1. 视图控制器可以通过向下平移以交互方式关闭。这是许多应用程序中常见的用户体验。

  2. 如果关闭的视图控制器包含(垂直滚动)滚动视图,则向下平移会按预期滚动该视图,直到用户到达顶部,然后滚动停止并发生平移关闭。

  3. 滚动视图应该正常运行。

我知道这在技术上是可能的 - 我在其他应用程序中看到过它,例如 Overcast 和 Apple 自己的音乐应用程序 - 但我无法找到协调我的平移手势的行为与滚动视图的行为。

我自己的大部分尝试都集中在尝试基于其contentOffset.y 有条件地启用/禁用滚动视图(或其关联的平移手势识别器),同时滚动并让视图控制器解除的平移手势识别器从那里接管,但这一直充满问题,我担心我想多了。

我觉得秘密主要在于以下平移手势识别器委托方法:

func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
   // ...
}

我创建了一个简化的示例项目,它应该更清楚地展示场景。非常欢迎任何代码建议!

https://github.com/Darchmare/SlidePanel-iOS

【问题讨论】:

    标签: ios swift uiviewcontroller uiscrollview uipangesturerecognizer


    【解决方案1】:

    解决方案

    • 使用UIScrollView's bounces propertyscrollViewDidScroll(_:) 方法使scrollView 在到达顶部后停止滚动。

      func scrollViewDidScroll(_ scrollView: UIScrollView) {
          scrollView.bounces = (scrollView.contentOffset.y > 10);
      }
      

      别忘了设置scrollView.delegate = self

    • 仅当scrollView 到达顶部时处理panGestureRecognizer - 这意味着当scrollView.contentOffset.y == 0 使用协议时。

      protocol PanelAnimationControllerDelegate {
          func shouldHandlePanelInteractionGesture() -> Bool
      }
      

      视图控制器

      func shouldHandlePanelInteractionGesture() -> Bool {
          return (scrollView.contentOffset.y == 0);
      }
      

      面板交互控制器

      class PanelInteractionController: ... {
      
        var startY:CGFloat = 0
      
        private weak var viewController: (UIViewController & PanelAnimationControllerDelegate)?
      
        @objc func handlePanGestureRecognizer(_ gestureRecognizer: UIPanGestureRecognizer) {
          switch gestureRecognizer.state {
          case .began:
            break
          case .changed:
            let translation    = gestureRecognizer.translation(in: gestureRecognizer.view!.superview!)
            let velocity    = gestureRecognizer.velocity(in: gestureRecognizer.view!.superview)
            let state      = gestureRecognizer.state
      
            // Don't do anything when |scrollView| is scrolling
            if !(viewController?.shouldHandlePanelInteractionGesture())! && percentComplete == 0 {
              return;
            }
      
            var rawProgress    = CGFloat(0.0)
      
            rawProgress    = ((translation.y - startTransitionY) / gestureRecognizer.view!.bounds.size.height)
      
            let progress    = CGFloat(fminf(fmaxf(Float(rawProgress), 0.0), 1.0))
      
            if abs(velocity.x) > abs(velocity.y) && state == .began {
              // If the user attempts a pan and it looks like it's going to be mostly horizontal, bail - we don't want it... - JAC
              return
            }
      
            if !self.interactionInProgress {
              // Start to pan |viewController| down
              self.interactionInProgress = true
              startTransitionY = translation.y;
              self.viewController?.dismiss(animated: true, completion: nil)
            } else {
              // If the user gets to a certain point within the dismissal and releases the panel, allow the dismissal to complete... - JAC
              self.shouldCompleteTransition = progress > 0.2
      
              update(progress)
            }
          case .cancelled:
            self.interactionInProgress = false
            startTransitionY = 0
      
            cancel()
          case .ended:
            self.interactionInProgress = false
            startTransitionY = 0
      
            if self.shouldCompleteTransition == false {
              cancel()
            } else {
              finish()
            }
          case .failed:
            self.interactionInProgress = false
            startTransitionY = 0
      
            cancel()
          default:
            break;
          }
        }
      }
      

    结果

    更多详情可以关注my sample project

    【讨论】:

    • 使用 panGesture 处理滚动视图的方法非常简洁。
    • 如此干净易懂:)
    【解决方案2】:

    对我来说,这段代码解决了我的很多问题,并极大地帮助了我在滚动视图中的自定义转换,它会在尝试开始转换或在顶部显示活动指示器时保持负的滚动视图偏移量。我的猜测是,这至少可以解决您的一些过渡/动画问题:

    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    
        if scrollView.contentOffset.y < -75 {
    
            scrollView.contentInset.top = -scrollView.contentOffset.y
    
        }
        // Do animation or transition
    }
    

    【讨论】: