【问题标题】:Hand off parent container's pan gesture to nested UICollectionView将父容器的平移手势移交给嵌套的 UICollectionView
【发布时间】:2018-02-25 01:41:39
【问题描述】:

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

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

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

我已经解决了这个问题,这样我就可以用一个决定性的平移手势向上滚动拆分视图,并用手指快速滑动来滚动集合视图(我想是滑动手势?) ,但这是一个非常低级的体验:您不能同时平移视图和滚动集合视图,并且期望用户始终如一地复制相似但不同的手势以控制视图太难了互动。

为了解决这个问题,我尝试了几种委托/协议解决方案,在这些解决方案中,我检测拆分视图中分隔线的位置,并根据底部视图是否完全展开。这在一定程度上有效,但不适用于以下两种情况:

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

这是一个说明这种行为的动画:

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

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


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

// 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: 联系,但我还没有。

【问题讨论】:

  • 你能分享你的代码来测试这个行为吗?在您提到的两种情况中的任何一种中,collectionView 似乎都应该使用触摸事件,直到其水平偏移量达到其边界,这似乎相当简单。
  • @Lukas 我可以,但我不确定它是否真的有助于解决问题 - 既是因为在 IB 中内置了很多具有 IB 约束的功能,而且因为您已经知道我有检测委托完全按照您描述的方式触发的方法:“当水平偏移量达到其上限时,collectionView 应该使用触摸事件。”不过,如果你想要一些代码,我可以添加一些 ¯\_(ツ)_/¯

标签: ios iphone swift uiscrollview uipangesturerecognizer


【解决方案1】:

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

更新:试试这个!!

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?()
    }
}

【讨论】:

  • 听起来是一个很有前途的想法......如果你在代码中尝试一下它可能值得赏金?
【解决方案2】:

您不能“交出”手势,因为手势识别器仍然是同一个对象,并且它的 view 是不变的——它是手势识别器所附加到的视图。

但是,没有什么能阻止您告诉其他视图如何响应手势。集合视图是一个滚动视图,因此您可以随时了解它是如何滚动的,并且可以并行执行其他操作。

【讨论】:

  • 这实际上是我早期的尝试之一——检测手势并使用集合视图(滚动视图)的内容偏移量来处理它。然而,在这样做的过程中,我牺牲了集合视图“反弹”的能力,并且交互感觉非常死气沉沉。
【解决方案3】:

您应该能够通过使用UICollectionViewDelegateFlowLayout 的单个集合视图来实现您正在寻找的东西。如果您的顶视图需要任何特殊的滚动行为(例如视差),您仍然可以通过实现从 UICollectionViewLayout 继承的自定义布局对象在单个集合视图中实现。

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

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

  • 将您的“分隔线”视图创建为UICollectionViewCell 的子类,并使用func register(_ viewClass: AnyClass?, forSupplementaryViewOfKind elementKind: String, withReuseIdentifier identifier: String) 将其作为补充视图注册到您的集合视图中

  • 让您的集合视图控制器符合UICollectionViewDelegateFlowLayout,创建一个布局对象作为UICollectionViewFlowLayout 的实例,将您的集合视图控制器指定为您的流程布局实例的委托,并使用您的流程初始化您的集合视图布局。

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

【讨论】:

  • 在实践中这听起来效果很好,但在我的情况下,我使用视图控制器容器作为拆分控制器 - 顶部和底部都是独立的视图控制器。我不完全有信心可以连接这样的流程布局并使用容器......
  • 好的,在这种情况下,我会在容器控制器的视图中添加一个平移手势识别器。然后在其处理程序中执行以下两项操作之一:1) 如果平移手势的状态为.changed,则根据容器视图中的翻译更新每个子视图控制器的视图帧。 2) 如果平移手势的状态为.ended,则根据平移方向和您可能决定设置的最小平移阈值显示或隐藏您的收藏视图。
猜你喜欢
  • 2014-07-09
  • 2015-08-07
  • 1970-01-01
  • 2021-11-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-10-20
  • 1970-01-01
相关资源
最近更新 更多