Swift 4 - Collection View Apply Section insets to header? - ios

I am using a Collection View and I am able to add Section insets to the cells inside the collection view, but it appears not to be applying to the Collection Views reusable view, is there away to set section insets for reusable views?
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
if(kind == UICollectionView.elementKindSectionFooter)
{
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "ReviewsFooter", for: indexPath) as! ReviewFooterCell
footerView.reviewFooterDelegate = self
return footerView
}
else if(kind == UICollectionView.elementKindSectionHeader)
{
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "ReviewHeader", for: indexPath) as! ReviewHeaderCell
headerView.totalReviews.text = "Total Reviews: " + String(self.reviews.count)
return headerView
}
fatalError()
}

First of all, let's be clear on what exactly is Section Insets.
Section insets are margins applied only to the items in the section.
They represent the distance between the header view and the first line
of items and between the last line of items and the footer view. They
also indicate the spacing on either side of a single line of items.
They do not affect the size of the headers or footers themselves.
Here is how you can add insets of a section in collectionView,
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
}

Related

How to create UICollectionView section Header which autosizes to label content + font size change?

I would like to add a section header to a UICollectionView which contains two lables to show a title and a info description. Both labels can show multi line texts and the header size should be automatically adopted to the label content.
TL;DR
Auto layout seems to fails when the label font is changed the appearance() proxy. Is there solution to solve this?
Complete description
This is how I set up the header view in IB:
As one can see, IB reports a problem. This is a "Content Priority Ambiguity" which can easily be solved by setting the Vertical Content Hugging Priority of InfoLabel to 250.
The header view is than added in the ViewController and referenceSizeForHeaderInSection is used to setup the size:
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
if kind == UICollectionView.elementKindSectionHeader {
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "header", for: indexPath) as? SectionHeaderView ?? SectionHeaderView()
headerView.titleLabel.text = "Title Label with long text. More text..."
headerView.infoLabel.text = "Info Label with long text. More text...."
return headerView
}
return UICollectionReusableView()
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
// Get the view for the first header
let indexPath = IndexPath(row: 0, section: section)
let headerView = self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionHeader, at: indexPath)
// Use this view to calculate the optimal size based on the collection view's width
return headerView.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingCompressedSize.height), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
}
So far everything looks good. However, when the label font is changed using the appearance() proxy, it fails:
// called in application:didFinishLaunchingWithOptions
UILabel.appearance(whenContainedInInstancesOf: [SectionHeaderView.self]).font = UIFont.systemFont(ofSize: 10)
The left screenshot shows the layout without the changing the font size, the right screenshot the layout after using UILabel.appearance:
It seems that the font size is updated after the header size was calculated and that now re-calculation is triggered.
How can this be solved?
If both labels have the same hugging priority (error in IB is simply ignored), the problem is almost the same. However, without explicit information auto layout simply decides that the title label is stretched instead of the info label.
Here is one approach:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
// Get the view for the first header
let indexPath = IndexPath(row: 0, section: section)
let headerView = self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionHeader, at: indexPath) as! SectionHeaderView
// appearance is not applied until view is added to the view hierarchy, so
// get the UIAppearance protocol for UILabel contained in SectionHeaderView class
let a = UILabel.appearance(whenContainedInInstancesOf: [SectionHeaderView.self])
// update the label(s) font property to the appearance font
headerView.titleLabel.font = a.font
headerView.infoLabel.font = a.font
// Use this view to calculate the optimal size based on the collection view's width
return headerView.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingCompressedSize.height), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
}

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.

CollectionViewSection Inset Does Not Consider Header/Footer Views

I have a collectionView with headers and footers. I want to add spacing at the end of the section.
I am using:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 0, left: 0, bottom: 16, right: 0)
}
However, this does not consider the footer view. therefore, the inset is added to the last index of that section. Is there any method or way to include the section footer before it applies the inset?
I need the inset under the footer view.
Illustration of existing functionality. (Yellow is the indexPath.item) the expected functionality is to have the inset (red) under the footer.
As you rightly pointed out, UICollectionView's section is located between header and footer.
If you want to add spacing below the footer, the best way would be to create custom footer view.
optional func collectionView(_ collectionView: UICollectionView,
viewForSupplementaryElementOfKind kind: String,
at indexPath: IndexPath) -> UICollectionReusableView
Here are the docs for this method.
Ideally, you would want to create a subclass for your custom Footer:]
class CustomFooterView: UICollectionReusableView {
}
Then, register it in viewDidLoad:
collectionView.register(CustomFooterView.self, forSupplementaryViewOfKind: .elementKindSectionFooter, withReuseIdentifier: "Your footer reuse ID")
Then, you could return it as so:
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
if kind == UICollectionView.elementKindSectionFooter {
return collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Your footer reuse ID", for: indexPath)
}
else {
}
}
Now you just need to ensure that auto layout is able to determine the size of your footer. So, add a label to your custom footer and ensure that its bottom constraint is equal to your desired padding.
Note: there is one caveat. Now that you implement the method which returns your custom footer, and the same method is called for header, you would probably need to use a custom header as well. Or, you would need to register the standard UICollectionReusableView and deque it for your header.

How to implement a horizontally-scrolling collection view with a header on top?

I want to have a collection view that scrolls horizontally, but I also want it to have a header on top of and to the right. I've tried implementing this as I would a vertically-scrolling collection view, and the header ends up to the left of the first cell.
Here is how I dequeue the header:
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let header = dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: identifier, for: indexPath) as! HeaderView
header.setWidth(to: headerWidth)
return header
}
Size of the header:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: collectionView.frame.width, height: 12)
}
Apart from that, the only other properties I initialize are the scroll direction and insets of the collection view
Headers positions : are in the Top of the section in case of scrolling vertically and in the left of the section in case of horizontally scrolling , So it's normal that it's showing in the left of the section in your case .
If you want to display your reusable view in the right of every section You can use UICollectionView.elementKindSectionFooter instead .
But if you want to display your reusable view between every section , You can set the size of header view in the very first section to .zero .
You can manipulate in every header view size with referenceSizeForHeaderInSection function like so:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
if section == 0 {
return .zero
}
return CGSize(width: collectionView.frame.width, height: 12)
}
In this case i removed (technically not removed but hide) the first header in the left , Which is in the first section , But you can set the size for every header .

UICollectionView sectionInsets not respected when using Header Cells

I am trying to create a collection view that has a header cell (dynamically sized) and some "normal" content cells (also dynamically sized). Therefore, the collection view is initialized as follows:
private lazy var timelineCollectionView: UICollectionView = {
let flowLayout = UICollectionViewFlowLayout()
flowLayout.scrollDirection = .vertical
flowLayout.sectionInset = UIEdgeInsets(top: self.coloredTitleBarHeight, left: 0, bottom: 0, right: 0) // assume that coloredTitlebarHeight is a constant of 120
flowLayout.estimatedItemSize = CGSize(width: self.view.frame.width, height: 10)
flowLayout.minimumInteritemSpacing = 0
flowLayout.minimumLineSpacing = 0
let cv = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
cv.alwaysBounceVertical = true
cv.contentInsetAdjustmentBehavior = .never
cv.backgroundColor = .clear
cv.scrollIndicatorInsets = UIEdgeInsets(top: self.coloredTitleBarHeight - self.getStatusBarHeight(), left: 0, bottom: 0, right: 0)
cv.delegate = self
cv.dataSource = self
cv.register(TLContentCell.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "content-header-cell")
cv.register(TLContentCell.self, forCellWithReuseIdentifier: "comment-cell")
return cv
}()
The collectionView is part of a ViewController that conforms to the following protocols: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout
Then, I use the following code to create a header cell:
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let cell = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "content-header-cell", for: indexPath) as! TLContentCell
cell.timelineContent = tlContentItem
cell.delegate = self
return cell
}
Last but not least, the dequeuing of the regular cells:
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "comment-cell", for: indexPath)
return cell
}
Output:
The collectionView displays the header cell as well as the item cells, however, the section Insets are not applied.
I would be very happy if you could provide me with any suggestion regarding this strange behavior.
Section insets are applied to contents only in UICollectionViewFlowLayout.
Using Section Insets to Tweak the Margins of Your Content
Section insets are a way to adjust the space available for laying out cells.
You can use insets to insert space after a section’s header view and
before its footer view. You can also use insets to insert space around
the sides of the content. Figure 3-5 demonstrates how insets affect
some content in a vertically scrolling flow layout. source

Resources