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);
Related
In my controller, I parse data from a JSON array and use it to populate a collection view. The code below successfully loads and displays the data, but fails to create individual sections.
As such, how do I modify the code to create sections for each object within the array. For example, if my array has a count of 50 urls, how do I create one section for each?
let lastItem = self.photos.count
self.photos.addObjectsFromArray(photoInfos)
let indexPaths = (lastItem..<self.photos.count).map { NSIndexPath(forItem: $0, inSection: 0) }
dispatch_async(dispatch_get_main_queue()) {
self.collectionView!.insertItemsAtIndexPaths(indexPaths)
}
Most of the time the apps use one section and many items in this section so that's why it may be a bit harder to find how to do your way.
It's all about how you use this 2 functions of the datasource:
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return self.items.count
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
Normally you return 1 section and the array count in the numberOfItemsInSection, but if you switch them you would have exactly what you want.
here is my code is in Objective-C, I hope it'll helpful to you.
Here HeaderView is sub class of UICollectionReusableView.
Where you can set view as you like.
-(CGSize) collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section {
return CGSizeMake(320.0f, 32.0f);
}
-(UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
{
HeaderView *view=(HeaderView *)[historyCollection dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:#"headerView" forIndexPath:indexPath];
NSArray *viewsToRemove = [view subviews];
for (UIView *v in viewsToRemove) {
[v removeFromSuperview];
}
view.backgroundColor=[UIColor clearColor];
self.month = [[UILabel alloc] initWithFrame:CGRectMake(0, 5, 320, 21)];
self.month.text =[[monthList objectAtIndex:indexPath.section] uppercaseString];
self.month.textColor = [UIColor greenColor];
self.month.font=[UIFont boldSystemFontOfSize:17];
[view addSubview:self.month];
return view;
}
- (CGFloat)collectionView:(UICollectionView *)collectionView heightForHeaderInSection:(NSInteger)section {
if (section == 0) {
return 0;
}
else if (section == 1) {
return 40.0;
}
else {
return 50;
}
}
Here is the code that worked for me
create the header cell. To do which I created a custom cell class and a nib to do the customization of the cell in the graphic editor
In viewDidLoad
self.collectionView?.registerNib(UINib(nibName: "KlosetCollectionHeaderViewCell", bundle: nil), forSupplementaryViewOfKind:UICollectionElementKindSectionHeader, withReuseIdentifier: "HeaderCell")
Then you add the delegate function
override func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> KlosetCollectionHeaderViewCell {
println("entring viewforsuppplementaryElementofKind")
var headerCell = collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: "HeaderCell", forIndexPath: indexPath) as? KlosetCollectionHeaderViewCell
return headerCell!
}
This will put the HeaderCell in the SectionView of the PFCollectionView
The controls that show in the cell you add them to the xib file as well as the outlets and actions
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()
}
I am trying to add footer to the UICollectionView.
Following is my code,
UICollectionView is added through IB
IN viewDidLoad I register the footer,
[mCollectionView registerClass:[UICollectionReusableView class] forSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:#"footer"];
And implemented following method
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
{
UICollectionReusableView *reusableview = nil;
if (kind == UICollectionElementKindSectionFooter) {
UICollectionReusableView *headerView = [mCollectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:#"footer" forIndexPath:indexPath];
[headerView addSubview:mFooterView];
reusableview = headerView;
}
return reusableview;
}
But My application keep crashing and below is the log,
*** Assertion failure in -[UICollectionView _dequeueReusableViewOfKind:withIdentifier:forIndexPath:], /SourceCache/UIKit/UIKit-2380.17/UICollectionView.m:2249
Any help is appreciated.
Thanks.
in your code why you are dequeing a header view and adding the footer to it ?
the normal implementation of this method is :
- (UICollectionReusableView *)collectionView:(UICollectionView *)theCollectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)theIndexPath
{
UICollectionReusableView *theView;
if(kind == UICollectionElementKindSectionHeader)
{
theView = [theCollectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:#"header" forIndexPath:theIndexPath];
} else {
theView = [theCollectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:#"footer" forIndexPath:theIndexPath];
}
return theView;
}
For Swift 4
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
var myView = UICollectionReusableView()
if kind == UICollectionView.elementKindSectionHeader {
myView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: myHeader, for: indexPath)
} else {
myView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: myFooter, for: indexPath)
}
return myView
}
Upon cell selection, I want to handle changing the cell appearance. I figured the delegate method collectionView:didSelectItemAtIndexPath: & collectionView:didDeselectItemAtIndexPath: is where I should edit the cell.
-(void)collectionView:(UICollectionView *)collectionView
didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
DatasetCell *datasetCell =
(DatasetCell *)[collectionView cellForItemAtIndexPath:indexPath];
[datasetCell replaceHeaderGradientWith:[UIColor skyBlueHeaderGradient]];
datasetCell.backgroundColor = [UIColor skyBlueColor];
}
and
-(void)collectionView:(UICollectionView *)collectionView
didDeselectItemAtIndexPath:(NSIndexPath *)indexPath {
DatasetCell *datasetCell =
(DatasetCell *)[collectionView cellForItemAtIndexPath:indexPath];
[datasetCell replaceHeaderGradientWith:[UIColor grayGradient]];
datasetCell.backgroundColor = [UIColor myDarkGrayColor];
}
This works fine, except when the cell gets reused. If I select cell at index (0, 0), it changes the appearance but when I scroll down, there is another cell in the selected state.
I believe I should use the UICollectionViewCell method -(void)prepareForReuse to prep the cell for resuse (ie, set the cell appearance to non selected state) but its giving me difficulties.
-(void)prepareForReuse {
if ( self.selected ) {
[self replaceHeaderGradientWith:[UIColor skyBlueHeaderGradient]];
self.backgroundColor = [UIColor skyBlueColor];
} else {
[self replaceHeaderGradientWith:[UIColor grayGradient]];
self.backgroundColor = [UIColor myDarkGrayColor];
}
}
When I scroll back to the top, the cell at index (0, 0) is in the deselected state.
When I just used the cell.backgroundView property, to prevent this from happening was to:
-(void)prepareForReuse {
self.selected = FALSE;
}
and the selection state worked as intended.
Any ideas?
Your observation is correct. This behavior is happening due to the reuse of cells. But you dont have to do any thing with the prepareForReuse. Instead do your check in cellForItem and set the properties accordingly. Some thing like..
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"cvCell" forIndexPath:indexPath];
if (cell.selected) {
cell.backgroundColor = [UIColor blueColor]; // highlight selection
}
else
{
cell.backgroundColor = [UIColor redColor]; // Default color
}
return cell;
}
-(void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *datasetCell =[collectionView cellForItemAtIndexPath:indexPath];
datasetCell.backgroundColor = [UIColor blueColor]; // highlight selection
}
-(void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *datasetCell =[collectionView cellForItemAtIndexPath:indexPath];
datasetCell.backgroundColor = [UIColor redColor]; // Default color
}
Framework will handle switching the views for you once you setup your cell's backgroundView and selectedBackgroundView, see example from Managing the Visual State for Selections and Highlights:
UIView* backgroundView = [[UIView alloc] initWithFrame:self.bounds];
backgroundView.backgroundColor = [UIColor redColor];
self.backgroundView = backgroundView;
UIView* selectedBGView = [[UIView alloc] initWithFrame:self.bounds];
selectedBGView.backgroundColor = [UIColor whiteColor];
self.selectedBackgroundView = selectedBGView;
you only need in your class that implements UICollectionViewDelegate enable cells to be highlighted and selected like this:
- (BOOL)collectionView:(UICollectionView *)collectionView
shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
- (BOOL)collectionView:(UICollectionView *)collectionView
shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath;
{
return YES;
}
This works me.
UICollectionView has changed in iOS 10 introducing some problems to solutions above.
Here is a good guide:
https://littlebitesofcocoa.com/241-uicollectionview-cell-pre-fetching
Cells now stay around for a bit after going off-screen. Which means that sometimes we might not be able to get hold of a cell in didDeselectItemAt indexPath in order to adjust it. It can then show up on screen un-updated and un-recycled. prepareForReuse does not help this corner case.
The easiest solution is disabling the new scrolling by setting isPrefetchingEnabled to false. With this, managing the cell's display with
cellForItemAt didSelect didDeselect works as it used to.
However, if you'd rather keep the new smooth scrolling behaviour it's better to use willDisplay :
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
let customCell = cell as! CustomCell
if customCell.isSelected {
customCell.select()
} else {
customCell.unselect()
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath) as! CustomCell
//Don't even need to set selection-specific things here as recycled cells will also go through willDisplay
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) as? CustomCell
cell?.select()
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath) as? CustomCell
cell?.unselect() // <----- this can be null here, and the cell can still come back on screen!
}
With the above you control the cell when it's selected, unselected on-screen, recycled, and just re-displayed.
Anil was on the right track (his solution looks like it should work, I developed this solution independently of his). I still used the prepareForReuse: method to set the cell's selected to FALSE, then in the cellForItemAtIndexPath I check to see if the cell's index is in `collectionView.indexPathsForSelectedItems', if so, highlight it.
In the custom cell:
-(void)prepareForReuse {
self.selected = FALSE;
}
In cellForItemAtIndexPath: to handle highlighting and dehighlighting reuse cells:
if ([collectionView.indexPathsForSelectedItems containsObject:indexPath]) {
[collectionView selectItemAtIndexPath:indexPath animated:FALSE scrollPosition:UICollectionViewScrollPositionNone];
// Select Cell
}
else {
// Set cell to non-highlight
}
And then handle cell highlighting and dehighlighting in the didDeselectItemAtIndexPath: and didSelectItemAtIndexPath:
This works like a charm for me.
I had a horizontal scrolling collection view (I use collection view in Tableview) and I too faced problems withcell reuse, whenever I select one item and scroll towards right, some other cells in the next visible set gets select automatically. Trying to solve this using any custom cell properties like "selected", highlighted etc didnt help me so I came up with the below solution and this worked for me.
Step1:
Create a variable in the collectionView to store the selected index, here I have used a class level variable called selectedIndex
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
MyCVCell *cell = (MyCVCell*)[collectionView dequeueReusableCellWithReuseIdentifier:#"MyCVCell" forIndexPath:indexPath];
// When scrolling happens, set the selection status only if the index matches the selected Index
if (selectedIndex == indexPath.row) {
cell.layer.borderWidth = 1.0;
cell.layer.borderColor = [[UIColor redColor] CGColor];
}
else
{
// Turn off the selection
cell.layer.borderWidth = 0.0;
}
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
MyCVCell *cell = (MyCVCell *)[collectionView cellForItemAtIndexPath:indexPath];
// Set the index once user taps on a cell
selectedIndex = indexPath.row;
// Set the selection here so that selection of cell is shown to ur user immediately
cell.layer.borderWidth = 1.0;
cell.layer.borderColor = [[UIColor redColor] CGColor];
[cell setNeedsDisplay];
}
- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath
{
MyCVCell *cell = (MyCVCell *)[collectionView cellForItemAtIndexPath:indexPath];
// Set the index to an invalid value so that the cells get deselected
selectedIndex = -1;
cell.layer.borderWidth = 0.0;
[cell setNeedsDisplay];
}
-anoop
What I did to solve this was to make the changes in the customized cell. You have a custom cell called DataSetCell in its class you could do the following (the code is in swift)
override var isSelected: Bool {
didSet {
if isSelected {
changeStuff
} else {
changeOtherStuff
}
}
}
What this does is that every time the cell is selected, deselected, initialized or get called from the reusable queue, that code will run and the changes will be made. Hope this helps you.
In your custom cell create public method:
- (void)showSelection:(BOOL)selection
{
self.contentView.backgroundColor = selection ? [UIColor blueColor] : [UIColor white];
}
Also write redefenition of -prepareForReuse cell method:
- (void)prepareForReuse
{
[self showSelection:NO];
[super prepareForReuse];
}
And in your ViewController you should have _selectedIndexPath variable, which defined in -didSelectItemAtIndexPath and nullified in -didDeselectItemAtIndexPath
NSIndexPath *_selectedIndexPath;
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"Cell";
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
if (_selectedIndexPath) {
[cell showSelection:[indexPath isEqual:_selectedIndexPath]];
}
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
[cell showSelection:![indexPath isEqual:_selectedIndexPath]];// on/off selection
_selectedIndexPath = [indexPath isEqual:_selectedIndexPath] ? nil : indexPath;
}
- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collectionView cellForItemAtIndexPath:indexPath];
[cell showSelection:NO];
_selectedIndexPath = nil;
}
Only #stefanB solution worked for me on iOS 9.3
Here what I have to change for Swift 2
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
//prepare your cell here..
//Add background view for normal cell
let backgroundView: UIView = UIView(frame: cell!.bounds)
backgroundView.backgroundColor = UIColor.lightGrayColor()
cell!.backgroundView = backgroundView
//Add background view for selected cell
let selectedBGView: UIView = UIView(frame: cell!.bounds)
selectedBGView.backgroundColor = UIColor.redColor()
cell!.selectedBackgroundView = selectedBGView
return cell!
}
func collectionView(collectionView: UICollectionView, shouldHighlightItemAtIndexPath indexPath: NSIndexPath) -> Bool {
return true
}
func collectionView(collectionView: UICollectionView, shouldSelectItemAtIndexPath indexPath: NSIndexPath) -> Bool {
return true
}
The problem you encounter comes from the lack of call to super.prepareForReuse().
Some other solutions above, suggesting to update the UI of the cell from the delegate's functions, are leading to a flawed design where the logic of the cell's behaviour is outside of its class. Furthermore, it's extra code that can be simply fixed by calling super.prepareForReuse(). For example :
class myCell: UICollectionViewCell {
// defined in interface builder
#IBOutlet weak var viewSelection : UIView!
override var isSelected: Bool {
didSet {
self.viewSelection.alpha = isSelected ? 1 : 0
}
}
override func prepareForReuse() {
// Do whatever you want here, but don't forget this :
super.prepareForReuse()
// You don't need to do `self.viewSelection.alpha = 0` here
// because `super.prepareForReuse()` will update the property `isSelected`
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
self.viewSelection.alpha = 0
}
}
With such design, you can even leave the delegate's functions collectionView:didSelectItemAt:/collectionView:didDeselectItemAt: all empty, and the selection process will be totally handled, and behave properly with the cells recycling.
you can just set the selectedBackgroundView of the cell to be backgroundColor=x.
Now any time you tap on cell his selected mode will change automatically and will couse to the background color to change to x.
Thanks to your answer #RDC.
The following codes works with Swift 3
// MARK: - UICollectionViewDataSource protocol
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
//prepare your cell here..
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath as IndexPath) as! MyCell
cell.myLabel.text = "my text"
//Add background view for normal cell
let backgroundView: UIView = UIView(frame: cell.bounds)
backgroundView.backgroundColor = UIColor.lightGray
cell.backgroundView = backgroundView
//Add background view for selected cell
let selectedBGView: UIView = UIView(frame: cell.bounds)
selectedBGView.backgroundColor = UIColor.green
cell.selectedBackgroundView = selectedBGView
return cell
}
// MARK: - UICollectionViewDelegate protocol
func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool {
return true
}
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
return true
}
Changing the cell property such as the cell's background colors shouldn't be done on the UICollectionViewController itself, it should be done inside you CollectionViewCell class. Don't use didSelect and didDeselect, just use this:
class MyCollectionViewCell: UICollectionViewCell
{
override var isSelected: Bool
{
didSet
{
// Your code
}
}
}
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;
}