I have custom class that load jSON data from server, and when that data is loaded, i load image for each cell using AFNetworking. However, there is annoying effect of sharping, not smoothy scrolling, when i scroll down to new images to be load. There is my method:
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
//Create cell
static NSString *cellIdentifier = #"cell";
MyCell *cell = (MyCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil)
{
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"MyCellView" owner:self options:nil];
cell = [nib objectAtIndex:0];
}
// Check condition, either we got objects
if (!isDataLoaded){
cell.myActivityIndicator = [[UIActivityIndicatorView alloc]
initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
cell.myActivityIndicator.center=self.view.center;
[cell.myActivityIndicator startAnimating];
[self.view addSubview:cell.myActivityIndicator];
NSLog(#"Current loading");
} else {
// Hide acitivity indicator
// Set image
// dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
//
// data = [[NSData alloc] initWithContentsOfURL:[[self.objectsToShow objectAtIndex:indexPath.row]valueForKey:#"low_resolutionImage"]];
// UIImage *image = [[UIImage alloc] initWithData:data];
//
// dispatch_async(dispatch_get_main_queue(), ^{
//
// cell.myActivityIndicator.hidden = YES;
// [cell.myActivityIndicator removeFromSuperview];
//
// [cell.myImageView setImage:image];
// });
//
// });
[cell.myImageView setImageWithURL:[[self.objectsToShow objectAtIndex:indexPath.row]valueForKey:#"low_resolutionImage"]];
}
return cell;
}
You don't want to download(async download is MUST) the image every time when the table cell is about to show. Instead, you save the image in a data model after it's downloaded for the first time. Afterward, you can simply retrieve the image from the model. I normally use NSMutableArray to implement the model because it's easier to add/remove its elements.
Related
I am facing one issue regarding UITableView cells get mixed up while scrolling, specially for images.
I am not sure why this is going one.I have change the patter for displaying then also its get mixed up. Below is my code.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"FriendsCell";
FriendsTableCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil)
{
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"FriendsTableCell" owner:self options:nil];
cell = [nib objectAtIndex:0];
}
return cell;
}
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
FriendsTableCell *simpleTableCell = (FriendsTableCell *)cell;
_model = [_contentArray objectAtIndex:indexPath.row];
simpleTableCell.nameLabel.text = _model.fullname;
if(_friendsSegmentControl.selectedSegmentIndex == 0) {
simpleTableCell.followButton.hidden = NO;
simpleTableCell.removeButton.hidden = NO;
simpleTableCell.unBlockButton.hidden = YES;
simpleTableCell.ignoreRequest_btn.hidden = YES;
simpleTableCell.rejectRequest_btn.hidden = YES;
simpleTableCell.acceptRequest_btn.hidden = YES;
simpleTableCell.removeButton.tag = indexPath.row;
[simpleTableCell.removeButton addTarget:self action:#selector(deleteFriend_btn:) forControlEvents:UIControlEventTouchUpInside];
simpleTableCell.followButton.tag = indexPath.row;
if([_model.isfollowed isEqualToString:#"YES"]) {
[simpleTableCell.followButton setImage:[UIImage imageNamed:#"follow_yellow.png"] forState:UIControlStateNormal];
[simpleTableCell.followButton addTarget:self action:#selector(unfollowFriend_btn:) forControlEvents:UIControlEventTouchUpInside];
}
else {
[simpleTableCell.followButton setImage:[UIImage imageNamed:#"follow_white.png"] forState:UIControlStateNormal];
[simpleTableCell.followButton addTarget:self action:#selector(followFriend_btn:) forControlEvents:UIControlEventTouchUpInside];
}
}
NSString *escapedString = (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(NULL,(__bridge CFStringRef) _model.prof_pic,NULL,(CFStringRef)#"!*'();:#&=+$,/?%#[]",kCFStringEncodingUTF8));
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^(void) {
NSData *imageData = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:[NSString stringWithFormat:#"http://www.touristtube.com/%#",escapedString]]];
UIImage *image = [UIImage imageWithData:imageData];
dispatch_async(dispatch_get_main_queue(), ^{
simpleTableCell.profileImageView.image = image;
});
});
}
Two things you need to ensure while creating cells for table view-
1) Regarding reusable cells- As you are using reusable cells, so in case if it's dequeued it will show the data for the index path for which it was originally created.
It's right that you are updating each cell(whether newly created/ dequeued) with the data for the corresponding index path just before displaying it in
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
Also you can update the data for each cell' container for corresponding index path in
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
as well.
So, please ensure if the cell is reused you should update the data of each container(cell's subviews) with the data for the current index path. Doing so will resolve your issue of cell's content getting overlapped.
2) Regarding asynchronously downloading the images for cells- Also as you are downloading the images asynchronously, so it will not block the main thread and as soon as the image is downloaded you need to switch to main thread to set the image to the cell.
Now the thing to remember in case of asynchronous image downloading is that you should always access the cell for the correct index path on main thread and set the downloaded image to the cell's image view for the index path for which it was scheduled to be downloaded.
So for the images you should update your image downloading method as
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^(void) {
NSData *imageData = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:[NSString stringWithFormat:#"http://www.touristtube.com/%#",escapedString]]];
UIImage *image = [UIImage imageWithData:imageData];
dispatch_async(dispatch_get_main_queue(), ^{
FriendsTableCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.profileImageView.image = image;
[cell setNeedsLayout];
});
});
this will resolve your issue of cell's image intended to be displayed for a index path but actually showing on a cell at some other index path.
Background
I have a UIViewController which handles 2 UITableViews, both with custom UITableViewCell subclasses. A click event on the top table (Categories) is supposed to trigger a reloadData on the bottom table (Article List from an RSS feed) depending on which category is selected. What is supposed to happen is that the new data gets pulled and the relevant array is repopulated, after which the data gets displayed on the bottom table.
The data that is meant to be displayed is:
An image
a UILabel (for the date)
A UITextView for the title
1) First problem
The list that loads by default upon starting the app loads properly (well almost but I'l get to the 'almost' in #2) but once a category is selected in the top table, the array containing the data to be displayed in the cells is rebuilt with the relevant data but the reloadData method does not immediately invoke the desired results. Only once scrolling downwards and then upwards does the new data show. Using debugging I can see that the data is being loaded correctly into the array, so I'm sure its a UITableViewController or UITableViewCell issue.
I have tried various solutions discussed here on StackOverflow, other than the obvious self.myTableView.reloadData the two most common being invoking ReloadData as shown below:
[self.myTableView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:NO];
and also
dispatch_async(dispatch_get_main_queue(), ^{
[self.myTableView reloadData];});
}
Each time I've attempted to call these from within the ArticlesTableViewController instance, to no success.
2) Second problem
DateLabel only shows on the first cell upon opening the app, and then for the dateLabel to show in the rest of the cells I actually have to scroll downwards, and then up again. Cells coming back into view from above then contain the dateLabel, but if a cell appears back into view from below then its gone again. Pretty confusing stuff.
Here is my relevant code
cellForRowAtIndexPath (in the ArticlesTableViewController):
// Method that gets fired when parsing is complete via NSNotification
- (void) parsingComplete {
//Tell the tableview to animate the changes automatically
self.articleList = [[NSMutableArray alloc]initWithArray:parser.articles];
[myTableView reloadData];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
ArticlesTableViewCell *cell = (ArticlesTableViewCell *)[myTableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell){
cell = [[ArticlesTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
dispatch_async(kBgQueue, ^{
NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[[self.articleList valueForKey:#"enclosure"] objectAtIndex:indexPath.row]]];
if (imgData) {
UIImage *image = [UIImage imageWithData:imgData];
if (image) {
dispatch_async(dispatch_get_main_queue(), ^{
cell.enclosure.image = image;
[cell.enclosure setNeedsDisplay];
});
}
}
});
[cell.dateLabel setText:[[self.articleList valueForKey:#"pubDate"] objectAtIndex:indexPath.row]];
[cell.headingTextView setText:[[self.articleList valueForKey:#"title"] objectAtIndex:indexPath.row]];
cell.headingTextView.editable = NO;
return cell;
}
CustomCell code (for ArticlesTableViewCell):
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.enclosure = [[UIImageView alloc] initWithFrame:CGRectMake(10,10,48,48)];
self.enclosure.tag = 1;
//self.imageView = nil;
self.dateLabel = [[UILabel alloc] initWithFrame: CGRectMake (75,-10,50,50)];
self.dateLabel.textColor = [UIColor grayColor];
self.dateLabel.font = [UIFont fontWithName:#"Arial" size:8.0f];
self.headingTextView= [[UITextView alloc] initWithFrame:CGRectMake(70, 20, 400, 80)];
self.headingTextView.textColor = [UIColor blackColor];
self.headingTextView.font = [UIFont fontWithName:#"Arial" size:10.0f];
[self addSubview:self.dateLabel];
[self addSubview:self.enclosure];
[self addSubview:self.headingTextView];
//Here the Date only appears in the first cell, but when I scroll down and up again it re-appears
}
return self;
}
EDIT
Below is the CellForRowAtIndexPath code in the CategoriesTableViewController (the first table):
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
CatTableViewCell *cell = (CatTableViewCell *)[_tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[CatTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
cell.nameLabel.text = [categories objectAtIndex:indexPath.row];
return cell;
}
And here is the code in the ViewController that instantiates these two tableViewControllers:
- (void)viewDidLoad {
[super viewDidLoad];
UIView *categoryView = [[UIView alloc] init];
[categoryView setFrame:CGRectMake(60,0, 100,-200)];
UIView *articlesView = [[UIView alloc] init];
[articlesView setFrame:CGRectMake(0,50, 400,400)];
CatBarTVC *categoryBar = [[CatBarTVC alloc] initWithStyle:UITableViewStylePlain];
categoryBar.view.transform = CGAffineTransformMakeRotation(-M_PI * 0.5);
categoryBar.view.autoresizesSubviews=NO;
ArticlesTVC *articles = [[ArticlesTVC alloc] initWithStyle:UITableViewStylePlain];
[articlesView addSubview:articles.view];
//[self addChildViewController:articles];
//[articlesView addSubview:articles.view];
[self.view addSubview:categoryBar.view];
[self.view addSubview:articles.view];
[self addChildViewController:categoryBar];
[self addChildViewController:articles];
categoryBar.view.frame =CGRectMake(0,0, 500,70);
articles.view.frame =CGRectMake(0,50, 400,400);
}
I see numerous problems In the code. Please look at the comments in the code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// Suggestion: Try to keep identifier in ArticlesTableViewCell if you want to use it in more than one table view controller. Don't use _tableView. _tableView is iVar. You should use it only in getters, setters and in init methods.
static NSString *CellIdentifier = #"Cell";
// Don't call "self.myTableView" of one specific tableView but for current one that is apassed by argument (tableView).
ArticlesTableViewCell *cell = (ArticlesTableViewCell *)[self.myTableView dequeueReusableCellWithIdentifier:CellIdentifier];
// You override here existing cell. Try to use if(cell==nil) before creating new cell.
cell = [[ArticlesTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
dispatch_async(kBgQueue, ^{
NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[[self.articleList valueForKey:#"enclosure"] objectAtIndex:indexPath.row]]];
if (imgData) {
UIImage *image = [UIImage imageWithData:imgData];
if (image) {
dispatch_async(dispatch_get_main_queue(), ^{
// You should call [cell setNeedsDisplay to inform app that this view have to be redrawn.
cell.enclosure.image = image;
});
}
}
});
// No need to call this on global quele. You should call this directly (e.g. cell.dataLabel = self.articleList[#"pubDate"][indexpath.row];)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[cell.dateLabel performSelectorOnMainThread:#selector(setText:) withObject:[[self.articleList valueForKey:#"pubDate"] objectAtIndex:indexPath.row] waitUntilDone:NO modes:#[NSRunLoopCommonModes]];
[cell.headingTextView performSelectorOnMainThread:#selector(setText:) withObject:[[self.articleList valueForKey:#"title"] objectAtIndex:indexPath.row] waitUntilDone:NO modes:#[NSRunLoopCommonModes]];
cell.headingTextView.editable = NO;
});
return cell;
}
I have created a UItableview with custom cells. The cells have two UILabels and an imageview that calls for an image on a background thread. The cells first load some placeholder data in case the API call takes long. Then, using a notification, the tableview is reloaded with the new data. When the app starts on the iPhone, it scrolls fine up and down. However, after scrolling fast both up and down, the cells start to break down. Sometimes a whole new list is generated below the current list. Other times, the last cell may be cut in half. What's the reason for this? Would you please help? Thank you very much!
TableViewVC.m:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.appEntries count];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return CELL_HEIGHT;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
ELAppCell *elAppCell = (ELAppCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
AppEntry *appEntry = [self.appEntries objectAtIndex:indexPath.row];
if (!elAppCell) {
elAppCell = [[ELAppCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier appEntry:appEntry];
}
else {
[elAppCell configureCellWithAppEntry:appEntry];
}
return elAppCell;
}
And the custom cell class:
#implementation ELAppCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier appEntry:(AppEntry *)appEntry
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.appNameLabel = [self buildAppNameLabel:appEntry.name];
self.thumbnailImageView = [self buildThumbnailImageView:appEntry.smallPictureURl];
self.appArtistLabel = [self buildAppArtistLabel:appEntry.artist];
[self.contentView addSubview:self.appNameLabel];
[self.contentView addSubview:self.thumbnailImageView];
[self.contentView addSubview:self.appArtistLabel];
}
return self;
}
- (void)configureCellWithAppEntry:(AppEntry *)appEntry{
dispatch_queue_t fetchQ = dispatch_queue_create("ConfigureCell", NULL);
dispatch_async(fetchQ, ^{
NSURL *address = [NSURL URLWithString:appEntry.smallPictureURl];
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:address]];
dispatch_async(dispatch_get_main_queue(), ^{
self.appNameLabel.text = appEntry.name;
self.thumbnailImageView.image = image;
self.appArtistLabel.text = [NSString stringWithFormat:#"By %#", appEntry.artist];
});
});
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *CellIdentifier = [NSString stringWithFormat:#"cell %ld %ld",(long)indexPath.row,(long)indexPath.section];
ELAppCell *elAppCell = (ELAppCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
elAppCell=nil;
AppEntry *appEntry = [self.appEntries objectAtIndex:indexPath.row];
if (elAppCell == nil)
{
NSArray *topLevelObjects = [[NSBundle mainBundle] loadNibNamed:#"ELAppCell" owner:nil options:nil];
for(id currentObject in topLevelObjects)
{
if([currentObject isKindOfClass:[MYTableViewCell class]])
{
cell = (MYTableViewCell *)currentObject;
break;
}
}
elAppCell = [[ELAppCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier appEntry:appEntry];
}
return elAppCell;
}
There is a fundamental problem with your code. Consider fetching of images in view controller. The problem that you can face with current code is if it would take a long time to load an image, there is possibility(quite hight in case user scrolls quickly) that cell can be dequeued for rendering of another row but image from previous row will be just download and displayed which will cause inconsistent UI.
There are 2 places in ELAppCell that cause the described bug:
- (void)configureCellWithAppEntry:(AppEntry *)appEntry{ that we already fixed
- (UIImageView *)buildThumbnailImageView: (NSString *)imageUrl you should remove(or comment out) all dispatch calls. Calls to dispatch... are redundant and cause the bug; TableViewController in charge of loading images. Should you need to load small images then consider loading them in view controller as well.
There is also a serious bug in your ELAppListTableVC, your should remove appListTableView property and all places in the code where you use it. And in receiveEvent:notification method you should call [self.tableView reloadData]; instead of reloading appListTableView. Basically you had two table views laying one on another, where appListTableView was added manually. NOTE: your ELAppListTableVC already inherited from UITableViewController so it already has tableView that get automatically instantiated by the view controller.
Code that loads image can be moved to view controller or be a part of a model class. Details bellow:
Load image in view controller
After image gets loaded you need to update the cell with particular index path because the index path represent AppEntry instance for which image was downloaded. It means that you should ask table view to return cell that represents that index path. It may turn that cell currently invisible, it this case you will get nil. So in callback block that gets invoked on main queue once download completed you need to have a reference to that index path. Since code is better that hundred of worlds, here is how it should look:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
ELAppCell *elAppCell = (ELAppCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
AppEntry *appEntry = [self.appEntries objectAtIndex:indexPath.row];
if (!elAppCell) {
elAppCell = [[ELAppCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier appEntry:appEntry];
}
[elAppCell configureCellWithAppEntry:appEnty];
self.thumbnailImageView.image = nil;
dispatch_queue_t fetchQ = dispatch_queue_create("ConfigureCell", NULL);
dispatch_async(fetchQ, ^{
NSURL *address = [NSURL URLWithString:appEntry.smallPictureURl];
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:address]];
dispatch_async(dispatch_get_main_queue(), ^{
ELAppCell *updateCell = (ELAppCell *)[tableView cellForRowAtIndexPath:indexPath];
if (updateCell) { // if nil then cell is not visible hence no need to update
updateCell.thumbnailImageView.image = image;
}
});
});
return elAppCell;
}
after refactoring this configureCellWithAppEntry: code should look like this:
- (void)configureCellWithAppEntry:(AppEntry *)appEntry {
self.appNameLabel.text = appEntry.name;
self.appArtistLabel.text = [NSString stringWithFormat:#"By %#", appEntry.artist];
}
Much better code but still youэдд be re-downloading images that were already downloaded when user scrolls back.
Move image loading code to model class (further improvement of the code above)
You perform download each time cell gets displayed, i.e. image that was already downloaded will be downloaded again. To cache images you can add thumbnailImage property to AppEntry class and load this image lazily, only when user need is. To do that you can update your AppEntry class with the code bellow:
AppEntry.h
#interface AppEntry // if it is CoreData object here should be AppEntry: NSManagedObject
#property (nonatomic, readonly) UIImage *thumbnailImage;
- (void)loadThumbnailImage:(void (^)())completionBlock;
#end
AppEntry.m
#interface AppEntry ()
#property (nonatomic, readwrite) UIImage *thumbnailImage;
#end
#implementation AppEntry
- (void)loadThumbnailImage:(void (^)())completionBlock {
dispatch_queue_t fetchQ = dispatch_queue_create("ConfigureCell", NULL);
dispatch_async(fetchQ, ^{
NSURL *address = [NSURL URLWithString:self.smallPictureURl];
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:address]];
dispatch_async(dispatch_get_main_queue(), ^{
self.thumbnailImage = image;
completionBlock();
});
});
}
#end
of course view controller should be updated as well:
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
ELAppCell *elAppCell = (ELAppCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
AppEntry *appEntry = [self.appEntries objectAtIndex:indexPath.row];
if (!elAppCell) {
elAppCell = [[ELAppCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier appEntry:appEntry];
}
[elAppCell configureCellWithAppEntry:appEnty];
if (appEntry.thumbnailImage) {
elAppCell.thumbnailImageView.image = appEntry.thumbnailImage;
} else {
[appEntry loadThumbnailImage: ^{
elAppCell.thumbnailImageView.image = appEntry.thumbnailImage;
}];
}
return elAppCell;
}
and as soon as we have thumbnail image in AppEntry model we can update cell code to use it in configureCellWithAppEntry: method:
- (void)configureCellWithAppEntry:(AppEntry *)appEntry{
self.appNameLabel.text = appEntry.name;
self.appArtistLabel.text = [NSString stringWithFormat:#"By %#", appEntry.artist];
self.thumbnailImageView.image = appEntry.thumbnailImage;
}
Even with image cache we can have re-downloading when user can scroll up and down so quickly that thumbnailImage can be nil for the AppEntry instance because image has not been downloaded yet. So we need to handle image downloading state some how, details bellow.
Optimization of image downloading and caching
To solve problem with sequential calls to loadThumbnailImage:, when we do perform a new download while we have one in the progress already we need to handle downloadInProgress state. AppEntry need to be tweaked to:
#implementation AppEntry {
dispatch_group_t _thumbnailDownloadGroup;
dispatch_queue_t _thumbnailFetchQueue;
dispatch_once_t _thumbnailQOnceToken;
BOOL _loadingThumbnail;
}
- (void)loadThumbnailImage:(void (^)())completionBlock {
if (self.thumbnailImage) { // return immediately since we have image
completionBlock();
} else if (_loadingThumbnail) { // return when previously requested download complete
dispatch_group_notify(_thumbnailDownloadGroup, _thumbnailFetchQueue, ^{
completionBlock();
});
} else { // download image
dispatch_once(&_thumbnailQOnceToken, ^{
_thumbnailDownloadGroup = dispatch_group_create();
_thumbnailFetchQueue = dispatch_queue_create("ThumbnailDownload", NULL);
});
__weak typeof(self) weakSelf = self;
dispatch_group_async(_thumbnailDownloadGroup, _thumbnailFetchQueue, ^{
NSURL *address = [NSURL URLWithString:weakSelf.smallPictureURl];
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:address]];
dispatch_async(dispatch_get_main_queue(), ^{
self.thumbnailImage = image;
completionBlock();
});
});
}
}
#end
I have not run the examples to test but I hope you can get the idea.
I'm using grand central dispatcher to load images from server but when i scroll the table the data, i.e. images, jumbles - means 1st image comes to some other place and like wise other images do.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"ItemImageCellIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] ;
cell.selectionStyle=UITableViewCellSelectionStyleNone;
}
NSDictionary *item=[responseDictionary objectAtIndex:[indexPath row]];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul);
NSString *actionForUser=[item objectForKey:#"action"];
objc_setAssociatedObject(cell,
kIndexPathAssociationKey,
indexPath,
OBJC_ASSOCIATION_RETAIN);
dispatch_async(queue, ^{
if([actionForUser isEqualToString:like])
{
NSURL *url = [NSURL URLWithString:[item objectForKey:#"user_image"]];
NSData *data1 = [[NSData alloc] initWithContentsOfURL:url];
UIImage *image1 = [[UIImage alloc] initWithData:data1];
//userProfileimage
UIButton *userImageButton = [[UIButton alloc] initWithFrame:CGRectMake(10,5, 40,40)];
userImageButton.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter;
userImageButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
[userImageButton setBackgroundImage:image1 forState:UIControlStateNormal];
[userImageButton addTarget:self
action:#selector(userImageButtonclick:)
forControlEvents:UIControlEventTouchDown];
[cell.contentView addSubview:userImageButton];
}
});
return cell;
}
This is because by the time your async method has finished, cell has been recycled and used for a different index path, so you're updating the wrong cell.
At the point of update, get the cell reference by using the tableview's (not the data source method) cellForRowAtIndexPath: method. This will return the correct cell, or nil if the cell isn't on the screen any more. You can update this cell safely.
You should probably be adding the image data to your model as well so you aren't downloading it repeatedly.
As an example, instead of this line:
[cell.contentView addSubview:userImageButton];
You should have something like this:
UITableViewCell *cellToUpdate = [tableView cellForRowAtIndexPath:indexPath];
[cellToUpdate.contentView addSubview:userImageButton];
There are further problems with your code; you are not caching the images, you will be adding this subview every time this cell comes on screen, and if the cell is reused for a case where it doesn't need the button, the button will still be present. I have only addressed the "GCD jumbling" as described in your question.
I'm trying to pull my own flavor of the usual UITableView + async download + cache technique. What I'm doing is, for each cell that gets dequeued in cellForRowAtIndexPath:
1-Check if it's corresponding thumbnail image is already 'cached' in /Library/Caches
2-If it is, just use that.
3-If not, load a default image and enqueue an NSInvocationOperation to take care of it:
4a-The NSInvocationOperation gets the image from a remote server
4b-Does the UIGraphicsBeginContext thing to scale down the image to 40x40
4c-saves the scaled down version to /Library/Cache
4d-'SHOULD' update the cell's image to the new downloaded and downsized image, if the cell is still visible.
However, I can't figure out how to get the cells to update their images unless I manually scroll them off and back on screen. The only hack I've been able to pull is having the NSOperation call the main thread when done, through performSelectorOnMainThread and the main thread can then call [viewtable reloadData]. But this seems wasteful: I'm reloading the whole table each time a cell's new image is ready.
As a less wasteful approach, I have the main thread instead set a bool flag and then, when scrollViewDidEndDecelerating, if the flag was set, a call to [viewtable reloadData] is made. With this approach the cells only refresh when the user is done scrolling.
But still, I'd like for just the visible cells to update, if their cached images are ready while they are still visible (meaning the user didn't scroll them off the view).
Here's my code so far:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle: UITableViewCellStyleSubtitle
reuseIdentifier: CellIdentifier] autorelease];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.selectionStyle = UITableViewCellSelectionStyleGray;
}
// Configure the cell...
cell.textLabel.text = [[dbData objectAtIndex:indexPath.row] objectAtIndex:0];
cell.detailTextLabel.text = [[dbData objectAtIndex:indexPath.row] objectAtIndex:1];
NSString *ImageName = [[dbData objectAtIndex:indexPath.row] objectAtIndex:2];
NSString *cachedImageName = [[[ImageName stringByDeletingPathExtension] stringByAppendingString:thumbnailSizeSuffix] stringByAppendingPathExtension:#"png"];
NSString *cachedImagePath = [cachePath stringByAppendingPathComponent:cachedImageName];
if([[NSFileManager defaultManager] fileExistsAtPath:cachedImagePath])
cell.imageView.image = [UIImage imageWithContentsOfFile:cachedImagePath];
else
{
cell.imageView.image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:defaultNeedsDownloadIconFile ofType:#"png"]];
NSArray *package = [NSArray arrayWithObjects:ImageName, cachedImagePath ,referencingTable, nil];
NSInvocationOperation *concurrentImageLoader = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(loadURI:) object:package];
[concurrentQueue addOperation: concurrentImageLoader];
[concurrentImageLoader release];
}
return cell;
}
For the "kernel" of the NSInvocationOperation, I've tried this:
- (void)loadURI:(id)package
{
NSArray *payload = (NSArray*)package;
NSString *imageName = [payload objectAtIndex:0];
NSString *cachedImagePath = [payload objectAtIndex:1];
NSString *imageURL = [NSString stringWithFormat:#"http://www.useanddisposeof.com/VentanaSurDB/%#/photo/%#",[payload objectAtIndex:2], imageName];
UIImage *newThumbnail = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:imageURL]]];
if(!newThumbnail)
newThumbnail = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:defaultNotFoundIconFile ofType:#"png"]];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, thumbnailSize.width, thumbnailSize.height)];
imageView.layer.borderColor = [UIColor blackColor].CGColor;
imageView.layer.cornerRadius = 4.0;
imageView.layer.masksToBounds = YES;
imageView.layer.borderWidth = 1.0;
imageView.image = newThumbnail;
UIGraphicsBeginImageContext(CGSizeMake(thumbnailSize.width, thumbnailSize.height));
[imageView.layer renderInContext:UIGraphicsGetCurrentContext()];
newThumbnail = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[imageView release];
[UIImagePNGRepresentation(newThumbnail) writeToFile:cachedImagePath atomically:YES];
[self performSelectorOnMainThread:#selector(updateCellImage) withObject:nil waitUntilDone:NO];
}
And this is the code back in the main thread for refreshing the tableview:
- (void)updateCellImage:(id)package
{
needReloadCachedImages = YES;
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
// I know, I know, there's a race condition here.. I'll fix it if this code stays.
if(needReloadCachedImages)
[self.tableView reloadData];
needReloadCachedImages = NO;
}
Any ideas?
But this seems wasteful: I'm reloading the whole table each time a
cell's new image is ready.
reloadData only reloads visible cells, not the whole table, which is what you say you want.
How about giving some open-source a try? This is too much effort for a simpler problem. There is also a nice tutorial on this, which might give you an idea on what you might be doing wrong.