What is correct form to configure UITableViewCell? - ios

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];
}

Related

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.

iOS Loading images in UITableViewCell with RAC

Am just starting with RAC and am wondering how to load images asynchronously in a cell within a TableView.
I was trying with the example in the doc, but to be honest, I didn't understand so well...
The thing is that the project is written with RAC, so I want to do the right things.
What have I tried?:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"tableCell";
CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
cell.elementName.text = self.myListOfElements[indexPath.row].elementName;
RAC(cell.imageView, image) = [[finalImage map:^(NSURL *url) {
return [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:self.myListOfElements[indexPath.row].url]]];
}] deliverOn:RACScheduler.mainThreadScheduler];
}
But this is not working...
Does anybody know the proper way to do this with RAC?
I have never used RAC, but based on the examples it looks you are referencing a URL that you do not seem have.
RAC(self.imageView, image) =
[
[
[
[
client fetchUserWithUsername:#"joshaber"
]
deliverOn:[RACScheduler scheduler]
]
map:^(User *user) {
// Download the avatar (this is done on a background queue).
return [[NSImage alloc] initWithContentsOfURL:user.avatarURL];
}
]
// Now the assignment will be done on the main thread.
deliverOn:RACScheduler.mainThreadScheduler
];
In the example this section:
[
[
client fetchUserWithUsername:#"joshaber"
]
deliverOn:[RACScheduler scheduler]
]
-fetchUserWithUsername is the function that returns the User object that contains the URL used to download the image.
This line assumes that there is already a UIImageView object created and set on the CustomTableViewCell's imageView property:
RAC(cell.imageView, image) = [[finalImage map:^(NSURL *url) {
Presumably this UIImageView object is being created in the CustomTableViewCell class's initializer or in -prepareForReuse method. If it's not, that could be part of your problem.
Be aware, though, that this code doesn't look that safe. If a CustomTableViewCell instance is reused, then you could end up calling RAC(cell.imageView, image) on the object a second time, which would be a problem. (On the other hand, if -prepareForReuse is creating a new UIImageView each time, then this shouldn't be a problem.)

Cell reuse, hide show the buttons inside cell based on some conditions

I have a created a tableview cell in the nib file and added a button to it, and created an outlet to the button named as actionButton. Now, based on some condition, I want the button to be hidden or unhidden. I used the code below, so when the model, object.hasButton property is YES, I unhide the button and show otherwise. This code looks simple to me and I dont think that there should be a reuse issue, since it has either/else condition, so it should hide for the false boolean and unhide for the true boolean conditions. But, all the cell, no matter what their value is shows the button. Could somebody please help me, I have been trying to debug this but I dont seem to figure out the problem.
- (UITableViewCell*)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
MyObject * object = [[self.tableData objectAtIndex:indexPath.section] objectAtIndex:indexPath.row];
MyCell *cell = [tableView dequeueReusableCellWithIdentifier:CELL_IDENTIFIER forIndexPath:indexPath];
cell.delegate = self;
if (cell == nil){
cell = [[MyCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CELL_IDENTIFIER];
cell.delegate = self;
}
cell.tableItem = object;
UIButton *button = cell.actionButton;
if(object.hasButton){
[button setHidden:NO];
}else{
[button setHidden:YES];
}
return cell;
}
It seems like the problem was with threading. I was doing some operation inside the managedObjectContext performBlock:andWait method like this,
[newChildContext performBlockAndWait:^{
count = [newChildContext countForFetchRequest:req error:NULL];
if(count > 0)
hasButton = YES;
else
hasButton = NO;
}];
And then updating the model like this,
myObject.hasButton = hasButton;
May be this was the problem, so I wrapped it inside the #synchronized(myObject) block to update the hasButton bool and it seems to be fine now.
#synchronzied(myObject){
myButton.hasButton = hasButton;
}
Could it be this thing ?
calling if (object.hasButton) simply checks if the property hasButton exists. it probably does exist, so it returns YES!
What you want is to check the value stored in that property, like this:
if (object.hasButton == YES)
If your MyObject is a subclass of NSManagedObject then it's hasButton property type is NSNumber * instead of BOOL. Since that NSNumber is an existing object - non nil - regardless if it's value is YES or NO myObject.hasButton evaluates to TRUE in boolean expression. Use [myObject.hasButton booleanValue] instead. also on setting that value use anObject.hasButton = #(hasButton).

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.

is dequeueReusableCellWithIdentifier mandatory to avoid memory leaks?

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).

Resources