Send parent gesture of container container to nested UICollectionView

I am trying to create a complex container controller with a split view that facilitates two containers with a variable height, each with its own nested view controller. There is a global gesture on the parent controller that allows the user to drag views anywhere in the container and move the “separator” between the views up and down. It also has some intelligent logic for determining the position threshold, which will expand either the view (or reset the position of the delimiter):

5qbWm.png4qDXc.png

It works great. There is also a lot of code to create this, which I am happy to pass on, but I do not think it is relevant, so I will not omit it for now.

Now I'm trying to complicate the situation by adding a collection view to the bottom view:

rhtxH.png

I was able to process it so that I could scroll through the split view with decisive hard gestures and scroll through the collection view with a swift movement of the finger (swipe gesture, I suppose it is?), But it's really a sub-pairing experience: you can't pan the view and scroll the view collections at the same time, expecting the user to play similar, but different gestures in order to control the presentation, interaction is too difficult.

To try to solve this problem, I tried several solutions for delegates and protocols in which I found the position of the separator in split mode and turned on / off canCancelTouchesInView and / or isUserInteractionEnable in the collection view based on whether the bottom view is fully opened. This works at a specific point, but not in the following two scenarios:

  • When the split view separator is in the default position, if the user clicks to the point where the bottom view is fully expanded, then continues panning, viewing the collection should begin by scrolling until the gesture ends.
  • When the split view separator is at the top (the bottom container view is fully expanded) and the collection view is not up, if the user turns down, the collection view should scroll, instead of the split view separator, move until the collection view reaches its upper position, after which the split the view should return to its default position.

Here is an animation that illustrates this behavior:

enter image description here

With this in mind, I'm starting to think that the only way to solve the problem is to create a delegate method on a split view that reports on the collection view when the bottom view is at maximum height and then can intercept the parent panning gesture or will the screen touch the collection forward? But I do not know how to do this. If I am on the right track with a solution, my question is simple: How can I send or transmit a panorama gesture as a collection, and viewing the collection will be the same as if it was captured by him in the first place? Is there anything you can do with the pointInside or touches____ ?

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


Update for bounty hunters: I had some fragmented luck creating a delegate method in the collection view and calling it in a container with a split view to set the shouldScroll property, with which I use some information about the pan and position direction to determine the scroll scroll. Then I return this value to the UIGestureRecognizerDelegate gestureRecognizer:shouldReceive touch: delegation 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 collectionview switch (viewState.currentPosition, viewState.pan?.directionTravelled, galleryScene.galleryCollectionView.isScrolled) { case (.top, .up?, _), (.top, .down?, true): return galleryScene default: return self } } } 

This works fine when you start scrolling, but it doesn't work well once scrolling is enabled in the collection view, because the scroll gesture almost always cancels the pan gesture. I am wondering if I can connect something using gestureRecognizer:shouldRecognizeSimultaneouslyWith: but I'm not there yet.

+5
source share
3 answers

How about the child view for the bottom view actually occupying the entire screen and setting the collection view collectionInset.top to a higher height. Then add another child view controller above the bottom view. Then the only thing you need to do is make the parent view controller a delegate in order to listen to the scroll offset in the bottom view and change the position of the top view. No sophisticated gesture recognition tools. Only one scroll view (collection view)

enter image description here

Update: try it!

 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
source

You cannot “cancel” a gesture because the gesture recognizer remains the same object and its view is unchanged - this is the view to which the gesture recognizer is attached.

However, nothing prevents you from telling about another vision of what to do in response to a gesture. A collection view is a scroll view, so you know how it scrolls at every moment in time and can do something else in parallel.

+3
source

You can achieve what you are looking for with one kind of collection using the UICollectionViewDelegateFlowLayout . If you need any special scroll behavior for your top view, such as parallax, you can still achieve this in one view of the collection by implementing a special layout object that inherits from UICollectionViewLayout .

Using the UICollectionViewDelegateFlowLayout approach UICollectionViewDelegateFlowLayout little more straight forward than implementing a custom layout, so if you want to give this snapshot, try the following:

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

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

  • Follow your collection view controller UICollectionViewDelegateFlowLayout , create a layout object as an instance of UICollectionViewFlowLayout , UICollectionViewFlowLayout your collection view controller as a delegate to your stream layout instance, and start viewing the collection using the stream location.

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

+1
source

Source: https://habr.com/ru/post/1271855/


All Articles