How to use custom UICollectionReusableView as section header of collection view? - ios

I've been driven insane for hours as I can't get around with the issue.
I have a collection view which can have different section with different no. of items in each. For each section I need to use a section header of different type. So for this, I'm going to use UICollectionReusableView. But I can't seem to succeed in using a custom subclass of UICollectionReusableView by means of UINib registration.
A crash happens when I downcast the reusable view to my subclass. Like:
let friendHeader = collectionView.dequeueReusableSupplementaryView(ofKind: kind,
withReuseIdentifier: "FriendHeaderView",
for: indexPath) as! FriendHeaderView
Below is the code snippet:
class ViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
private let viewModel = ProfileViewModel()
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dataSource = self
collectionView.delegate = self
// more code
collectionView.register(UINib(nibName: "FriendHeaderView", bundle: nil),
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: "FriendHeaderView")
}
}
Now here is the data source implementation:
extension ViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
// valid implementation
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// valid implementation
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// valid implementation
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionView.elementKindSectionHeader:
let friendHeader = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "FriendHeaderView", for: indexPath) as! FriendHeaderView
// *** Crash happens here *** //
return friendHeader
default:
assert(false, "Invalid element type")
}
}
}
And I don't know why the collectionView(_:layout:referenceSizeForHeaderInSection:) needs to be also implemented. So here it is:
extension ViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
let size = CGSize(width: collectionView.bounds.width, height: 100)
return size
}
}
Okay, now come to the point: The above mentioned crash doesn't happen
at all if I don't downcast with as! operator. Well, if I use section
header from the storyboard instead of UINib registration, there is
no crash.
If I'm going to need multiple type header, then I can't also use storyboard approach or without down-casting approach as I need to feed data to those headers.
What can I do to have multiple type headers with view built from interface builder?
I've made a demo project with what I've said above. If anyone is interested please check out that.

Once you assign proper class and identifier in your Xib file, then it will work without crashes.

Well, after some more investigation and the input from #good4pc in the accepted answer (actually I found out that by myself before looking at the answer) it seems that the issue is actually happening for some unwanted issue with Xcode.
When we create any view (preferably, UITableViewCell or UICollectionViewCell) with .xib, the class identity is provided automatically for that .xib in the identity inspector. But this was not the case for UICollectionReusableView. See the attached screenshot below for easy understanding.
This is a UICollectionViewCell subclassed with .xib:
This is a UICollectionReusableView subclassed with .xib:
So the key is to provide the class identity of the .xib file which
is done from the attributes inspector.

Related

Crash in CollectionView For Invalid Header

I want to understand what this error means?
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason:
'the view returned from -collectionView:viewForSupplementaryElementOfKind:atIndexPath: was not
retrieved by calling -dequeueReusableSupplementaryViewOfKind:withReuseIdentifier:forIndexPath:
for element kind 'UICollectionElementKindSectionHeader' at index path <NSIndexPath: 0x8aeb905cf5be0ed2>
{length = 2, path = 0 - 0}; supplementary view:
<UICollectionReusableView: 0x7f9236dc4ff0; frame = (0 0; 0 0); layer = <CALayer: 0x600001018620>>'
I am using custom header for UICollectionView.I am getting this crash as soon as the view is loaded. even before cellforrowatindexpath is called and the issue is not with the custom header, it is with the return UICollectionReusableView()
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
if kind == UICollectionView.elementKindSectionHeader && indexPath.section == 2
{
return someCustomHeader
}
return UICollectionReusableView()
}
You must always obtain the section header with the method dequeueReusableSupplementaryViewOfKind:withReuseIdentifier:forIndexPath:, before calling it you must also register a header class. If you want to show only the header of the third section you must implement collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) to hide unwanted headers by giving them a .zero size (you can' t simply return an instance of UICollectionReusableView for unwanted headers).
import UIKit
class CollectionViewController: UICollectionViewController {
private let headerId = "headerId"
override func viewDidLoad(){
super.viewDidLoad()
// Registers a header class
self.collectionView.register(YourClass.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: self.headerId) // Replace YourClass with the name of your header class
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
// This method must always call dequeueReusableSupplementaryView, even if section!=2
if kind == UICollectionView.elementKindSectionHeader{
return self.collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: self.headerId, for: indexPath)
}
return UICollectionReusableView()
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize{
// Shows only the header of the third section
return section == 2 ? desiredSize : .zero // Replace desiredSize with the size of the visible header
}
}
When you work with custom header you have to register first the header class.
collectionView.register(AppHeaderCollectionView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: AppHeaderCollectionView.headerIdentifier)
and after that for use the header you have to do it this.
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: AppHeaderCollectionView.headerIdentifier, for: indexPath) as! AppHeaderCollectionView
return header
}
After the iOS update to 15.0, I had to subclass UICollectionReusableView for my header view.
guard let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: MenuTitleHeaderView.sectionHeaderID, for: indexPath) as? MenuTitleHeaderView else {
fatalError("Unexpected Sumplementary View")
}
switch kind {
case UICollectionView.elementKindSectionHeader:
...
}
Don't forget to return the size for the header or footer as well.
One more thing, don't forget to return the number of sections. Or you will only see one section display.

CollectionView didSelectItem does not respond. Why?

first of all, i did set the delegates. every other protocol is working with collection views.
I tested it with the sizeFotItem function. and that worked just fine. But how come that cellForItem function does not respond at all?
Anyone know why?
here is my code;
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView.delegate = self
self.collectionView.dataSource = self
let flow = collectionView.collectionViewLayout as! UICollectionViewFlowLayout
layoutSettings(flow)
playButtonPressed(self)
}
And this is in my extension of my viewController;
extension ViewController: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 9
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionCell", for: indexPath) as! CollectionCell
print(menu.menuArray[indexPath.row])
cell.cellText.text = menu.menuArray[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("Do something")
}
}
The funny part is.. I have almost the same code in an other project, and every is working fine.
Only difference is that the project has an collectionView INSIDE a TableView.
Hopefully one of you find an obvious reason why this is not working. I'm gladly to know why :)
thanks.
PS. Is it normal that the editor doesn't recognize the "extension ViewController" part as code?
Solved by just deleting the CollectionViewCell and making a new one.
I still don't know what was wrong with my previous collectionViewCell, because every protocol was working just fine except for didSelectItemAt..
If anybody had a familiar problem and knows what was going on, I'm still happy to hear what was going on ;)

Collection View Cells not appearing in Collection View?

I created a Collection View using purely the storyboard interface builder. This is what the storyboard looks like:
My collection view's properties are default as well. I haven't written anything into my ViewController.swift yet.
For some reason, when I run on my phone / emulator, none of the buttons are showing.
UICollectionView does not support static cells like UITableView. You will have to set its dataSource,delegate and configure your cells in code.
Just configure the collectionView properly see below code and image:
Implement the delegate methods of collectionView:
class yourClassController: UICollectionViewDataSource, UICollectionViewDelegate {
override func numberOfSectionsInCollectionView(collectionView:
UICollectionView!) -> Int {
return 1
}
override func collectionView(collectionView: UICollectionView!,
numberOfItemsInSection section: Int) -> Int {
return yourArray.count
}
override func collectionView(collectionView: UICollectionView!,
cellForItemAtIndexPath indexPath: NSIndexPath!) ->
UICollectionViewCell! {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("CollectionViewCell", forIndexPath: indexPath) as CollectionViewCell
// Configure the cell
cell.backgroundColor = UIColor.blackColor()
cell.textLabel?.text = "\(indexPath.section):\(indexPath.row)"
cell.imageView?.image = UIImage(named: "circle")
return cell
}
Then from your storyboard set the delegate and datasource by drag and drop see image:
Note: collectionView appears when you do complete above formality with its relevant class.

UICollectionView cannot display customized cell content

I tried to create a dummy photo app with UICollectionView. My Xcode version is 7.2 and Swift version 2.1.1. I use Storyboard to build the UI part by following the tutorial from http://www.raywenderlich.com/78550/beginning-ios-collection-views-swift-part-1
In Swift 2.0, the UICollectionViewDataSource is inherited from UICollectionViewController, we don't need to explicitly declare those protocols. I implemented the required override methods for DataSource, and also register the customized cell in viewDidLoad() in the UICollectionViewController. I put a test Label in my cell to check whether it works or not. Unfortunately, the label never appear after I launch the app. I attach some of my code below as reference:
UICollectionViewController
class VacationsCollectionViewController: UICollectionViewController {
private let reuseIdentifier = "VacationCell"
override func viewDidLoad() {
super.viewDidLoad()
self.collectionView!.registerClass(VacationsCollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
}
// MARK: UICollectionViewDataSource
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of items
return 1
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! VacationsCollectionViewCell
// Configure the cell
cell.backgroundColor = UIColor.blueColor()
cell.CellText.text = "Show me the money"
return cell
}
}
UICollectionViewCell
class VacationsCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var CellText: UILabel!
}
Any idea why the label never show up in my dummy app?
It looks like you have everything you need. The issue is probably with your storyboard. Just to be safe, add constraints to the label so it's definitely going to be displayed properly. Then, add an extension to your collection view controller and be sure the size allows the label to be displayed as well. This, at least, worked for me and I was in your exact situation.
extension OfficesCollectionViewController: UICollectionViewDelegateFlowLayout{
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
return CGSize(width: 200, height: 200)
}
}

Is completely static UICollectionView possible?

At UITableView, completely static tableView config is possible. You can disconnect UITableView's datasource and put each cell on storyboard(or xib) by using IB.
I tried same thing with UICollectionView. disconnect UICollectionView's datasource. Put each cell on UICollectionView on storyboard. I built it without any errors. But it didin't work. cells were not displayed at all.
Is UICollectionView without datasource possible?
No.
Creating a static UICollectionViewController is not allowed. You must have a data source delegate.
I also want to point out that there is not a static UITableView, but a static UITableViewController. It's a difference.
You can easily create a static UICollectionViewController.
Just create every cell in interface builder, give them re-use identifiers(e.g. "Home_1" "Home_2" "Home_3"), and populate the methods as follows:
class HomeViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
let cellIdentifiers:[String] = ["Home_1","Home_2","Home_3"]
let sizes:[CGSize] = [CGSize(width:320, height:260),CGSize(width:320, height:160),CGSize(width:320, height:100)]
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return cellIdentifiers.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
return collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifiers[indexPath.item], for: indexPath)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return sizes[indexPath.item]
}
}
Then set the view controller to be of the proper class, and, hey presto, a (basically) static collection. I'm sorry to say but this is BY FAR the best way to support portrait and landscape views when you have groups of controls...
I did a little experimenting and wanted to add my own method since it helped me achieve the truly static, highly custom Collection View I was looking for.
You can create custom UICollectionViewCells for each cell you want to display in your Collection View, and register them with all the Cell IDs in your Collection View, like this:
Create your static cell:
class MyRedCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = .red
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Make as many of these as you want.
And then back in your Collection View Controller, register them with their corresponding cellId:
let cellIds = ["redCell","blueCell","greenCell"]
override func viewDidLoad() {
super.viewDidLoad()
collectionView.register(MyRedCell.self, forCellWithReuseIdentifier: "redCell")
collectionView.register(MyBlueCell.self, forCellWithReuseIdentifier: "blueCell")
collectionView.register(MyGreenCell.self, forCellWithReuseIdentifier: "greenCell")
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIds[indexPath.item], for: indexPath)
return cell
}
Each cell will display exactly what's in its class.

Resources