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.
Related
SO I have two UICollectionViews in my UIViewController in Storyboard and both are linked with delegate and datasource to my ViewController. All the associated UICollectionView delegate methods are implemented and checks for the UICollectionViews are implemented. But it's so frustrating that one UICollectionView is getting catered while the other one is getting completely ignored. I have scratched my head in all the available aspects but it is kind of putting me further towards the edge, please help.
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if collectionView == self.variantsCollectionView {
// let count = (item?.variant_groups?.count)!
return 1
} else {
return 2//(item?.extra_groups?.count)!
}
}
and
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell{
if collectionView == self.variantsCollectionView {
//IT DOESNT EVEN COME HERE AT ALL
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell_variant", for: indexPath)
return cell
} else {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
//HERE IT COMES ALWAYS FOR THE NUMBER OF CELLS
return cell
}
}
Whereas the UICollectionViews are connected like this:
and:
Please please help. Thank you so much
Via comments the TS found solution by following these steps:
Ensure both collection views have non-nil data sources (and delegates).
Check that data source methods are executed for both collection view.
Check that both collection views' cells have valid size.
Finally the problem was found after checking the heights of each collection view inside stack view.
basically CollectionView has a specific height whereas
VariantCollectionView didnt, and both were in a stackView. When first
was created in view it took up the entire size where as the other one
kind of actually disappeared. Hence the issue.
I have 5 columns in UICollectionView (horizontal scroll) and everything is working fine.
Every UICollectionViewCell contains its own UITableView inside and sometimes there is a lot of data in it, so the UICollectionView scroll is not so smooth.
I realized that the main problem is reusing the cell since the problem occurs only when the table contains a lot of data (10+ rows). Since the data is not updated so often and there is really no need to reuse it while scrolling, I believe that this will fix the 'smooth scroll' problem.
One of the solutions could be to add pagination to UITableView, so I can keep reuseIdentifier and not think about memory, but I just wanted to check this way as well.
Currently, I'm using this:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MyCustomIdentifier", for: indexPath) as? MyCustomCell {
// Cell configuration code
return cell
}
return UICollectionViewCell()
}
I tried with something like this:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let cell = UICollectionViewCell() as? MyCustomCell {
// Cell configuration code
return cell
}
return UICollectionViewCell()
}
but every time when I remove reuseIdentifier it's giving me this error:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'the cell returned from -collectionView:cellForItemAtIndexPath: does not have a reuseIdentifier - cells must be retrieved by calling -dequeueReusableCellWithReuseIdentifier:forIndexPath:'
I'm using a collection view and every time I select the cell I change the cell background color to red. Simple enough:
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)! as! CustomCell
cell.backgroundColor = .red
}
This works absolutely fine. When I select the top 3 cells going from left to right, the background color changes exactly as I expect:
However If I reload the collectionView after I select the cell the selection ordering begins to behave strangely. When I select the same top 3 cells in the same order from left to right, different cells become selected:
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)! as! CustomCell
cell.backgroundColor = .red
collectionView.reloadData()
}
Apple's documentation is cryptic. https://developer.apple.com/documentation/uikit/uicollectionview/1618078-reloaddata
They say that "This causes the collection view to discard any currently visible items (including placeholders) and recreate items based on the current state of the data source object. " But this makes me think that upon calling reloadData() the collectionViewCells would go back to gray and not jump indexPaths.
Can anyone explain what is going on in reloadData() to make the cell selection at index path ordering so strange?
First You need to use dequeue cell
let cell = collectionView.dequeueReusableCell(withReuseIdentifier:"CustomCell", for: indexPath) as! CustomCell
And in cell you can use
-(void)prepareForReuse {
// Set default implementation
}
I have 3 collection views on 1 view controller. I've tried a few of the other suggestions I've found on Stack but nothing seems to work.
All 3 Collection Views are in a separate cell of a HomeTableViewController. I tried to create the outlet connection to the HomeTableViewController but I get the error Outlets cannot be connected to repeating content.
I've read many people being able to hook up their multiple collectionViews so I am a bit confused as to where I'm going wrong...
The UICollectionView instances cannot be hooked up to IBOutlet properties in a separate UITableViewController.
As you describe, the UICollectionViews are actually each children of their own parent UITableViewCell, and as such are not direct descendants of the UITableViewController. This is because the cells will be added to the UITableView at run time.
If you are set on creating the outlets within the HomeTableViewController I would suggest creating them like so:
private weak var collectionViewA: UICollectionView?
and overriding cellForRow like so:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = super.tableView(tableView, cellForRowAt: indexPath)
// cast cell as collection view parent
collectionViewA = cell.collectionView
return cell
}
As has been mentioned, the better solution would be to manage the UICollectionView instances from within their own UITableViewCell parents. Example:
final class CollectionViewAParentTableViewCell: UITableViewCell {
#IBOutlet private weak var collectionView: UICollectionView!
}
extension CollectionViewAParentTableViewCell: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
…
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
…
}
}
You should create outlet in the UITableViewCell. Then you can provide tags to collectionViews in each cell in tableView:cellForRowAtIndexPath Method:
yourCell.collectionViewOutlet.tag = indexPath.row + 1000
you should replace 1000 with Any Constant Integer if the tags conflict with tags of other views.
Then use these tags to differentiate all collectionviews in collectionview:cellForItemForIndexpath method:
if(collectionView.tag == 1000){
//Code for collection view in first row of the table
}
else if(collectionView.tag == 1001){
//Code for collection view in second row of the table
}
else if(collectionView.tag == 1002){
//Code for collection view in third row of the table
}
You should also keep in mind to return number of items for each collection view just like above.
Tags make the life whole lot easier , don't they?
Happy Coding (Y)
You should create outlet in the UITableViewCell. Then you can provide tags to collectionViews in each cell in tableView:cellForRowAtIndexPath Method:
yourCell.collectionViewOutlet.tag = indexPath.row + 1000
you should replace 1000 with Any Constant Integer if the tags conflict with tags of other views.
Then use these tags to differentiate all collectionviews in collectionview:cellForItemForIndexpath method:
if(collectionView.tag == 1000){
//Code for collection view in first row of the table
}
else if(collectionView.tag == 1001){
//Code for collection view in second row of the table
}
else if(collectionView.tag == 1002){
//Code for collection view in third row of the table
}
You should also keep in mind to return number of items in collectionView:numberOfItemsInSection for each collection view just like above.
Tags make the life whole lot easier , don't they?
Happy Coding (Y)
Create three different collection view with different collection view cell, then after you just need to add in dataSource method like below:-
if collectionView == collectionViewA{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellA", for: indexPath) as! collectionCell
return cell
}else if collectionView == collectionViewB{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellB", for: indexPath) as! collectionCell
return cell
}else if collectionView == collectionViewC{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellC", for: indexPath) as! collectionCell
return cell
}else{
return UICOllectionViewCell()
}
also perform same for other dataSource method.
I am new to iOS developing and I am using Swift 3.
In my gallery app when user selects a cell, the picture inside that cell displays in a UIImageView above and the cell itself becomes bordered red to show that it is selected right now. However, when I scroll up or down and as soon as the selected cell is destroyed UICollectionView selects another cell from the visible ones.
I want to know how can I restore the selected state of a cell when it reused and prevent UICollectionView from doing that. In conclusion I want to know how to prevent from cell reusabilities effects on selection state.
Sorry for my bad English, not a native speaker.
you can either have an array of the images states if it's true then it's selected else it's not and in the
collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
function you check on the state and do the required setup for the cell or you can make the collection view don't reuse the cells by changing the cell identifier dynamically like this
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "HomeCircularCollectionViewCell\(indexPath.row)", for: indexPath) as? HomeCircularCollectionViewCell
//setup cell
return cell
}
hope my answer helps you in your problem