Hand off parent container's pan gesture to nested UICollectionView

主宰稳场 提交于 2019-12-09 12:58:12

问题


I'm trying to build a complex split view container controller that facilitates two variable height containers, each with their own nested view controller. There's a global pan gesture on the parent controller that allows the user to drag anywhere in the view container and slide the "divider" between views up and down. It also has some intelligent position threshold detection logic that will expand either view (or reset the divider position):

     

This works fine. There's also a lot of code to construct this, which I'm happy to share, but I don't think it's relevant, so I'll omit it for the time being.

I'm now trying to complicate things by adding a collection view to the bottom view:

I've been able to work it out so that I can scroll the split view up with a decisive pan gesture, and scroll the collection view with a quick flick of the finger (a swipe gesture, I suppose it is?), but this is a really sub-par experience: you can't pan the view and scroll the collection view at the same time, and expecting a user to consistently replicate similar, yet different gestures in order to control the view is too difficult of an interaction.

To attempt to solve this, I've tried several delegate/protocol solutions in which I detect the position of the divider in the split view and enable/disable canCancelTouchesInView and/or isUserInteractionEnable on the collection view based on whether the bottom view is fully expanded. This works to a point, but not in the following two scenarios:

  1. When the split view divider is in its default position, if the user pans up to where the bottom view is fully expanded, then keeps on panning up, the collection view should begin scrolling until the gesture ends.
  2. When the split view divider is at the top (bottom container view is fully expanded) and the collection view is not at the top, if the user pans down, the collection view should scroll instead of the split view divider moving, until the collection view reaches its top position, at which point the split view should return to its default position.

Here is an animation that illustrates this behavior:

Given this, I'm starting to think the only way to solve the problem is by creating a delegate method on the split view that tells the collection view when the bottom view is at maximum height, which then can intercept the parent's pan gesture or forward the screen touches to the collection view instead? But, I'm not sure how to do that. If I'm on the right track with a solution, then my question is simply: How can I forward or hand off a pan gesture to a collection view and have the collection view interact the same way it would if the touches had been captured by it in the first place? can I do something with pointInside or touches____ methods?

If I can't do it this way, how else can I solve this problem?


Update for bounty hunters: I've had some fragmented luck creating a delegate method on the collection view, and calling it on the split view container to set a property shouldScroll, by which I use some pan direction and positioning information to determine whether or not the scroll view should scroll. I then return this value in UIGestureRecognizerDelegate's gestureRecognizer:shouldReceive touch: delegate method:

// 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
        }
    }
}

This works OK for when you begin scrolling, but doesn't perform well once scrolling is enabled on the collection view, because the scroll gesture almost always overrides the pan gesture. I'm wondering if I can wire something up with gestureRecognizer:shouldRecognizeSimultaneouslyWith:, but I'm not there yet.


回答1:


What about making the child view for bottom view actually takes up the entire screen and set the collection view's contentInset.top to top view height. And then add the other child view controller above the bottom view. Then the only thing you need to do is make the parent view controller the delegate to listen to the bottom view's collection view's scroll offset and change the top view's position. No complicated gesture recognizer stuff. Only one scroll view(collection view)

Update: Try this!!

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:


You can't "hand off" a gesture, because the gesture recognizer remains the same object and its view is unvarying — it's the view to which the gesture recognizer is attached.

However, nothing stops you from telling some other view what to do in response to a gesture. The collection view is a scroll view, so you know how it is being scrolled at every instant and can do something else in parallel.




回答3:


You should be able to achieve what you're looking for with a single collection view using UICollectionViewDelegateFlowLayout. If you need any special scrolling behavior for your top view such as parallax, you can still achieve that in a single collection view by implementing a custom layout object that inherits from UICollectionViewLayout.

Using the UICollectionViewDelegateFlowLayout approach is a little more straightforward than implementing a custom layout, so if you want to give that a shot, try the following:

  • Create your top view as a subclass of UICollectionViewCell and register it with your collection view.

  • Create your "divider" view as a subclass of UICollectionViewCell and register it with your collection view as a supplementary view using func register(_ viewClass: AnyClass?, forSupplementaryViewOfKind elementKind: String, withReuseIdentifier identifier: String)

  • Have your collection view controller conform to UICollectionViewDelegateFlowLayout, create a layout object as an instance of UICollectionViewFlowLayout assign your collection view controller as the delegate of your flow layout instance, and init your collection view with your flow layout.

  • Implement optional func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize returning the desired size of each of your diffrent views in your collecton view controller.



来源:https://stackoverflow.com/questions/46246763/hand-off-parent-containers-pan-gesture-to-nested-uicollectionview

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!