collectionView:viewForSupplementaryElementOfKind:atIndexPath: called only with UICollectionElementKindSectionHeader - ios

I have a collection view and I would like each section to have both a header and a footer. I'm using the default flow layout.
I have my own subclasses of UICollectionReusableView and I register each for both the header and the footer in the viewDidLoad method of my view controller.
I've implemented the method collectionView:viewForSupplementaryElementOfKind:atIndexPath: but, for each section, it is only called with kind being UICollectionElementKindSectionHeader. Therefore my footer isn't even created.
Any ideas why this happens?

It seems that I have to set the footerReferenceSize for the collection view layout. Weird that I didn't have to do that with the header.

(Using Swift 3.1, Xcode 8.3.3)
First, register header's class or nib
collectionView.register(ShortVideoListHeader.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "header")
Second, set headerReferenceSize; alternatively, you can return headerReferenceSize in collectionView's delegate
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
(collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.headerReferenceSize = CGSize(width: view.bounds.width, height: 156)
}
Third, write your own header class, such as,
class ShortVideoListHeader: UICollectionReusableView {
let titleLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(titleLabel)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
titleLabel.sizeToFit()
titleLabel.frame.origin = CGPoint(x: 15, y: 64 + (frame.height - 64 - titleLabel.frame.height) / 2) // navigationBar's height is 64
}
}
Fourth, return your header instance in collectionView's dataSource methods,
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionElementKindSectionHeader:
let header = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "header", for: indexPath) as! ShortVideoListHeader
header.titleLabel.text = title
header.setNeedsLayout()
return header
default:
return UICollectionReusableView()
}
}
By the way, Apple's Document answers how to hide section header.
This method must always return a valid view object. If you do not want a supplementary view in a particular case, your layout object should not create the attributes for that view. Alternatively, you can hide views by setting the
hidden
property of the corresponding attributes to YES or set the
alpha
property of the attributes to 0. To hide header and footer views in a flow layout, you can also set the width and height of those views to 0.

I found some code maybe can help you
- ( UICollectionReusableView * ) collectionView : ( UICollectionView * ) collectionView viewForSupplementaryElementOfKind : ( NSString * ) kind atIndexPath : ( NSIndexPath * ) indexPath
{
UICollectionReusableView * reusableview = nil ;
if ( kind == UICollectionElementKindSectionHeader )
{
RecipeCollectionHeaderView * headerView = [ collectionView dequeueReusableSupplementaryViewOfKind : UICollectionElementKindSectionHeader withReuseIdentifier : # "HeaderView" forIndexPath : indexPath ] ;
NSString * title = [ [ NSString alloc ] initWithFormat : # "Recipe Group #%i" , indexPath.section + 1 ] ;
headerView.title.text = title;
UIImage * headerImage = [ UIImage imageNamed : # "header_banner.png" ] ;
headerView.backgroundImage.image = headerImage;
reusableview = headerView;
}
if ( kind == UICollectionElementKindSectionFooter )
{
UICollectionReusableView * footerview = [ collectionView dequeueReusableSupplementaryViewOfKind : UICollectionElementKindSectionFooter withReuseIdentifier : # "FooterView" forIndexPath : indexPath ] ;
reusableview = footerview;
}
return reusableview;
}

Related

How can I access UISegmentedControl in header of UICollectionViewController?

So this problem is fairly straightforward. I have a UICollectionViewController (MyProfile.swift) with a header section (MyProfileHeader.swift). Within the latter, I have a UISegmentedControl to return different numbers of items AND items in the collection view cells (I don't want to initialize an instance of the latter's class within the UICollectionViewController). This is my code for MyProfile.swift class. I tried adding a target in the viewForSupplementaryElementOfKind method to return different queries (which works), but I ultimately have to access the segmented control within the numberOfItemsInSection and cellForItemAtIndexPath methods. The "testObjects" and "writeObjects" are array values that are queried via the addTarget method in the viewForSupplementaryElementOfKind. I set the indexPath but it returns an error for obvious reasons... How can I access segmented control?
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
var numberOfItems: Int? = 0
let indexPath = NSIndexPath(forItem: 0, inSection: 0)
let header = collectionView.dequeueReusableSupplementaryViewOfKind(UICollectionElementKindSectionHeader, withReuseIdentifier: "header", forIndexPath: indexPath) as! MyProfileHeader
if header.userContent.selectedSegmentIndex == 1 {
numberOfItems = textObjects.count
} else if header.userContent.selectedSegmentIndex == 2 {
numberOfItems = 0
} else {
numberOfItems = photoObjects.count
}
print("F: \(numberOfItems!)")
return numberOfItems!
}
-1st create a UICollectionReusableView subclass and name it SegmentedHeader
-2nd Inside the SegmentedHeader class add a protocol to keep track of which segment was selected. When a segment is selected inside the collectionView's header the protocol/delegate will get passed the value of that segment
-3rd make sure you set the delegate weak var delegate: SegmentedHeaderDelegate?
-4th when programmatically creating the segmentedControl add a target named selectedIndex(_ sender: UISegmentedControl). When a segment is pressed, you pass the value of that segment to the protocols trackSelectedIndex() function
protocol SegmentedHeaderDelegate: class {
func trackSelectedIndex(_ theSelectedIndex: Int)
}
class SegmentedHeader: UICollectionReusableView {
//MARK:- Programmatic Objects
let segmentedControl: UISegmentedControl = {
let segmentedControl = UISegmentedControl(items: ["Zero", "One", "Two"])
segmentedControl.translatesAutoresizingMaskIntoConstraints = false
segmentedControl.tintColor = UIColor.red
segmentedControl.backgroundColor = .white
segmentedControl.isHighlighted = true
segmentedControl.addTarget(self, action: #selector(selectedIndex(_:)), for: .valueChanged)
return segmentedControl
}()
//MARK:- Class Property
weak var delegate: SegmentedHeaderDelegate?
//MARK:- Init Frame
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
setupAnchors()
}
//MARK:- TargetAction
#objc func selectedIndex(_ sender: UISegmentedControl){
let index = sender.selectedSegmentIndex
switch index {
case 0: // this means the first segment was chosen
delegate?.trackSelectedIndex(0)
break
case 1: // this means the middle segment was chosen
delegate?.trackSelectedIndex(1)
break
case 2: // this means the last segment was chosen
delegate?.trackSelectedIndex(2)
break
default:
break
}
}
fileprivate func setupAnchors(){
addSubview(segmentedControl)
segmentedControl.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 0).isActive = true
segmentedControl.rightAnchor.constraint(equalTo: self.rightAnchor, constant: 0).isActive = true
segmentedControl.topAnchor.constraint(equalTo: self.topAnchor, constant: 0).isActive = true
segmentedControl.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0).isActive = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Inside the class that has the UICollectionViewController:
IMPORTANT - Make sure you set the delegate inside viewForSupplementaryElementOfKind or none of this will work
// MAKE SURE YOU INCLUDE THE SegmentedHeaderDelegate so the class conforms to it
class ViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, SegmentedHeaderDelegate{
// add a class property for the header identifier
let segmentedHeaderIdentifier = "segmentedHeader"
// add a class property to keep track of which segment was selected. This gets set inside the tracktSelectedIndex() function. You will need this for cellForRowAtIndexPath so you can show whatever needs to be shown for each segment
var selectedSegment: Int?
// register the SegmentedHeader with the collectionView
collectionView.register(SegmentedHeader.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: segmentedHeaderIdentifier)
// inside the collectionView's delegate below add the header
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
var header: UICollectionReusableView?
if kind == UICollectionElementKindSectionHeader{
let segmentedHeader = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: segmentedHeaderIdentifier, for: indexPath) as! SegmentedHeader
// IMPORTANT >>>>MAKE SURE YOU SET THE DELEGATE or NONE OF THIS WILL WORK<<<<
segmentedHeader.delegate = self
// when the scene first appears there won't be any segments chosen so if you want a default one to show until the user picks one then set it here
// for eg. when the scene first appears the last segment will show
segmentedHeader.segmentedControl.selectedSegmentIndex = 2
header = segmentedHeader
}
return header!
}
// inside cellForRowAtIndexPath check the selectedSegmented class property to find out which segment was chosen
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TheCell, for: indexPath) as! TheCell
// selectedSegment is the class property that gets set inside trackSelectedIndex()
switch selectedSegment {
case 0:
// have the cell display something for the first segment
break
case 1:
// have the cell display something for the middle segment
break
case 2:
// have the cell display something for the last segment
break
default:
break
}
return cell
}
// whenever a segment is selected, this delegate function will get passed the segment's index. It runs a switch statement on theSelectedIndex argument/parameter. Based on that result it will set the selectedIndex class property to match the value from theSelectedIndex argument/parameter
func trackSelectedIndex(_ theSelectedIndex: Int) {
switch theSelectedIndex {
case 0: // this means the first segment was chosen
// set the selectedSegment class property so you can use it inside cellForRowAtIndexPath
selectedSegment = 0
print("the selected segment is: \(theSelectedIndex)")
break
case 1: // this means the middle segment was chosen
selectedSegment = 1
print("the selected segment is: \(theSelectedIndex)")
break
case 2: // this means the last segment was chosen
selectedSegment = 2
print("the selected segment is: \(theSelectedIndex)")
break
default:
break
}
}
When the header is retrieved for the collection view in viewForSupplementaryElementOfKind, you can store a weak reference to it in MyProfile.
class MyProfile: UICollectionViewController {
...
...
weak var header: MyProfileHeader?
...
...
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView {
header = collectionView.dequeueReusableSupplementaryViewOfKind(UICollectionElementKindSectionHeader, withReuseIdentifier: "header", forIndexPath: indexPath) as! MyProfileHeader
return header
}
You can then access this from any other function in your UICollectionViewController.
Note that numberOfItemsInSection and cellForItemAtIndexPath can be called before the header has been created in viewForSupplementaryElementOfKind, so when you access it in numberOfItemsInSection, cellForItemAtIndexPath, or anywhere else you should check for null and then assume that the segmented control is on the default value (as it will be since this is the first time the view is being displayed). Something like
let selectedSegmentIndex = header?.userContent.selectedSegmentIndex ?? 0 //0 is the default value here

Custom UICollectionViewLayout and floating supplementary header views crash

I am trying to create a UICollectionView which represents a grid and I would like to create a ruler (floating header view) at the top edge which floats along in the y-axis.
So, much like in UITableViews, this view should basically just scroll with the content at the top of the screen. I hope that description makes it clear enough. So much for theory, let's get to the practical part!
Here is what I am working with now:
I registered the class GridRulerView which is a subclass of
UICollectionReusableView with the collection view.
I return the correct Size for the supplementary view for the right kind and CGSizeZero where there is no header.
I implemented viewForSupplementaryElementOfKind::
func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView
{
var reusableHeaderView : UICollectionReusableView!
if kind == GridRulerView.Kind{
reusableHeaderView = collectionView.dequeueReusableSupplementaryViewOfKind(GridRulerView.Kind, withReuseIdentifier: GridRulerView.Kind, forIndexPath: indexPath) as! UICollectionReusableView
}
return reusableHeaderView
}
I have a custom UICollectionViewLayout with the following
layoutAttributesForElementsInRect: method:
override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? {
var attribs = [UICollectionViewLayoutAttributes]()
for (indexPath, attrs) in self.attributes{
if CGRectIntersectsRect(rect, attrs.frame) {
attribs.append(attrs)
}
}
var headerAttributes = GridViewRulerAttributes(forSupplementaryViewOfKind: GridRulerView.Kind, withIndexPath: NSIndexPath(forItem: 0, inSection: 0))
headerAttributes.zIndex = 500
headerAttributes.frame = CGRectMake(0, self.collectionView!.contentOffset.y, self.preCalculatedContentSize.width, 80)
attribs.append(headerAttributes)
return attribs
}
When I start the app, everything looks great, exactly how I want it.
BUT as soon as I scroll the collectionView beyond it's bounds (collectionView's contentOffset = (-0.5,-0.5) <- any negative values for example) I get a crash:
*** Assertion failure in -[UICollectionViewData validateLayoutInRect:], /SourceCache/UIKit/UIKit-3318.16.25/UICollectionViewData.m:426
The interesting thing is that if I set the header view attribute's frame's y property to 0 instead of the variable contentOffset.y (which becomes negative), everything works fine.
Anybody any idea why this happens ?
EDIT
Here is a more informative error message:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'layout attributes for supplementary item at index path ( {length = 2, path = 0 - 0}) changed from index path: ( {length = 2, path = 0 - 0}); element kind: (GridRulerViewKind); frame = (0 0; 425.227 24); zIndex = 500; to index path: ( {length = 2, path = 0 - 0}); element kind: (GridRulerViewKind); frame = (0 -5; 425.227 24); zIndex = 500; without invalidating the layout'
Wow, actually disabled "All Exceptions" breakpoints and got a more informative error to work with:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'layout attributes for supplementary item at index path ( {length = 2, path = 0 - 0}) changed from index path: ( {length = 2, path = 0 - 0}); element kind: (GridRulerViewKind); frame = (0 0; 425.227 24); zIndex = 500; to index path: ( {length = 2, path = 0 - 0}); element kind: (GridRulerViewKind); frame = (0 -5; 425.227 24); zIndex = 500; without invalidating the layout'
Now, I still had no idea what to do, or where to
"invalidate the layout"
Then while browsing similar questions, I came across a similar question here on SO where I stumbled upon this simple function:
override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool{
return true
}
And sure enough, that seemed to work! No crash anymore!
EDIT
Actually, this is causing the collection view layout to invalidate on every scroll, so this does not seem to be the solution I am looking for...
Edit #2
After a bit of research, I don't think it is possible to create a supplementary view that changes by itself without invalidating the layout (which kind of seems perfectly logical - how would the layout know to just update the ruler view without explicitly saying so ?). And updating the layout on each scroll is just way too expensive.
So I figured I will just create a separate view which is entirely independent of the collection view and simply listening to the collection view's scroll delegate and adjusting its frame according to the scroll offset. I think this approach is much more efficient in terms of performance and is equally easy to implement.
Solution
Just check representedElementKind
class CustomFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributesForElementsInRect = super.layoutAttributesForElements(in: rect)
var newAttributesForElementsInRect = [UICollectionViewLayoutAttributes]()
for attributes in attributesForElementsInRect! {
if !(attributes.representedElementKind == UICollectionElementKindSectionHeader
|| attributes.representedElementKind == UICollectionElementKindSectionFooter) {
// cells will be customise here, but Header and Footer will have layout without changes.
}
newAttributesForElementsInRect.append(attributes)
}
return newAttributesForElementsInRect
}
}
class YourViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let headerNib = UINib.init(nibName: "HeaderCell", bundle: nil)
collectionView.register(headerNib, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "HeaderCell")
let footerNib = UINib.init(nibName: "FooterCell", bundle: nil)
collectionView.register(footerNib, forSupplementaryViewOfKind: UICollectionElementKindSectionFooter, withReuseIdentifier: "FooterCell")
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionElementKindSectionHeader:
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "HeaderCell", for: indexPath) as! HeaderCell
return headerView
case UICollectionElementKindSectionFooter:
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "FooterCell", for: indexPath) as! FooterCell
return footerView
default:
return UICollectionReusableView()
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: collectionView.frame.width, height: 45)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
return CGSize(width: collectionView.frame.width, height: 25)
}
}
Why you are passing GridRulerView.Kind for ReuseIdentifier?
reusableHeaderView = collectionView.dequeueReusableSupplementaryViewOfKind(GridRulerView.Kind, withReuseIdentifier: **GridRulerView.Kind**, forIndexPath: indexPath) as! UICollectionReusableView
It should be as below.
var supplementaryView =
collectionView.dequeueReusableSupplementaryViewOfKind(kind,
withReuseIdentifier:Identifiers.HeaderIdentifier.rawValue,
forIndexPath: indexPath) as UICollectionReusableView

How to add a background image to UICollectionView that will scroll and zoom will cells

I'm building a mosaic view using UICollectionView.
I have subclassed UICollectionViewFlowLayout to layout a fixed grid that can be scrolled both horizontally and vertically. I have also attached a UIPinchGestureRecognizer so the collection can scaled/zoomed.
Each cell in the collection contains a UIImage with some transparency. I want to add a background image that will scroll and zoom with the cells.
I've attempted a number of different solutions.
setting the background color of the UICollectionView using colorWithPatternImage. (does not scroll/resize with content)
setting a background image view on each cell to the relevant cropped portion of the background image. (uses far too much memory)
I've been looking into Supplementary and Decoration Views but struggling to get my head around it. I think I need to use Supplementary views as the image used in the background will change depending on the datasource.
What I don't understand is how I can register just one Supplementary View to span the width and height of the whole collectionview. They seem to be tied to an indexPath i.e each cell.
Don't know if you found an answer...!
You're on the right track with wanting to use supplementary views. The index path of the supplementary view isn't tied to a cell, it has its own index path.
Then in your subclass of UICollectionViewFlowLayout you need to subclass a few methods. The docs are pretty good!
In the layoutAttributesForElementsInRect: method you'll need to call super and then add another set of layout attributes for your supplementary view.
Then in the layoutAttributesForSupplementaryViewOfKind:atIndexPath: method you set the size of the returned attributes to the size of the collection view content so the image fills all the content, and not just the frame. You also probably want to set the z-order to, to make sure it's behind the cells. See the docs for UICollectionViewLayoutAttributes
#implementation CustomFlowLayout
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray *attributes = [[super layoutAttributesForElementsInRect:rect] mutableCopy];
// Use your own index path and kind here
UICollectionViewLayoutAttributes *backgroundLayoutAttributes = [self layoutAttributesForSupplementaryViewOfKind:#"background" atIndexPath:[NSIndexPath indexPathWithItem:0 inSection:0]];
[attributes addObject:backgroundLayoutAttributes];
return [attributes copy];
}
-(UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
if ([kind isEqualToString:#"background"]) {
UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:kind withIndexPath:indexPath];
attrs.size = [self collectionViewContentSize];
attrs.zIndex = -10;
return attrs;
} else {
return [super layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:indexPath];
}
}
#end
In your collection view data source you need this method:
collectionView:viewForSupplementaryElementOfKind:atIndexPath:
-(void)viewDidLoad
{
[super viewDidLoad];
// Setup your collection view
UICollectionView *collectionView = [UICollectionView initWithFrame:self.view.bounds collectionViewLayout:[CustomFlowLayout new]];
[collectionView registerClass:[BackgroundReusableView class] forSupplementaryViewOfKind:#"background" withReuseIdentifier:#"backgroundView"];
}
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
{
if ([kind isEqualToString:#"background"]) {
BackgroundReusableView *view = [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:#"backgroundView" forIndexPath:indexPath];
// Set extra info
return view;
} else {
// Shouldn't be called
return nil;
}
}
Hopefully all that should get you on the right track :)
To implement custom section background in CollectionView in Swift 5,
class CustomFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var attributes = super.layoutAttributesForElements(in: rect)
for section in 0..<collectionView!.numberOfSections{
let backgroundLayoutAttributes:UICollectionViewLayoutAttributes = layoutAttributesForSupplementaryView(ofKind: "background", at: IndexPath(item: 0, section: section)) ?? UICollectionViewLayoutAttributes()
attributes?.append(backgroundLayoutAttributes)
}
return attributes
}
override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attrs = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: elementKind, with: indexPath)
if elementKind == "background"{
attrs.size = collectionView!.contentSize
//calculate frame here
let items = collectionView!.numberOfItems(inSection: indexPath.section)
let totalSectionHeight:CGFloat = CGFloat(items * 200)
let cellAttr = collectionView!.layoutAttributesForItem(at: indexPath)
attrs.frame = CGRect(x: 0, y: cellAttr!.frame.origin.y, width: collectionView!.frame.size.width, height: totalSectionHeight)
attrs.zIndex = -10
return attrs
}else{
return super.layoutAttributesForSupplementaryView(ofKind: elementKind, at: indexPath)
}
}
}
In your CollectionView DataSource,
override func viewDidLoad() {
super.viewDidLoad()
//register collection view here
...
//setup flow layout & register supplementary view
let customFlowLayout = CustomFlowLayout()
collectionView.collectionViewLayout = customFlowLayout
collectionView.register(UINib(nibName: "BackgroundReusableView", bundle: nil), forSupplementaryViewOfKind: "background", withReuseIdentifier: "BackgroundReusableView")
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
if kind == "background"{
let view = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "BackgroundReusableView", for: indexPath) as! BackgroundReusableView
return view
}
return UICollectionReusableView()
}

UICollectionView & custom UICollectionReusableView not works

I'm trying to use custom UICollectionReusableView (which has own class and XIB) in my UICollectionView header. But after fetching data in the place of header I have nothing.
My steps:
Registering class in viewDidLoad:
[self.collectionView registerClass:[CollectionViewHeader class]
forSupplementaryViewOfKind: UICollectionElementKindSectionHeader
withReuseIdentifier:#"HeaderView"];
Trying to show:
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
{
UICollectionReusableView *reusableView = nil;
if (kind == UICollectionElementKindSectionHeader) {
CollectionViewHeader *collectionHeader = [self.collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:#"HeaderView" forIndexPath:indexPath];
NSInteger section = [indexPath section];
id <NSFetchedResultsSectionInfo> sectionInfo = [fetchRecipes sections][section];
collectionHeader.headerLabel.text = #"bla-bla-bla";
reusableView = collectionHeader;
}
return reusableView;
}
Can anybody tell me what's wrong? )
Thanks for any advice
I think you are adding label to the xib. So you need to registerNib: for the header view instead of registerClass:
Register Your header nib/xib in the viewDidLoad section.
[self.collectionView registerNib: [UINib nibWithNibName:#"headerCollectionViewCell" bundle:nil] forCellWithReuseIdentifier:#"headerCell"];
Create the custom supplementary view cell.
- (headerCollectionViewCell *)collectionView:(UICollectionView *)collectionViews viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
{
UICollectionReusableView *reusableView = nil;
if (kind == UICollectionElementKindSectionHeader) {
UINib *nib = [UINib nibWithNibName:#"headerCollectionViewCell" bundle:nil];
[collectionViews registerNib:nib forCellWithReuseIdentifier:#"headerCell"];
headerCollectionViewCell *collectionHeader = [collectionViews dequeueReusableCellWithReuseIdentifier:#"headerCell" forIndexPath:indexPath];
collectionHeader.titleLabel.text = #"What";
reusableView = collectionHeader;
}
return reusableView;
}
Just in case if someone needs solution.
You don't have to register the header class using
[self.collectionView registerClass:...]
Just use the layout's delegate method to return the header class.
Actually there is can be few reasons:
(most common)
If u add u'r suplementaryView in storyboard, and then try to register class
[self.stickyCollectionView registerClass:[<CLASSFORSUPPLEMENTARYVIEWW> class]
forSupplementaryViewOfKind:UICollectionElementKindSectionHeader
withReuseIdentifier:NSStringFromClass([<CLASSFORSUPPLEMENTARYVIEWW> class])];
u will get suplementaryView, but not that u create (standard one) - u will see obj, but actually this is will be like placeholder.
Example:
Here u can see that obj was created, but outlets are nil (in this simple case only one outlet - _monthNameLabel).
I see few times this problem also
If u create separate Obj for handling dataSourse/delegate for collectionView and add reference outlet to it, and in init methods try to register class to u'r outlet (assumed that view created in separate nib file), u will also receive same result like previous, but reason another - u'r outlet is nil here:
As solution u for example can use custom setter for this like:
#pragma mark - CustomAccessors
- (void)setcCollectionView:(UICollectionView *)collectionView
{
_collectionView = collectionView;
[self.stickyCollectionView registerNib:...];
}
as suggested by #Anil Varghese
u can use registerClass instead of registerNib
Xcode 9, Swift 4:
Register UICollectionReusableView in viewDidLoad
self.collectionView.register(UINib(nibName: "DashBoardCollectionReusableView", bundle: nil), forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "ReuseIdentifier")
class CustomFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let attributesForElementsInRect = super.layoutAttributesForElements(in: rect)
var newAttributesForElementsInRect = [UICollectionViewLayoutAttributes]()
for attributes in attributesForElementsInRect! {
if !(attributes.representedElementKind == UICollectionElementKindSectionHeader
|| attributes.representedElementKind == UICollectionElementKindSectionFooter) {
// cells will be customise here, but Header and Footer will have layout without changes.
}
newAttributesForElementsInRect.append(attributes)
}
return newAttributesForElementsInRect
}
}
class YourViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let headerNib = UINib.init(nibName: "HeaderCell", bundle: nil)
collectionView.register(headerNib, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "HeaderCell")
let footerNib = UINib.init(nibName: "FooterCell", bundle: nil)
collectionView.register(footerNib, forSupplementaryViewOfKind: UICollectionElementKindSectionFooter, withReuseIdentifier: "FooterCell")
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionElementKindSectionHeader:
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "HeaderCell", for: indexPath) as! HeaderCell
return headerView
case UICollectionElementKindSectionFooter:
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "FooterCell", for: indexPath) as! FooterCell
return footerView
default:
return UICollectionReusableView()
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: collectionView.frame.width, height: 45)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
return CGSize(width: collectionView.frame.width, height: 25)
}
}
You was registering correctly the class and the delegate is implemented correctly.
The problem can be that the flow layout (by default) is not configured for header view, do you have this line in the code or interface file?
UICollectionViewFlowLayout *flowLayout = (UICollectionViewFlowLayout *)_collectionView.collectionViewLayout;
flowLayout.headerReferenceSize = CGSizeMake(CGRectGetWidth(_collectionView.bounds), 100);

How to change background color of a whole section in UICollectionView?

In UICollectionView, I want to give the whole section a uniform background color, instead of for a single cell or for the whole collection view.
I don't see any delegate method to do that, any suggestions?
The new UICollectionViewCompositionalLayout introduced in iOS 13 have a property named decorationItems for adding decoration items conveniently, which you could use to add a background for the section.
let section = NSCollectionLayoutSection(group: group)
section.decorationItems = [
NSCollectionLayoutDecorationItem.background(elementKind:"your identifier")
]
First, make a UICollectionReusableView to be your background view. I've simply set mine to be red.
class SectionBackgroundDecorationView: UICollectionReusableView {
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .red
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Then, in YourCollectionViewController:
static let background = "background-element-kind"
init() {
super.init(collectionViewLayout: YourCollectionViewController.createLayout())
}
static func createLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int,
layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
// Use sectionIndex to control the layout in each section
if sectionIndex == 0 {
// Customise itemSize, item, groupSize, group to be whatever you want
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1),
heightDimension: .absolute(170))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
// Create a sectionBackground
let sectionBackground = NSCollectionLayoutDecorationItem.background(
elementKind: background)
section.decorationItems = [sectionBackground]
return section
} else {
// Your layout for other sections, etc
// You don't have to set a background for other sections if you don't want to
}
}
layout.register(SectionBackgroundDecorationView.self, forDecorationViewOfKind: background)
return layout
}
These links to documentation helped me:
https://developer.apple.com/documentation/uikit/nscollectionlayoutdecorationitem
https://developer.apple.com/documentation/uikit/uicollectionviewcompositionallayoutsectionprovider
The idea is to override UICollectionViewLayoutAttributes to add a color attribute. And then override UICollectionReusableView apply the color to the view background.
https://github.com/strawberrycode/SCSectionBackground
I haven't tried this out yet, but it looks to me that you need to use decoration views if you want a background behind your cells (like the shelf in the Books app). I think you should be able to have different views for each section, and set them up using the delegate method layoutAttributesForDecorationViewOfKind:atIndexPath:.
refer to:
strawberrycode: Use DecorationView as
background
devxoul: Use SupplementaryElement as background
airbnb: Use SupplementaryElement as background
I need to compatible with IGListKit, so I use decorationView as
background. The layout is subclass from UICollectionViewFlowLayout for common use case. My implementation:
In collection view every section can have a supplementary views, so put supplementary views for each section then set background color to supplementary views instead of section cells. I hope it will help.
One of the classic approach is to create a Custom Supplementary Kind and provide your custom view in CollectionView Section background. It will give you the ability to customize section backgrounds.
Refer to https://stackoverflow.com/a/63598373/14162081
I went off of this repo here https://github.com/SebastienMichoy/CollectionViewsDemo/tree/master/CollectionViewsDemo/Sources/Collections%20Views
Swift 3
subclass uicollectionreusableview
class SectionView: UICollectionReusableView {
static let kind = "sectionView"
}
subclass uicollectionViewFlowLayout
class CustomFlowLayout: UICollectionViewFlowLayout {
// MARK: Properties
var decorationAttributes: [IndexPath: UICollectionViewLayoutAttributes]
var sectionsWidthOrHeight: [IndexPath: CGFloat]
// MARK: Initialization
override init() {
self.decorationAttributes = [:]
self.sectionsWidthOrHeight = [:]
super.init()
}
required init?(coder aDecoder: NSCoder) {
self.decorationAttributes = [:]
self.sectionsWidthOrHeight = [:]
super.init(coder: aDecoder)
}
// MARK: Providing Layout Attributes
override func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return self.decorationAttributes[indexPath]
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var attributes = super.layoutAttributesForElements(in: rect)
let numberOfSections = self.collectionView!.numberOfSections
var xOrYOffset = 0 as CGFloat
for sectionNumber in 0 ..< numberOfSections {
let indexPath = IndexPath(row: 0, section: sectionNumber)
let numberOfItems = self.collectionView?.numberOfItems(inSection: sectionNumber)
let sectionWidthOrHeight = numberOfItems == 0 ? UIScreen.main.bounds.height : collectionViewContentSize.height//self.sectionsWidthOrHeight[indexPath]!
let decorationAttribute = UICollectionViewLayoutAttributes(forDecorationViewOfKind: SectionView.kind, with: indexPath)
decorationAttribute.zIndex = -1
if self.scrollDirection == .vertical {
decorationAttribute.frame = CGRect(x: 0, y: xOrYOffset, width: self.collectionViewContentSize.width, height: sectionWidthOrHeight)
} else {
decorationAttribute.frame = CGRect(x: xOrYOffset, y: 0, width: sectionWidthOrHeight, height: self.collectionViewContentSize.height)
}
xOrYOffset += sectionWidthOrHeight
attributes?.append(decorationAttribute)
self.decorationAttributes[indexPath] = decorationAttribute
}
return attributes
}
}
implement this
CollectionView delegate function
func collectionView(_ collectionView: UICollectionView, willDisplaySupplementaryView view: UICollectionReusableView, forElementKind elementKind: String, at indexPath: IndexPath) {
Log.printLog(identifier: elementKind, message: indexPath)
if elementKind == UICollectionElementKindSectionHeader, let view = view as? ProfileViewHeaderView {
view.backgroundColor = UIColor(red: (102 / 255.0), green: (169 / 255.0), blue: (251 / 255.0), alpha: 1)
} else if elementKind == SectionView.kind {
let evenSectionColor = UIColor.black
let oddSectionColor = UIColor.red
view.backgroundColor = (indexPath.section % 2 == 0) ? evenSectionColor : oddSectionColor
}
}
This is important
let layout = CustomFlowLayout()
layout.register(SectionView.self, forDecorationViewOfKind: SectionView.kind)
register the UICollectionReusableView with layout not collectionView.
one more thing. I messed around with the height in layoutAttributesForElements. you should change it for your own project.
Its very simple just use this default UICollectionViewDelegate's method, it will works
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
print(indexPath.item)
let evenSectionColor = UIColor.clear
let oddSectionColor = UIColor.white
cell.contentView.backgroundColor = (indexPath.item % 2 == 0) ? evenSectionColor : oddSectionColor
}
I have changed the background color of each section in a very simple manner in the following method: But I was not sure whether it is the right thing to do. But it did work.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
FamilyCalendarCellItemCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"calendarItem" forIndexPath:indexPath];
Event *event;
_headerView = [collectionView dequeueReusableSupplementaryViewOfKind:
UICollectionElementKindSectionHeader withReuseIdentifier:#"EventHeader" forIndexPath:indexPath]; //headerView is declared as property of Collection Reusable View class
if(indexPath.section==0) {
cell.backgroundColor=[UIColor orangeColor];
}
else if(indexPath.section==1) {
cell.backgroundColor=[UIColor yellowColor];
}
return cell;
}

Resources