Accurate progress displayed with UIProgressView for ASIHTTPRequest in an ASINetworkQueue - cocoa-touch

Summary: I want to track the progress of file downloads with progress bars inside cells of a tableview.
I'm using ASIHTTPRequest in an ASINetworkQueue to handle the downloads.
It works, but the progress bars stay at 0%, and jump directly at 100% at the end of each download.
Details:
I set up my ASIHTTPRequest requests and ASINetworkQueue this way:
[Only an extract of my code]
- (void) startDownloadOfFiles:(NSArray *) filesArray {
for (FileToDownload *aFile in filesArray) {
ASIHTTPRequest *downloadAFileRequest = [ASIHTTPRequest requestWithURL:aFile.url];
UIProgressView *theProgressView = [[UIProgressView alloc] initWithFrame:CGRectMake(20.0f, 34.0f, 280.0f, 9.0f)];
[downloadAFileRequest setDownloadProgressDelegate:theProgressView];
[downloadAFileRequest setUserInfo:
[NSDictionary dictionaryWithObjectsAndKeys:aFile.fileName, #"fileName",
theProgressView, #"progressView", nil]];
[theProgressView release];
[downloadAFileRequest setDelegate:self];
[downloadAFileRequest setDidFinishSelector:#selector(requestForDownloadOfFileFinished:)];
[downloadAFileRequest setDidFailSelector:#selector(requestForDownloadOfFileFailed:)];
[downloadAFileRequest setShowAccurateProgress:YES];
if (! [self filesToDownloadQueue]) {
// Setting up the queue if needed
[self setFilesToDownloadQueue:[[[ASINetworkQueue alloc] init] autorelease]];
[self filesToDownloadQueue].delegate = self;
[[self filesToDownloadQueue] setMaxConcurrentOperationCount:2];
[[self filesToDownloadQueue] setShouldCancelAllRequestsOnFailure:NO];
[[self filesToDownloadQueue] setShowAccurateProgress:YES];
}
[[self filesToDownloadQueue] addOperation:downloadAFileRequest];
}
[[self filesToDownloadQueue] go];
}
Then, in a UITableViewController, I create cells, and add the name of the file and the UIProgressView using the objects stored in the userInfo dictionary of the request.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"fileDownloadCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
[[NSBundle mainBundle] loadNibNamed:#"FileDownloadTableViewCell" owner:self options:nil];
cell = downloadFileCell;
self.downloadFileCell = nil;
}
NSDictionary *userInfo = [self.fileBeingDownloadedUserInfos objectAtIndex:indexPath.row];
[(UILabel *)[cell viewWithTag:11] setText:[NSString stringWithFormat:#"%d: %#", indexPath.row, [userInfo valueForKey:#"fileName"]]];
// Here, I'm removing the previous progress view, and adding it to the cell
[[cell viewWithTag:12] removeFromSuperview];
UIProgressView *theProgressView = [userInfo valueForKey:#"progressView"];
if (theProgressView) {
theProgressView.tag = 12;
[cell.contentView addSubview:theProgressView];
}
return cell;
}
The progress bar are all added, with the progress set to 0%.
Then, at end of download, they instantly jump to 100%.
Some of the download are very big (more than 40Mb).
I do not do anything tricky with threads.
Reading the forums of the ASIHTTPRequest, it seems I'm not alone, but I couldn't find a solution.
Am I missing something obvious? Is this a bug in ASI* ?

ASIHTTPRequest can only report progress if the server is sending Content-Length: headers, as otherwise it doesn't know how big the response will be. (ASINetworkQueue also sends HEAD requests at the start to try to figure out document sizes.)
Try collecting all the network traffic with charlesproxy or wireshark, see if these headers are present and/or what is happening with the HEAD requests.

Related

Preloading images into the memory cache at application launch, using objective c for iOS

The issue I am having, concerns trying to display thumbnail images in a list of UITableViewCells. I have 200 thumbnails to display.
My app downloads a zip file of images from my remote server, unzips the contents into the NSDocumentDirectory. After an update, which happens once a week, the app then displays the thumbnails using [UIImage imageWithContentsOfFile:]
Once, I know this has been cached, I display the thumbnail using [UIImage imageNamed:]
My problem is that when I display 200 thumbnails using
[UIImage imageWithContentsOfFile:] on the first display event after an update, the app sometimes freezes after a few minutes, saying that too many image files are open.
ImageIO: CGImageRead_mapData 'open' failed '/Users/cdesign/Library/Application Support/iPhone Simulator/6.1/Applications/B458A3F5-5B21-49CD-B4D8-17E5189678FA/Documents/91.png'
error = 24 (Too many open files)
This never happens once the images are cached in memory.
Then, when I try and click on a UITableViewCell to proceed to my 'DetailController', I get the following error:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Could not load NIB in bundle: 'NSBundle (loaded)' with name '9ka-eC-wSa-view-LRp-aI-LGN' and directory 'MainStoryboard.storyboardc''
*** First throw call stack:
(0x1bd0012 0x1473e7e 0x1bcfdeb 0x5d3ef9 0x4987e7 0x498dc8 0x5e728e 0x498ff8 0x499232 0x4994da 0x4b08e5 0x4b09cb 0x4b0c76 0x4b0d71 0x4b189b 0x4b1e93 0x4b1a88 0x80de63 0x7ffb99 0x7ffc14 0x467249 0x4674ed 0xe715b3 0x1b8f376 0x1b8ee06 0x1b76a82 0x1b75f44 0x1b75e1b 0x16ef7e3 0x16ef668 0x3b7ffc 0x2a1d 0x2945)
libc++abi.dylib: terminate called throwing an exception
This is strange, because I do not have a file called 'MainStoryboard.storyboardc'.
My app has no localisation. My project does not even have a 'en.lproj' folder. I only have 'MainStoryboard.storyboard' in the root.
I am not sure if the 2 error are related. Or, if the second error is responsible for the first. This would suggest that there is no issue with my images?
I must say that when I have tested my app, by using a modified 'getImage' method that only returns cached images, the first error disappears but the second error, concerning the 'MainStoryboard.storyboard' does still occur, occasionally, if I leave the app idle for more than a couple of minutes, on the 'ListController' screen.
Both these issues ONLY ever occur on the 'ListController' screen. The app always launches successfully to display the 'home' screen. On the home screen there is a link to the 'ListController' screen.
Assuming, though, that the image display is the problem, is there a way to preload images during application launch to preload the newly updated images from the document folder into the memory cache, so that I never have to use
[UIImage imageWithContentsOfFile:] to display images in the - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath?
If, this is possible, do I use NSCache to achieve the image preload?
And, if I use NSCache, do files with the same name overwrite entries in NSCache, or do I have to delete files, with the same name, first?
Sorry, for preempting, the answer to my initial question, but this may save time?
My app contains a storyboard, called MainStoryboard.storyboard
Target device: iPhone 5
Xcode version: 4.6.3
Here is some relevant code:
AppDelegate.m
- (BOOL)hasImageBeenUpdated:(NSString *)postureid{
BOOL result = NO;
YTAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
if (appDelegate.imagesupdated == nil) {
appDelegate.imagesupdated = [[NSMutableArray alloc] init];
}
NSString *imageid = [NSString string];
for (int i = 0; i < appDelegate.imagesupdated.count; i++) {
imageid = [appDelegate.imagesupdated objectAtIndex:i];
if ([imageid isEqualToString: postureid]) {
result = YES;
}
}
return result;
}
- (UIImage *)getImage:(NSString *)postureid{
self.nsLogsOn = YES;
YTAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
UIImage *result = [[UIImage alloc] init];
UIImageView *imageview = [[UIImageView alloc] init];
NSString *imagePath = [NSString stringWithFormat:#"%#.png",postureid];
if (self.nsLogsOn) {
NSLog(#"appDelegate.imagesHaveBeenDownloaded: %i",appDelegate.imagesHaveBeenDownloaded);
}
if(!appDelegate.imagesHaveBeenDownloaded){
imageview.image = [UIImage imageNamed:imagePath];
if (self.nsLogsOn) {
NSLog(#"image cached no download: %#",imagePath);
}
}
else{
if (appDelegate.imagesupdated == nil) {
appDelegate.imagesupdated = [[NSMutableArray alloc] init];
}
BOOL imageHasBeenUpdated = [self hasImageBeenUpdated:postureid];
if(!imageHasBeenUpdated){
imageview.image = [UIImage imageWithContentsOfFile:[[self documentsFilePath] stringByAppendingPathComponent:imagePath]];
[appDelegate.imagesupdated addObject:postureid];
if (self.nsLogsOn) {
NSLog(#"image download: %#",imagePath);
}
}
else{
imageview.image = [UIImage imageNamed:imagePath];
if (self.nsLogsOn) {
NSLog(#"image cached download: %#",imagePath);
}
}
}
if(imageview.image==nil){
imageview.image = [UIImage imageWithContentsOfFile:[[self documentsFilePath] stringByAppendingPathComponent:imagePath]];
if (self.nsLogsOn) {
NSLog(#"image cache cleared by iOS: %#",imagePath);
}
}
if(imageview.image==nil){
imageview.image = [UIImage imageNamed:#"logo-lotus-imageview-white-c.png"];
if (self.nsLogsOn) {
NSLog(#"image cannot be found: %#",imagePath);
}
}
result = imageview.image;
return result;
}
ListController.m
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return [keys count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSString *key = [keys objectAtIndex:section];
NSArray *nameSection = [posturegroups objectForKey:key];
return [nameSection count];
}
- (NSString *)tableView:(UITableView *)tableView
titleForHeaderInSection:(NSInteger)section {
NSString *key = [keys objectAtIndex:section];
return [key capitalizedString];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *identifier = #"plainCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
NSString *key = [keys objectAtIndex:indexPath.section];
NSArray *nameSection = [posturegroups objectForKey:key];
YTPosture *thePosture = [nameSection objectAtIndex:indexPath.row];
YTAppDelegate *appDelegate = [[YTAppDelegate alloc] init];
UIImageView *imageview = (UIImageView *)[cell viewWithTag:3];
imageview.image = [appDelegate getImage:thePosture.postureid];
imageview.backgroundColor = [appDelegate defaultColor];
UILabel *cellLabel1 = (UILabel *)[cell viewWithTag:1];
NSString *aStr = [NSString string];
NSString *bStr = [NSString string];
NSString *title = [NSString string];
if([selection valueForKey:#"symptomExists"]){
aStr = [NSString stringWithFormat:#"%# ",thePosture.memberOrder];
bStr = thePosture.title;
title = [aStr stringByAppendingString:bStr];
}
else{
title = thePosture.title;
}
cellLabel1.text = title;
UILabel *cellLabel2 = (UILabel *)[cell viewWithTag:2];
cellLabel2.text = thePosture.sanskritTransliteration;
YTKeyController *keyController = [[YTKeyController alloc] init];
NSMutableArray *branch = [keyController compileData];
NSString *branchid = thePosture.branchid;
NSString *list = #"1.0,1.0,1.0";
NSArray *listItems = [list componentsSeparatedByString:#","];
for (int i = 0; i < [branch count]; i++) {
YTBranch *theBranch = [branch objectAtIndex:i];
if ([branchid isEqualToString:theBranch.branchid]) {
list = theBranch.key;
listItems = [list componentsSeparatedByString:#","];
break;
}
}
[cell setBackgroundColor:[UIColor colorWithRed:[[listItems objectAtIndex:0] floatValue] green:[[listItems objectAtIndex:1] floatValue] blue:[[listItems objectAtIndex:2] floatValue] alpha:1]];
return cell;
}
#pragma mark - Table view delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// Navigation logic may go here. Create and push another view controller.
/*
DetailViewController *detailViewController = [[DetailViewController alloc] initWithNibName:#"Nib name" bundle:nil];
// ...
// Pass the selected object to the new view controller.
[self.navigationController pushViewController:detailViewController animated:YES];
*/
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
UIViewController *destination = segue.destinationViewController;
if ([destination respondsToSelector:#selector(setDelegate:)]) {
[destination setValue:self forKey:#"delegate"];
}
if ([destination respondsToSelector:#selector(setSelection:)]) {
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
NSUInteger section = [indexPath section];
NSString *key = [keys objectAtIndex:section];
NSArray *nameSection = [posturegroups objectForKey:key];
YTPosture *thePosture = [nameSection objectAtIndex:indexPath.row];
id object = thePosture;
NSDictionary *aselection = [NSDictionary dictionaryWithObjectsAndKeys:
indexPath, #"indexPath",
object, #"object",
nil];
[destination setValue:aselection forKey:#"selection"];
}
}
I have found what the problem was. I feel a little embarrassed!
The first point is that 'MainStoryboard.storyboardc' is meant to be present. The 'c' on the end stands for the compiled version of 'MainStoryboard.storyboard'.
Well, the image issue, was to do with the fact that I was trying to call a method that amongst other things, was compiling an SQLite statement from inside the method below:
(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
Plus I was also calling another method that was looping through about 200 items from inside the same method.
Effectively, these 2 routines were being run for every UITableViewCell that was scrolling into view.
These two issues were crash and burning the system.
Once I removed these issues I decided to call the images using [UIImage imageWithContentsOfFile], without any problems. In fact the 'ListController' now has about 200 thumbnails in it, and it scrolls as smooth as silk.
I feel like a total idiot for wasting your time. Most definitely, I have learnt a big lesson from this. I need to debug & check more thoroughly, and think about every line of code and the context it is written in. In fact, I have learnt that the only information loaded into the method above, should be data that is already indexed.
Champoul's suggestion to use Instruments in Memory Leaks mode, was how I found out the source of this issue. Thanks very much for your help...

Update UITableView after background work done

I'm new to iOS developing and have an silly problem
I have an project to parse RSS feed and it all works fine but I tried to fetch images in the background to improve the UI and stuck on how to update the UIImageView in my TableView cells?
Here the code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"CustomCell";
MyTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
cell.titleLabel.text = [[feeds objectAtIndex:indexPath.row] objectForKey:#"title"];
NSString* test = [[feeds objectAtIndex:indexPath.row] objectForKey:#"url"];
NSInvocationOperation *operation = [[NSInvocationOperation alloc]
initWithTarget:self
selector:#selector(doNow:)
object:test];
[queue addOperation:operation];
cell.myImages = theImagePropery;
return cell;
}
-(void)doNow:(NSString*)myData
{
NSString* url = myData;
url = [url stringByReplacingOccurrencesOfString:#" " withString:#""];
NSURL* imageURL = [NSURL URLWithString:url];
NSData* imageData = [NSData dataWithContentsOfURL:imageURL];
UIImage* image = [[UIImage alloc]initWithData:imageData];
[self performSelectorOnMainThread:#selector(displayImage:) withObject:image waitUntilDone:NO];
}
Now in displayImage method I don't know what to do and thats my last try:
-(void)displayImage:(UIImage *)myImage{
[[self tableView] beginUpdates];
theImageProbery = [[UIImageView alloc] initWithImage:myImage];
[[self tableView] endUpdates];
What can I do?
Take a look at the LazyTableImages sample.
It's dated, and you can replace the IconDownloader class with NSURLSesssionDataTask, but it will give you the basic idea of how to structure your code.
Do not use the synchronous method dataWithContentsOfURL to request network-based URLs.This method can block the current thread for tens of seconds on a slow network, resulting in a poor user experience. Use the NSURLSession methods ( if your are targeting iOS 7 and later).
- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler
or use the NSURLConnection method (is you are targeting iOS 6 and later).
+ (void)sendAsynchronousRequest:(NSURLRequest *)request queue:(NSOperationQueue *)queue completionHandler:(void (^)(NSURLResponse*, NSData*, NSError*))handler
PS : The best is to use open source librairies that handle all this for you ( downloading the images in a background thread, caching,..).
AFNetworking+UIImageView

Objective C - UITableViewCell loading image asynchronously

I am displaying a table. Each row has an image icon, loaded from an URL.
Since downloading images synchronously blocks the UI, I've implemented am asynchronous way via grand central dispatch.
My problem is that when I scroll down and up, since cells are being re-used, the incorrect images show up.
I can guess why this is happening - it's because the re-used cells update the image and therefore, previous cells will now have the newly downloaded, and wrong, image. What would be an ideal way to resolve this?
Here's my code.
For each image downloaded, I'm storing it in a singleton class called "ImageStore".
// set the data for each cell - reusing the cell
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"UITableViewCell"];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:#"UITableViewCell"];
}
// setting the image for each cell
// first check if there is UIImage in the ImageStore already
NSString *imageUrl = [obj objectForKey:#"image"];
if (imageUrl) {
if ([[ImageStore sharedStore] imageForKey:imageUrl]) {
[[[tableView cellForRowAtIndexPath:indexPath] imageView] setImage:[[ImageStore sharedStore] imageForKey:imageUrl]];
} else {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:
[NSURL URLWithString:[obj objectForKey:#"image"]]]];
dispatch_sync(dispatch_get_main_queue(), ^{
[[ImageStore sharedStore]setImage:image forKey:imageUrl];
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]withRowAnimation:UITableViewRowAnimationNone];
[[[tableView cellForRowAtIndexPath:indexPath] imageView] setImage:image];
});
});
}
}
return cell;
}
Try this:
NSString *imageUrl = [obj objectForKey:#"image"];
if (imageUrl) {
if ([[ImageStore sharedStore] imageForKey:imageUrl]) {
[[cell imageView] setImage:[[ImageStore sharedStore] imageForKey:imageUrl]];
} else {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:
[NSURL URLWithString:[obj objectForKey:#"image"]]]];
dispatch_sync(dispatch_get_main_queue(), ^{
[[cell imageView] setImage:image];
[cell setNeedsLayout];
});
});
}
EDIT : Check this grand-central-dispatch-for-ios-lazy-loading for tableview lazy loading
A little bit of modification from Prince Answer
NSString *imageUrl = [obj objectForKey:#"image"];
if (imageUrl)
{
if ([[ImageStore sharedStore] imageForKey:imageUrl])
{
//This condition means the current cell's image has been already downloaded and stored. So set the image to imageview
[[cell imageView] setImage:[[ImageStore sharedStore] imageForKey:imageUrl]];
}
else
{
//While reusing this imageView will have previous image that will be visible till the image is downloaded. So i am setting this image as nil.
[[cell imageView] setImage:nil];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
//Called Immediately.
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:
[NSURL URLWithString:[obj objectForKey:#"image"]]]];
dispatch_sync(dispatch_get_main_queue(), ^{
//Called when the image is downloaded
//Store in any external object. So that next time reuse this will not be downloaded
[[ImageStore sharedStore]setImage:image forKey:imageUrl];
//Also set the image to the cell
[[cell imageView] setImage:image];
[cell setNeedsLayout];
});
});
}
}
Take a look at AFNetworking... They make dealing with network callbacks EASY!!! I recommend AFNetworking over ASIHTTPRequest because AFNetworking is keeping updated and ASIHTTPRequest is not... they kinda just stopped developing.
Here is an example of how to use AFNetworking to download images asynchronously:
NSDictionary * object = [self.array objectAtIndex:indexPath.row];
NSDictionary * image = [object valueForKey:#"image"];
NSString *imageUrl = [image valueForKeyPath:#"url"];
NSURL *url = [NSURL URLWithString:imageUrl];
[cell.imageView setImageWithURL:url placeholderImage:[UIImage imageNamed:#"empty-profile-150x150.png"]];
The setImageWithURL: placeholderImage: method is what I used to do this... Rather than multiple methods and lines of code, I accomplish everything with this line.
This entire line of code does exactly what you want to do. There is really no need to recreate the wheel :). It does help though going through the lower level of programming to really understand the implementation of what is REALLY going on under the hood.
View the link to download the library and view more examples on how to use it... Seriously, it makes your life alot easier not having to worry THAT MUCH about threads and GCD.
AFNetworking Demo
I haven't dived into the code of AFNetworking but my application runs like BUTTER when loading the images into the cells. It looks great :)
Oh and here the docs for AFNetworking: AFNetworking Documentation
I've noticed this behavior, too.
The only general solution I could find is to disable cell reuse.

Data doesn't load in UITableView until I scroll

I am trying to load parsed data in cells, but the problem is that it is happening synchronously and UitableView doesn't show until the data has finished loading. I tried to solve the problem by using performSelectorInBackground, but now data isn't loaded in the cells until I start scrolling. Here is my code:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[self performSelectorInBackground:#selector(fethchData) withObject:nil];
}
- (void)viewDidUnload
{
[super viewDidUnload];
// Release any retained subviews of the main view.
self.listData = nil;
self.plot=nil;
}
-(void) fethchData
{
NSError *error = nil;
NSURL *url=[[NSURL alloc] initWithString:#"http://www.website.com/"];
NSString *strin=[[NSString alloc] initWithContentsOfURL:url encoding:NSUTF8StringEncoding error:nil];
HTMLParser *parser = [[HTMLParser alloc] initWithString:strin error:&error];
if (error) {
NSLog(#"Error: %#", error);
return;
}
listData =[[NSMutableArray alloc] init];
plot=[[NSMutableArray alloc] init];
HTMLNode *bodyNode = [parser body];
NSArray *contentNodes = [bodyNode findChildTags:#"p"];
for (HTMLNode *inputNode in contentNodes) {
[plot addObject:[[inputNode allContents] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]];
}
NSArray *divNodes = [bodyNode findChildTags:#"h2"];
for (HTMLNode *inputNode in divNodes) {
[listData addObject:[[inputNode allContents] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]];
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
//here you check for PreCreated cell.
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
//Fill the cells...
cell.textLabel.text = [listData objectAtIndex:indexPath.row];
cell.textLabel.font = [UIFont boldSystemFontOfSize:14];
cell.textLabel.numberOfLines=6;
cell.textLabel.textColor=[UIColor colorWithHue:0.7 saturation:1 brightness:0.4 alpha:1];
cell.detailTextLabel.text=[plot objectAtIndex:indexPath.row];
cell.detailTextLabel.font=[UIFont systemFontOfSize:11];
cell.detailTextLabel.numberOfLines=6;
return cell;
}
Put this somewhere after the data is loaded successfully:
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
This fix the problem of calling a GUI update while you're not in the main thread.
This code uses the GCD Technology from Apple to force the reload data function to run on main thread. Read more about
Concurrency Programming Guide for more understanding (it's quite large field so that it's hard to explain in the comment)
Anyway, it's not very recommended if you don't understand it well because it causes the program to crash some rare cases.
For swift 3:
DispatchQueue.main.async(execute: { () -> Void in
self.tableView.reloadData()
})
For swift 2:
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
All you really need to do is any time you have an update to your back-end data, call
[tableView reloadData];
Since this is happening synchronously, you should probably have a function like
-(void) updateTable
{
[tableView reloadData];
}
and after adding the data in your download call
[self performSelectorOnMainThread:#selector(updateTable) withObject:nil waitUntilDone:NO];
U can use [cell setNeedsDisplay];
for example:
dispatch_async(dispatch_get_main_queue(), ^{
[cell setNeedsDisplay];
[cell.contentView addSubview:yourView];
});
I had this problem and I was dealing with it all the day.
I am using static cells and reloadData is causing the wrong loading, it displays only the visible cells and remove the others.
What I noticed is that when I scrolled down (y in negative value) the cells where loaded correctly, so I wrote this code and it worked, even though I don't like to let it in this way.
Shoot if you find any better solution.
-(void)reloadTableView{
CGPoint point = self.tableSettings.tableView.contentOffset;
[self.tableSettings.tableView reloadData];
[UIView animateWithDuration:.001f animations:^{
[self.tableSettings.tableView setContentOffset:CGPointMake(point.x, -10)];
} completion:^(BOOL finished) {
[UIView animateWithDuration:.001f animations:^{
[self.tableSettings.tableView setContentOffset:point];
} completion:^(BOOL finished) {
}];
}];
}
I was having the exact same problem! I wanted the UITableView to be fully populated before the view controller appeared. Envil's post gave me the information I needed, but my solution ended up being different.
Here's what I did (remodeled to fit the context of the question asker).
- (void)viewDidLoad {
[super viewDidLoad];
[self performSelectorInBackground:#selector(fethchData) withObject:nil];
}
- (void)viewWillAppear {
[tableView reloadData];
}
First, this semi-solved my related problem. I want to round corners of an image in a table cell. Dispatching asynchronously fixed the problem for some but not all of the images. Any ideas?
Second, I think you are supposed to avoid creating a strong reference cycle by using a closure capture list like this:
DispatchQueue.main.async(execute: { [weak weakSelf = self] () -> Void in
weakSelf!.tableView.reloadData()
})
See: https://developer.apple.com/library/prerelease/content/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html#//apple_ref/doc/uid/TP40014097-CH20-ID52
I experienced the same issue when using self-sizing cells, and I found that setting the estimatedHeight to 50 would fix the problem.
Set estimatedHeight on the tableView itself or return an estimate from estimatedHeightForRowAt in your UITableViewDelegate.
It seems to work as long as the estimate is more than 0.

iOS Refresh button in View Controller Nav: reloading all tableViewCells created from parsed JSON when clicked

I've got a fairly important conceptual issue that many people have asked about, but there isn't a readily available clear answer to be found by searching.
My application is simple: Several rows of TableViewCells populated with data from a parsed JSON feed. When a cell is clicked on, that cell's info is passed to a SecondViewController and displayed. The JSON feed is also stored to a .plist and in the case that the internet is not available, the TableViewCells are populated from the .plist.
This is all working great.
However, the last thing I need is a refresh button at the top of my FirstViewController to refresh the JSON feed, and all of the cells in the table with the new data from the new variables. However, I've encountered an issue with implementing this:
My original JSON call, and variables to populate the cells are located in the ViewDidLoad method. When the view loads, these variables are "set" and don't refresh. Further, I can move the JSON call and variables into viewWillLoad - which will refresh the table each time after clicking on a cell, and then clicking "back" to the firstViewController -- this will update the JSON and cells successfully, however it does impact the speed and makes the view controller "pause" when going back to the MainViewController, which makes calling my original JSON and setting my variables in viewWillLoad an unviable option.
I have created a reload button in ViewDidLoad, which is linked to an IBAction method "refresh":
Create Button Programitically in ViewDidLoad:
// Reload issues button
UIBarButtonItem *button = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh
target:self
action:#selector(refresh:)];
self.navigationItem.rightBarButtonItem = button;
[button release];
Action Method it's linked to:
- (IBAction)refresh:(id)sender {
myRawJson = [[NSString alloc] initWithContentsOfURL:[NSURL
URLWithString:#"http://www.yoursite.com/json.JSON"]
encoding:NSUTF8StringEncoding
error:nil];
SBJsonParser *parser = [[SBJsonParser alloc] init];
NSDictionary * myParsedJson = [parser objectWithString:myRawJson error:NULL];
// New updated dictionary built from refreshed JSON
allLetterContents = [myParsedJson objectForKey:#"nodes"];
// Log the new refreshed JSON
NSLog(#"You clicked refresh. Your new JSON is %#", myRawJson);
//Maybe use the notification center?? But don't know how to implement.
//[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(refreshView:)
name:#"refreshView" object:nil];
//[[NSNotificationCenter defaultCenter] postNotificationName:#"refreshView"
object:nil];
}
[self.tableView reloadRowsAtIndexPaths:[self.tableView indexPathsForVisibleRows]
withRowAnimation:UITableViewRowAnimationNone];
[myRawJson release];
}
In the code above you can see that I'm re-calling the JSON each time the button is clicked and logging a message to console with the new JSON. This is working. I've even re-built a dictionary which is successfully adding the new content.
My question is: How can I make the tableViewCells "refresh" with this new data as well? Can I just make the button re-load the entire view controller - so it would call ViewDidLoad again? Do I need to re-think my apps structure, or move my original variables out of viewDidLoad?
I've been reading some posts on the NSNotificationCenter, but the implementation of this still baffles me, as I'm fairly new to iOS development.
Thanks~
Update:
It's still not updating. Here is my full refresh button code with [self.tableView reloadData]; called at the end of my IBAction.
- (IBAction)refresh:(id)sender {
[DSBezelActivityView newActivityViewForView:
self.navigationController.navigationBar.superview
withLabel:#"Loading Feed..." width:160];
myRawJson = [[NSString alloc] initWithContentsOfURL:[NSURL
URLWithString:#"http://site.com/mobile.JSON"]
encoding:NSUTF8StringEncoding
error:nil];
SBJsonParser *parser = [[SBJsonParser alloc] init];
NSDictionary * myParsedJson = [parser objectWithString:myRawJson error:NULL];
allLetterContents = [myParsedJson objectForKey:#"nodes"];
BOOL isEmpty = ([myParsedJson count] == 0);
if (isEmpty) {
NSString *refreshErrorMessage = [NSString
stringWithFormat:#"An internet or network connection is required."];
UIAlertView *alert = [[UIAlertView alloc]
initWithTitle:#"Alert"
message: refreshErrorMessage
delegate:self
cancelButtonTitle:#"Close"
otherButtonTitles:nil];
[alert show];
[alert release];
allLetterContents = [NSMutableDictionary
dictionaryWithContentsOfFile:[self saveFilePath]];
//NSLog(#"allLetterContents from file: %#", allLetterContents);
} else {
NSLog(#"Your new allLetterContents is %#", allLetterContents);
// Fast enumeration through the allLetterContents NSMutableDictionary
for (NSMutableDictionary * key in allLetterContents) {
NSDictionary *node = [key objectForKey:#"node"];
NSMutableString *contentTitle = [node objectForKey:#"title"];
NSMutableString *contentNid = [node objectForKey:#"nid"];
NSMutableString *contentBody = [node objectForKey:#"body"];
// Add each Title and Nid to specific arrays
//[self.contentTitleArray addObject:contentTitle];
[self.contentTitleArray addObject:[[contentTitle
stringByReplacingOccurrencesOfString:#"&"
withString:#"&"] mutableCopy]];
[self.contentNidArray addObject:contentNid];
[self.contentBodyArray addObject:contentBody];
}
}
[self.tableView reloadData];
[DSBezelActivityView removeViewAnimated:YES];
[myRawJson release];
}
I'm configuring the cell at cellForRowAtIndexPath (Updated: Posted entire method):
- (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];
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;
}
}
// Configure the cell.
cell.textLabel.text = [self.contentTitleArray objectAtIndex: [indexPath row]];
cell.detailTextLabel.text = [self.contentNidArray objectAtIndex: [indexPath row]];
return cell;
}
Setting it on didSelectRowAtIndexPath:
self.detailViewController.currentNodeTitle = [contentTitleArray
objectAtIndex:indexPath.row];
self.detailViewController.currentNodeNid= [contentNidArray
objectAtIndex:indexPath.row];
self.detailViewController.currentNodeBody = [contentBodyArray
objectAtIndex:indexPath.row];
So when clicking my refresh button the table should* refresh with the new json, but does not.. Am I missing a step?
Additionally this may not be important, but I'm changing the colors for every other row with:
// Customize the appearance of table view cells.
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row % 2)
{
[cell setBackgroundColor:[UIColor colorWithRed:221.0/255.0 green:238.0/255.0 blue:255.0/255.0 alpha:1]];
cell.textLabel.textColor = [UIColor colorWithRed:2.0/255.0 green:41.0/255.0 blue:117.0/255.0 alpha:1];
cell.detailTextLabel.textColor = [UIColor colorWithRed:2.0/255.0 green:41.0/255.0 blue:117.0/255.0 alpha:1];
} else [cell setBackgroundColor:[UIColor clearColor]];
}
Update
You need to call the reload method.
[self.tableView reloadData];
This will fire the dataSource and delegate events an will refresh the UITableView.
You can find more info in the UITableView Class Reference:
Call this method to reload all the data that is used to construct the table, including cells, section headers and footers, index arrays, and so on. For efficiency, the table view redisplays only those rows that are visible.