Sending NSOperationQueue to UITableView as a DataSource - ios

i have written code to downloading data from server using NSOperationQueue and NSOperation. and now i want to show progress on UserInterface. i used UITableView and used NSOpeartionQueue as a datasource in tableview delegate
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [[[Downloadmanager sharedInstance] downloadOperationQueue] count];
}
and bind NSOperation`s properties to UITableViewCell.
1) Is this a fisible solution to sending NSOperationQueue as a datasource to tableview delegate ?
2) How to implement notification to reload tableview when NSOperation's state changes?
Thanks.

I don't think it's the proper way of showing progress using NSOperationQueue as a datasource to tableview. You can use networking library like AFNetworking for downloading data and use setDownloadProgressBlock: method for showing progress. Refer this link for the code download progress.
It's easy to reload tableview when the download completes, just call [tableView reloadData] in completionblock.
Here is the code which shows image downloading using AFNetworking which you can easily change for data download.(refer this gist)
- (void)downloadMultiAFN {
// Basic Activity Indicator to indicate download
UIActivityIndicatorView *loading = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
[loading startAnimating];
[self.imageView.superview addSubview:loading];
loading.center = self.imageView.center;
// Create a request from the url, make an AFImageRequestOperation initialized with that request
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:self.picUrl]];
AFImageRequestOperation *op = [[AFImageRequestOperation alloc] initWithRequest:request];
// Set a download progress block for the operation
[op setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
if ([op.request.URL.absoluteString isEqualToString:#"http://www.pleiade.org/images/hubble-m45_large.jpg"]) {
self.progressBar.progress = (float) totalBytesRead/totalBytesExpectedToRead;
} else self.progressBar2.progress = (float) totalBytesRead/totalBytesExpectedToRead;
}];
// Set a completion block for the operation
[op setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
self.imageView.image = responseObject;
self.image = responseObject;
if ([op.request.URL.absoluteString isEqualToString:#"http://www.pleiade.org/images/hubble-m45_large.jpg"]) {
self.progressBar.progress = 0;
} else self.progressBar2.progress = 0;
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {}];
// Start the image download operation
[op start];
// Remove the activity indicator
[loading stopAnimating];
[loading removeFromSuperview];
}

That is an interesting idea, but I don't think it's a good practice make such a "high coupling" - linking model so tightly to the view.
I'd approach it as - download the data on the background thread as you already do - with NSOperationQueue but save it to some kind of an object; say NSMutableArray that serves as the data source for the table view.
Every time a single operation ends (use completion handlers or KVO to get informed) - update the table view. The update can be done two ways - reloading or updating. I'll leave the choice up to you - you can read further discussion about that in this question.

Related

Download huge number of images in the most efficient and fast way

In my app, I am needing to download about 6,000 images. I realize this is a lot, but it is needed.
Currently I am using doing the following:
NSArray *photos = #[hugeAmountOfPhotoObjects];
for (ZSSPhoto *photo in photos) {
[self downloadImageWithURL:photo.mobileURL progress:^(double progress) {
} completion:^(UIImage *image) {
// Save the image
} failure:^(NSError *error) {
}];
}
...
- (void)downloadImageWithURL:(NSURL *)url progress:(void (^)(double progress))progress completion:(void (^)(UIImage *image))completion failure:(void (^)(NSError *error))failure {
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setTimeoutInterval:600];
self.operationQueue.maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount;
AFHTTPRequestOperation *requestOperation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
requestOperation.responseSerializer = [AFImageResponseSerializer serializer];
[requestOperation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
completion(responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[self processOperation:operation error:error failure:failure];
}];
[requestOperation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
double percentDone = (double)totalBytesRead / (double)totalBytesExpectedToRead;
progress(percentDone);
}];
[self.operationQueue addOperation:requestOperation];
}
The problem here is that it takes forever to download using this method, and some of my users are reporting crashing because of high memory usage.
Is there a better method that I could be using to download such a large number of image files?
You could try this somewhat recursively
NSMutableArray *undownloaded;
- (void) startDownload {
undownloaded = [photos mutableCopy]; //get a list of the undownloaded images
for(int i = 0; i < 3;i++) //download 3 at a time
[self downloadImage];
}
- (void) downloadImage {
if(undownloaded.count > 0){
ZSSPhoto *photo = undownloaded.firstObject;
[undownloaded removeObjectAtIndex:0];
[self downloadImageWithURL:photo.mobileURL progress:^(double progress) {
} completion:^(UIImage *image) {
// Save the image
[self downloadImage];
} failure:^(NSError *error) {
[self downloadImage];
//[undownloaded addObject:photo]; //insert photo back into the array maybe to retry? warning, could cause infinite loop without some extra logic, maybe the object can keep a fail count itself
}];
}
}
warning: untested code, may need some tweaking
The speed problem can be solved (the speed will increase, but it might still be slow) with multithreading, downloading all the images at the same time instead of one per time. However, the memory problem is a bit more complicated.
ARC will release all the images after everything is finished, but right before that you gonna have 6,000 images in the device memory. You could optimize the images, reduce their resolution or download them in steps, like Google Images do (you download the images that will be visible at first, then when the user scrolls down you load the images in the new visible area; downloading the images only when they are needed).
Considering that you are downloading enough images to give a memory problem, you will probably take a lots of space in your user's device if you download all of them, and the 'steps' solution may solve that as well.
Now, let's suppose you must download all of them at the same time and space isn't a problem: I suppose that if you put the downloadImageWithURL:progress: method inside a concurrent queue, the images are gonna be freed from memory just after saving (it's just a supposition). Add this to your code:
dispatch_queue_t defaultPriorityQueueWithName(const char* name)
{
dispatch_queue_t dispatchQueue = dispatch_queue_create(name, DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t priorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_set_target_queue(dispatchQueue, priorityQueue);
return dispatchQueue;
}
And change your code to that:
dispatch_queue_t threadItemLoadImage = defaultPriorityQueueWithName("DownloadingImages");
NSArray *photos = #[hugeAmountOfPhotoObjects];
for (ZSSPhoto *photo in photos)
{
dispatch_async(threadItemLoadImage, ^
{
[self downloadImageWithURL:photo.mobileURL progress:^(double progress) {
} completion:^(UIImage *image) {
// Save the image
} failure:^(NSError *error) {
}];
});
}
You will need to remove setDownloadProgressBlock: in case it updates some view, since they will be downloaded simultaneously. Also, a warning: totalBytesExpectedToRead not always will be correctly retrieved at first, containing 0, which might make your app crash for dividing by zero as well in some rare occasions. In future cases, when you need to use setDownloadProgressBlock:, check totalBytesExpectedToRead value before doing that division.

UI still delayed even using GCD

In my modal UI there is a "DONE" button linked with IBAction -done:, it will upload a text to (lets say Dropbox server). Its code looks like this
- (IBAction)done:(id)sender {
// must contain text in textview
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
if (![_textView.text isEqualToString:#""]) {
// check to see if we are adding a new note
if (!self.note) {
DBFile *newNote = [[DBFile alloc] init];
newNote.root = #"dropbox";
self.note = newNote;
}
_note.contents = _textView.text;
_note.path = _filename.text;
// - UPLOAD FILE TO DROPBOX - //
NSLog(#"Initializing URL...");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
NSURL *url = [Dropbox uploadURLForPath:self.note.path];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setHTTPMethod:#"PUT"];
NSData *noteContents = [self.note.contents dataUsingEncoding:NSUTF8StringEncoding];
NSLog(#"Creating session task...");
NSURLSessionUploadTask *uploadTask = [self.session uploadTaskWithRequest:request
fromData:noteContents
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSHTTPURLResponse *resp = (NSHTTPURLResponse *) response;
if (!error && resp.statusCode == 200) {
NSLog(#"OK");
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate noteDetailsViewControllerDoneWithDetails:self];
});
} else {
NSLog(#"Status code: %d", resp.statusCode);
}
}];
[uploadTask resume];
});
} else {
UIAlertView *noTextAlert = [[UIAlertView alloc] initWithTitle:#"No text"
message:#"Need to enter text"
delegate:nil
cancelButtonTitle:#"Ok"
otherButtonTitles:nil];
[noTextAlert show];
}
}
The delegate method noteDetailsViewControllerDoneWithDetails: of this class is look like this
-(void)noteDetailsViewControllerDoneWithDetails:(NoteDetailsViewController *)controller{
// refresh to get latest
[self dismissViewControllerAnimated:YES completion:nil];
[self notesOnDropbox];}
(notesOnDropbox is a time-consuming task). When DONE button is tapped, I expect this modal VC/UI to dismiss immediately and it fetches data on background (by notesOnDropbox method). However, when I try tapping DONE button, my UI stop responding for about seconds, after that the modal UI is dismissed. I cannot figure out where I misuse the GCD. Please help me.
if you want to dismiss your modal VC/UI immediately, just ask the delegate to dismiss,
like is:
- (IBAction)done:(id)sender {
[self.delegate noteDetailsViewControllerDoneWithDetails:self];
// ...
}
In your sample code,
you do the dismiss action after the upload task completed, but the upload task is asynchronous.
and you ask the delegate to dismiss use GCD dispatch_async, this is asynchronous task, too.
After all, you have to consider the what time to do upload, who to do upload task and what time to invoke notesOnDropbox.
First, if notesOnDropbox is a time-consuming task, then you should not be performing it on the main thread (as you are doing). If it is sufficiently time-consuming and you do it on the main thread, the WatchDog process will kill your app dead right before the user's eyes.
Second, there is no need to get off the main thread to do an upload. If you use NSURLSession correctly, it will be asynchronous.
Your code only calls noteDetailsViewControllerDoneWithDetails when the whole upload task is completed, because that's how you wrote your code. Actually, the situation seems worse. If the upload task has any kinds of problems, noteDetailsViewControllerDoneWithDetails will never be called.
You need to call noteDetailsViewControllerDoneWithDetails as soon as possible, and then think about what you are going to do when the upload fails - which might easily happen a long time later.

How to track progress of multiple simultaneous downloads with AFNetworking?

I am using AFNetworking to download files that my app uses for a sync solution. At certain times, the app downloads a series of files as a batch unit. Following this example, I run the batch like this:
NSURL *baseURL = <NSURL with the base of my server>;
AFHTTPRequestOperationManager *manager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:baseURL];
// as per: https://stackoverflow.com/a/19883392/353137
dispatch_group_t group = dispatch_group_create();
for (NSDictionary *changeSet in changeSets) {
dispatch_group_enter(group);
AFHTTPRequestOperation *operation =
[manager
POST:#"download"
parameters: <my download parameters>
success:^(AFHTTPRequestOperation *operation, id responseObject) {
// handle download success...
// ...
dispatch_group_leave(group);
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// handle failure...
// ...
dispatch_group_leave(group);
}];
[operation start];
}
// Here we wait for all the requests to finish
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// run code when all files are downloaded
});
This works well for the batch downloads. However, I want to display to the user an MBProgressHUD which shows them how the downloads are coming along.
AFNetworking provides a callback method
[operation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
}];
... which lets you update a progress meter pretty easily, simply by setting the progress to totalBytesRead / totalBytesExpectedToRead. But when you have multiple downloads going simultaneously that is hard to keep track of on a total basis.
I have considered having an NSMutableDictionary with a key for each HTTP operation, with this general format:
NSMutableArray *downloadProgress = [NSMutableArray arrayWithArray:#{
#"DownloadID1" : #{ #"totalBytesRead" : #0, #"totalBytesExpected" : #100000},
#"DownloadID2" : #{ #"totalBytesRead" : #0, #"totalBytesExpected" : #200000}
}];
As each operation's download progresses, I can update the totalBytesRead for that specific operation in the central NSMutableDictionary -- and then total up all the totalBytesRead and totalBytesExpected' to come up with the total for the whole batched operation. However, AFNetworking's progress callback methoddownloadProgressBlock, defined as^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead){}does not include the specific operation as a callback block variable (as opposed to thesuccessandfailure` callbacks, which do contain the specific operation as a variable, making it accessible). Which makes it impossible, as far as I can tell, to determine which operation specifically is making the callback.
Any suggestions on how to track the progress of multipole simultaneous downloads using AFNetworking?
If your block is inlined, you can access the operation directly but the compiler might warn you of the circular referencing. You can work around by declaring a weak reference and use it inside the block:
__weak AFHTTPRequestOperation weakOp = operation;
[operation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
NSURL* url = weakOp.request.URL; // sender operation's URL
}];
Actually, you can access anything inside the block, but you need to understand block to go for that. In general, any variable referred in the block is copied at the time the block created i.e. the time that the line got executed. It means my weakOp in the block will refer to the value of the weakOp variable when the setDownloadProgressBlock line got executed. You can think it like what would each variable you refer in the block would be if your block got executed immediately.
Blocks are made just to makes these things easyer ;-)
HERE you can find an example project. Simply push the + button and insert the direct URL for a file to download. There is no error checking and no URL redirection so insert only direct URLs.
For the relevant part look in these methods of the DownloadViewController:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
Here the explanation:
When you pass a variable to a block, the block makes a copy of the variables passed from outside.
Because our variable is simply an object pointers (a memory address), the pointer is copied inside the block, and since the default storage is __strong the reference is maintained until the block is destroyed.
It means that you can pass to the block a direct reference to your progress view (I use an UIProgressView since I've never used MBProgressHUD):
UIProgressView *progressView = // create a reference to the view for this specific operation
[operation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
progressView.progress = // do calculation to update the view. If the callback is on a background thread, remember to add a dispatch on the main queue
}];
Doing this, every operation will have a reference to its own progressView. The reference is conserved and the progress view updated until the progress block exists.
You must be downloading some zip file , video file etc.
Make a model of that file to download containing fields like
(id, url, image , type , etc...)
Create a NSOperationQueue
and set maxConcurrentOperationCount according to your requirement
make public method. (in a singleton class)
- (void)downloadfileModel:(ModelClass *)model {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *path = [[paths objectAtIndex:0] stringByAppendingPathComponent:[NSString stringWithFormat:#"%#.zip",model.iD]];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:model.url]];
operation = [[AFDownloadRequestOperation alloc] initWithRequest:request targetPath:path shouldResume:YES];
operation.outputStream = [NSOutputStream outputStreamToFileAtPath:path append:NO];
[operation setUserInfo:[NSDictionary dictionaryWithObject:model forKey:#"model"]];
////// Saving model into operation dictionary
[operation setProgressiveDownloadProgressBlock:^(AFDownloadRequestOperation *operation, NSInteger bytesRead, long long totalBytesRead, long long totalBytesExpected, long long totalBytesReadForFile, long long totalBytesExpectedToReadForFile) {
////// Sending Notification
////// Try to send notification only on significant download
totalBytesRead = ((totalBytesRead *100)/totalBytesExpectedToReadForFile);
[[NSNotificationCenter defaultCenter] postNotificationName:DOWNLOAD_PROGRESS object:model userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:#"%lld",totalBytesRead] forKey:#"progress"]];
/// Sending progress notification with model object of operation
}
}];
[[self downloadQueue] addOperation:operation]; // adding operation to queue
}
Invoke this method for multiple times with different models for multiple downloads.
Observe that notification on controller
where you show the download progress. (possibly tableView Controller).
Show all downloading operations list in this class
For showing progress Observe the Notification and fetch the model object from notification and get the file id from notification and Find that id in your Table View and Update that particular cell with the progress.
when starting the operations, you could safe each operation, downloadID and the values for totalbytesRead and totalBytesExpected together in a NSDictionary and all dicts to your downloadProgressArray.
Then when the callback methods is invoked, loop through your array and compare the calling operation with the operation in each dict. this way you should be able to identify the operation.
I just did something very similar (uploaded a bunch of files instead of downloading)
Here's an easy way to solve it.
Lets say you are downloading maximum of 10 files in one batch.
__block int random=-1;
[operation setUploadProgressBlock:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) {
if (random == -1) // This chunk of code just makes sure your random number between 0 to 10 is not repetitive
{
random = arc4random() % 10;
if(![[[self myArray]objectAtIndex:random]isEqualToString:#"0"])
{
while (![[[self myArray]objectAtIndex:random]isEqualToString:#"0"])
{
random = arc4random() % 10;
}
}
[DataManager sharedDataManager].total += totalBytesExpectedToWrite;
}
[[self myArray] replaceObjectAtIndex:random withObject:[NSString stringWithFormat:#"%lu",(unsigned long)totalBytesWritten]];
Then you calculate it like this:
NSNumber * sum = [[self myArray] valueForKeyPath:#"#sum.self"];
float percentDone;
percentDone = ((float)((int)[sum floatValue]) / (float)((int)[DataManager sharedDataManager].total));
[self array] will look like this:
array: (
0,
444840, // <-- will keep increasing until download is finished
0,
0,
0,
442144, // <-- will keep increasing until download is finished
0,
0,
0,
451580 // <-- will keep increasing until download is finished
)

UITableView - AFNetworking operation in background freezes scrolling

I have a sample application with a UITableViewController.
As in the facebook newsfeed, the app is supposed to download a first time X news, and then fetch news progressively as the user scroll.
Here is my implementation :
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
if (indexPath.row == self.newsList.count-PADDLE_BEFORE_FETCHING && !cantFetchMore)
if (!fetching){
fetching = YES;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
[self fetchNews];
});
}
}
(the idea is to start fetching additional news when we reach the N-PADDLE_BEFORE_FETCHING cell, only if we can still fetch some - see below- and if fetching is still not currently running)
and then the implementation of fetchNews :
-(void)fetchNews{
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSString *url = [NSString stringWithFormat:#"%#%#%#%#%d%#",HOSTNAME,GET_NEWS,[defaults objectForKey:#"oAuthToken"],#"&limit=",FETCH_SIZE_NEWS,[NSString stringWithFormat:#"&offset=%d",self.newsList.count]];
NSURLRequest *request =[[NSURLRequest alloc] initWithURL:[NSURL URLWithString:url]];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
#if DEVELOPMENT_MODE
NSLog(#"News : %#",JSON);
NSLog(#"Response : %#\n Request : %#",response,request);
#endif
//NSLog(#"Number of news fetched : %d",((NSArray*)JSON[#"data"]).count);
for (NSDictionary *d in JSON[#"data"]){
News *new = [[News alloc] initWithDictionary:d];
[self.newsList addObject:new];
new = nil;
}
if ((((NSArray*)JSON[#"data"]).count)%FETCH_SIZE_NEWS !=0) cantFetchMore = YES;
//NSLog(#"%d cantFetch",cantFetchMore);
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
[self.tableView reloadData];
fetching = NO;
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
NSLog(#"Request error : %# %# %#",request,error, JSON);
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
fetching = NO;
}];
[operation start];
}
This will fetch FETCH_SIZE_NEWS additional news from the server starting at the good offset which is the current size of the newsList array.
Also, if the count of fetched news % FETCH_SIZE_NEWS is different from 0, that means that we cannot fetch additional news (which will prevent from calling the webservice while scrolling the UITableView).
My issue is that when the fetching is done (exactly when I see the activity wheel running in the status bar), it blocks the GUI, and I cant continue to scroll down from the n-PADDLE_BEFORE_FETCHING cells to the n cells, or even scroll up to the previously loaded cells.
I don't really understand why as AFNetworking is supposed to run asynchronously.
Any ideas?
Thanks,
The for-loop in the completion block is running on the main thread and may be causing the slow-down. Try sending that code to another thread/queue.
As stated above buy Guy Kogus, the processing within the complete block of the AFNetworking operation was adding a bit a freezer on the main thread while scrolling.
Just added
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{//processing block}
Within the complete block (notably the foreach loop) and this is far better.
Thanks

Waiting on asynchronous methods using NSCondition

I am downloading four plist files asynchronously over the internet. I need to wait until all four files are downloaded, until I either on the first run, push a UIViewController, or on all subsequent runs, refresh the data, and reload all my UITableViews.
On the first run, everything works perfectly. When refreshing though, all four url requests are called, and started, but never call their completion or failure blocks, and the UI freezes. Which is odd since I preform all operations in a background thread. I have not been able to figure out why this is happening.
The first load and the refresh methods call the four "update" methods in the same way, and use NSCondition in the same way.
For the first run:
- (void)loadContentForProgram:(NSString *)programPath
{
NSLog(#"Start Load Program");
AppDelegate *myDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
hud = [[MBProgressHUD alloc] initWithView:myDelegate.window];
[myDelegate.window addSubview:hud];
hud.labelText = #"Loading...";
hud.detailsLabelText = #"Loading Data";
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
//Do stuff here to load data from files
//Update From online files
hud.detailsLabelText = #"Updating Live Data";
resultLock = NO;
progressLock = NO;
recallLock = NO;
stageLock = NO;
condition = [[NSCondition alloc] init];
[condition lock];
[self updateCurrentCompsText];
[self updateCompetitionResults];
[self updateCompetitionRecalls];
[self updateCompetitionProgress];
while (!resultLock) {
[condition wait];
}
NSLog(#"Unlock");
while (!stageLock) {
[condition wait];
}
NSLog(#"Unlock");
while (!recallLock) {
[condition wait];
}
NSLog(#"Unlock");
while (!progressLock) {
[condition wait];
}
NSLog(#"Unlock");
[condition unlock];
updateInProgress = NO;
//Reset Refresh controls and table views
self.refreshControlsArray = [[NSMutableArray alloc] init];
self.tableViewsArray = [[NSMutableArray alloc] init];
NSLog(#"Finished Loading Program");
[[NSNotificationCenter defaultCenter] postNotificationName:#"WMSOFinishedLoadingProgramData" object:nil]; //Pushes view controller
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUD hideHUDForView:myDelegate.window animated:YES];
});
});
}
When refreshing data:
- (void)updateProgramContent
{
if (!updateInProgress) {
updateInProgress = YES;
for (int i = 0; i < self.refreshControlsArray.count; i++) {
if (!((UIRefreshControl *)self.refreshControlsArray[i]).refreshing) {
[self.refreshControlsArray[i] beginRefreshing];
[self.tableViewsArray[i] setContentOffset:CGPointMake(0.0, 0.0) animated:YES];
}
}
resultLock = NO;
stageLock = NO;
recallLock = NO;
progressLock = NO;
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
condition = [[NSCondition alloc] init];
[condition lock];
[self updateCompetitionProgress];
[self updateCompetitionRecalls];
[self updateCompetitionResults];
[self updateCurrentCompsText];
while (!resultLock) {
[condition wait];
}
NSLog(#"Unlock");
while (!stageLock) {
[condition wait];
}
NSLog(#"Unlock");
while (!recallLock) {
[condition wait];
}
NSLog(#"Unlock");
while (!progressLock) {
[condition wait];
}
NSLog(#"Unlock");
[condition unlock];
});
for (int i = 0; i < self.refreshControlsArray.count; i++) {
[self.refreshControlsArray[i] performSelector:#selector(endRefreshing) withObject:nil afterDelay:1.0];
[self.tableViewsArray[i] performSelector:#selector(reloadData) withObject:nil afterDelay:1.0];
}
updateInProgress = NO;
}
}
The block below that appears in each loading method above, corresponds to a method that will download and update a specific piece of data.
[self updateCompetitionProgress];
[self updateCompetitionRecalls];
[self updateCompetitionResults];
[self updateCurrentCompsText];
which runs:
- (void)updateCompetitionResults
{
__block NSDictionary *competitionResultsData = nil;
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:#"Some URL",[self.programName stringByReplacingOccurrencesOfString:#" " withString:#"%20"]]] cachePolicy:NSURLCacheStorageNotAllowed timeoutInterval:20.0];
AFPropertyListRequestOperation *operation = [AFPropertyListRequestOperation propertyListRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id propertyList) {
competitionResultsData = (NSDictionary *)propertyList;
[competitionResultsData writeToFile:[#"SOME LOCAL PATH"] atomically:NO];
[self updateCompetitionResultsWithDictionary:competitionResultsData];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id propertyList) {
competitionResultsData = [NSDictionary dictionaryWithContentsOfFile:[#"SOME LOCAL PATH"]];
NSLog(#"Failed to retreive competition results: %#", error);
[self updateCompetitionResultsWithDictionary:competitionResultsData];
}];
[operation start];
}
and the completion and failure blocks call the same method to update the data
- (void)updateCompetitionResultsWithDictionary:(NSDictionary *)competitionResultsData
{
//Do Stuff with the data here
resultLock = YES;
[condition signal];
}
So, Why does this work on the first run, but not any of the subsequent runs?
As I mentioned in my comments, above, the most obvious problem is that you're invoking methods that use condition before you initialize condition. Make sure initialize condition before you start calling updateCompetitionResults, etc.
In terms of a more radical change, I might suggest retiring NSCondition altogether, and use operation queues:
I might use NSOperationQueue (or you can use dispatch groups, too, if you want, but I like the operation queue's ability to configure how many concurrent operations you can operate ... also if you get to a point that you want to cancel operations, I think NSOperationQueue offers some nice features there, too). You can then define each download and processing as a separate NSOperation (each of the downloads should happen synchronously, because they're running in an operation queue, you get the benefits of asynchronous operations, but you can kick off the post-processing immediately after the download is done). You then just queue them up to run asynchronously, but define a final operation which is dependent upon the other four will kick off as soon as the four downloads are done. (By the way, I use NSBlockOperation which provides block-functionality for NSOperation objects, but you can do it any way you want.)
And whereas your updateProgramContent might download asynchronously, it processes the four downloaded files sequentially, one after another. Thus, if the first download takes a while to download, it will hold up the post-processing of the others. Instead, I like to encapsulate both the downloading and the post processing of each of the four plist files in a single NSOperation, each. Thus, we enjoy maximal concurrency of not only the downloading, but the post-processing, too.
Rather than using the AFNetworking (which I'm generally a big fan of) plist-related method, I might be inclined to use NSDictionary and NSArray features that allow you to download a plist from the web and load them into the appropriate structure. These dictionaryWithContentsOfURL and arrayWithContentsOfURL run synchronously, but because we're doing this in a background operation, everything runs asynchronously like you want. This also bypasses the saving them to files. If you wanted them saved to files in your Documents directory, you can do that easily, too. Clearly, if you're doing something sophisticated in your downloading of the plist files (e.g. your server is engaging in some challenge-response authentication), you can't use the convenient NSDictionary and NSArray methods. But if you don't need all of that, the simple NSDictionary and NSArray methods, ___WithContentsOfURL make life pretty simple.
Pulling this all together, it might look like:
#interface ViewController ()
#property (nonatomic, strong) NSArray *competitions;
#property (nonatomic, strong) NSDictionary *competitionResults;
#property (nonatomic, strong) NSDictionary *competitionRecalls;
#property (nonatomic, strong) NSDictionary *competitionProgress;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[self transfer];
}
- (void)allTransfersComplete
{
BOOL success;
if (self.competitions == nil)
{
success = FALSE;
NSLog(#"Unable to download competitions");
}
if (self.competitionResults == nil)
{
success = FALSE;
NSLog(#"Unable to download results");
}
if (self.competitionRecalls == nil)
{
success = FALSE;
NSLog(#"Unable to download recalls");
}
if (self.competitionProgress == nil)
{
success = FALSE;
NSLog(#"Unable to download progress");
}
if (success)
{
NSLog(#"all done successfully");
}
else
{
NSLog(#"one or more failed");
}
}
- (void)transfer
{
NSURL *baseUrl = [NSURL URLWithString:#"http://insert.your.base.url.here/competitions"];
NSURL *competitionsUrl = [baseUrl URLByAppendingPathComponent:#"competitions.plist"];
NSURL *competitionResultsUrl = [baseUrl URLByAppendingPathComponent:#"competitionresults.plist"];
NSURL *competitionRecallsUrl = [baseUrl URLByAppendingPathComponent:#"competitionrecalls.plist"];
NSURL *competitionProgressUrl = [baseUrl URLByAppendingPathComponent:#"competitionprogress.plist"];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4; // if your server doesn't like four concurrent requests, you can ratchet this back to whatever you want
// create operation that will be called when we're all done
NSBlockOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
// any stuff that can be done in background should be done here
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// any user interface stuff should be done here; I've just put this in a separate method so this method doesn't get too unwieldy
[self allTransfersComplete];
}];
}];
// a variable that we'll use as we create our four download/process operations
NSBlockOperation *operation;
// create competitions operation
operation = [NSBlockOperation blockOperationWithBlock:^{
// download the competitions and load it into the ivar
//
// note, if you *really* want to download this to a file, you can
// do that when the download is done
self.competitions = [NSArray arrayWithContentsOfURL:competitionsUrl];
// if you wanted to do any post-processing of the download
// you could do it here.
NSLog(#"competitions = %#", self.competitions);
}];
[completionOperation addDependency:operation];
// create results operation
operation = [NSBlockOperation blockOperationWithBlock:^{
self.competitionResults = [NSDictionary dictionaryWithContentsOfURL:competitionResultsUrl];
NSLog(#"competitionResults = %#", self.competitionResults);
}];
[completionOperation addDependency:operation];
// create recalls operation
operation = [NSBlockOperation blockOperationWithBlock:^{
self.competitionRecalls = [NSDictionary dictionaryWithContentsOfURL:competitionRecallsUrl];
NSLog(#"competitionRecalls = %#", self.competitionRecalls);
}];
[completionOperation addDependency:operation];
// create progress operation
operation = [NSBlockOperation blockOperationWithBlock:^{
self.competitionProgress = [NSDictionary dictionaryWithContentsOfURL:competitionProgressUrl];
NSLog(#"competitionProgress = %#", self.competitionProgress);
}];
[completionOperation addDependency:operation];
// queue the completion operation (which is dependent upon the other four)
[queue addOperation:completionOperation];
// now queue the four download and processing operations
[queue addOperations:completionOperation.dependencies waitUntilFinished:NO];
}
#end
Now, I don't know which of your plist's are arrays and which are dictionaries (in my example, I made competitions an array and the rest were dictionaries keyed by the competition id), but hopefully you get the idea of what I was shooting for. Maximize concurrency, eliminate NSCondition logic, really make the most of NSOperationQueue, etc.
This may be all to much to take in, but I only mention it as an alternative to NSCondition. If your current technique works, that's great. But the above outlines how I would tackle a challenge like this.

Resources