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:
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.
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?()
}
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With