Swift - UICollectionView pagination within self sizing UITableViewCell - ios

I am implementing self sizing UITableViewCell containing UICollectionView which extends its height based on the number of items received from API.
The views are structured like this
UITableView
UITableViewCell
UICollectionView
UICollectionViewCell
UICollectionViewCell
UICollectionViewCell
When the UICollectionView receives new data, UITableViewCell frame is updated with the following code
layoutIfNeeded()
collectionView.reloadData()
collectionView.collectionViewLayout.invalidateLayout()
collectionView.layoutIfNeeded()
collectionView.snp.updateConstraints { make in
make.height.equalTo(collectionView.collectionViewLayout.collectionViewContentSize).priority(.high)
}
The problematic part is adding pagination on scroll to the bottom of the UICollectionView. The self sizing code causes that willDisplay cell is called for each cell of the UICollectionView causing // Fetch more data request to trigger, which fetches new data, updates UICollectionView height, triggering the API request again, and it's an infinite loop.
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.item == collectionView.numberOfItems(inSection: indexPath.section) - 1 {
// Fetch more data
}
}
Any idea how to avoid this cycle? Thanks.

Related

UICollectionView cell creation bypassed `willDisplay cell:` call?

I have an UICollectionView with .layer.masksToBounds set to false.
I do this so that way I can achieve a fade-out effect of any cells that scroll out of the collection view's proper bounds.
In order to ensure that I don't display out of bounds cells (when the UICollectionView loads them in), I use the delegate callback:
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// pseudo code
if !cell.frame.intersects(collectionView.frame) {
cell.alpha = 0
}
}
However, it appears this callback is not invoked when the CollectionView populates an out-of-bound cell?
While obviously I am always informed if a cell is constructed through the cell provider function (i.e. the function where I invoke the dequeueReusableCell call), I cannot determine whether the cell I construct is out of bounds.
Is there any way I can be informed of when the UICollectionView constructs an out-of-bounds cell?

Keep UICollectionView position after a reload using dynamic cell's height

I have a vertical UICollectionView with flow layout and I'm trying to implement an infinite scroll behaviour. When the view comes near to the end, the api request 10 more registers on the server side and then I update the collection data. But when I do that my table don't keeps on the same cell I was before the reload. How can I keep the position after a reload?
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.row == viewModel.numberOfItemsInSection - 3 {
self.viewModel.fetchNextRepositoriesPage() { shouldUpdateData in
self.repositoriesListView.repositoriesCollectionView.reloadData()
self.repositoriesListView.layoutIfNeeded()
}
}
}
private func reloadCollectionData() {
let offset = collectionView.contentOffset
collectionView.reloadData()
view.layoutIfNeeded()
collectionView.contentOffset = offset
}
EDIT:
I noticed that this problem only occurs when I use estimadedItemSize on my collectionView. When the itemSize is a constant the problem does not occur.

Nested collectionView in a TableView

I've been successfully nesting collections views into tableview for a while now.
What I still don't know how to do, is to do it while respecting the MVC pattern?
Right now I declare my tableview and its cells and in the cell (where the collectionView sits), I attach my collectionView (I got 1 per cell) and do the data mapping. It works, but it's spaghetti code where my View is acting like a controller.
I tried a few times to respect the MVC patterns. I can get my controller to control both my tableview and my collection. Where I struggle is to tell my collection View delegate which data it should pick as all I have as reference is the indexPath (of the collectionView) but not in which tableView that specific collectionView sits.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! CollectionViewCell
return cell
}
The delegate only gives me the indexPath of the cell not which collectionView it is. To use a concrete example - let's assume that my tableview cells represents messages and that each message has a collectionView that controls reactions (like Discord). How do I tell my collectionView delegate which Message it is linked to?
Thanks a lot for the help!
This is very simple. Every view and its subclass has a property tag. You must have an IBOutlet or a simple reference to the CollectionView that sits in the TableViewCell. When you dequeue the TableViewCell just set the tag of your CollectionView equal to the indexPath.row of your tableViewCell like this:
tableViewCell.collectionView.tag = indexPath.row
Then in the UICollectionViewDataSource or UICollectionViewDelegate methods you may find which collectionView it is in other words which tableViewCell does this collectionView sits in. Here's an example:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! CollectionViewCell
let dataObject = dataSourceArray[indexPath.row] as! YourDataModelObject
// Here you may set any property of the collectionView cell
return cell
}
P.S: It is not necessary to respect MVC to that extent. You may manage your CollectionView from the TableViewCell. Have a look at this example.

Swift - separate views have subviews that are sharing the same memory?

new to swift. I have a nested CollectionView from one viewcontroller. The main viewcontroller has 7 collectionviewcell ("Level1Cell" in the code below). Each time I click a button or trigger an event, I want the collectionView to reload with the new data.
func eventHandler() {
// updates data
myCollectionView.reloadData()
}
Then, after it calls reload, it will call the reload again on each of the the nested CollectionViewCell.
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Level1Cell", for: indexPath) as! Level1Cell
cell.appsCollectionView.reloadData()
return cell
}
The problem is, let say I want to, for the first cell, set a particular row some text.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if(self.index == 0 && indexPath.row == 30){
rightCell.textLabel.text = "asdasd"
}
The fourth "Level1Cell" cell somehow has its label set also at the 30th row, but not the second and third. After stepping through the debugger, I realize that the cells, after reloading, the fourth cell "Level1Cell" is set to have the the same memory address as the first cell ( why does reload do this - shouldn't it allocate a new memory for each "Level1Cell"? - how can I get around this). Also, should I not use reload to update the data in the view and nested view of those from the view controller?
Thanks!
UIcollectionview will reuse cells. You must provide needed data for row in method
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
Or just clear previous data at cell.

UICollectionViewCell inside UICollectionView

I am trying to place UICollectionView inside UICollectionViewCell. I have prepared my cell in xib file using autolayout, same for cells inside collectionView.
But what about autolayout?
Some of my UICollectionViews inside my cell are different size that the others because of its content so I fail using sizeForItemAt indexPath: to calculate proper size.
Is there any fairly simple way to do this using autolayout, different from changing this main UICollectionView to UITableView with UITableViewAutomaticDimension as a heightForRowAt indexPath:?
EDIT:
To explain everything i've done so far to make this work i'll provide a cell layout:
and some algorithm to calculate cell's size:
First of all i declared an array of cells heights inside my UIViewController subclass like so:
fileprivate var cellsHeight: [CGFloat] = [].
After that inside UICollectionView delegate method collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell i am populating this array with height of each individual collectionView content size like so:
let cell = collectionView.dequeueReusableCell...
if cell.collectionView != nil {
cellsHeight.append(cell.collectionView.collectionViewLayout.collectionViewContentSize.height)
}
And then in inside collectionView(_ collectionView: UICollectionView, sizeForItemAt indexPath: IndexPath) -> CGSize i am calculating each label size using boundingRect method, adding up some padding and previously fetched collectionView height.
But i think this is a pretty overkill.

Resources