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.
Related
I added images to my tableView cells and it made it laggy. I am still new to objective c and I do not understand what is causing this or how to fix it. Any help is greatly appreciated!
group[PF_GROUP_LOGO] is simply a string in my database that is unique to each object. The code works, it is just really laggy when trying to scroll.
//-------------------------------------------------------------------------------------------------------------------------------------------------
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
//-------------------------------------------------------------------------------------------------------------------------------------------------
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cell"];
if (cell == nil) cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"cell"];
PFObject *group = groups[indexPath.row];
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:group[PF_GROUP_LOGO]]]];
cell.imageView.image = image;
cell.detailTextLabel.text = [NSString stringWithFormat:#"%d users", (int) [group[PF_GROUP_MEMBERS] count]];
cell.detailTextLabel.textColor = [UIColor lightGrayColor];
return cell;
}
There are so many tools out there that can help you with this.
At a base level, the issue is that you are running a long process on the main thread, which blocks the UI. Loading an image from a non-local URL is time consuming, and you should do this on a background thread, so the UI is not blocked.
Again, there are so many ways to do this, and I strongly suggest you do some research on async resource loading, but this is one thing you can do within the confines of your own example:
//-------------------------------------------------------------------------------------------------------------------------------------------------
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
//-------------------------------------------------------------------------------------------------------------------------------------------------
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cell"];
if (cell == nil) cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"cell"];
PFObject *group = groups[indexPath.row];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{ // go to a background thread to load the image and not interfere with the UI
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:group[PF_GROUP_LOGO]]]];
dispatch_async(dispatch_get_main_queue(), ^{ // synchronize back to the main thread to update the UI with your loaded image
cell.imageView.image = image;
});
});
cell.detailTextLabel.text = [NSString stringWithFormat:#"%d users", (int) [group[PF_GROUP_MEMBERS] count]];
cell.detailTextLabel.textColor = [UIColor lightGrayColor];
return cell;
}
I would also recommend using AFNetworking as the author has built a very good category on top of UIImageView that allows you to load an image from a web URL in the background automatically. Again, there are many schools of thought on this process, and this is just one idea. I would recommend reading this for a full on tutorial on the topic.
I hope this is helpful!
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 am able to fetch images asynchronously on to uitableview.i am fetching these images are from a url.on scrolling up uitableview these images disappear and they take time to load again and sometimes they dont load at all.i dont want to use any 3rd party libraries.i dont want to go with synchronous approach.please suggest any correct approach to improve performance.thanks for help in advance.my code is below:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *identifier=#"cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"identifier"];
}
UIImageView *imgVw=[[UIImageView alloc]initWithFrame:CGRectMake(250, 10, 40, 30)];
[cell.contentView addSubview:imgVw];
Attributes *att = [listOfObjects objectAtIndex:indexPath.row];
strImgUrl=#"http:image url";
strImgName=att.classifiedImg;
if (strImgName == nil) {
UIImage *myImg=[UIImage imageNamed:#"user_circle.png"];
imgVw.image=myImg;
}
else{
strImg=[strImgUrl stringByAppendingString:strImgName];
}
dispatch_async(dispatch_get_global_queue(0,0), ^{
NSData *data = [[NSData alloc] initWithContentsOfURL: [NSURL URLWithString:strImg]];
if ( data == nil )
return;
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *img=[UIImage imageWithData: data];
imgVw.image=img;
});
});
return cell;
}
UITableViews are designed to reuse cells. When you scroll up for example the first cell might get reused to show the 5th cell since the 1st one is now off screen. When you scroll back up cellForRowAtIndexPath is called again, and you are async downloading the same image again. If you want to have it load instantly you will need to cache the images after downloading them the first time so the next time that image is needed you can directly pull it from the cache.
Many third party libraries do this (AFNetworking), but if you don't want to use them, you will have to cache the images manually.
It seems like your reuse code contains bug, which causes everytime to create new cells. The following code uses reuseIdentifier as #"identifier"
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"identifier"];
}
Change it to:
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:identifier];
}
EDIT:
On reusing tableView cells, you dont need to create and add subviews to cell each time. Instead, just create the subview when creating cell and then if cell is reusing, just get the subview using tag and update your content from datasource.
Your code can be modified like below:
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:identifier];
UIImageView *imgVw=[[UIImageView alloc]initWithFrame:CGRectMake(250, 10, 40, 30)];
imgVw.tag = indexPath.row;
[cell.contentView addSubview:imgVw];
}
UIImageView imgVw = (UIImageView*)[cell.contentView viewWithTag:indexPath.row];
//Rest is same as you posted.
Hope this will fix the issue.
I am having a problem viewing my tableview when i get the data of my cells from a server. If i do not use photos there is no breaks in the scrolling, but i want to use the images also. Can anyone knows how can i resolve this? I am getting the data from a plist in my server.
Here is the code of the image that makes the scrolling breaks (i am using custom style cell)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
NSURL *URL = [[NSURL alloc] initWithString:[[self.content objectAtIndex:indexPath.row] valueForKey:#"imageName"]];
NSData *URLData = [[NSData alloc] initWithContentsOfURL:URL];
UIImage *img = [[UIImage alloc] initWithData:URLData];
UIImageView *imgView = (UIImageView *)[cell viewWithTag:100];
imgView.image = img;
....
If you mean that the scrolling stops and starts, this might be because if the images are loaded from a server (which might take a noticeable amount of time to do), executing on the main thread causes freezing.
If this is the case, the fix is to fetch the images in another thread. Fortunately iOS has a fairly easy to use multithreading system called Grand Central Dispatch.
Here's some example code:
dispatch_queue_t q = dispatch_queue_create("FetchImage", NULL);
dispatch_async(q, ^{
/* Fetch the image from the server... */
dispatch_async(dispatch_get_main_queue(), ^{
/* This is the main thread again, where we set the tableView's image to
be what we just fetched. */
});
});