将 parent 容器的平移手势传递给嵌套的 UICollectionView

Hand off parent container's pan gesture to nested UICollectionView

我正在尝试构建一个复杂的拆分视图容器控制器,它有利于两个可变高度容器,每个容器都有自己的嵌套视图控制器。 parent 控制器上有一个全局平移手势,允许用户拖动视图容器中的任何位置并在视图之间上下滑动 "divider"。它还具有一些智能位置阈值检测逻辑,可以扩展任一视图(或重置分隔线位置):

这很好用。还有很多代码可以构建这个,我很乐意分享,但我认为它不相关,所以我暂时省略它。

我现在试图通过向底部视图添加集合视图来使事情复杂化:

我已经能够解决这个问题,这样我就可以用果断的平移手势向上滚动拆分视图,并用快速轻弹手指滚动集合视图(我想是滑动手势) ?),但这是真正的 sub-par 体验:您不能同时平移视图和滚动集合视图,并期望用户始终如一地复制相似但不同的手势来控制视图太难互动了

为了尝试解决这个问题,我尝试了几种 delegate/protocol 解决方案,其中我检测了拆分视图中分隔线的位置,并且 enable/disable canCancelTouchesInView and/or isUserInteractionEnable 基于底部视图是否完全展开的集合视图。这在一定程度上有效,但在以下两种情况下无效:

  1. 当拆分视图分隔线处于默认位置时,如果用户向上平移到底部视图完全展开的位置,然后继续向上平移,集合视图应开始滚动直到手势结束。
  2. 当拆分视图分隔符位于顶部(底部容器视图已完全展开)并且集合视图位于顶部时,如果用户向下平移,集合视图视图应滚动而不是移动拆分视图分隔线,直到集合视图到达其顶部位置,此时拆分视图应 return 到其默认位置。

这是一个演示此行为的动画:

鉴于此,我开始认为解决问题的唯一方法是在拆分视图上创建一个委托方法,当底部视图处于最大高度时通知集合视图,然后它可以拦截parent 的平移手势或将屏幕触摸转发到集合视图?但是,我不确定该怎么做。如果我的解决方案走在正确的轨道上,那么我的问题很简单:如何将平移手势转发或移交给集合视图,并让集合视图以与如果它首先捕获了触摸? 我可以用 pointInsidetouches____ 方法做点什么吗?

如果我不能这样做,我还能如何解决这个问题?


赏金猎人的更新:我在集合视图上创建了一个委托方法,并在拆分视图容器上调用它来设置 属性 shouldScroll,通过它我使用一些平移方向和定位信息来确定滚动视图是否应该滚动。然后我在 UIGestureRecognizerDelegategestureRecognizer:shouldReceive touch: 委托方法中 return 这个值:

// protocol delegate
protocol GalleryCollectionViewDelegate {
    var shouldScroll: Bool? { get }
}

// shouldScroll property
private var _shouldScroll: Bool? = nil
var shouldScroll: Bool {
    get {
        // Will attempt to retrieve delegate value, or self set value, or return false
        return self.galleryDelegate?.shouldScroll ?? self._shouldScroll ?? false
    }
    set {
        self._shouldScroll = newValue
    }
}

// UIGestureRecognizerDelegate method
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
    return shouldScroll
}

// ----------------
// Delegate property/getter called on the split view controller and the logic:
var shouldScroll: Bool? {
    get {
        return panTarget != self
    }
}

var panTarget: UIViewController! {
    get {
        // Use intelligent position detection to determine whether the pan should be
        // captured by the containing splitview or the gallery's collectionview
        switch (viewState.currentPosition,
                viewState.pan?.directionTravelled,
                galleryScene.galleryCollectionView.isScrolled) {
        case (.top, .up?, _), (.top, .down?, true): return galleryScene
        default: return self
        }
    }
}

这在您开始滚动时工作正常,但在集合视图上启用滚动后效果不佳,因为滚动手势几乎总是覆盖平移手势。我想知道我是否可以用 gestureRecognizer:shouldRecognizeSimultaneouslyWith: 连接一些东西,但我还没有。

您不能 "hand off" 手势,因为手势识别器保持相同的对象并且其 view 不变 — 这是手势识别器所附加的视图。

但是,没有什么能阻止您告诉其他视图如何响应某个手势。集合视图是一个滚动视图,所以你知道它是如何每时每刻滚动的,并且可以并行地做其他事情。

您应该能够使用 UICollectionViewDelegateFlowLayout 通过单个集合视图实现您正在寻找的内容。如果您的顶视图需要任何特殊的滚动行为,例如视差,您仍然可以通过实现继承自 UICollectionViewLayout.

的自定义布局对象在单个集合视图中实现。

使用 UICollectionViewDelegateFlowLayout 方法比实现自定义布局更直接一些,因此如果您想尝试一下,请尝试以下方法:

  • 创建您的顶部视图作为 UICollectionViewCell 的子类并将其注册到您的集合视图。

  • 创建您的 "divider" 视图作为 UICollectionViewCell 的子类,并使用 func register(_ viewClass: AnyClass?, forSupplementaryViewOfKind elementKind: String, withReuseIdentifier identifier: String)

    [将其作为补充视图注册到您的集合视图中=36=]
  • 让您的集合视图控制器符合 UICollectionViewDelegateFlowLayout,创建一个布局对象作为 UICollectionViewFlowLayout 的实例,将您的集合视图控制器指定为流布局实例的委托,并使用流布局初始化您的集合视图。

  • 实施 optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize 在您的 collection 视图控制器中返回每个不同视图的所需大小。

如何使底部视图的 child 视图实际占据整个屏幕并将 collection 视图的 contentInset.top 设置为顶部视图高度。然后在底部视图上方添加另一个 child 视图控制器。然后你唯一需要做的就是让 parent 视图控制器成为代理来监听底部视图的 collection 视图的滚动偏移并改变顶部视图的位置。没有复杂的手势识别器的东西。只有一个滚动视图(collection 视图)

更新:试试这个!!

import Foundation
import UIKit

let topViewHeight: CGFloat = 250

class SplitViewController: UIViewController, BottomViewControllerScrollDelegate {

    let topViewController: TopViewController = TopViewController()
    let bottomViewController: BottomViewController = BottomViewController()

    override func viewDidLoad() {
        super.viewDidLoad()

        automaticallyAdjustsScrollViewInsets = false

        bottomViewController.delegate = self
        addViewController(bottomViewController, frame: view.bounds, completion: nil)
        addViewController(topViewController, frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: topViewHeight), completion: nil)
    }

    func bottomViewScrollViewDidScroll(_ scrollView: UIScrollView) {
        print("\(scrollView.contentOffset.y)")

        let offset = (scrollView.contentOffset.y + topViewHeight)
        if offset < 0 {
            topViewController.view.frame.origin.y = 0
            topViewController.view.frame.size.height = topViewHeight - offset
        } else {
            topViewController.view.frame.origin.y = -(scrollView.contentOffset.y + topViewHeight)
            topViewController.view.frame.size.height = topViewHeight
        }
    }
}

class TopViewController: UIViewController {

    let label = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()

        automaticallyAdjustsScrollViewInsets = false
        view.backgroundColor = UIColor.red

        label.text = "Top View"
        view.addSubview(label)
    }

    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        label.sizeToFit()
        label.center = view.center
    }
}

protocol BottomViewControllerScrollDelegate: class {
    func bottomViewScrollViewDidScroll(_ scrollView: UIScrollView)
}

class BottomViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {

    var collectionView: UICollectionView!

    weak var delegate: BottomViewControllerScrollDelegate?

    let cellPadding: CGFloat = 5

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = UIColor.yellow
        automaticallyAdjustsScrollViewInsets = false

        let layout = UICollectionViewFlowLayout()
        layout.minimumInteritemSpacing = cellPadding
        layout.minimumLineSpacing = cellPadding
        layout.scrollDirection = .vertical
        layout.sectionInset = UIEdgeInsets(top: cellPadding, left: 0, bottom: cellPadding, right: 0)

        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        collectionView.contentInset.top = topViewHeight
        collectionView.scrollIndicatorInsets.top = topViewHeight
        collectionView.alwaysBounceVertical = true
        collectionView.backgroundColor = .clear
        collectionView.dataSource = self
        collectionView.delegate = self
        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: String(describing: UICollectionViewCell.self))
        view.addSubview(collectionView)
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 30
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: UICollectionViewCell.self), for: indexPath)
        cell.backgroundColor = UIColor.darkGray
        return cell
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let width = floor((collectionView.frame.size.width - 2 * cellPadding) / 3)
        return CGSize(width: width, height: width)
    }

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        delegate?.bottomViewScrollViewDidScroll(scrollView)
    }
}

extension UIViewController {

    func addViewController(_ viewController: UIViewController, frame: CGRect, completion: (()-> Void)?) {
        viewController.willMove(toParentViewController: self)
        viewController.beginAppearanceTransition(true, animated: false)
        addChildViewController(viewController)
        viewController.view.frame = frame
        viewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.addSubview(viewController.view)
        viewController.didMove(toParentViewController: self)
        viewController.endAppearanceTransition()
        completion?()
    }
}