Lazy loading custom subviews for UICollectionViewCell - ios

In -collectionView:cellForItemAtIndexPath: I’m adding custom subviews to the UICollectionViewCell like this:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
NSString *const cellIdentifier = #"cellIdentifier";
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];
MyCustomViewClass *carouselView = [[MyCustomViewClass alloc] init];
[cell.contentView addSubview:carouselView];
return cell;
}
According to the documentation, dequeueReusableCellWithReuseIdentifier:forIndexPath: “dequeues an existing cell if one is available or creates a new one based on the class or nib file you previously registered.”
The problem is that my implementation of cellForItemAtIndexPath is constantly creating new instances of MyCustomViewClass. Even though instances of the latter are removed from the collection view when they’re out of the screen, it still seems wrong to create a new one every single time dequeueReusableCellWithReuseIdentifier:forIndexPath: is called.
My question is, given that MyCustomViewClass instances are graphics-intensive and take up memory, what’s the best way to lazily load them? Do I have to implement my own queue? Or shall I make it a subclass of UICollectionViewCell?

Because you did this MyCustomViewClass *carouselView = [[MyCustomViewClass alloc] init];, everytime the system call dequeueReusableCellWithReuseIdentifier:forIndexPath: it will create a new instance of MyCustomViewClass for you. So what you need to do is to check whether an instance of MyCustomViewClass has already been added to that cell, if not then create a new one.

I have no idea about what your MyCustomViewClass does but you can create a custom UICollectionViewCell that has already that CustomViewClass associated.
If your CustomViewClass extends UIView is simpler. And if you are using storyboards it is even more simple. In your storyboard you don't need to create a custom UICollectionViewCell just for that. You can drag an UIVIew to your CollectionViewCell and set the customView to MyCustomViewClass. This way it will only be created once and then it will be reused.
If your MyCustomViewClass has some kind of state (imagine that is a status bar with a percentage) you can reset that state you have to extend the UICollectionViewCell and override prepareForReuse.

Related

Reuse of UICollectionViewCells during scrolling

I'm having an issue,
I have a simple UICollectionView with a static 200 cells that load images from Flickr.
my CellForItemAtIndexPath looks like this:
- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [cv dequeueReusableCellWithReuseIdentifier:#"FlickrCell" forIndexPath:indexPath];
cell.backgroundColor = [self generateRandomUIColor];
if(![[cell.subviews objectAtIndex:0] isKindOfClass:[PFImageView class]])
{
NSURL *staticPhotoURL = [self.context photoSourceURLFromDictionary:[self.photos objectAtIndex:indexPath.row] size:OFFlickrSmallSize];
PFImageView *imageView = [[PFImageView alloc] initWithFrame:CGRectMake(0, 0, cell.frame.size.height, cell.frame.size.width) andImageURL:staticPhotoURL andOwningCell:cell];
[cell addSubview:imageView];
}
return cell;
}
PFImageView is a subclass of UIImageView that loads a Flickr photo URL on a background thread and then updates it's own image on the main thread - this works fine.
The logic is really simple - I create a cell if there isn't one dequeueable.
If the cell (which I'm expecting to be dequeued and already have a PFImageView) doesn't have a PFImageView, I alloc and init an imageView for the cell and add it as a subview of the cell.
Thus I expect if the cell has been dequeued it should already have a PFImageView as a subview and as we should not get into the if statement to create a new imageView and kick off a new photo download request
Instead what I see is that the cells at the top and bottom of the UICollectionView that 'go off screen' momentarily - when they come back on screen they are not being reused and seemingly a new cell is created and the picture refreshed.
1) How can I achieve a static image once the cell has been created (i.e. not refreshing when the cell goes slightly off screen.
2) Why are the cells not being reused?
Many thanks for your time.
John
UICollectionView will reuse cells for maximum efficiency. It does not guarantee any particular reuse or population strategies. Anecdotally, it seems to place and remove cells based on integer power of two regions — e.g. on a non-retina iPad it might divide your scroll area up into regions of 1024x1024 and then populate and depopulate each of those regions as they transition into and out of the visible area. However you should not predicate any expectations on its exact behaviour.
In addition, your use of collection view cells is incorrect. See the documentation. A cell explicitly has at least two subviews — backgroundView and contentView. So if you add a subview it will be at index 2 at the absolute least and, in reality, the index will be undefined. In any case you should add subviews to contentView, not to the cell itself.
The most normal way of doing what you're doing would be to create a custom UICollectionView subclass that inherently has a PFImageView within it.
I see several potential issues:
You are looking specifically at index 0 of the cell for the child class that you are adding. The UICollectionViewCell may have other views as children, so you can't just assume that the only (or first) child is the one you added.
I don't see that you are calling registerClass:forCellWithReuseIdentifier: or registerNib:forCellWithReuseIdentifier:, one of which is required for proper use of dequeue (https://developer.apple.com/library/ios/documentation/uikit/reference/UICollectionViewCell_class/Reference/Reference.html).
You are only setting the URL of the PFImageView in the case that you have to construct the PFImageView. The idea with dequeuing reusable views is that you will only construct a small subset of the views needed, and the UITableView will recycle them as they move offscreen. You need to reset the value for the indexPath that is being requested, even when you don't construct the new content.
If your case is as simple as you describe, you can probably get away with adding your PFImageView to the contentView property of your dequeued UICollectionView.
In your controller:
// solve problem 2
[self.collectionView registerClass:[UICollectionViewCell class] forReuseIdentifer:#"FlickrCell"];
In collectionView:cellForItemAtIndexPath
UICollectionViewCell *cell = [cv dequeueReusableCellWithReuseIdentifier:#"FlickrCell" forIndexPath:indexPath];
cell.backgroundColor = [self generateRandomUIColor];
// solve problem 1 by looking in the contentView for your subview (and looping instead of assuming at 0)
PFImageView *pfImageView = nil;
for (UIView *subview in cell.contentView.subviews)
{
if ([subview isKindOfClass:[PFImageView class]])
{
pfImageView = (PFImageView *)subview;
break;
}
}
NSURL *staticPhotoURL = [self.context photoSourceURLFromDictionary:[self.photos objectAtIndex:indexPath.row] size:OFFlickrSmallSize];
if (pfImageView == nil)
{
// No PFImageView, create one
// note the use of contentView!
pfImageView = [[PFImageView alloc] initWithFrame:CGRectMake(0, 0, cell.contentView.frame.size.height, cell.frame.size.width) andImageURL:staticPhotoURL andOwningCell:cell.contentView];
[cell.contentView addSubview:pfImageView];
}
else
{
// Already have recycled view.
// need to reset the url for the pfImageView. (Problem 3)
// not sure what PFImageView looks like so this is an e.g. I'd probably remove the
// URL loading from the ctr above and instead have a function that loads the
// image. Then, you could do this outside of the if, regardless of whether you had
// to alloc the child view or not.
[pfImageView loadImageWithUrl:staticPhotoURL];
// if you really only have 200 static images, you might consider caching all of them
}
return cell;
For less simple cases (e.g. where I want to visually lay out the cell, or where I have multiple children in the content), I typically customize my UICollectionViewCell's using Interface Builder.
Create a subclass of UICollectionViewCell in the project (In your case, call it PFImageCell).
Add an IBOutlet property to that subclass for the view I want to change in initialization (In your case, a UIImageView).
#property (nonatomic, assign) IBOutlet UIImageView *image;
In Interface Builder, create a prototype cell for the UITableView.
In the properties sheet for that prototype cell, identify the UICollectionViewCell subclass as the class.
Give the prototype cell an identifier (the reuse identifier) in the property sheet.
Add the view child in interface builder to the prototype cell (here, a UIImageView).
Use IB to map the IBOutlet property to the added UIImageView
Then, on dequeue in cellForRowAtIndexPath, cast the dequeued result to the subclass (PFImageCell) and set the value of the IBOutlet property instance. Here, you'd load the proper image for your UIImageView.
I am not sure if the cell is being re-used or not. It may be being reused but the subview may not be there. My suggestion would be to create a PFImageViewCollectionViewCell Class (sub class of UICollectionViewCell) and register it as the CollectionView Cell and try. That's how I do and would do if I need a subview inside a cell.
Try adding a tag on this particular UIImageView
- (UICollectionViewCell *)collectionView:(UICollectionView *)cv cellForItemAtIndexPath:(NSIndexPath *)indexPath {
static int photoViewTag = 54353532;
UICollectionViewCell *cell = [cv dequeueReusableCellWithReuseIdentifier:#"FlickrCell" forIndexPath:indexPath];
cell.backgroundColor = [self generateRandomUIColor];
PFImageView *photoView = [cell.contentView viewWithTag:photoViewTag];
// Create a view
//
if (!photoView) {
photoView = [[PFImageView alloc] initWithFrame:CGRectMake(0, 0, cell.frame.size.height, cell.frame.size.width) andImageURL:staticPhotoURL andOwningCell:cell];
imageView.tag = photoViewTag;
[cell.contentView addSubview:imageView];
}
// Update the current view
//
else {
NSURL *staticPhotoURL = [self.context photoSourceURLFromDictionary:[self.photos objectAtIndex:indexPath.row] size:OFFlickrSmallSize];
photoView.imageURL = staticPhotoURL;
}
return cell;
}
I would really recommend to create your own UICollectionViewCell subclass though.
EDIT: Also, note that I used the contentView property instead of adding it directly to the cell.

Re-using a UICollectionViewCell

I am dequeueing a vanilla UICollectionViewCell and setting its background color:
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kCellId forIndexPath:indexPath];
cell.backgroundColor = [UIColor redColor];
Of course, this color is being set every time a cell is recycled, even though it never changes.
Is there a way to only set the color when the cell is first created without subclassing UICollectionViewCell for such a small thing? I ask because UITableView allows for this without subclassing.
You can do that without subclass UICollectionViewCell in StoryBoard like this code
But UICollectionViewCell have nothing in its contentView, you should subclass then you can drag many other view you like to it and hook up with IBOutlet. Initial setting for these views are in awakeFromNib if Cell create in storyboard or Xib, in init if cell create by code.

Initializing a custom UICollectionViewCell

I have a custom UICollectionViewCell that has a custom background view which is drawn using one of several colour schemes. The colour scheme for the background view is set in my custom initializer -(id)initWithFrame:andColourPalette: for the View.
I have a similar custom initialiser in my UICustomViewCell subclass but I can't figure out how to call this initialiser when I am setting up the cell in cellForItemAtIndexPath:
Can anyone help me do this? Or offer alternative solution for passing this Dictionary of colours into the Cell to pass on to the subView?
EDIT to show more detail:
This is what I have in my UICollectionView VC:
In ViewWillAppear:
[self.collectionView registerClass:[OPOLawCollectionViewCell class] forCellWithReuseIdentifier:CELL_ID];
self.colourPalette = [OPOColourPalette greenyColourPalette];
In cellForItemAtIndexPath:
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:CELL_ID forIndexPath:indexPath];
OPOLawCollectionViewCell *lawCell = (OPOLawCollectionViewCell *)cell;
MainLevel *level = self.collectionData[indexPath.row];
lawCell.delegate = self;
lawCell.colourPalette = self.colourPalette;
In my Custom UICollectionViewCell
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
// get background view
OPOLawBook *lawBookView = [[OPOLawBook alloc]initWithFrame:CGRectMake(0, 0, 200, 265) andColourPalette:self.colourPalette];
But that doesn't work - I guess because the propertys are not set up.
If I change the last line to this, then it works fine:
OPOLawBook *lawBookView = [[OPOLawBook alloc]initWithFrame:CGRectMake(0, 0, 200, 265) andColourPalette:[OPOColorPalette greenyColorPalette]];
So i guess I need to use a custom intialiser here but I cant figure out how to call it , or from where...
Thanks
Yuo have to register your customCells in collectionView:
[self.collectionView_ registerClass:[YourCustomClass class]
forCellWithReuseIdentifier:#"CustomCell"];
And then in your method cellForItemAtIndexPath:
YourCustomClass *cell = (YourCustomClass *)[collectionView
dequeueReusableCellWithReuseIdentifier:#"CustomCell" forIndexPath:indexPath];
It is done because collectionView might have 1000 cells and 10 visible. You don't keep all of them initialized and reuse when possible.
EDIT
You should set colorPaletter after you deque the reusable cell. Think of it as a container which can hold any color. You need to determine (by indexpath) what color to paint.
You shouldn't do below if your custom cell is in the Storyboard,
[self.collectionView registerClass:[OPOLawCollectionViewCell class] forCellWithReuseIdentifier:CELL_ID];
Because Storyboard take responsibility to register Cell_ID own.
Now, It will conflict to be generated invalid Cell if you use both.
Way off, every answer. The questioner is looking for a way to uniquely identify each cell upon initialization, which happens prior to dequeuing a cell, and prior to a cell's access to its index path property.
The only way to do this is to assign a unique reuse identifier to every cell based on what the index path value will be (assuming you will know what that will be—and, in your case, you will); then, when dequeuing the cell, use the index path to find the cell with the corresponding reuse identifier.
Does this negates the purpose of reuse identifiers? Absolutely not. You'll be reusing that cell every time you need to use it again. Reuse identifiers were not meant to limit you to a cookie-cutter cell for every cell in your collection view; they are also intended to be "unique use" identifiers.

Setting style of UITableViewCell when using iOS 6 UITableView dequeueReusableCellWithIdentifier:forIndexPath:

I'm trying to work out how to set the UITableViewCellStyle when using the new methods in iOS 6 for UITableView.
Previously, when creating a UITableViewCell I would change the UITableViewCellStyle enum to create different types of default cells when calling initWithStyle: but from what I can gather, this is no longer the case.
The Apple documentation for UITableView states:
Return Value:
A UITableViewCell object with the associated reuse identifier. This method always returns a valid cell.
Discussion:
For performance reasons, a table view's data source should generally reuse UITableViewCell objects when it assigns cells to rows in its tableView:cellForRowAtIndexPath: method. A table view maintains a queue or list of UITableViewCell objects that the data source has marked for reuse. Call this method from your data source object when asked to provide a new cell for the table view. This method dequeues an existing cell if one is available or creates a new one based on the class or nib file you previously registered.
Important: You must register a class or nib file using the registerNib:forCellReuseIdentifier: or registerClass:forCellReuseIdentifier: method before calling this method.
If you registered a class for the specified identifier and a new cell must be created, this method initializes the cell by calling its initWithStyle:reuseIdentifier: method. For nib-based cells, this method loads the cell object from the provided nib file. If an existing cell was available for reuse, this method calls the cell’s prepareForReuse method instead.
This is how my new cellForRowAtIndexPath looks after implementing the new methods:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"cell_identifier";
[tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:cellIdentifier];
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
return cell;
}
The code I have so far works fine but always returns the default style. How can I change this so I can create cells with the other styles such as UITableViewCellStyleDefault, UITableViewCellStyleValue1, UITableViewCellStyleValue2 and UITableViewCellStyleSubtitle?
I don't want to subclass UITableViewCell, I just want to change the default type as I could do prior to iOS 6. It seems odd that Apple would provide enhanced methods but with minimal documentation to support their implementation.
Has anyone mastered this, or run in to a similar problem? I'm struggling to find any reasonable information at all.
I know you said you didn't want to create a subclass, but it looks inevitable. Based on the assembly code while testing in the iOS 6.0 simulator, UITableView creates new instances of UITableViewCell (or its subclasses) by performing
[[<RegisteredClass> alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:<ReuseIdentifier>]
In other words, the style sent (UITableViewCellStyleDefault) appears to be hard-coded. To get around this, you will need to create a subclass that overrides the default initializer initWithStyle:reuseIdentifier: and passes the style you wish to use:
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
// ignore the style argument, use our own to override
self = [super initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:reuseIdentifier];
if (self) {
// If you need any further customization
}
return self;
}
Also, it might be better to send registerClass:forCellReuseIdentifier: in viewDidLoad, instead of doing it every time a cell is requested:
- (void)viewDidLoad
{
[super viewDidLoad];
[self.tableView registerClass:<RegisteredClass> forCellReuseIdentifier:<ReuseIdentifier>];
}
dequeueReusableCellWithIdentifier isn't deprecated so you aren't required to use the new dequeueReusableCellWithIdentifier:forIndexPath:.
Use the new way along with the appropriate register method (in viewDidLoad) if you are using a custom cell class but use the old way if you want to use one of the UITableViewCellStyle enums.
You can avoid an extraneous subclass by using the storyboard interface builder:
In the Storyboard view, select the table view cell prototype cell (on the table view)
In the Utilities view, in the Attributes inspector, modify the Style value
(Optionally) Modify other values such as Selection and Accessory
The new iOS 6.0 dequeueReusableCellWithIdentifier:forIndexPath: does use those values when allocating new cells and returning them. (Tested on an iOS 6.0 compilation using Xcode 4.5.2)
Another alternative that saves one file is to create a Nib and use registerNib:forCellReuseIdentifier: instead.
Making the Nib is easy: Create a new .xib file in Interface Builder. Delete the default view. Add a Table View Cell object. Using the Attributes Inspector, change the style for the cell. (Here you also have the opportunity to customize the cell further by adjusting other attributes.)
Then in your table view controller's viewDidLoad method call something like:
[self.tableView registerNib:[UINib nibWithNibName:#"StyleSubtitleTableCell" bundle:[NSBundle mainBundle]] forCellReuseIdentifier:#"Cell"];
Bolot's answer is the correct. Simple and you don't need to create any XIB file.
I just wanted to update his answer for whoever is doing it using Swift instead of Objective-C:
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: .value1, reuseIdentifier: reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
My solution to this is to call initWithStyle: reuseIdentifier: after I've obtained it using [self.tableView dequeueReusableCellWithIdentifier:#"cellId" forIndexPath:indexPath]. After all, init is just another selector, and the compiler makes no restrictions on calling it on an already initialised object. It will however complain about not using the result of calling init, so I do:
UITableViewCell* cell = [self.tableView dequeueReusableCellWithIdentifier:#"cellId" forIndexPath:indexPath];
cell = [cell initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"cellId"];
I imagine this won't work in Swift...

Added table view cells follow 1 custom style?

I am wanting to create a custom UITableView cell. I would like to know how to do this. I understand how to actually create it and write code for it, but how can i create 1 style and then when i have more cells added, i want the same style. How can i do this? Is there a way to create 1 custom cell and have all the other cells that i want to add later follow this cells style?Thanks for the help!
In my projects I'm implementing method that creates custom style programmatically. Also it is possible to make custom cell via IB and when you need just take custom cell from it.
Don't forget that if you will write your code correctly then your cells will be reused and that method will be called only for number of cells that are visible in your table view.
may be this can help you http://iphone-bitcode.blogspot.com/2011/06/custom-tableview-cell.html
Write a separate .h/.m/.xib for the cell, and in the .xib set File's Owner to the class you want multiple copies of it in (your table view controller class, most likely). Attach it to an IBOutlet you created in the table view controller for new cells.
Then, each time you want a cell, try and dequeueReusableCellWithIdentifier: on your tableView, and if that doesn't work (you have no reusable ones), make a new cell using your custom class by simply loading the nib file. It will automatically create an instance of the cell and attach it to your IBOutlet, and then just retain the cell and set the outlet back to nil for the next time you need to create a cell. Essentially, I mean this (I have an IBOutlet UITableViewCell *cellOutlet):
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *reuseIdentifier = #"CustomCell";
UITableView *cell = [self.tableView
dequeueReusableCellWithIdentifier:reuseIdentifier];
if (cell == nil) {
[[NSBundle mainBundle] loadNibNamed:#"MyCustomTableViewCell"
owner:self options:nil];
cell = cellOutlet;
self.cellOutlet = nil; // autoreleases
cell.reuseIdentifier = reuseIdentifier;
}
// configure the cell here
return cell;
}

Resources