UICollectionViewCell's are not retaining with ARC - ios

I have a UICollectionView, which receives data from a webservice. Based on the data it receives, it draws cells on my UICollectionView. Each cell is a custom Class which extends UICollectionViewCell. Each UICollectionViewCell is loaded via a .nib. My init method looks like this :
#implementation GridCell
- (id)initWithFrame:(CGRect)frame
{
if (self)
{
// Initialization code
NSArray *arrayOfViews = [[NSBundle mainBundle] loadNibNamed:#"GridCell" owner:self options:nil];
if ([arrayOfViews count] < 1) {
return nil;
}
if (![[arrayOfViews objectAtIndex:0] isKindOfClass:[UICollectionViewCell class]]) {
return nil;
}
self = [arrayOfViews objectAtIndex:0];
// It is important that you set the properties of the view after the above assignment
// because self is assigned to the nib after that call.
self.layer.masksToBounds = NO;
self.layer.shadowColor = [[UIColor blackColor] CGColor];
self.layer.shadowOffset = CGSizeMake(0.0, 1.0);
self.layer.shadowOpacity = 0.17;
self.layer.shadowRadius = 0.35f;
self.layer.shouldRasterize = YES;
}
return self;
}
I have a GridViewController which is basically a UIViewController :
#implementation GridViewController
- (id)initWithSize:(CGFloat)frameWidth :(CGFloat)frameHeight {
self = [super init];
if(self) {
// Have the getGridView returning the initialized view
// then assign it to the GridViewControllers view property
self.view = [self getGridView:frameWidth:frameHeight];
}
return self;
}
-(UIView *)getGridView:(CGFloat)width :(CGFloat)height {
UIView *gridHolder = [[UIView alloc] initWithFrame:CGRectMake(0, 0, width, height)];
UICollectionViewFlowLayout *flow = [[UICollectionViewFlowLayout alloc]init];
[flow setScrollDirection:UICollectionViewScrollDirectionHorizontal];
CGRect screen = [[UIScreen mainScreen] bounds];
CGFloat screenheight = screen.size.height;
// It is an iphone 5, setup the cellsize and the spacing between cells.
if(screenheight > 480) {
[flow setItemSize:CGSizeMake( (gridHolder.frame.size.width / 1.85) , (gridHolder.frame.size.height / 3.25) )];
[flow setSectionInset:UIEdgeInsetsMake(0, 0, 0, 0)];
[flow setMinimumInteritemSpacing:0];
[flow setMinimumLineSpacing:10];
// It is an iphone 4, setup the cellsize and the spacing between cells.
} else {
[flow setItemSize:CGSizeMake( (gridHolder.frame.size.width / 2.5) , (gridHolder.frame.size.height / 3.1) - 10)];
[flow setSectionInset:UIEdgeInsetsMake(0, 0, 0, 0)];
[flow setMinimumInteritemSpacing:0];
[flow setMinimumLineSpacing:10];
}
self.grid = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, gridHolder.frame.size.width, gridHolder.frame.size.height) collectionViewLayout:flow];
[_grid setBackgroundColor:[UIColor colorWithRed:0.961 green:0.961 blue:0.961 alpha:1]];
[_grid setDataSource:self];
[_grid setDelegate:self];
[_grid setBounces:NO];
[_grid registerClass:[GridCell class] forCellWithReuseIdentifier:Cell];
[gridHolder addSubview:self.grid];
return gridHolder;
}
This UIViewController is also my UICollectionViewDataSource. So i have the following methods for it :
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
return [releases count];
}
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
return UIEdgeInsetsMake(10, 10, 10, 10);
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
GridCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:Cell forIndexPath:indexPath];
return cell; // <-- finally return the cell
}
I added an UIButton over the cell. And i connected an IBAction to the button. But each time i click on this button, i get a nullpointer exception because the GridIcon is already thrown away by ARC. Must i retain the GridIcon somewhere? Or what is the best practice to overcome this (simple?) problem?
The method which resides in my GridIcon.m is :
- (IBAction)cellClicked {
NSLog(#"test");
}
And my GridIcon.h file has an IBAction described :
#interface GridCell : UICollectionViewCell
- (IBAction)cellClicked;
#end
Edit :
jackslash, thank you for your time and effort to make me realize this really mad structure. Let me explain how i setup this project.
I have a MainViewController which i set as the RootViewController in my AppDelegate.m. Then in my MainViewController i init 2 UIVIewController's. One of them is my GridViewController which sets the UICollectionView.
So the datasource for this UICollectionView is the UIViewController i init'ed. I needed to draw a region of the screen for this UIViewController so i give the height of the other UIViewController to the GridViewController. And the getGridView method gets the height of the screen minus the height of the view of the other UIViewConroller's view.
The structure is like this :
#interface MainViewController: UIViewController
// These next two classes both extend UIViewController
#property(nonatomic, strong) GridViewController *gridViewcontroller;
#property(nonatomic, strong) BottomBarController *bottomBarcontroller;
#end
In my MainViewController.m [viewDidLoad] i both init those UIViewcontrollers and add their views as a subView to the MainViewController.
What would be the best way to accomplish this then? Should i make a UICollectionViewController separate from my GridViewController? But how does this UICollectionView knows which region of the screen to draw on?
And for the init method within my GridCell, i actually use this to draw a shadow underneath my GridCell. How would i achieve the drawing underneath my cells without this init method?
Edit 2:
In addition to my above edit:
I have an UIImageView and a UILabel on each GridCell. How would i target those in my code?

So whats happening here is messed up in quite a few ways.
Your GridCell init method is mental
You have left some objective-c method parameters unnamed (bad form)
You have a method - (id)initWithSize:(CGFloat)frameWidth :(CGFloat)frameHeight which one would expect to take a CGSize from the name yet it takes two floats
For some reason you are using the -(id)initWithSize:: method to init your view controller
Your GridCell class files have a different name from the class they contain (bad form)
You seem to fundamentally have misunderstood how all this is supposed to work. I'm going to help you here, but bear in mind that you should take the time to understand everything here and to also go and read a lot more about iOS. Perhaps look at the iOS course on iTunes U (CS193p) which is free and will help you write better code.
1.
This is madness. If you want to load your UICollectionViewCell subclass from a xib you dont do this, you register the nib with the collection view in a UICollectionViewController subclass like this:
UINib * cellNib = [UINib nibWithNibName:#"GridCell" bundle:[NSBundle mainBundle]];
[self.collectionView registerNib:cellNib forCellWithReuseIdentifier:cellReuseIdentifier];
Then the cell is loaded from the xib file for you when you get the cell in
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
Make sure you have the class of the cell in your GridCell.xib file set to your GridCell subclass. Never write an init method like this again.
2.
-(UIView *)getGridView:(CGFloat)width :(CGFloat)height
Always name all the parameters in an Objective-c method signature.
-(UIView *)getGridViewWithWidth:(CGFloat)width andHeight:(CGFloat)height
Is miles and miles better. But you would probably better using a CGSize...
3
- (id)initWithSize:(CGFloat)frameWidth :(CGFloat)frameHeight
In Objective-c we would expect this to take a CGSize parameter because your method is named "initWithSize". A CGSize is a C struct with a width and height. You can make one with CGSizeMake(<width>,<height>). You would make your init method look like this:
- (id)initWithSize:(CGSize)size
But you don't really want to be initing a UIViewController with a CGSize...
4
There is a reason UIViewController does not have ANY kind of init method that takes any kind of size or dimension. The idea of a view controller is that it has the size of its view set by its parent (or window) and then is responsible for drawing its contents within its view, whatever size that may be. As a UIViewController subclass In UIKit there is a place for loading other views and view controllers that are needed to display whatever it is you are supposed to display. Check out the method -(void)viewDidLoad and override it. But even so you would be much better off if you simply subclassed UICollectionViewController as you wouldn't need any sort of container and your view controller's view property would already be a collectionView along with many other benefits.
5
Rename your GridIcon.h and GridIcon.m files to be the name of the class they contain. Always follow this rule for the sanity of future you, if not everyone else who works on your project.
Finally the reason its crashing isn't because of ARC or other technology failure its because the implementation here has real fundamental issues.

Related

Faster UICollectionView cells

There are a lot of answers out there for loading images into UITableViews or UICollectionViews. But what if my UICollectionView is displaying views from other view controllers?
Say in my UICollectionViewCell subclass I have this:
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.catViewController = [[CatViewController alloc] initWithNibName:nil bundle:nil];
self.catViewController.view.frame = self.bounds;
[self addSubview:self.catViewController.view];
}
return self;
}
And in my collection view Datasource:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
MyCell* cell = [collectionView dequeueReusableCellWithReuseIdentifier:kSomeId forIndexPath:indexPath];
cell.catViewController.data = self.data[indexPath.row]; //see below
return cell;
}
The catViewController has a setter for the data property. When this property is set, the cat will load it's image, along with some other related images for that view. So how do I properly reuse the MyCell cells so that the collection view doesn't stutter each time it creates (or reuses) a cell? Each MyCell takes up the full width of the collection view, which scrolls horizontally, so every time a new cell scrolls into view, the collection view stalls for a moment.
For High performance CollectionView cells , Use following new stuffs in iOS10 with xcode8
Implement protocol "UICollectionViewDataSourcePrefetching" in you ViewController as
class ViewController: UIViewController , UICollectionViewDataSourcePrefetching {
Set following delegates to your collection view in storyboard (see the attached image)
or programmatically
In ViewController's viewDidLoad method
collectionView.delegate = self
collectionView.dataSource = self
collectionView.prefetchDataSource = self

cellForRowAtIndexPath not called but numberOfRowsInSection called

I have a simple problem but I can't understand what is.
I have a UIViewControler (called RootController) that load a UIView (called SecondView) that contain a tableView. The problem is that the UIView call the numberOfSectionsInTableView and the numberOfRowsInSection but don't call cellForRowAtIndexPath and the table view is not displayed.
The code of the RootViewController is:
SecondView *secondView = [[seconddView alloc] initWithFrame:CGRectMake(0, 60, self.view.bounds.size.width, self.view.bounds.size.height)];
[self.view addSubview:secondView];
And the code of the SecondView is:
#interface SecondView () <UITableViewDelegate, UITableViewDataSource>
#property (nonatomic,retain) UITableView *table;
#end
#implementation SecondView
#synthesize table;
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.table = [[UITableView alloc] init];
self.table.delegate = self;
self.table.dataSource = self;
[self addSubview:self.table];
}
return self;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
cell.textLabel.text = #"Prova";
return cell;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 5;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
Can you help me to find the problem? Thank you.
You need to set the Frame of UITableView
My problem was that I had a simple class doing the implementing of my delegate and data source, but the lifetime of the simple class was too short.
I was doing
MyDataSourceClass* myClass = [[MyDataSourceClass alloc] initWithNSArray:someArray];
tableView.dataSource = self.tableViewDataSource;
tableView.delegate = self.tableViewDataSource;
[tableView reloadData];
// end of function, myClass goes out of scope, and apparently tableView has a weak reference to it
Needed to be doing
self.tableDataSource = [[MyDataSourceClass alloc] initWithNSArray:someArray];
tableView.dataSource = self.tableDataSource;
tableView.delegate = self.tableDataSource;
[tableView reloadData];
// now at the end of the function, tableDataSource is still alive, and the tableView will be able to query it.
Note that the code above is pseudocode from memory. Take from it the concept of "make sure your data source/delegate lives long", but don't copy paste it, because there's other stuff you need to do (like set your frame etc etc).
This could also happen if reloadData is called on a different thread. Make sure it is run on the main thread since all UI stuff has to happen on the main thread.
dispatch_async(dispatch_get_main_queue(),{
self.myTableView.reloadData()
});
In XCode6 same weird problem was occurring to me when "Add Missing Constraints" is applied on tableView on Storyboard to adjust views.
To resolve this issue on Storyboard first clear constraints:
then apply constraints in following fashion:
You can only call the viewcontroller's view AFTER viewDidLoad is called.
You can't interact with self.view in your init method
- (void)viewDidLoad {
[super viewDidLoad];
self.table = [[UITableView alloc] initWithFrame:self.view.bounds];
self.table.delegate = self;
self.table.dataSource = self;
[self addSubview:self.table];
}
In your case, you need to init your tableview with a frame (like a suggested in the code above). Just make sure you add the code in viewDidLoad in your viewController
SecondView *secondView = [[seconddView alloc] initWithFrame:CGRectMake(0, 60, self.view.bounds.size.width, self.view.bounds.size.height)];
[self.view addSubview:secondView];
I have same issue like you:
I have only one method from delegate protocol that called and this is numberOfRowsInSection. Meanwhile I have second method cellForRowAtIndexPath that was not called.
The reason of this behaviour was that my numberOfRowsInSection returned 0 rows and cellForRowAtIndexPath didn't call because it needed to draw 0 cells.
I would comment on Hussain Shabbir's answer, to clarify it, but as I am not yet able to comment, I will post an answer instead.
I had exactly the same issue. numberOfRowsInSection would fire, but cellForRowAt would not. I tore my code apart looking for the reason, but the reason is not in the code.
The solution (for me) was in the storyboard. I had not set constraints for the Table View. Select the table view, click the "Add New Constraints" icon (looks like a square TIE fighter) and set constraints for top, bottm, leading and trailing. Then cellForRowAt will be called and your table will populate.
I hope this helps someone.
This solution was specific for my case but maybe helps someone.
I had the same problem. I was using a computed property instead of stored property; so every time I call the tableView, I was getting a new one.
I had this code:
var tableView: UITableView{
let tableView = UITableView()
tableView.dataSource = self
tableView.delegate = self
tableView.register(SomeCell.self, forCellReuseIdentifier: cellID)
return tableView
}
And this is the fixed code:
lazy var tableView: UITableView = {
let tableView = UITableView()
tableView.dataSource = self
tableView.delegate = self
tableView.register(SomeCell.self, forCellReuseIdentifier: cellID)
return tableView
}()
Delete UITableView and add again in your ViewController

UICollectionView datasource methods not getting called, but are being set in the init

Here is my source code
- (id)initWithCollectionView:(UICollectionView *)collectionView
{
self = [super init];
if (self)
{
self.collectionView = collectionView;
self.collectionView.dataSource = self;
self.collectionView.delegate = self;
[self.collectionView registerClass:[TWNTweetCell class] forCellWithReuseIdentifier:kCell];
self.collectionViewLayout = self.collectionView.collectionViewLayout;
self.tweetArray = #[];
self.tweetTextArray = #[];
self.twitter = [STTwitterAPI twitterAPIOSWithFirstAccount];
}
return self;
}
#pragma mark - CollectionView
#pragma mark DataSource
-(NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection: (NSInteger)section
{
return [self.tweetArray count];
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
TWNTweetCell *cell = (TWNTweetCell *)[collectionView dequeueReusableCellWithReuseIdentifier:kCell forIndexPath:indexPath];
NSDictionary *status = [self.tweetArray objectAtIndex:indexPath.row];
NSString *text = [status valueForKey:#"text"];
cell.usernameLabel.text = screenName;
// cell.createdAtLabel.text = dateString;
cell.autoresizingMask = UIViewAutoresizingFlexibleWidth;
UITextView *textView = [self.tweetTextArray objectAtIndex:indexPath.row];
[cell setTweet:text withTweetTextView:textView];
return cell;
}
All the methods don't get interupted at all by breakpoints. The tweets are getting loaded in the log so I know everything else is ok, its just not recognizing the collection view. And yes i've set the
Anyone have any idea whats going on?
It is not your case, it might be helpful for others who will came here having problem with data source methods not being called. It could be assigning data source like:
collectionView.dataSource = MyDataSource()
which is wrong as dataSource is a weak reference so it needs to be stored by some strong reference to be alive after creating it. Added a private property in a ViewController to keep the strong reference, initialising and then assigning it fixes the issue.
A few suggestions:
Do all your UICollectionView setup and configuration in viewDidLoad.
Ensure you calling the create init method from your other class
Your tweetArray is also empty, so if the number of items method is called, it will return nothing and the other methods will not be called
A couple things:
1) in (and only in) your "init" method, use the underlying instance variable for your #property. That is,
_collectionView = collectionView;
_collectionView.dataSource = self;
_collectionView.delegate = self;
This is called "direct access", and more information can be seen in this related question.
2) in your .h file, make certain to declare that your object conforms to the data source & delegate protocols. E.G.
#interface JustinViewController : UIViewController <UICollectionViewDelegate, UICollectionViewDataSource>
for swift do this , set a property
//MARK: props
let dataSource = MyDataSource()
and in
viewDidLoad(){
// your other code
..
..
collectionView.dataSource = dataSource // it is a strong reference
}
apart form these other general pitfall are
not returning the count or the data source
not populating the data source
Add the collectionView to a view hierarchy.
In the init method you set the property (self.collectionView) but you do not add the collectionView to a view hierarchy. So the collectionView won't call any dataSource or delegate method.
I created collection view in storyboard and linked datasource and delegate but they were not being called in Xcode 8.0 with Swift 3.0. Tried multiple things but the solution was to declare the delegate and datasource in class declaration line:
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
...
}
Previously when we linked delegate and datasource through storyboard it was not required, may be a bug :)
Call [collectionView reloadData] at the end of your init method. The collection view needs to be told to populate itself. I assume UICollectionViewController does this internally, but you don't seem to be using UICollectionViewController (or at least not in the usual way).

How do I alternate a UICollectiveViewCell's background color based on row?

I know this is fairly easy for UITableViewCells but I'm not sure how to approach this using a UICollectionView.
EDIT. Pictures for clarification. Text content of the cells are not the same here but they should be.
In landscape:
In portrait:
I tried to naively switch the color of my cell's text label with the cell's background color based on the index path's row property in the cellForItemAtIndexPath: method. However Index Path's row property isn't really a row in UICollectionViewFlowLayout.
Neither the collection view nor its layout will tell you a “row number” for items. You have to compute it yourself.
If all of your measurements are uniform (which is the case if you didn't implement any UICollectionViewDelegateFlowLayout methods), you can compute it pretty easily.
There are a few places you could do the computation and assign the color. I'm going to show you the “proper” place to do it: in the layout manager.
The “Knowing When to Subclass the Flow Layout” section of the Collection View Programming Guide for iOS tells you the basic things you need to do, but it's pretty bare bones, so I'll walk you through it.
Note: since layoutAttributes is a pretty cumbersome identifier, I usually use pose instead.
UICollectionViewLayoutAttributes subclass
First of all, we need a way to pass the background color from the layout manager to the cell. The right way to do that is by subclassing UICollectionViewLayoutAttributes. The interface just adds one property:
#interface MyLayoutAttributes : UICollectionViewLayoutAttributes
#property (nonatomic, strong) UIColor *backgroundColor;
#end
The implementation needs to implement the NSCopying protocol:
#implementation MyLayoutAttributes
- (instancetype)copyWithZone:(NSZone *)zone {
MyLayoutAttributes *copy = [super copyWithZone:zone];
copy.backgroundColor = self.backgroundColor;
return copy;
}
#end
UICollectionViewCell subclass
Next, the cell needs to use that backgroundColor attributes. You probably already have a UICollectionViewCell subclass. You need to implement applyLayoutAttributes: in your subclass, like this:
#implementation MyCell
- (void)applyLayoutAttributes:(MyLayoutAttributes *)pose {
[super applyLayoutAttributes:pose];
if (!self.backgroundView) {
self.backgroundView = [[UIView alloc] init];
}
self.backgroundView.backgroundColor = pose.backgroundColor;
}
#end
UICollectionViewFlowLayout subclass
Now you need to make a subclass of UICollectionViewFlowLayout that uses MyLayoutAttributes instead of UICollectionViewLayoutAttributes, and sets the backgroundColor of each MyLayoutAttributes correctly. Let's define the interface to have a property which is the array of colors to assign to rows:
#interface MyLayout : UICollectionViewFlowLayout
#property (nonatomic, copy) NSArray *rowColors;
#end
The first thing we need to do in our implementation is specify what layout attributes class we want it to use:
#implementation MyLayout
+ (Class)layoutAttributesClass {
return [MyLayoutAttributes class];
}
Next, we need to override two methods of UICollectionViewLayout to set the backgroundColor property of each pose. We call on super to get the set of poses, and then use a helper method to set the background colors:
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *poses = [super layoutAttributesForElementsInRect:rect];
[self assignBackgroundColorsToPoses:poses];
return poses;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
MyLayoutAttributes *pose = (MyLayoutAttributes *)[super layoutAttributesForItemAtIndexPath:indexPath];
[self assignBackgroundColorsToPoses:#[ pose ]];
return pose;
}
Here's the method that actually assigns the background colors:
- (void)assignBackgroundColorsToPoses:(NSArray *)poses {
NSArray *rowColors = self.rowColors;
int rowColorsCount = rowColors.count;
if (rowColorsCount == 0)
return;
UIEdgeInsets insets = self.sectionInset;
CGFloat lineSpacing = self.minimumLineSpacing;
CGFloat rowHeight = self.itemSize.height + lineSpacing;
for (MyLayoutAttributes *pose in poses) {
CGFloat y = pose.frame.origin.y;
NSInteger section = pose.indexPath.section;
y -= section * (insets.top + insets.bottom) + insets.top;
y += section * lineSpacing; // Fudge: assume each prior section had at least one cell
int row = floorf(y / rowHeight);
pose.backgroundColor = rowColors[row % rowColorsCount];
}
}
Note that this method makes several assumptions:
It assumes the scroll direction is vertical.
It assumes all items are the same size.
It assumes all sections have the same insets and line spacings.
It assumes every section has at least one item.
The reason for the last assumption is that all rows in a section are followed by the minimum line spacing, except for the last row, which is followed by the bottom section inset. I need to know how many rows preceded the current item's row. I try to do that by dividing the Y coordinate by the height of a row… but the last row of each section is shorter than the others. Hence the fudging.
Applying Your New Classes
Anyway, now we need to put these new classes to use.
First, we need to use MyLayout instead of UICollectionViewFlowLayout. If you're setting up your collection view in code, just create an instead of MyLayout and assign it to the collection view. If you're setting things up in a storyboard, find the collection view's layout (it is in the storyboard outliner as a child of the collection view) and set its custom class to MyLayout.
Second, we need to assign an array of colors to the layout. If you're using a storyboard, you probably want to do this in viewDidLoad. You could ask the collection view for its layout and then cast it to MyLayout. I prefer to use an outlet of the proper type, connected to the layout object in the storyboard.
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.layout.rowColors = #[
[UIColor lightGrayColor],
[UIColor cyanColor],
];
}
Result
If you got everything set up correctly, you'll get a result like this:
and in landscape orientation it looks like this:
I've put my test project in this github repository.
I don' think there's any good general way to do this without subclassing the flow layout. For a more specific case, you can use integer math combined with the modulus operator to get "rows" from the indexPath. So, if you had 5 items on each row, you could return either 0 or 1 from this expression:
NSInteger rowTest = (indexPath.row / 5) % 2;
Just test this value, and change your color accordingly. Of course, you'll have to change the expression on rotation since you'll have a different number of items on each row.
Use the indexPath.section property to get a row.
Something like this should do it. May take some tweaking, etc. Assuming that you are not using sections which would have to have some changes..
#import <UIKit/UIKit.h>
#interface ViewController : UIViewController
<UICollectionViewDelegate,UICollectionViewDataSource,UICollectionViewDelegateFlowLayout>
#end
#import "ViewController.h"
#define WIDTH 200
#define HEIGHT 200
#define XMARGIN 50
#define YMARGIN 20
#interface ViewController ()
#property (weak, nonatomic) IBOutlet UICollectionView *collectionView;
#property (assign, nonatomic) int numberOfCols;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
UINib* cellNib = [UINib nibWithNibName:#"CollectionCell" bundle:nil];
[self.collectionView registerNib:cellNib forCellWithReuseIdentifier:#"CollectionCell"];
}
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 6;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"CollectionCell" forIndexPath:indexPath];
NSLog(#"Row number %d",indexPath.row);
int rem = (indexPath.row / self.numberOfCols);
NSLog(#"col number %d", rem);
if ( rem % 2 == 0 )
cell.backgroundColor = [UIColor yellowColor];
else
cell.backgroundColor = [UIColor greenColor];
return cell;
}
#pragma mark – UICollectionViewDelegateFlowLayout
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
self.numberOfCols = (collectionView.frame.size.width - (XMARGIN*2)) / (WIDTH);
NSLog(#"Number of cols is %d",self.numberOfCols);
return CGSizeMake(WIDTH,HEIGHT);
}
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section
{
return UIEdgeInsetsMake(XMARGIN, YMARGIN, XMARGIN, YMARGIN);
}
- (void) didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
[self.collectionView reloadData];
}
This is yucky, subject to breaking if there's a header, but at least this will get you going. In cellForItemAtIndexPath...
// dequeue cell
CGFloat originY = cell.frame.origin.y;
// if there are headers, then grab the header view and...
originY -= header.bounds.size.height * indexPath.section;
NSInteger row = originY / cell.bounds.size.height;
BOOL evenRow = row%2 == 0;

UICollectionView Header Compatible Views

I've constructed an UICollectionView in a storyboard and implemented all of its required data source and delegate methods in the view controller. In the storyboard, I checked the Section Header property on the collection view and set the header view's class to a subclass of UICollectionResusableView (in the storyboard).
From here, I dragged in two UI elements onto the header view via the storyboard--a label and a segmented control:
When the program is executed, the label appears in the header view of the collection view (with no actual code required), but the segmented control does not. However, when a segmented control is dragged onto a typical UIView, it displays and is manipulatable with no code required. Even when instantiated through code in an IBOutlet, the segmented control does not appear.
Why is the segmented control not visible on the collection view's header while it is in a typical UIView, and why does the label display without issue?
UPDATE
Here is the init method for the custom header view, in which I attempted adding the segmented control programmatically (as opposed to in the storyboard):
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
_segmentedControl = [[UISegmentedControl alloc] initWithItems:[NSArray arrayWithObjects:#"One", #"Two", nil]];
[_segmentedControl setFrame:CGRectMake(0, 0, 100, 50)];
[_segmentedControl addTarget:self action:#selector(segmentedControlChanged:) forControlEvents:UIControlEventValueChanged];
[self addSubview:_segmentedControl];
}
return self;
}
As requested, here is the -[UICollectionReusableView viewForSupplementaryElementOfKind:] method in the main view controller:
- (UICollectionReusableView *)collectionView:(UICollectionView *)cv viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath {
GalleryHeader *headerView = [cv dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:#"HeaderView" forIndexPath:indexPath];
return headerView;
}
I'm not able to reproduce your storyboard problem, it works just fine for me when i add the segmented control by dragging it directly in the storyboard (with no code required). As for your alternative way of adding it programmatically, the problem here is that when a view is initialized from the storyboard (as it is in this case), the initWithCoder initializer method is used (not the initWithFrame initializer method). Hence, if you override that method, inserting the code in there, it should work:
-(id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if(self){
_segmentedControl = [[UISegmentedControl alloc] initWithItems:[NSArray arrayWithObjects:#"One", #"Two", nil]];
_segmentedControl.bounds = CGRectMake(0, 0, 100, 50);
[_segmentedControl addTarget:self action:#selector(segmentedControlChanged:) forControlEvents:UIControlEventValueChanged];
[self addSubview:_segmentedControl];
}
return self;
}
P.S. It doesnt affect this specific case, but you should just do:
GalleryHeader *headerView = [cv dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:#"HeaderView" forIndexPath:indexPath];
instead of:
GalleryHeader *headerView = [cv dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:#"HeaderView" forIndexPath:indexPath];
As it is the collection view that will ask for the right kind of view, you should worry about specifying it!
EDIT: The steps i followed to create the header from storyboard are:
Select the collection view and tick the box labelled Section Header
Select the newly created header and select the right class in the identity inspector
Give the header section a unique identifier
Drag the UI elements in the header in the storyboard (i also changed its background color)
Finally implement the collectionView:viewForSupplementaryElementOfKind:atIndexPath: method in your collection view's data source class
-(UICollectionReusableView *)collectionView:(UICollectionView *)collectionView
viewForSupplementaryElementOfKind:(NSString *)kind
atIndexPath:(NSIndexPath *)indexPath
{
return [self.collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:#"collectionViewHeader" forIndexPath:indexPath];
}
Let me know if you can spot any difference between what you did and what I did!

Resources