UITableViewCell using reuseidentifier giving unwanted result with callback block - objective-c

When the callback block for loadImage is run below, the table cell may have since been reused. So the image is applied to "imageView" is not relevant to this reused cell, it's the image for the old cell.
If I make the identifier unique for each cell that has an image, the problem goes away. But this gives poor performance with many results.
Can I somehow use the same reuse identifier with a callback block and have the images turn up in the correct cells?
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSDictionary *place;
PlaceTableViewCell *cell; // UITableViewCell subclass
NSString *identifier = #"PlaceTableViewCell";
if (cell == nil) {
NSArray *objects;
objects = [[NSBundle mainBundle] loadNibNamed:#"PlaceTableViewCell" owner:self options:nil];
for(id object in objects) {
if([object isKindOfClass:[PlaceTableViewCell class]]) {
cell = (PlaceTableViewCell *)object;
break;
}
}
}
UIImageView *imageView;
if((imageView = (UIImageView*)[cell viewWithTag:1])) {
NSString *filename;
int placeImageId = 0;
place = [places objectAtIndex:indexPath.row];
if(place) {
placeImageId = [[d objectForKey:#"placeImageId"] intValue];
if(placeImageId) {
[[RestAPIConnector sharedInstance] loadImage :placeImageId :#"thumb" :^(NSString *response){
NSDictionary *image = [response JSONValue];
if ([image objectForKey:#"img"]) {
NSString *b64Img = [image objectForKey:#"img"];
UIImage *ui = [UIImage imageWithData:[Base64 decode:b64Img]];
imageView.image = ui;
}
}];
}
}
}
return cell;
}

here is what I'm doing.
instead of using the cell directly, I'm passing in the index path
if(user.profileImage == nil)
{
if (self.tableView.dragging == NO && self.tableView.decelerating == NO) {
NSLog(#"file for user %d doesn't exist", [user.userId intValue]);
[self startUserProfileImageDownload:user forIndexPath:indexPath];
}
}
else
{
cell.profileImageView.image = user.profileImage;
}
once the download is complete, use the index path to retrieve the cell, and update the image
MessageCell *cell = (MessageCell *)[self.tableView cellForRowAtIndexPath:path];
// Display the newly loaded image
cell.profileImageView.image = user.profileImage;
CALayer *roundedLayer = [cell.profileImageView layer];
MessageCell is my custom cell. if you don't have use customer cell, you can use Tag to retrieve the imageView back.

I'd create a dictionary to hold the images, then attempt to read from the dictionary in cellForRowAtIndexPath:
#property(retain)NSMutableDictionary *imageData;
//...
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
//...
UIImageView *imageView;
if((imageView = (UIImageView*)[cell viewWithTag:1])) {
int placeImageId = 0;
place = [places objectAtIndex:indexPath.row];
if(place) {
placeImageId = [[d objectForKey:#"placeImageId"] intValue];
if(placeImageId) {
NSObject *image = [imageData objectForKey:[NSNumber numberWithInt:placeImageId]];
if ([image isKindOfClass:[UIImage class]) {
imageView.image = (UIImage *)image;
} else if (![image isEqual:#"downloading"]) {
[imageData addObject:#"downloading" forKey:[NSNumber numberWithInt:placeImageId]];
[[RestAPIConnector sharedInstance] loadImage:placeImageId onSuccess:^(NSString *response){
NSDictionary *image = [response JSONValue];
if ([image objectForKey:#"img"]) {
NSString *b64Img = [image objectForKey:#"img"];
[imageData addObject:[UIImage imageWithData:[Base64 decode:b64Img]] forKey:[NSNumber numberWithInt:placeImageId]];
}
}];
}
}
}
}
return cell;
}
Some potential optimizations:
As in #Jun1st's sample, don't load images for cells while scrolling
Add download operations to an NSOperationQueue and prioritize the most recently requested ones first (deprioritizing the ones that have scrolled by)
Save downloaded images to the filesystem and check there first

Related

Accessory checkmarks disappear when scrolling Objective-C

I have a tableview that I can add and remove multiple checkmarks. The only issue is if I put 3 checkmarks and scroll away, when I return the checkmarks are gone. I can't find anywhere on the internet a solution that works, and I've tried several variation and still nothing.
This is my code in cellForRowAtIndex that should be holding the checkmarks in place.
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *reuseIdentifier = #"contactCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier];
NSDictionary *contact = [self.tableData objectAtIndex:indexPath.row];
UILabel *nameLabel = (UILabel *)[cell viewWithTag:1];
NSString *firstName = contact[#"firstName"];
nameLabel.text = [firstName stringByAppendingString:[NSString stringWithFormat:#" %#", contact[#"lastName"]]];
UILabel *phoneNumber = (UILabel *)[cell viewWithTag:2];
NSArray *phones = contact[#"phones"];
if ([phones count] > 0) {
NSDictionary *phoneItem = phones[0];
phoneNumber.text = phoneItem[#"value"];
}
UIImageView *cellIconView = (UIImageView *)[cell.contentView viewWithTag:888];
UIImage *image = contact[#"image"];
cellIconView.image = (image != nil) ? image : [UIImage imageNamed:#"smiley-face"];
cellIconView.contentScaleFactor = UIViewContentModeScaleAspectFill;
cellIconView.layer.cornerRadius = CGRectGetHeight(cellIconView.frame) / 2;
// Need to fix
if([checkedIndexPath isEqual:indexPath])
{
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
else
{
cell.accessoryType = UITableViewCellAccessoryNone;
}
return cell;
}
Here is the didSelectRowAtIndexPath method
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath: (NSIndexPath *)indexPath
{
UITableViewCell* checkCell = [tableView cellForRowAtIndexPath:indexPath];
if(checkCell.accessoryType == UITableViewCellAccessoryCheckmark)
{
checkCell.accessoryType = UITableViewCellAccessoryNone;
NSMutableArray *i = [[NSMutableArray alloc] init];
for (NSIndexPath *indexPath in [self.tableView indexPathsForSelectedRows]) {
[i addObject:self.tableData[indexPath.row]];
// Go inside pull the numbers from the users and save in an NSArray
// NSArray *contacts = i;
// self.recipients = [[NSMutableArray alloc] init];
for (NSDictionary* dict in i) {
// Grab phones
NSDictionary *contactNumber = [dict objectForKey:#"phones"];
for (NSDictionary* dict2 in contactNumber) {
// Grabs the phone numbers
NSString* value = [dict2 objectForKey:#"value"];
int index = [self.recipients indexOfObject:value];
[self.recipients removeObjectAtIndex:index];
[self.selectedUsers removeObjectAtIndex:index];
NSLog(#"The number that has a checkmark%#", value);
NSLog(#"the array of all%#", self.recipients);
NSLog(#"At index %lu", (unsigned long)[self.recipients indexOfObject:value]);
// [_recipients addObject:value];
}
}
// NSLog(#"Phone Numbers: %#",_recipients);
}
}
else
{
[self getNumber];
NSLog(#"clicking %#", self.recipients);
UITableViewCell* cell = [tableView cellForRowAtIndexPath:indexPath];
cell.accessoryType = UITableViewCellAccessoryCheckmark;
checkedIndexPath = indexPath;
}
}
I found The Solution:
You must save each indexPath into an array(put this code in didSelectRowAtIndexPath) and then in cellForRowAtIndexPath add the following code
if([self.checkedCells containsObject:indexPath]) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
} else {
cell.accessoryType = UITableViewCellAccessoryNone;
}
Also in the didSelectRowAtIndexPath
Make sure to delete the indexPath when deselecting the row.
if(checkCell.accessoryType == UITableViewCellAccessoryCheckmark) {
checkCell.accessoryType = UITableViewCellAccessoryNone;
[self.checkedCells removeObject:indexPath];
I hope this helps someone. I been wrestling with this all day.
Make checkedIndexPath a #property (nonatomic, strong) and use self.checkedIndexPath whenever you refer to it. You're losing the reference after didSelectRowAtIndexPath exits. Set a breakpoint in cellForRowAtIndexPath and look at checkedIndexPath, I bet it's nil.
Maybe you should check if the isEqual functionality does what you expect. You could make sure by trying:
if (_checkedIndexPath.section == indexPath.section &&
_checkedIndexPath.row == indexPath.row)
If you still do not get the expected result, perhaps log the values of section and row to see where it goes wrong.
Please note that if for some reason _checkedIndexPath is a weak variable or gets deallocated, this check will fail.
You could also check that your cells are properly dequeued before being modified and that you are returning the correct cells.
If you want to store more than one checked row, of course, you will need more than one indexPath variable (just one _checkedIndexPath will not do it).

Add different images to UIImageView of cells in UITableView

Needless to say, i am a novice. I am trying to add images and text to a UITableView. I have been able to add the text but I am having problems with adding different images to the desired text.
I have 2 separate PHP codes for 1 database:
The first gets the text and an id for the image assigned to the text (imageID)
The second uses the id (from the first PHP) to acquire the image
My code is fine but currently only 1 image is appearing for all the text
My question is how do I assign each image to their text ?
And how do I not assign an image to the text that do not have images as some of the texts don't have images with them?
My code is as follows:
Connection and data download:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
// Create an array to store the locations
NSMutableArray *list = [[NSMutableArray alloc] init];
// Parse the JSON that came in
NSError *error;
jsonArray = [NSJSONSerialization JSONObjectWithData:downloadedData options:NSJSONReadingAllowFragments error:&error];
// Loop through Json objects, create question objects and add them to our questions array
for (int i = 0; i < jsonArray.count; i++)
{
jsonElement = jsonArray[i];
// Create a new cell object and set its props to JsonElement properties
if (![jsonElement [#"Thought"] isEqual:[NSNull null]])
{
NSString *listText =jsonElement [#"Thought"];
if ([listText length] > 0)
{
NSString *cellText = jsonElement[#"Thought"];
[list addObject:cellText];
NSLog(#"list Cell: %#", cellText);
}
if (![jsonElement [#"filename"] isEqual:[NSNull null]])
{
imageID = jsonElement[#"id"];
}
NSLog(#"Cell Image: %#", imageID);
}
}
[[self tableView1] reloadData];
}
Cells in the table:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *simpleIdentifier = #"SimpleIdentifier";
cell = [tableView dequeueReusableCellWithIdentifier:simpleIdentifier];
if (cell == nil)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:simpleIdentifier];
}
cell.textLabel.text = [list objectAtIndex:indexPath.row];
cell.textLabel.font = [UIFont italicSystemFontOfSize:20];
if ([imageID length] > 0)
{
urlStr = [NSString stringWithFormat:#"http://database.name/getimage.php?id=%#", imageID];
NSURL *imageUrl = [NSURL URLWithString:urlStr];
NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
UIImage *imageLoad = [[UIImage alloc] initWithData:imageData];
cell.imageView.image = imageLoad;
}
return cell;
}
You should not leave any case untouched in tableview:cellForRowAtIndexPath. What happens when you try this
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *simpleIdentifier = #"SimpleIdentifier";
cell = [tableView dequeueReusableCellWithIdentifier:simpleIdentifier];
if (cell == nil)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:simpleIdentifier];
}
cell.textLabel.text = [list objectAtIndex:indexPath.row];
cell.textLabel.font = [UIFont italicSystemFontOfSize:20];
if ([imageID length] > 0)
{
urlStr = [NSString stringWithFormat:#"http://database.name/getimage.php?id=%#", imageID];
NSURL *imageUrl = [NSURL URLWithString:urlStr];
NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
UIImage *imageLoad = [[UIImage alloc] initWithData:imageData];
cell.imageView.image = imageLoad;
}
else //I assume this is the case when a row does not contain an image right?
{
cell.imageView.image = nil; //Or the below line for better aestethics
//cell.imageView.image = [UIImage imageNamed:#"placeholderimage.png"];
}
return cell;
}
By the way NSData *imageData = [NSData dataWithContentsOfURL:imageUrl]; is called everytime tableview:cellForRowAtIndexPath is triggered and this happens alot. As far as I know dataWithContentsOfURL: is a heavy process and stalls the UI until it downloads the image. So it would be better to download one specific image once and save it in a NSMutableArray or NSMutableDictionary. For instance:
//in your .m
#interface ViewController ()
#property NSMutableDictionary * imageIdDictionary;
#end
#implementation ViewController
-(void)viewDidLoad
{
[super viewDidLoad];
imageIdDictionary = [[NSMutableDictionary alloc] init];
}
And change your tableView:cellForRowAtIndexPath: to:
//Added this. You need a second array just like `list` which has all the different imageID's stored
imageID = [list2 objectAtIndex:indexPath.row];
if ([imageID length] > 0) {
if([self.imageIdDictionary objectForKey:imageID]) {
cell.imageView.image = [self.imageIdDictionary objectForKey:imageID];
}
else {
urlStr = [NSString stringWithFormat:#"http://database.name/getimage.php?id=%#", imageID];
NSURL *imageUrl = [NSURL URLWithString:urlStr];
NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
UIImage *imageLoad = [[UIImage alloc] initWithData:imageData];
cell.imageView.image = imageLoad;
[self.imageIdDictionary setValue:imageLoad forKey:imageID];
//added these two lines!!!!!!!
NSArray * array = [NSArray arrayWithObject:[NSIndexPath indexPathForRow:indexPath.row inSection:indexPath.section]];
[table reloadRowsAtIndexPaths:array withRowAnimation:UITableViewRowAnimationFade];
}
}
else {
cell.imageView.image = nil; //Or the below line for better aestethics
//cell.imageView.image = [UIImage imageNamed:#"placeholderimage.png"];
}
This will make your application run much smoother
UITableView's cells are reusable. It means that the cells which are not visible will show the same content before you reset.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *simpleIdentifier = #"SimpleIdentifier";
cell = [tableView dequeueReusableCellWithIdentifier:simpleIdentifier];
if (cell == nil)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:simpleIdentifier];
}
cell.textLabel.text = #"";
cell.imageView.image = nil;
cell.textLabel.text = [list objectAtIndex:indexPath.row];
cell.textLabel.font = [UIFont italicSystemFontOfSize:20];
...
}
Just add the reset codes.

UITableView reloadData lose elements from cell

I have a UITableView and his datasource is an NSMutableArray declared in my interface:
#property (strong, nonatomic) NSMutableArray *arrElements;
Now I implemented a simple "load more" in this way:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
float endScrolling = scrollView.contentOffset.y + scrollView.frame.size.height;
if (endScrolling >= scrollView.contentSize.height)
{
//only if i have others results i will call load more
if([self.items_amount integerValue] > self.arrElements.count) {
if(DEBUG_MODE == 1) {
NSLog(#"Load more");
}
//get start param from array size and call load more
NSNumber *start = [NSNumber numberWithInteger:([self.arrElements count]+1)];
NSArray *passParams = [NSArray arrayWithObjects:self.menuItem,start,[NSNumber numberWithInteger:NUM_RESULTS_FOR_PAGE], nil];
[self performSelector:#selector(loadMore:) withObject:passParams afterDelay:0.1];
}
}
}
and this is my loadMore method:
//load more elements
- (void)loadMore:(NSArray *)arrParams {
//(MenuItem *)menuItem startingFrom:(NSNumber *)start numResults:(NSNumber *)results;
MenuItem *menuItem = [arrParams objectAtIndex:0];
NSNumber *start = [arrParams objectAtIndex:1];
NSNumber *results = [arrParams objectAtIndex:2];
if(DEBUG_MODE == 1) {
NSLog(#"Before load more %lu", (unsigned long)[self.arrElements count]);
}
//call API and parse it
WebServicesClient *restClient = [[WebServicesClient alloc] init];
NSData *data = [restClient callWorkAPI:[menuItem pathAPI] inLanguage:[[NSLocale preferredLanguages] objectAtIndex:0] withLat:self.latitude withLng:self.longitude startingFrom:start numRows:results];
for(Work* work in [[restClient parseWorks:data] objectForKey:#"items"]) {
[self.arrElements addObject:work];
}
if(DEBUG_MODE == 1) {
NSLog(#"After load more %lu", (unsigned long)[self.arrElements count]);
}
[self.tableElem reloadData];
}
And this is my custom cell:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier;
CellIdentifier = #"HomeCell";
HomeCell *cell = (HomeCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(!cell) {
cell = [[HomeCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
Work *work = (Work *)[[self arrElements] objectAtIndex:indexPath.row];
cell.backgroundColor = [UIColor clearColor];
cell.labelTitleItem.text = [work title];
cell.labelSubtitleItem.text = #"my category"
//if exists image
if([[work image] isKindOfClass:[NSString class]] && [[work image] length] > 0) {
dispatch_async(dispatch_get_global_queue(0,0), ^{
NSData * data = [[NSData alloc] initWithContentsOfURL: [NSURL URLWithString:[work image]]];
if ( data == nil )
return;
dispatch_async(dispatch_get_main_queue(), ^{
cell.imageItem.image = [UIImage imageWithData: data];
});
});
} else {
//remove image setting height to 0
[cell.imageItem setFrame:CGRectMake(cell.imageItem.frame.origin.x, cell.imageItem.frame.origin.y, cell.imageItem.frame.size.width, 0)];
}
if([[work distance] isKindOfClass:[NSString class]]) {
[cell.imagePriority setBackgroundColor:[UIColor lightGrayColor]];
cell.labelDistanceItem.text = [self convertDistance:[work distance]];
cell.labelDistanceItem.textColor = [UIColor whiteColor];
} else {
//remove image setting height to 0
[cell.imagePriority setFrame:CGRectMake(cell.imagePriority.frame.origin.x, cell.imagePriority.frame.origin.y, cell.imagePriority.frame.size.width, 0)];
//remove label distance setting height to 0
[cell.labelSubtitleItem setFrame:CGRectMake(cell.labelSubtitleItem.frame.origin.x, cell.labelSubtitleItem.frame.origin.y, cell.labelSubtitleItem.frame.size.width, 0)];
}
return cell;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [[self arrElements] count];
}
Now the problem is that after reloadData i lose the text of a UILabel (my category) of my CustomCell
please, Any suggests?
I've had similar problems with TalbeViews. Try using the method
- (void) tableView:(UITableView *)tableView willDisplayCell:(RCGameTableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
to add the elements that you want. Keep the cell initialization that you do at the beginning of cellForRowAtIndexPath, but then return the cell right after and set the background color, labels, images, etc. in the willDisplayCell delegate function I mentioned.

Why are my UITableViewCell images only being loaded when I leave then return to a controller and not during viewDidLoad?

Having a slight problem with my UITableViewCell images. I'm loading my data straight from parse.com. My objects array that returns PFObject's is stored inside an NSMutable array named "people".
This is how I display the data in my table:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [[self tableView] dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
// Configure the cell...
Person *current;
if (tableView == [[self searchDisplayController] searchResultsTableView]) {
current = [searchResults objectAtIndex:indexPath.row];
} else {
current = [people objectAtIndex:[indexPath row]];
}
[[cell textLabel] setText: [current valueForKey:#"name"]];
PFFile *userImageFile = [current valueForKey:#"image"];
[userImageFile getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error) {
UIImage *image = [UIImage imageWithData:imageData];
[[cell imageView] setImage: image];
}];
// [[cell imageView] setImage: [current image]];
[[cell detailTextLabel] setText: [current valueForKey:#"notes"]];
return cell;
}
The problem is when I load the app up and this view which is my main loads it doesn't load any images. However when I tap on a row just before the next controller is popped on screen I see the image for that row load and then when I tap the back button and go back to the main view again the rest of the tableViews images load.
Is this something to do with the images not being thumbnail versions?
I've tried wrapping the code in dispatch_async(dispatch_get_main_queue(), ^ { )}; with no luck. Can someone help me solve this issue?
Kind regards
Update to show where I call reload data:
-(void)viewDidAppear:(BOOL)animated {
dispatch_async(dispatch_get_main_queue(), ^{
[[self tableView] reloadData];
});
}
- (void)viewDidLoad
{
[super viewDidLoad];
people = [[NSMutableArray alloc] init];
PFQuery *query = [PFQuery queryWithClassName:#"People"];
[query whereKey:#"active" equalTo:#1];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
people = objects.mutableCopy;
dispatch_async(dispatch_get_main_queue(), ^ {
[[self tableView] reloadData];
});
I don't think there is anything wrong with your loading in your viewDidLoad.
My suspicion is that the UIImageView's frame is actually zero as you did not have a placeholder image while loading the actual images. The cell will not be redrawn again until the next time layoutSubviews is called again, even if your fetched image has loaded. So either set a placeholder image, or call:
[cell setNeedsLayout];
once your image is fully loaded.
Another alternative is to use PFImageView, a subclass of UIImageView, which takes care of everything for you.
PFFile *userImageFile = [current valueForKey:#"image"];
[cell imageView].image = [UIImage imageNamed:#"placeholder.jpg"]; // placeholder image
[cell imageView].file = userImageFile;
[[cell imageView] loadInBackground];
Instead of loading my data directly from parse.com into my tableView I loaded it into an object first. So each object was no longer an PFObject and now a Person object and I stored these in a mutable array which I accessed in my tableView.
Try it:
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear]; //this is necessary for most time
//viewDidAppear be called in main thread, so just call reloadData directly
[self.tableView reloadData];
}
As mentioned in Apple document about - (id)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath:
You must register a class or nib file using the registerNib:forCellReuseIdentifier: or registerClass:forCellReuseIdentifier: method before calling this method.
If you registered a class for the specified identifier and a new cell must be created, this method initializes the cell by calling its initWithStyle:reuseIdentifier: method.
For nib-based cells, this method loads the cell object from the provided nib file. If an existing cell was available for reuse, this method calls the cell’s prepareForReuse method instead.
So, do you forget to use the registerNib:forCellReuseIdentifier: or registerClass:forCellReuseIdentifier: method before calling cellForRowAtIndexPath method?
Here is a discussion about this.
How I am doing this
In my UIViewController.m
#property (nonatomic, strong) NSMutableDictionary *imageDownloadsInProgress;
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
self.imageDownloadsInProgress = [NSMutableDictionary dictionary];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
SRKProduct *productRecord = [stockArray objectAtIndex:indexPath.row];
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
if (!productRecord.image || [productRecord.image isEqualToData:NULL] || productRecord.image.length == 0) {
if (_itemTableView.dragging == NO && _itemTableView.decelerating == NO)
{
[self startIconDownload:productRecord forIndexPath:indexPath];
}
cell.imageView.image = [[UIImage imageNamed:#"Placeholder.png"] makeThumbnailOfSize:CGSizeMake(50,50)];//This is just a placeholder and will be removed when original image is downloaded.
}
return cell;
}
#pragma mark -
- (void)startIconDownload:(SRKProduct *)srkproduct forIndexPath:(NSIndexPath *)indexPath
{
SRKIconDownloader *iconDownloader = [self.imageDownloadsInProgress objectForKey:indexPath];
if (iconDownloader == nil)
{
iconDownloader = [[SRKIconDownloader alloc] init];
iconDownloader.srkproduct = srkproduct;
[iconDownloader setCompletionHandler:^{
UITableViewCell *cell = [_itemTableView cellForRowAtIndexPath:indexPath];
// Display the newly loaded image
cell.imageView.image = [UIImage imageWithData:srkproduct.image];
NSLog(#"Image %d",[productAdapter updateproductImage:srkproduct]);
// Remove the IconDownloader from the in progress list.
// This will result in it being deallocated.
[self.imageDownloadsInProgress removeObjectForKey:indexPath];
}];
[self.imageDownloadsInProgress setObject:iconDownloader forKey:indexPath];
[iconDownloader startDownload];
}
}
Then in SRKIconDownloader.h
#interface SRKIconDownloader : NSObject
#property (nonatomic, strong) SRKProduct *srkproduct;
#property (nonatomic, copy) void (^completionHandler)(void);
And in SRKIconDownloader.m
#implementation SRKIconDownloader
#pragma mark
- (void)startDownload
{
PFQuery *queryCouple = [PFQuery queryWithClassName:#"Product"];
[queryCouple whereKey:#"Name" equalTo:_srkproduct.productName];
[queryCouple findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
if (!error) {
if ([objects count] > 0) {
for (PFObject *object in objects) {
PFFile *image = (PFFile *)[object objectForKey:#"Image"];
[image getDataInBackgroundWithBlock:^(NSData *data, NSError *error){
_srkproduct.image = data;
// call our delegate and tell it that our icon is ready for display
if (self.completionHandler)
self.completionHandler();
}];
break;
}
}
else{
}
}
}];
}
#end

Dispatch Queue and global NSMutableDictionary - Objective C

I'm trying to use a global NSMutableDictionary from a dispatch queue. However, the items keep coming back NULL.
What I'm trying to do is access an external json file with a dispatch_queue, then populate a UITableView with this info.
Here's what I have
vc.h:
#interface viewcontroller {
NSMutableDictionary *jsonArray;
}
vc.m:
#define kBgQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) //1
#define jsonTest [NSURL URLWithString:#"http://www.sometest.com/test.php"]
-(void)viewDidLoad {
dispatch_async(kBgQueue, ^{
NSData* data = [NSData dataWithContentsOfURL:
jsonTest];
[self performSelectorOnMainThread:#selector(fetchedData:)
withObject:data waitUntilDone:YES];
// if I run the log here, I can access jsonArry and the log prints correctly
NSLog(#"City: %#", [jsonArray objectForKey:#"city"];
});
}
-(NSMutableDictionary *)fetchedData:(NSData *)responseData {
NSError *error;
jsonArray = [NSJSONSerialization JSONObjectWithData:responseData options:kNilOptions error:&error];
return jsonArray;
}
/********************* Table formatting area **********************/
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if (tableView == self.ipTable) {
if ([ipArray count] == 0){
return 1;
} else { // meta table
return [ipArray count];
}
} else { // IP Meta Data
return [jsonArray count];
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (tableView == self.myTable) {
NSString *CellIdentifier = NULL;
if ([ipArray count] == 0) {
CellIdentifier = #"No Cells";
} else {
CellIdentifier = #"IP Cell";
}
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
if ([ipArray count] == 0)
{
[cell.textLabel setText:NSLocalizedString(#"None Found", nil)];
return cell;
} else {
IPAddr *theip = [ipArray objectAtIndex: [indexPath row]];
NSString *theipname = [theip ipName];
if ([theipname isEqualToString:#""]) {
[cell.textLabel setText: [theip ipNum]];
[cell.detailTextLabel setText:NSLocalizedString(#"noName", nil)];
} else {
[cell.textLabel setText: [theip ipName]];
[cell.detailTextLabel setText: [theip ipNum]];
}
return cell;
}
} else { // meta table
static NSString *CellIdentifier = #"metaCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
// jsonArray content would go here to fill the cells.
/******************** something here to fill the cells using jsonArray ********************/
return cell;
}
} // END UITAbleViewCell
If I access the jsonArray inside the queue, it returns fine and prints the log for the city.
However, if I try to use it outside the queue, it returns NULL.
I'm trying to figure out what is happening, any ideas?
I need to use jsonArray in different methods in the same view, so I need it to be global.
I am fairly sure that the problem is that the data source methods (numberOfRowsInSection,
cellForRowAtIndexPath) are called before the background thread has finished and
filled the jsonArray. Therefore you have to reload the table view when the background
thread has finished:
- (void)viewDidLoad
{
[super viewDidLoad];
dispatch_async(kBgQueue, ^{
NSData *data = [NSData dataWithContentsOfURL:jsonTest];
NSError *error;
NSArray *tmpArray = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
dispatch_sync(dispatch_get_main_queue(), ^{
// Assign new data to data source and reload the table view:
jsonArray = tmpArray;
[self.metaTableView reloadData];
});
});
}
So the table view would be empty initially, and reloaded later when the data has
arrived.
Try to call the other method(which is using your jsonarray) through nsnotification...I am not sure there might some other ideas/ways of doing this.But i am presenting what i have in my mind.
Put this code inside your fetchedData method,
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self selector:#selector(someMethod:) name:#"JSonDownloaded" object: jsonArray];
[[NSNotificationCenter defaultCenter] postNotificationName:#"JSonDownloaded" object: jsonArray];
-(void)someMethod:(NSNotification *)nspk
{
NSLog(#"%#",nspk.object);
//Only after this you can able to access the jsonArray.
}
Don't forget to unregister the observer.
jsonArray is just an instance variable, but not a property. Thus, assigning an object to it does not retain it, and the object may be released as soon as the program returns to the run loop.
I suggest replacing the iVar by #property (strong) NSMutableDictionary *jsonArray; and #synthesize jsonArray;, and assigning the object to it by self.jsonArray = ...
EDIT (see comment of Martin R below):
Thus, if you are not using ARC, assigning an object to it does not retain it, and the object may be released as soon as the program returns to the run loop.
In this case, I suggest replacing the iVar by #property (retain) NSMutableDictionary *jsonArray; and #synthesize jsonArray;, and assigning the object to it by self.jsonArray = ...