I noticed that UICollectionView calls collectionView:cellForItemAtIndexPath: on its data source quite a few times. For example, on each layoutSubviews all cells are "reconfigured", no matter if they were already visible or not.
When collectionView:cellForItemAtIndexPath: is called, we're expected to:
Your implementation of this method is responsible for creating,
configuring, and returning the appropriate cell for the given item.
You do this by calling the
dequeueReusableCellWithReuseIdentifier:forIndexPath: method of the
collection view and passing the reuse identifier that corresponds to
the cell type you want. That method always returns a valid cell
object. Upon receiving the cell, you should set any properties that
correspond to the data of the corresponding item, perform any
additional needed configuration, and return the cell.
The problem is that configuring a cell is not always a cheap operation, and I don't see why I should reconfigure cells that are already configured.
How can we avoid redundant configuration of the cells? Or is there something I'm not understanding correctly?
I don't think you can avoid this. The problem is that UICollectionView is so general and FlowLayout isn't the only layout. Since you are allowed to make crazy layouts, any layout change, like a layoutsubviews, could completely change the layout you want -- a grid in portrait and a triangular arrangement in landscape... The only way to know what the layout should be is to find out the location and size of each cell.
UICollection view is not cheap for lots and lots of elements.
Simply after configuring a cell just use cell.tag = 1; , then next time you can avoid reconfiguring same cell by using if(!cell.tag).
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:YourCellReuseIdentifier forIndexPath:indexPath];
if(!cell){
//Create cell ...
}
if(!cell.tag){
//Do things you have to do only once
}
else{
//DO things you have to do every time.
}
return cell;
}
Related
I'm trying to imitate UITableView layout using UICollectionView.
layout.itemSize = CGSizeMake(CGRectGetWidth(self.view.bounds), 44.0f);
I register the reusable cell class.
[self.collectionView registerClass:[SampleCell class]
forCellWithReuseIdentifier:NSStringFromClass([SampleCell class])];
Note: SampleClass is just a subclass of UICollectionViewCell which contains nothing.
And conformed to the data source:
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 28;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass([SampleCell class])
forIndexPath:indexPath];
return cell;
}
I found that the SampleCell is not reused. To validate it, we can simply log the number of subviews in the UICollectionView.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
NSLog(#"number of subviews in collection view is: %li", (long)self.collectionView.subviews.count);
}
And after scrolling, I got this log:
number of subviews in collection view is: 30
number of subviews in collection view is: 30
number of subviews in collection view is: 30
number of subviews in collection view is: 30
Notice that there are 30 subviews (2 of those are the scrollview indicator).
That means that all of the 28 items are displayed without the invisible cells removed from superview. Why does this happen?
To make it easier for you, I made a sample project available on Github.
https://github.com/edwardanthony/UICollectionViewBug
Update:
I also checked the memory allocation using the memory graph hierarchy debugger and it's allocated 28 times.
I does work, it's just keeping a little more in memory due to more aggressive caching. If you try changing the number of items from 28 to 100 you will see that it stays at 33 subviews when you scroll.
Try adding the following code to your SampleCell class and you will see it gets called, but maybe not quite as you expect.
- (void)prepareForReuse {
[super prepareForReuse];
NSLog(#"prepareForReuse called");
}
UICollectionView has a more advanced caching scheme than UITableView (or at least as it used to have), which is the reason you see what you do. According to docs it says Cell prefetching is enabled by default:
UICollectionView provides two prefetching techniques you can use to
improve responsiveness:
Cell prefetching prepares cells in advance of
the time they are required. When a collection view requires a large
number of cells simultaneously—for example, a new row of cells in grid
layout—the cells are requested earlier than the time required for
display. Cell rendering is therefore spread across multiple layout
passes, resulting in a smoother scrolling experience. Cell prefetching
is enabled by default.
Data prefetching provides a mechanism whereby
you are notified of the data requirements of a collection view in
advance of the requests for cells. This is useful if the content of
your cells relies on an expensive data loading process, such as a
network request. Assign an object that conforms to the
UICollectionViewDataSourcePrefetching protocol to the
prefetchDataSource property to receive notifications of when to
prefetch data for cells.
You can turn off cell prefetching by adding this line to setupCollectionView function in your sample:
self.collectionView.prefetchingEnabled = NO;
Doing so will make your sample work as you expected. The subview count will drop to 18 in my case.
I suspect that counting subviews does not reflect cell reuse. It could be that subviews contains more than one reference to the same cell. To count the number of cells used, you could log how many times the UICollectionViewCell subclass gets initialised. Just override it's init method and put a print statement in there.
One other thing to note (sorry if it's aleady obvious), if all cells are visible on screen no reuse will occur. Cell reuse occurs when cells go off screen during scrolling.
I am working on an ios application,
I have a normal table view. When calling heightForRowAtIndexPath I am doing the folowing
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *cellId = [self getCellIds][indexPath.row];
BaseTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellId];
return [cell calculateHeigh];
}
Basically I am dequeueing the cell because I have a function calculateHeigh inside every cell that will do the height calculation. this is working fine as intended however I have a concern:
Is it safe to call dequeueReusableCellWithIdentifier: inside the heightForRowAtIndexPath ? will it cause any issue?
EDIT:
Just to clarify why I did this, I have a big amount of custom cells with different identifier that needs to be loaded. and to avoid having a huge if-else statement in my heightForRowAtIndexPath I placed the getter of the cell height in the custom cell that way I just ask it to return it (no calculation is made there), I can't do it as a class method as I don't know which class, I can get the object from the identifier and not the class. And I want to avoid a big if-else just for code readability.
So my concern was with the dequeueReusableCellWithIdentifier: is it heavy to call it when getting the height? will it cause memory issues or lags? or is it worth to just do a bug if-else of use a dictionary?
First of all you should avoid any calculations in table drawing methods(such as heightForRow, cellForRow, etc). These methods are called a lot and although your table may be short and/or not complicated(with custom cells with a lot of labels, buttons and images) you should always try to optimize this drawing process or otherwise user will experience some nasty lag when scrolling.
So you should call some method to prepare data before calling 'reloadTable'
-(void)prepareMethod
{
//get only one cell to calculate all row heights
BaseTableViewCell *cell = [_myTableView dequeueReusableCellWithIdentifier:cellId];
for (NSDictionary* dataObj in _dataArray)
{
//loop through all rows data and set new property for row height
dataObj[#"rowHeight"] = [cell calculateHeigh];
}
}
And then when calling heightForRow just pass this value without any expensive operation(such as probably string calculations):
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
//always make sure you don't access unexisting array index
return ( indexPath.row < _dataArray.count ) ? _dataArray[indexPath.row][indexPath.row][#"rowHeight"] : 1.0;
}
Of course you don't need separate method just to populate row heights in your data array - you can populate this value when populating(formatting) your data array to avoid second array iteration. It all depends on your current implementation.
Just remember that expensive drawing methods(not only for table though) should always be as short as possible and just get data needed for drawing and draw. It's really so simple. If you need to make some complicated calculations do it before that(maybe in view init) so your data is prepared before actual drawing. This way your application will be working smoothly even with bigger tables(because no matter how big the table is, UITableViewController draws only visible cells).
Regards,
hris.to
I don't like to have big if statements in heightForRowAtIndexPath and accessing a cell using dequeueReusableCellWithIdentifier. Your approach getting cell height from each cell quite is reasonable. I believe your calculateHeigh return value depends on the table data you pass into the cell.
In BaseTableViewCell.h
+ (CGFloat)heightWithData:(id)data;
In BaseTableViewCell.m
+ (CGFloat)heightWithData:(id)data
{
//put your calculateHeigh logic here. I believe your calculateHeigh depends on the data each cell has.
}
Then you can do
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return [BaseTableViewCell heightWithData:[self.tableData objectAtIndex:indexPath.row]];
}
If you do this, you don't need to access each cell object to get cell height.
You should not use this method to provide the calculation. Based on what I can see on your setup, you are calculating the height based on the values already on your cell. What happens is that the cell dequeue system will give you a cell to reuse, but because it's sharing cells from multiple index paths, that cell probably has data that belongs to a record of an index path different from the current one. Get the calculate height code and try to reproduce it inside the datasource callback you are using.
At my experiment I need to have reference to first UITableViewCell in tableView. By some action I need to set image and some other cell properties and to keep this state of this only cell even if the tableView will be scrolled. All of this properties can be potentially nulled via scrolling (and they actually are) because of reusing. For set this properties every time cell appears on screen, inside of `-cellForRowAtIndexpath' I tried to catch first cell using:
UITableViewCell *firstCell = (UITableViewCell *)[atableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]];
but looks like this way I can only catch every next first cell on next scrollable "screen".
So, how can I get ref to first UITableView cell?
If I understand you correctly, you are trying to do something special if the cell at (0, 0) is about to be displayed, right? If that's the case, you can easily implement UITableViewDelegate's tableView:willDisplayCell:forRowAtIndexPath: method as follows:
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath) {
// Do something special
}
}
There is also a corresponding tableView:didEndDisplayingCell:forRowAtIndexPath: method if you need to undo things.
Hope it helps!
There is no "first" table view cell. The entire table view typically uses a single cell to improve performance.
You can change that, by implementing your own cell reuse system (search for reuse in the documentation). But generally the cell is the wrong place to store any data related to a specific index in the table view.
I have what probably seems like a really weird problem (it does to me!)
I am using a UITableView to display cells which each contain a UIWebView. I realise that this is a bad idea on the face of it, but I can't really do this any other way.
I am caching the heights of each cell when the UIWebView finishes loading, and then calling:
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:#[cellIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
All of the germane code is in a Gist here.
I also have the UIWebViews cached in a dictionary on the data source, so it can be reused when the cell is reloaded.
This seems to sort of work, but I am encountering a lot of issues whereby the cells' contents will randomly disappear. I have added some logging into determine what's going on, and in what order, and it seems like some of the cells are being reused while they're still on-screen.
I see this in my logs while scrolling down:
2014-02-11 13:45:49.091 EApp[45936:70b] Generating cell for 1: Panning
2014-02-11 13:45:49.245 EApp[45936:70b] Generating cell for 2: Calibration
2014-02-11 13:45:50.063 EApp[45936:70b] Generating cell for 3: Aperture Priority
2014-02-11 13:45:50.063 EApp[45936:70b] Reusing cell: Stopping down
"Stopping down" in this case is a cell that is still on-screen. The "generating cell" items are logged inside the data source's cellForRowAtIndexPath and the "reusing" messages inside the cells' prepareForReuse.
Does anyone know what could be happening here? I know this seems complex.
The following line in your prepareForReuse is probably the culprit:
if ([self.contentWebView isDescendantOfView:self.contentWebView]) {
[self.contentWebView removeFromSuperview];
}
As the contentWebView is never a descendant of itself, it will not be removed from the cell, and the contentView will contain two webviews after the cellForRowAtIndexPath:
You probably meant to say:
if ([self.contentWebView isDescendantOfView:self.contentView]) {
[self.contentWebView removeFromSuperview];
}
Or simply:
[self.contentWebView removeFromSuperview];
One of the features/limitations of UITableView is that you don't know if, and can't depend on, a cell is being created or reused. You should always be able to handle both.
GENERALLY, when you call -reloadRowsAtIndexPaths:withRowAnimation:, you will get the cell from that indexPath to reuse. If that indexPath was on screen, it will be a cell that was on screen.
I don't know if it's the problem, but in the code you provided, you don't even initialize your cell...
I'm even surprise it works.
static NSString *CellIdentifier = #"FeedItemCell";
EFeedItemCell *cell = [self.tableViewController.tableView dequeueReusableCellWithIdentifier:CellIdentifier];
you should add to it :
if (!cell) {
cell = [EfeedItemCell alloc] initWithReus....];
}
From your code it seems that you are caching the webViews and then are adding them to cells programmatically. This can create random problems similar to what I had faced in the passed.
You must use EFeedItemCellWebView in your storyboard. Just add a UIWebView and change the class name to your custom class. And then when the data is loaded just simply change its contents in - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
I have an UICollectionViewController and my custom cells, and in my cellForRowAtIndexPath: method I set the cells based on indexPath.row.
But I am getting wrong results, this cell appears even after first position, and if you scroll back and forth, it pops up in random places. How do i fix that?
Here is the code:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
DVGCollectionViewCell *cell;
cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:#"Cell" forIndexPath:indexPath];
if (indexPath.row == 0)
{
cell.imageView.image = [UIImage imageNamed:#"something1.png"];
cell.buyLabel.text = #"170";
cell.textLabel.text = #"11.2011";
}
return cell;
}
Cell in both UITableView and UICollectionView are recycled, that means that when one goes off screen it is put in an NSSet until you need it again. When it's need it's removed from the set ad added again at UICollectionView views hierarchy. If you do not clean the value inside the cell or set them again, the cell will show the same data when it was created.
This is made for performance reason creating cell takes more time instead of value them again.
If your problem is in layout check the layout flow object, which size did you set?
I have found the problem, once the cell contents was set it was never cleaned. So I added cleaning every cell properties as additional clause and it works fine.
You can perform any clean up necessary to prepare the view for use again if you override prepareForReuse in your custom cell implementation.
One of the answers in this SO post helped: override prepareForReuse and reset the cell to its default state. Don't forget to call super.prepareForReuse.