I have a collection view and want to run some things when it is reloaded by the user. This reloading is caused by the user dragging down on the view until the reload icon comes out. Putting this function at anything else other than when the user does an interaction to reload the collection, will make this an unfeasible solution for me.
What about the following solution. I implemented it for collection-, table- and scroll views, in case other people have similar issues:
final class ScrollViewRefreshDemo: UIViewController {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
let tableView = UITableView()
let scrollView = UIScrollView()
override func viewDidLoad() {
super.viewDidLoad()
attachRefreshListener(to: collectionView)
attachRefreshListener(to: tableView)
attachRefreshListener(to: scrollView)
}
func attachRefreshListener(to scrollView: UIScrollView) {
scrollView.refreshControl?.addTarget(self, action: #selector(scrollViewDidRefresh), for: .valueChanged)
}
#objc
func scrollViewDidRefresh() {
// Execute any task while the scroll view is refreshing
}
}
I have a view controller which has a container view.
I would like to add a UICollectionViewController as a child view controller i.e., add the view of the UICollectionViewController as a subview of the container view and call didMove(toParent:).
I would like the height of the container view to be dynamic so that it depends on the height of the UICollectionViewController's view. I want the height UICollectionView to be equal to its content height.
Can anyone point out how to do this?
This is what I have done till now-
While adding the UICollectionViewController, set translatesAutoresizingMaskIntoConstraints to false, as given in https://stackoverflow.com/a/35431534.
let viewController = CustomCollectionViewController()
viewController.view.translatesAutoresizingMaskIntoConstraints = false
addChild(viewController)
containerView.addSubview(viewController.view)
viewController.didMove(toParent: self)
viewController.view.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
viewController.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
viewController.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
viewController.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true
I created a custom UICollectionView such that its height depends on its content size as given in https://stackoverflow.com/a/49297382.
class CustomCollectionView: UICollectionView {
override func reloadData() {
super.reloadData()
invalidateIntrinsicContentSize()
}
override var intrinsicContentSize: CGSize {
return collectionViewLayout.collectionViewContentSize
}
}
Set the above custom UICollectionView as the collectionView of the CustomCollectionViewController using the answer given in https://stackoverflow.com/a/41404946
class CustomCollectionViewController: UICollectionViewController {
override func loadView() {
collectionView = CustomCollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
}
}
What I have done does not work. Can anyone point out how to do this?
I attempted a bunch of answers on here with no luck.
I am attempting to reload my collectionview within my custom cell after fetching data from firebase. I am unable to even get the print statement to show from the custom cell. Not sure what I'm doing wrong.
On my ViewController I have the following delegate and call, I made a button to test to see if the delegate print statement on my custom cell will appear but no luck.
protocol DiscoverControllerDelegate {
func didFetchData()
}
class DiscoverController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
let cellId = "cellId"
let headerId = "headerId"
let database = FirebaseData.sharedInstance
var delegate: DiscoverControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
title = "Discover"
collectionView?.backgroundColor = .white
collectionView?.register(CategoriesCell.self, forCellWithReuseIdentifier: cellId)
collectionView?.register(HeaderCell.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: headerId)
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Refresh", style: .plain, target: self, action: #selector(reloadData))
database.retrieveData {
print("fetching data")
self.delegate?.didFetchData()
}
}
#objc func reloadData () {
print("attemping to reload")
self.delegate?.didFetchData()
}
*edit to add on my override init
On my custom cell:
class CategoriesCell: UICollectionViewCell, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout, DiscoverControllerDelegate {
let itemCollectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.backgroundColor = .clear
return collectionView
}()
func didFetchData() {
print("RELOADING NOW!")
itemCollectionView.reloadData()
}
override init(frame: CGRect) {
super.init(frame: frame)
itemCollectionView.dataSource = self
itemCollectionView.delegate = self
itemCollectionView.register(ItemCell.self, forCellWithReuseIdentifier: cellId)
itemCollectionView.reloadData()
createLayout()
}
Many things wrong here. The reason "RELOADING NOW!" never logs is because you never set the delegate in your view controller. If you inspect self.delegate in DiscoverController it is probably nil. However, your entire setup right now seems flawed unless I'm misunderstanding what you're doing.
What does your UI look like? Why does CategoriesCell have a UICollectionView property? It's itemCollectionView property doesn't even have a datasource or delegate set and its not an #IBOutlet yet you're calling reloadData() on it. Not sure what it's even displaying or what you're tyring to do.
I feel like there is probably a lot more wrong here than just a single thing. You probably want to go through some UITableView and UICollectionView tutorials to learn about delegates and datasources in iOS.
I dont know about collection view but I use similar protocol structures with a table view. They are not much different. your protocol should be in the cell class not in the controller.
I've gone through the steps for a table view in the following post:
Firebase update
Maybe you can adapt it for your needs...
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:
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.
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.
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.
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.
I'm trying to add a custom UICollectionViewController in a child UIView and it crashes my app. Is there something wrong with my code or how my CustomCollectionViewController is implemented?
#IBOutlet weak var childView: UIView!
let myCollectionView = CustomCollectionViewController()
//inside viewDidLoad
self.addChildViewController(self.myCollectionView)
self.myCollectionView.didMoveToParentViewController(self)
self.myCollectionView.view.frame = self.childView.bounds<--crashes after executing this line
self.childView.addSubview(self.myCollectionView.view)
self.myCollectionView.delegate = self
This may be a little late, but initialize your CollectionViewController with a layout parameter.
Instead of:
let myCollectionView = CustomCollectionViewController()
use:
let myCollectionView = CustomCollectionViewController.init(collectionViewLayout: UICollectionViewFlowLayout())
That worked for me.