Different layout for different sections in collection view - ios

I have a collection view with 3 sections, the first two sections work fine with UICollectionViewFlowLayout but the 3rd one needs a custom implementation.
I understand that a collection view can only have one layout, so I've been trying to only perform work in the overriden prepare function if I'm in the right section. This is where it tends to fall apart, I can't work out a straightforward way to figure out which section the cell is in which I'm calculating layout attributes for.
Not sure if there's a better approach than conditionally performing calculations in the prepare function either.
A point in the right direction would be great!

I think this is a good approach:
1- Create your custom section with its own .xib file
class CustomSection: UICollectionReusableView {
override func awakeFromNib() {
super.awakeFromNib()
}
}
2- Implement the UICollectionViewDelegateFlowLayout delegate method
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
// In case its a section header (UICollectionElementKindSectionFooter for footer)
if kind == UICollectionElementKindSectionHeader {
// This will be the section number
let section = indexPath.section
if section == 3 {
let customSection = collectionView.dequeueReusableSupplementaryView(ofKind: kind,withReuseIdentifier: "CustomSection", for: indexPath) as! CustomSection
return customSection
}
else {
//TODO: Return your default section
}
}
}
3- Don't forget to register the section with your UICollectionView
let customSectionNib = UINib.init(nibName: "CustomSection", bundle: nil)
collectionView.register(customSectionNib, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "CustomSection")

Related

Keep a reference to UICollectionView header

I need to store a view to use as a UICollectionView header. I don't want it to cycle out of memory though, because it needs to keep its state/data, etc.
With a table view you can just do tableView.tableHeaderView = view.
Here's what I'm trying:
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case MagazineLayout.SupplementaryViewKind.sectionHeader:
if let t = headerView { //headerView is an instance var
return t
} else {
let view = collectionView.dequeueReusableSupplementaryView(ofKind: MagazineLayout.SupplementaryViewKind.sectionHeader, withReuseIdentifier: "MyHeaderView", for: indexPath) as! MyHeaderView
view.titleLabel.text = "test"
view.switch.addAction(for: .valueChanged, { [weak self] in
self?.switchValueChanged()
})
headerView = view
return view
}
...
}
I don't want to re-create it every time the user scrolls it away and then back, so I'm trying to store a reference to it. This isn't working though. Hard to explain but the view it displays is cut off and the switch isn't responsive. If I comment out the "if" part and just create a new one every time, it looks correct but state is lost (i.e. the switch gets turned off) What's the best way to do this?
Since you're keeping the reference and not letting it deallocate when it scrolls out of the view, remove the register and dequeuing entirely. It worked fine for me, here's how:
let view = MyHeaderView()
override func viewDidLoad() {
super.viewDidLoad()
view.titleLabel.text = "test"
view.switch.addAction(for: .valueChanged, { [weak self] in
self?.switchValueChanged()
})
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case MagazineLayout.SupplementaryViewKind.sectionHeader:
return view
//...
}
}

Different Classes with Similar Methods

Let's say I am making scrollable pages with a UICollectionView. The pages are all different and are populated by a pages array like the one below:
let pages = [GreenPage(), YellowPage(), OrangePage(), YellowPage(), GreenPage()]
So, to clarify, there would be a page that's green, then followed by yellow, then orange ...
Now, let's say I want to make it so that when one is tapped, it runs a function called tapped() which occurs in each GreenPage(), YellowPage(), and OrangePage().
Now, the only way I see to do this would be the following:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let greenPage = collectionView.cellForItem(at: indexPath) as! GreenPage {
greenPage.tapped()
} else if let yellowPage = collectionView.cellForItem(at: indexPath) as! YellowPage {
yellowPage.tapped()
} else if let orangePage = collectionView.cellForItem(at: indexPath) as! OrangePage {
orangePage.tapped()
}
}
This seems super redundant. Is there another way to do this assuming the tapped function for each class does the same thing?
This is a good example for a protocol. Create it
protocol Tappable {
func tapped()
}
adopt the protocol
class GreenPage : Tappable { ...
class YellowPage : Tappable { ...
class OrangePage : Tappable { ...
This reduces the code in didSelectItemAt considerably
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
(collectionView.cellForItem(at: indexPath) as? Tappable)?.tapped()
}
This is a great time to use protocols. If they all conform to a protocol that has tapped() as a requirement. You then say the array of pages is an array of tour protocol with this:
let pages: [YourProtocol] = [...]
It then your usage be getting the cell and calling tapped()
For more on protocols read this
Also sorry for formatting, I’m on my phone.

UICollection ContentInsetAdjustment behavior modifying 'ghost' reusable view instead of my header

I am having difficulty getting UICollectionView Content Insets Adjustment Behavior setting to work. It appears to be modifying the location of 'ghost' ReusableView instead of my header view. In this screenshot the ghost view is selected, the header view I'm trying to control is green (its bg color).
I have tried using both the story board and programatic options, both are moving the position of the ghost view instead of my visible view. I cannot find where these ghost views are being generated nor can I see them in storyboard.
screenshot of ghost views
Here is my reusable view function:
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
if(kind == "UICollectionElementKindSectionHeader"){
let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "questionHeader", for: indexPath) as! QuestionheaderCollectionReusableView
if(self.mode == .Test){
view.scoreRing.alpha = 0.5
view.scoreRing.style = .ontop
}else{
view.updateScore(session: QKSession.default)
}
if((activeQuestion) != nil){
view.questionLabel.text = activeQuestion?.question
view.updateProgress(session: QKSession.default, question: activeQuestion!)
}else{
view.questionLabel.text = "Please select a MQF"
}
return view
}else{
let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "questionFooter", for: indexPath) as! QuestionFooterCollectionReusableView
return view
}
}
Has anyone else seen anything like this?

Why two collection views in two table view cells won't work in Swift 4?

I read similar questions such as how to have multiple collection view in multiple table view cells and I connected my collection views cells and use identifier names for them but I don't know why I receive this Error:
* Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'could not dequeue a view of kind: UICollectionElementKindCell with identifier extera_infoCollectionViewCell - must register a nib or a class for the identifier or connect a prototype cell in a storyboard'
* First throw call stack:
**Remember that I read Similar questions and the first table view cell with collection view working well and the problem is for second one **
here is my code for main view controller that has a table view and the table view has two cells
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if collectionView == fieldOfActivityCell().fieldofActivitiesCollectionView {
let fullfields : String = self.adv.resultValue[0].work_field!
let fullfieldsArr : [String] = fullfields.components(separatedBy: ",")
print(fullfieldsArr)
return fullfieldsArr.count
} else {
let extera_infofields : String = self.adv.resultValue[0].extera_info!
let extera_infofieldsArr : [String] = extera_infofields.components(separatedBy: ",")
print(extera_infofieldsArr)
return extera_infofieldsArr.count
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if collectionView == fieldOfActivityCell().fieldofActivitiesCollectionView {
let fieldsCells = collectionView.dequeueReusableCell(withReuseIdentifier: "fieldOfActivityCollectionViewCell", for: indexPath) as! fieldOfActivityCollectionViewCell
let fullfields : String = self.adv.resultValue[0].work_field!
let fullfieldsArr : [String] = fullfields.components(separatedBy: ",")
fieldsCells.title.text = fullfieldsArr[indexPath.row]
return fieldsCells
}
else {
let extera_infoCells = collectionView.dequeueReusableCell(withReuseIdentifier: "extera_infoCollectionViewCell", for: indexPath) as! extera_infoCollectionViewCell
let extera_info : String = self.adv.resultValue[0].extera_info!
let extera_infoArr : [String] = extera_info.components(separatedBy: ",")
extera_infoCells.infoText.text = extera_infoArr[indexPath.row]
return extera_infoCells
}
}
and here is the table view codes in same view controller:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0{
let fieldCell = self.showAdvTableView.dequeueReusableCell(withIdentifier: "fieldOfActivityCell", for: indexPath) as! fieldOfActivityCell
return fieldCell
} else {
let fieldCell = self.showAdvTableView.dequeueReusableCell(withIdentifier: "extera_infoCell", for: indexPath) as! extera_infoCell
return fieldCell
}
here is table view first cell class:
class fieldOfActivityCell: UITableViewCell {
#IBOutlet weak var fieldofActivitiesCollectionView: UICollectionView!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
if let flowLayout = fieldofActivitiesCollectionView.collectionViewLayout as? UICollectionViewFlowLayout { flowLayout.estimatedItemSize = CGSize.init(width: 1.0, height: 1.0) }
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
extension fieldOfActivityCell {
func setCollectionViewDataSourceDelegate
<D: UICollectionViewDelegate & UICollectionViewDataSource>
(_ dataSourceDelegate:D , forRow row : Int )
{
fieldofActivitiesCollectionView.delegate = dataSourceDelegate
fieldofActivitiesCollectionView.dataSource = dataSourceDelegate
fieldofActivitiesCollectionView.reloadData()
}
}
and here is the second tableview cell class:
#IBOutlet weak var extra_infoCollectionView: UICollectionView!
override func awakeFromNib() {
super.awakeFromNib()
if let flowLayout = extra_infoCollectionView.collectionViewLayout as? UICollectionViewFlowLayout { flowLayout.estimatedItemSize = CGSize.init(width: 1.0, height: 1.0) }
}
}
extension extera_infoCell {
func setCollectionViewDataSourceDelegate
<D: UICollectionViewDelegate & UICollectionViewDataSource>
(_ dataSourceDelegate:D , forRow row : Int )
{
extra_infoCollectionView.delegate = dataSourceDelegate
extra_infoCollectionView.dataSource = dataSourceDelegate
extra_infoCollectionView.reloadData()
}
}
First step: using Tags - you just need to use tag for them and use if else to choose which collection view has selected with tag so the answer is this :
if collectionView.tag == 1 {
do some thing//////
}else {
do some thing else}
and you should use this in both cellForRowAtIndexPath and numberOfRows methods you can use this for table view too
Second step: you have to change the name of 'collection view' that you are dequeueing inside the cellForRowAt method in CollectionView data source:
if collectionView.tag == 1 {
let cell = yourFirstCollectionView.dequeueReusableCell(...) as yourCell
....
return cell
} else {
let cell = yourSecondCollectionView.dequeueReusableCell(...) as yourCell
....
return cell
}
According to your error your reuse identifier doesn't match any cell in your storyboard. Click on your extera_info collectionView cell in interface builder. Select the attributes inspector tab. Under reuse identifier make sure you put in extera_infoCollectionViewCell
If you take the other tableview cell In different class , with NSObject features of storyboard it can help you , And it is easy to maintain .
Saeed's tag option above is likely the simplest answer, but found his description a little short so adding a more complete answer below for those who've never used tags before...
If abiding by MVC and placing collectionView dataSource methods inside the UITableView class (instead of inside the UITableViewCell classes), and wanting to avoid this " error:
Each Collection View you use will need its own dequeueReusableCell identifier:
In interface-builder, name all your identifiers for your collection view cells. CatPicCell & DogPicCell for instance.
In your CellForItemAt collectionView method, set up if-statements or switch statement such that each reuse identifier is set equal to the identifiers you created in interface-builder (step 1). If using switch/case, your value can be set to collectionView.tag. Tags can be numbered to identify each different collectionView. The tags are like turning your set of collectionViews into a dictionary or array, such that each collectionView gets its own unique key/index.
Go back into interface-builder, and go into your storyboard and select each collection view (each of which should be inside its own tableView cell). In Xcode's "attribute inspector" scroll down to the "View" section and 3 spaces down (Xcode 11, Swift 5) you'll see a field called "Tag". Assign an integer value to that collection view, and then repeat this process for each collection view which is going to be embedded in your UITableView cells.
Once you have all the collection views tagged with unique integers, you simply set your cases to the integers, and give each dequeueReusableCell identifier the same integer index as you provided in the storyboard.
Now when you tableView cell calls on the collectionView you've outletted in the TableViewCell classes, it will be able to acquire the proper dequeueReusable ID. You can put your data inside each switch case.
Voila, you now have ONE collectionView datasource set of required methods, but serving ALL of your collection views. EVEN BETTER, when someone expands the project and adds another collectionView it will be as easy as adding another case to the switch and identifier in the storyboard.
Example code could look like this:
// I need a switch statement which will set the correct (of the 3 collectionViews) dequeueReusable IDENTIFIER for the collectionView
switch collectionView.tag {
//if tableView is doing cell == 1, then "CatsCell"
//if ... cell == 3, then "DogsCell"
//if ... cell == 5, then "BirdsCell"
case 1:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CatsCell", for: indexPath) as! CatsCVCell
// put your required data here
return cell
case 3:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "DogCell", for: indexPath) as! DogsCVCell
// example data
let dogs = dogController.fetch()
cell.name = dogs[indexPath.item].dogName
if let image = UIImage(data: groups[indexPath.item].image!) {
cell.image = image
}
return cell
case 5:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "BirdCell", for: indexPath) as! BirdCVCell
// put data code here for birds collection view cells
return cell
default:
return UICollectionViewCell() // or write a fatalError()
}
note: you have two options for your default to the switch statement...
1. like above, a generic but empty cell instance
2. throw an error. The error should never throw bc you'll have all the cases, but an error could occur if someone else improves your code and add another collectionView but forgets to to add the switch case-- so make your error statement explain what's wrong precisely.

What does Apple mean by term "registers" in regards to creating new tableView cells?

I'm working through an exercise which uses tableviews. I noticed within a test during the exercise, they use a method I haven't needed in the past when implementing tableviews from storyboards. The method is:
func register(AnyClass?, forCellReuseIdentifier: String)
After reading the short description of this function in the reference pages. I'm curious to know what does apple mean by term "registers"? I half assume that since we are doing this exercise programmatically at the moment, this function is only needed if you're creating UITableviews programmatically. If this statement is incorrect, please let me know as I'd like to learn more.
Here is the code from the example:
func test_CellForRow_DequesCellFromTableView(){
let mockTableView = MockTableView()
mockTableView.dataSource = sut
mockTableView.delegate = sut
mockTableView.register(ItemCell.self, forCellReuseIdentifier: "ItemCell")
sut?.itemManger?.add(ToDoItem.init(title: "Foo"))
mockTableView.reloadData()
_ = mockTableView.cellForRow(at: IndexPath.init(row: 0, section: 0))
XCTAssertTrue(mockTableView.cellGotDequeed)
}
The DequeueReusable methods are there to check if any reusable cells are left before creating new ones. Hope you have an idea about the working of reusable cells
What happens when the queue is empty? Now we do need to create a cell. We can follow 2 methods to create a cell,
Create cell manually
Create it automatically by registering cell with a valid xib file
METHOD 1
if you do it with manually, you must check cell is empty or not after dequeueReusableCell check. Just like below,
// create a cell for each table view row
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Reuse an old cell if exist else return nil
let cell:UITableViewCell = self.tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as UITableViewCell!
//check cell is nil if nil you want to allocate it with proper cell
if(cell == nil){
//create cell manually
cell = UITableViewCell(style: UITableViewCellStyle.Subtitle, reuseIdentifier: "CellSubtitle")
}
// do stuff to the cell here
return cell
}
METHOD 2
We could create the cell manually like above which is totally fine. But it would be convenient if the table view would create the cell for us directly.
That way we don't have to load it from a nib or instantiate it.
For registering a cell with a xib or class we use func register(AnyClass?, forCellReuseIdentifier: String) method. Let see an example,
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.register(MyCell.self, forCellReuseIdentifier: "Cell")
}
// ...
override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath:indexPath) as MyCell
// no "if" - the cell is guaranteed to exist
// ... do stuff to the cell here ...
cell.textLabel.text = // ... whatever
// ...
return cell
}
You are "registering" your custom Cell class - ItemCell - for reuse as a cell for your tableview.
See: https://developer.apple.com/reference/uikit/uitableview/1614888-register
"Register" tells XCode that the cell exists. A cell is registered under a "reuse identifier." This is a unique string that corresponds to your TableViewCell, in this case ItemCell.
A cell can also be registered in the Storyboard by filling out the "Identifier" in the cell's attributes inspector.

Resources