is dequeueReusableCellWithIdentifier mandatory to avoid memory leaks? - ios

I use the "famous" boilerplate code that almost everybody uses for handling cellForRowAtIndexPath:
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"someCustomCellID"];
if (cell == nil) // nothing to recycle from the queue: create a new cell
but this give me lots of problems 'cause my cells contains images that I load async and the two functionalities (dequeueing and async load) often conflict. So I try creating every time a new cell and it works pretty well and fast.
But I have a doubt: should I still call dequeueReusableCellWithIdentifier to free the memory even if I ignore the returned value and create the cells each time?
I suppose the cells not used anymore are automatically deallocated (as they should be), but I wonder if the caching queue may require the explicit "free" with the dequeue call...

dequeueReusableCellWithIdentifier is not designed to prevent memory leaks but it is designed for performance (one way is by reducing memory usage). When using the dequeue method, scrolling a table view will be much smoother when there are several rows of data. I would recommend you get your async loading to work with the dequeue method, especially if you see any lag while scrolling. If you would like an example of how to do this see Apple's LaxyTableImages Example. If you do, however, determine that you do not want to reuse cells, then simply pass nil as the reuseIdentifier when creating you cells.

dequeueReusableCellWithIdentifier helps keep down the number of times you load an reload things from files or XIB. If you have a custom cell or cells with non-standard contents, it can be a lot faster to re-use a cell that already has it all setup.
I recall working on something similar, but in our case, we loaded images into Core Data objects asynchronously and had the cell observe the image in that object. When the image was loaded, the cell was notified and it updated its image view.
When the cell came back out to us via dequeueReusableCellWithIdentifier, we stopped it from observing and set it to observe the next object's image.

Just use prepareForReuse: in your UITableViewCell's class, to stop the async load, before the cell is used again.

Yes, you need to use dequeueReusableCellWithIdentifier to avoid leaks, if you are dynamically allocating cells.
Presumably your asynchronous image loading works something like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
if (!cell) {
...
}
NSURLRequest *request = [self URLRequestForIndexPath:indexPath];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
cell.imageView.image = [UIImage imageWithData:data];
}];
return cell;
}
That code has a problem. By the time the completion handler runs, the table view may have reused the cell for a different row!
We need to change the completion block so it looks up the cell for the row when it's ready to set the image:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
if (!cell) {
...
}
NSURLRequest *request = [self URLRequestForIndexPath:indexPath];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
if (cell)
cell.imageView.image = [UIImage imageWithData:data];
}];
return cell;
}

You should use the method, always. If it returns a cell, use that cell. If it returns nil, then and only then should you create a cell.
If the method is demanding a cell for an image which you have not yet loaded, then you should provide an alternative. Perhaps a UIImageView with a customized, animated progress spinner? Then, when your image arrives, you can use the proper image when necessary.
Do not worry about deallocating cells. Once you give them to the table view, they are the table view's responsibility (at least as far as memory management is concerned).

Related

No Retain Cycle, But why still got retain cycle warning?

I am trying to use UIImageView extension of AFNetworking2.6.3 to get images from a remote server. everything works fine, images have returned and rendered successfully. but I get a retain cycle warning in Xcode7.3.1: Capturing 'cell' strongly in this block is likely to lead to a retain cycle
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cell"];
if(self.dataSourceModel) {
Model *model = self.dataSourceModel.items[indexPath.row];
NSURL *url = [NSURL URLWithString:model.imageURL];
NSURLRequest *theRequest = [NSURLRequest requestWithURL:url];
UIImage *placeholderImage = [UIImage imageNamed:#"placeholder"];
//I don't use __weak cell in the block
[cell.imageView setImageWithURLRequest:theRequest placeholderImage:placeholderImage success:^(NSURLRequest * _Nonnull request, NSHTTPURLResponse * _Nullable response, UIImage * _Nonnull image) {
cell.imageView.image = image; //get warning at this line
[cell setNeedsLayout];
} failure:nil];
}
return cell;
}
I can see since the cell instance has been captured in the block, so the block has the ownership of the cell instance(and cell.imageView as well). if the retain cycle really exists, the cell or imageView should have the ownership of the block.
but I have reviewed the UIImageView+AFNetworking.h and UIImageView+AFNetworking.m source code, the UIImageView doesn't have any property of blocks. the block is just a parameter of the method, not a instance variable. UITableViewCell doesn't have the ownership of the blocks either.
I even used Leaks to check, and Leaks didn't report any error.
So I believe there is no retain cycle here, but why I still get the Xcode warning here ?? If I use __weak cell__weak UITableViewCell *weakCell = cell;, the warning will go away. But I still wanna know:
does it have a retain cycle here?
do I really need to use __weak cell?
or maybe imageView or cell really has the ownership of the block which I didn't realize?
Any hints will help, thanks a lot.
There's certainly no retain cycle, and you can avoid the warning by omitting the code in the completion block. Taking a look at the AFNetworking imageView subclass (line 152), the culminating point of setImage... is to set the image view's image. There's need for your code to set it again, or to change the layout state.
There should also be no need for if (self.dataSourceModel). If the datasource is built right, we've answered the number of rows in tableView:numberOfRowsInSection:, and that answer is zero when the model is empty, avoiding the call to cellForRow... altogether.

IOS Objc how to properly load UICollectionViewCell after Scrolling

i'm pretty new in obj/Xcode, and i need to do a UIcollectionView how take images from url request.
I've try to catch the scroll event with scrollViewDidScroll, then call the my request function with the parameter page + 1 or -1; depending of the scroll.
But everytime i'm going on infinite loop, and my cell images donc stop loading, i think its because my way is wrong :
Catching the scroll action :
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
{
[self reload];
}
The function reload send the request and set the imgs.
How can i load properly the pages, and not 1000times for every scrolling.
Sorry for my english, and thank you
You dont need to call reload like that. As you scroll, 'cellForItemAtIndexPath' is automatically called. If theres a previously used cell in the table (out of view) then the cell is re-used for the new position it is populating - hence 'dequeueReusableCellWithReuseIdentifier' (hope you're using that?!).
So what you need to do is have code on that method itself, that will automatically 'lazy load' the image you want. Heres a copy of a cellForItemOfIndexPath method that achieves just that - the image is populated when its downloaded.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
VideoSearchCell *cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:#"cell" forIndexPath:indexPath];
YoutubeSearchResult *cellSearchResult = [self.ytSearchResults objectAtIndex:indexPath.row];
if (cellSearchResult.thumbnailImage.image == nil)
{
//NSLog(#"loading image");
__weak VideoSearchCell *weakCell = cell;
__weak YoutubeSearchResult *weakResult = cellSearchResult;
NSURL *url = [NSURL URLWithString:cellSearchResult.thumbUrl];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
UIImage *placeholderImage = [UIImage imageNamed:#"your_placeholder"];
[cellSearchResult.thumbnailImage setImageWithURLRequest:request placeholderImage:placeholderImage success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image)
{
weakResult.thumbnailImage.image = image;
weakCell.imageView.image = image;
[weakCell setNeedsLayout];
} failure:nil];
} else {
if (cell.imageView.image != cellSearchResult.thumbnailImage.image)
{
cell.imageView.image = cellSearchResult.thumbnailImage.image;
}
}
return cell;
}
So to explain - VideoSearchCell is my custom cell type. Its fairly simple, it just has a UIImageView on it called 'imageView'. Ive also got an array called 'ytSearchResults' - this contains an array of objects and some contain images, and some dont. As the images download, the results also get populated to that array so I dont download them more than necessary.
The code checks to see if theres an image in that array. If not, it does the downloading code.
By the way, I use __weak references to properties, because of ARC and not wanting to accidentally retain the cell, or the search result. Not enough time to explain all that here though! Hope the code helps.
One more thing - setImageWithURLRequest is a method from the very handy and extremely popular library AFNetworking (2.0). Get hold of that as you will use it all the time.
I do not recommend to load images before cell is actually used, because you never know if user will see that cell. You can start image loading when
- collectionView:cellForItemAtIndexPath: is called, and before starting download request you must check if download of this image is in progress, otherwise you will load it more than once.

What is correct form to configure UITableViewCell?

I am using ReusableCell in my cellForRowAtIndexPath method like that;
NSDictionary *cellData = [_cellArray objectAtIndex:indexPath.row];
static NSString *kCellID = #"homePage";
_cell = (HomePageCell*) [tableView dequeueReusableCellWithIdentifier:kCellID];
if(_cell == nil)
{
_cell = [[HomePageCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:kCellID withDictionary:cellData];
}
__weak typeof(_cell) weakSelf = _cell;
[_cell.rootImageView setImageWithURLRequest:_cell.requestImage placeholderImage:nil success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image)
{
weakSelf.rootImageView.image=image;
} failure:nil];
and my rootImageView object is configuring. My problem is start after scrolling like that;
In Cell-1 rootImageView.image should be Image-1
In Cell-2 rootImageView.image should be Image-2
In Cell-3 rootImageView.image should be Image-3
When i starting scroll and my code is coming out if(_cell == nil), my all images turned back same image as Image-1. However, when i logged [_cellArray description], data is totally true. What is the point i missed? Why all cell images turned back Image-1 after cell is not nil?
EDIT : This is really weird, if i don't use kCellID images are loading correct. I switched _cell property to HomePageCell *cell.
As others have already pointed out, it looks like you’re storing _cell as an instance variable. Never do that, as each cell instance must be dequeued/created ‘on the fly’ as they are requested to be displayed onscreen.
If your UITableViewCell instance is a custom subclass (which I assume it is), you should override its -prepareForReuse method, where you should set the imageView’s image property to nil. Otherwise you could assign it a nil value in your -tableView: cellForRowAtIndexPath: implementation.
Also, because of the way that cells are quickly queued/reused/discarded, you should check that you are assigning the requested image by the time it arrives from the network to the right cell. To do so, instead of keeping a weak reference to the cell (as you’re doing), inside the network request completion block get a new reference to the cell with the desired indexPath.
In all, your implementation should look like this:
static NSString *kCellID = #"homePage”; // defined somewhere else, probably at the top of your .m file
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
HomePageCell *cell = (HomePageCell *)[tableView dequeueReusableCellWithIdentifier:kCellID];
if(cell == nil) {
cell = [[HomePageCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:kCellID withDictionary:cellData];
}
// here we reset the cell’s imageView image property to nil
// you can/should also do this inside the subclass implementation by overriding -prepareForReuse
cell.rootImageView.image = nil;
// request the remote image and set it as the imageView image property
[cell.rootImageView setImageWithURLRequest:cell.requestImage placeholderImage:nil success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image)
{
// since the response could come back some time after the request,
// we need to get a fresh, new reference to the cell (which might
// have been dequeued and it’s no longer the same one we got before)
HomePageCell *aCell = (HomePageCell *)[tableView cellForRowAtIndexPath:indexPath];
aCell.rootImageView.image = image;
} failure:nil]
return cell;
}
I assume that _cell is a property on your class which is just wrong. Never store a cell as a property, it's not meant to be stored! Just create a local variable and assign the image to that. It's also not such a good idea but it should work at least. The theory of UITableView is you provide the cell once, then you don't modify it! If you want, you reload the table view, it will ask you again for the cell and then you can modify that way. You should never store any kind of pointer to a UITableViewCell or any subclass of it.
I assume you get that URL from cellData, in which case the problem is that you do not reset this property, so when a cell is reused you are still using the same cellData from the initWithStyle:reuseIdentifier:withDictionary: method. You should do something like:
if(_cell == nil)
{
_cell = [[HomePageCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:kCellID withDictionary:cellData];
} else {
[_cell setCellData:cellData];
}

UITabelView changed Image in scrolling

Every time I scroll the TableView, my images gets messed up, mainly the first row. I realy don`t kwon what to do.
Here is the code:
- (UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"Cell";
BarCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
[cell.activityFotoBar startAnimating];
cell.activityFotoBar.hidesWhenStopped = YES;
if(!cell){
cell = [[BarCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
}
NSMutableDictionary *infoBar = [self.bares objectAtIndex:indexPath.row];
NSString *nomeImagem = [infoBar objectForKey:#"foto"];
NSURL *url = [NSURL URLWithString:nomeImagem];
NSURLRequest *requestImagem = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:requestImagem queue:[NSOperationQueue currentQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if(connectionError == nil){
cell.imageViewImagemBar.image = [UIImage imageWithData:data];
[cell.activityFotoBar stopAnimating];
}
}];
cell.labelNomeBar.text = [infoBar objectForKey:#"nome"];
cell.labelEnderecoBar.text = [infoBar objectForKey:#"endereco"];
cell.labelAvaliacaoBar.text = [NSString stringWithFormat:#"Votos: %#", [infoBar objectForKey:#"votos"]];
return cell;
}
Thanks in advance!
The problem happens because the asynchronous image request finishes after your cell scrolls off the screen and gets reused. Downloads complete "out of order", contributing to a visual confusion. Essentially, some of the cells put up for reuse by scrolling, are still "hot", in the sense that their image load is in progress. Reusing such cell creates a race between the old and the new image downloads.
You should change the strategy that you use to load the images: rather than sending a request and "forgetting" it, consider using connectionWithRequest:delegate: method, storing the connection in the cell, and calling cancel on it when prepareForReuse method is called. This way your reused cells would be "cold".
You can use SDWebcache library. It contains a UIImageView category class which can load images from urls. I find it works well with tableviews
The code should "almost" work. You just need to fix one issue to get it working (though not optimally):
When the asynchronous request sendAsynchronousRequest:queue:completionHandler: has been finished, the completion handler will be called. Then, you need to retrieve the cell again from the table view specifying the indexPath at the time the request has been started.
You just need to capture the indexPath within the block in order to get a reference that stays valid until after the block finishes.
The UITableView's method to retrieve the cell is
- (UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath
If the return value (a cell) is nil the cell at index path indexPath is not visible.
So, the completion block will look as follows:
if (error == nil && ... ) {
BarCell* cell = [self.tableView cellForRowAtIndexPath:indexPath]
if (cell) {
cell.imageViewImagemBar.image = [UIImage imageWithData:data];
[cell.activityFotoBar stopAnimating];
}
}
Which also safely handles the case where the cell is nil.
Note: while this "works" it's still a naïve approach. A more sophisticated approach would cache the (decoded) images and possibly has some "look ahead and eager eviction strategy" for better user experience and lower memory foot-print.
Note that imageWithData: may still be costly since it may require to decode, decompress and resize the image before it can be rendered. Basically, this can be performed beforehand, with an offscreen context.

iOS: Updating tableViewCell infinite loop

This is the problem: I update my tableview after saving and fetching an thumbnail to Core Data and then I tell the cell to update itself - so it can show a thumbnail when the image has been loaded into Core Data. I Use two different threads as Core Data is not threadsafe and all GUI elements of course needs to happen in the main thread.
But this whole method just keep looping forever, and what is causing it is when I reload the thread:
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
Why? and how do I fix this?
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Photo"];
Photo *photo = [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = photo.title;
cell.detailTextLabel.text = photo.subtitle;
NSLog(#"Context %#", self.photographer.managedObjectContext);
[self.photographer.managedObjectContext performBlock:^{
[Photo setThumbnailForPhoto:photo];
dispatch_async(dispatch_get_main_queue(), ^{
cell.imageView.image = [UIImage imageWithData:photo.thumbnail];
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
});
}];
return cell;
}
The infinite loop is caused by calling the cellForRowAtIndexPath: again after the fetch is loaded and the reload on a certain cell is called.
A reload will force the cellForRowAtIndexPath: to be called again... and in your case again and again to infinity.
The solution is simple... Do not reload your cell in the cellForRowAtIndexPath but rather in a callback-method of the fetchrequest. Then reload it there rather then the creation of the cell.
Rather do not load the image inside the cellForRowAtIndexpath: at all.
Whenever your table is instantiated make a method that loops over your datasource and get the respective cell for each item. Then load an image for each cell you deem needed. And reload the cell whenever the fetching of the item is done (for instance a callback method).
If you do want the image to be loaded inside the creation of the cell as you have done now (Although I don't think that is the proper way to do it). You can surround the whole performBlock: with an if-statement checking if the image has already been set or not.
As already been said by others, there should be no need to call reloadRowsAtIndexPaths when the image has been loaded. Assigning a new image to cell.imageView.image is sufficient.
But there is another problem with your code. I assume that self.photographer.managedObjectContext is a managed object context of the "private concurrency type", so that performBlock executes on a background thread. (Otherwise there would be no need to use dispatch_async(dispatch_get_main_queue(), ...).)
So performBlock: executes the code asynchronously, on a background thread. (Which is good because the UI is not blocked.) But when the image has been fetched after some time, the cell might have been reused for a different row. (This happens if you scroll the table view so that the row becomes invisible while the image is fetched.)
Therefore, you have to check if the cell is still at the same position in the tableview:
[self.photographer.managedObjectContext performBlock:^{
[Photo setThumbnailForPhoto:photo];
dispatch_async(dispatch_get_main_queue(), ^{
if ([[tableView indexPathForCell:cell] isEqualTo:indexPath]) {
cell.imageView.image = [UIImage imageWithData:photo.thumbnail];
}
});
}];
I'd replace the following code:
cell.imageView.image = [UIImage imageWithData:photo.thumbnail];
[self.tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
With:
UITableViewCell *blockCell = [tableView cellForRowAtIndexPath:indexPath];
blockCell.imageView.image = [UIImage imageWithData:photo.thumbnail];
[blockCell setNeedsLayout];
This solves the issue of reloading recursively, as well as the possibility that cell has gotten reused in the interim. I'd also check for whether or not you've loaded the photo data already and not update if so.
Since you are using an NSFetchedResultsController though, you may be better off going with Totomus Maximus' answer, if the delegate callbacks are already informing you when the photo data updates. This way just updates the image view and wouldn't update any other information that the Photo update method could change.

Resources