downloadImages is a button and whenever I press on it, a spinner should start rolling, an async request should ping Google (to make sure there is a connection) and after a response is received, I start to synchronically downloading images.
Somehow the spinner won't go and it seems as if the request is sync and not async.
- (IBAction)downloadImages:(id)sender {
NSString *ping=#"http://www.google.com/";
GlobalVars *globals = [GlobalVars sharedInstance];
[self startSpinner:#"Please Wait."];
NSURL *url = [[NSURL alloc] initWithString:ping];
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:5.0];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (data) {
for(int i=globals.farmerList.count-1; i>=0;i--)
{
//Definitions
NSString * documentsDirectoryPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
//Get Image From URL
NSString *urlString = [NSString stringWithFormat:#"https://myurl.com/%#",[[globals.farmerList objectAtIndex:i] objectForKey:#"Image"]];
UIImage * imageFromURL = [self getImageFromURL:urlString];
//Save Image to Directory
[self saveImage:imageFromURL withFileName:[[globals.farmerList objectAtIndex:i] objectForKey:#"Image"] ofType:#"jpg" inDirectory:documentsDirectoryPath];
}
[self stopSpinner];
}
}];
}
The spinner code:
//show loading activity.
- (void)startSpinner:(NSString *)message {
// Purchasing Spinner.
if (!connectingAlerts) {
connectingAlerts = [[UIAlertView alloc] initWithTitle:NSLocalizedString(message,#"")
message:nil
delegate:self
cancelButtonTitle:nil
otherButtonTitles:nil];
connectingAlerts.tag = (NSUInteger)300;
[connectingAlerts show];
UIActivityIndicatorView *connectingIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
connectingIndicator.frame = CGRectMake(139.0f-18.0f,50.0f,37.0f,37.0f);
[connectingAlerts addSubview:connectingIndicator];
[connectingIndicator startAnimating];
}
}
//hide loading activity.
- (void)stopSpinner {
if (connectingAlerts) {
[connectingAlerts dismissWithClickedButtonIndex:0 animated:YES];
connectingAlerts = nil;
}
// [self performSelector:#selector(showBadNews:) withObject:error afterDelay:0.1];
}
As asked: the getImageFromURL code
-(UIImage *) getImageFromURL:(NSString *)fileURL {
UIImage * result;
NSData * data = [NSData dataWithContentsOfURL:[NSURL URLWithString:fileURL]];
result = [UIImage imageWithData:data];
return result;
}
-(void) saveImage:(UIImage *)image withFileName:(NSString *)imageName ofType:(NSString *)extension inDirectory:(NSString *)directoryPath {
if ([[extension lowercaseString] isEqualToString:#"png"]) {
[UIImagePNGRepresentation(image) writeToFile:[directoryPath stringByAppendingPathComponent:[NSString stringWithFormat:#"%#.%#", imageName, #"png"]] options:NSAtomicWrite error:nil];
} else if ([[extension lowercaseString] isEqualToString:#"jpg"] || [[extension lowercaseString] isEqualToString:#"jpeg"]) {
[UIImageJPEGRepresentation(image, 1.0) writeToFile:[directoryPath stringByAppendingPathComponent:[NSString stringWithFormat:#"%#.%#", imageName, #"jpg"]] options:NSAtomicWrite error:nil];
} else {
NSLog(#"Image Save Failed\nExtension: (%#) is not recognized, use (PNG/JPG)", extension);
}
}
That's because you're creating an asynchronous operation and then telling it to execute on the main thread by using [NSOperationQueue mainQueue];.
Instead, create a new instance of NSOpeartionQueue and pass that as the parameter.
NSOperationQueue *myQueue = [[NSOperationQueue alloc] init];
This is an asynchronous problem. Asynchronism is infectious. That means, if any small part of the problem is asynchronous, the whole problem becomes asynchronous.
That is, your button action would invoke an asynchronous method like this (and itself becomes "asynchronous" as well):
- (IBAction)downloadImages:(id)sender
{
self.downloadImagesButton.enabled = NO;
[self asyncLoadAndSaveImagesWithURLs:self.urls completion:^(id result, NSError* error){
if (error != nil) {
NSLog(#"Error: %#", error);
}
dispatch_async(dispatch_get_main_queue(), ^{
self.downloadImagesButton.enabled = YES;
};
}];
}
So, your asynchronous problem can be described as this:
Given a list of URLs, asynchronously load each URL and asynchronously save them to disk. When all URLs are loaded and saved, asynchronously notify the call-site by calling a completion handler passing it an array of results (for each download and save operation).
This is your asynchronous method:
typedef void (^completion_t)(id result, NSError* error);
- (void) asyncLoadAndSaveImagesWithURLs:(NSArray*)urls
completion:(completion_t) completionHandler;
Asynchronous problems can be solved properly only by finding a suitable asynchronous pattern. This involves to asynchronize every part of the problem.
Lets start with your getImageFromURL method. Loading a remote resource is inherently asynchronous, so your wrapper method ultimately will be asynchronous as well:
typedef void (^completion_t)(id result, NSError* error);
- (void) loadImageWithURL:(NSURL*)url completion:(completion_t)completionHandler;
I leave it undefined how that method will be eventually implemented. You may use NSURLConnection's asynchronous convenient class method, a third party helper tool or your own HTTPRequestOperation class. It doesn't matter but it MUST be asynchronous for achieving a sane approach.
Purposefully, you can and should make your saveImage method asynchronous as well. The reason for making this asynchronous is, that this method possibly will get invoked concurrently, and we should *serialize* disk bound (I/O bound) tasks. This improves utilization of system resources and also makes your approach a friendly system citizen.
Here is the asynchronized version:
typedef void (^completion_t)(id result, NSError* error);
-(void) saveImage:(UIImage *)image fileName:(NSString *)fileName ofType:(NSString *)extension
inDirectory:(NSString *)directoryPath
completion:(completion_t)completionHandler;
In order to serialize disk access, we can use a dedicated queue disk_queue where we assume it has been properly initialized as a serial queue by self:
-(void) saveImage:(UIImage *)image fileName:(NSString *)fileName ofType:(NSString *)extension
inDirectory:(NSString *)directoryPath
completion:(completion_t)completionHandler
{
dispatch_async(self.disk_queue, ^{
// save the image
...
if (completionHandler) {
completionHandler(result, nil);
}
});
}
Now, we can define an asynchronous wrapper which loads and saves the image:
typedef void (^completion_t)(id result, NSError* error);
- (void) loadAndSaveImageWithURL:(NSURL*)url completion:(completion_t)completionHandler
{
[self loadImageWithURL:url completion:^(id image, NSError*error) {
if (image) {
[self saveImage:image fileName:fileName ofType:type inDirectory:directory completion:^(id result, NSError* error){
if (result) {
if (completionHandler) {
completionHandler(result, nil);
}
}
else {
DebugLog(#"Error: %#", error);
if (completionHandler) {
completionHandler(nil, error);
}
}
}];
}
else {
if (completionHandler) {
completionHandler(nil, error);
}
}
}];
}
This loadAndSaveImageWithURL method actually performs a "continuation" of two asynchronous tasks:
First, asynchronously load the image.
THEN, if that was successful, asynchronously save the image.
It's important to notice that these two asynchronous tasks are sequentially processed.
Up until here, this all should be quite comprehensive and be straight forward. The tricky part follows now where we try to invoke a number of asynchronous tasks in an asynchronous manner.
Asynchronous Loop
Suppose, we have a list of URLs. Each URL shall be loaded asynchronously, and when all URLs are loaded we want the call-site to be notified.
The traditional for loop is not that appropriate for accomplishing this. But imagine we would have a Category for a NSArray with a method like this:
Category for NSArray
- (void) forEachApplyTask:(task_t)transform completion:(completion_t)completionHandler;
This basically reads: for each object in the array, apply the asynchronous task transform and when all objects have been "transformed" return a list of the transformed objects.
Note: this method is asynchronous!
With the appropriate "transform" function, we can "translate" this to your specific problem:
For each URL in the array, apply the asynchronous task loadAndSaveImageWithURL and when all URLS have been loaded and saved return a list of the results.
The actual implementation of the forEachApplyTask:completion: may appear a bit tricky and for brevity I don't want to post the complete source here. A viable approach requires about 40 lines of code.
I'll provide an example implementation later (on Gist), but lets explain how this method can be used:
The task_t is a "block" which takes one input parameter (the URL) and returns a result.
Since everything must be treated asynchronously, this block is asynchronous as well, and the eventual result will be provided via a completion block:
typedef void (^completion_t)(id result, NSError* error);
typedef void (^task_t)(id input, completion_t completionHandler);
The completion handler may be defined as follows:
If the tasks succeeds, parameter error equals nil. Otherwise, parameter error is an NSError object. That is, a valid result may also be nil.
We can quite easily wrap our method loadAndSaveImageWithURL:completion: and create a block:
task_t task = ^(id input, completion_t completionHandler) {
[self loadAndSaveImageWithURL:input completion:completionHandler];
};
Given an array of URLs:
self.urls = ...;
your button action can be implemented as follows:
- (IBAction)downloadImages:(id)sender
{
self.downloadImagesButton.enabled = NO;
task_t task = ^(id input, completion_t completionHandler) {
[self loadAndSaveImageWithURL:input completion:completionHandler];
};
[self.urls forEachApplyTask:task ^(id results, NSError*error){
self.downloadImagesButton.enabled = YES;
if (error == nil) {
... // do something
}
else {
// handle error
}
}];
}
Again, notice that method forEachApplyTask:completion: is an asynchronous method, which returns immediately. The call-site will be notified via the completion handler.
The downloadImages method is asynchronous as well, there is no completion handler though. This method disables the button when it starts and enables it again when the asynchronous operation has been completed.
The implementation of this forEachApplyTask method can be found here: (https://gist.github.com/couchdeveloper/6155227).
From your code what I can understand is its not due to assyncronous call to load url. but the following code may heavy.
For assynchronous image loading try https://github.com/rs/SDWebImage
//Get Image From URL
NSString *urlString = [NSString stringWithFormat:#"https://myurl.com/%#",[[globals.farmerList objectAtIndex:i] objectForKey:#"Image"]];
UIImage * imageFromURL = [self getImageFromURL:urlString];
//Save Image to Directory
[self saveImage:imageFromURL withFileName:[[globals.farmerList objectAtIndex:i] objectForKey:#"Image"] ofType:#"jpg" inDirectory:documentsDirectoryPath];
Happy coding :)
Related
I am capturing video in preview mode and would like to display a still image captured by the camera.
I currently save the image and capture output to ivars defined in the interface as:
UIImage *snapshot
AVCaptureStillImageOutput* stillImageOutput;
The video displays fine. However, when I try to capture and display a still image, nothing is appearing and, in fact, the debugger shows the stillImageOutput and image are nil. I think this may be a timing issue with the asynchronous capture and that I need to use a completion handler, but I am weak on completion handlers.
What is the proper way to display a still image immediately after capturing it without tying up UI:
Code to capture still:
- (void)takeSnapshot {
AVCaptureConnection *videoConnection = nil;
for (AVCaptureConnection *connection in stillImageOutput.connections) {
for (AVCaptureInputPort *port in [connection inputPorts]) {
if ([[port mediaType] isEqual:AVMediaTypeVideo]) {
videoConnection = connection;
break;
}
}
if (videoConnection) {
break;
}
}
[stillImageOutput captureStillImageAsynchronouslyFromConnection:videoConnection
completionHandler:^(CMSampleBufferRef imageDataSampleBuffer, NSError *error) {
if (imageDataSampleBuffer != NULL) {
NSData *imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation:imageDataSampleBuffer];
snapshot = [UIImage imageWithData:imageData];
}
}];
}
Code to display still. Note absence of completion handler which may be issue, however, I'm not sure how to write that...
[self takeSnapshot];
self.imageView.image = snapshot;
I would change the takeSnapshot method to take in a completion block and then call that completion block within the completion block of your other async method:
captureStillImageAsynchronouslyFromConnection:completionHandler
Here's an example of a method taking a completion block and then calling back to it in the completion block of a method called internally:
// this correlates to your takeSnapshot method
// you want to add a completion portion to this method
- (void)doSomethingAsynchronouslyWithCompletion:(void (^)(NSData *completionData))completion {
// call your other async method
[self anotherAsyncMethodWithItsOwnCompletion:^(NSData *completionDataFromSecondMethod) {
if (completionDataFromSecondMethod.length > 0) {
// this is where you would receive the CMSampleBufferRef from the completion handler of captureStillImageAsynchronouslyFromConnection:completionHandler
// and convert it over to to data
// make sure the completion block isn't nil if it's nullable
if (completion) {
// you would want to pass back the NSData imageData in the completion block here
completion(completionDataFromSecondMethod);
}
}
}];
}
// this method would simulate the captureStillImageAsynchronouslyFromConnection:completionHandler: method
- (void)anotherAsyncMethodWithItsOwnCompletion:(void (^)(NSData * completionDataFromSecondMethod))anotherCompletion {
// this is just to simulate some time waiting for the asnyc task to complete
// never call sleep in your own code
sleep(3);
if (anotherCompletion) {
// this simulates the fake CFSampleBufferRef passed back by the captureStillImage...
NSData *fakeCompletionData = [#"FakeCompletionString" dataUsingEncoding:NSUTF8StringEncoding];
anotherCompletion(fakeCompletionData);
}
}
And an example of how you would call it:
[self doSomethingAsynchronouslyWithCompletion:^(NSData *completionData) {
if (completionData.length > 0) {
// come back on the main queue to modify any UI Elements
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// this is where you want want to set your self.imageView.image
// self.imageView.image = [UIImage imageWithData:{{dataFromCompletion}}]
NSLog(#"The completionString result = %#", [[NSString alloc] initWithData:completionData encoding:NSUTF8StringEncoding]);
}];
}
}];
This link may be helpful for getting you started with block syntax: http://goshdarnblocksyntax.com
As
NSURLConnection sendSynchronousRequest:returningResponse:error:&connectionError
is set deprecated I will have to replace an Importer I wrote a long time ago.
The Importer does the following:
It fetches data from API-A. The data there can be on multiple pages.
It uses the data from the first fetch (also multipage) to query data from API-B and merges
Results from the API-B query will be merged with data from API-A
I implemented this with a background-operation where I use methods for each API which called recursively if there are mulitple pages for the request.
But as NSURLSession does not support synchronous request I currently only see the option to have lot of overhead (e.g. iVars) control what's called in the completion block (e.g. next Page or start to query API-B).
So, what would be an elegant solution to bring this to NSURLSession.
NB: Just to make sure, my previous solution does not block the main thread at all. But back then it was the easiest way to control the merge of two sources.
This answer is not supposed to be best practice. It was just practical for me.
Faced with the situation when a bunch of synchronous requests is executed in background and the order of execution matters I've ended up using the following:
SyncRequestSender.h
#import <Foundation/Foundation.h>
#interface SyncRequestSender : NSObject
+ (NSData *)sendSynchronousRequest:(NSURLRequest *)request
returningResponse:(NSURLResponse **)response
error:(NSError **)error;
#end
SyncRequestSender.m
#import "SyncRequestSender.h"
#implementation SyncRequestSender
+ (NSData *)sendSynchronousRequest:(NSURLRequest *)request
returningResponse:(NSURLResponse **)response
error:(NSError **)error
{
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
NSError __block *err = NULL;
NSData __block *data;
NSURLResponse __block *resp;
[[[NSURLSession sharedSession] dataTaskWithRequest:request
completionHandler:^(NSData* _data, NSURLResponse* _response, NSError* _error) {
resp = _response;
err = _error;
data = _data;
dispatch_group_leave(group);
}] resume];
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
if (response)
{
*response = resp;
}
if (error)
{
*error = err;
}
return data;
}
#end
This's an example in AFNetworking, it shows how to wait an asynchronous task.
- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
__block NSArray *tasks = nil;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
if ([keyPath isEqualToString:NSStringFromSelector(#selector(dataTasks))]) {
tasks = dataTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(#selector(uploadTasks))]) {
tasks = uploadTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(#selector(downloadTasks))]) {
tasks = downloadTasks;
} else if ([keyPath isEqualToString:NSStringFromSelector(#selector(tasks))]) {
tasks = [#[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:#"#unionOfArrays.self"];
}
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return tasks;
}
I have some networking code with heavy JSON parsing going on. It needs to be done in the background to not block the main thread. The code looks like this :
-(void) getSomeDataWithParameters:(...)parameters completion:(void (^)(NSArray *data))completion
{
NSURLRequest *req = ...
AFJSONRequestOperation *op = [[AFJSONRequestOperation alloc] initWithRequest:req];
// sometimes I have more requests
// startOperations is a wrapper on AFHTTPClient enqueueBatchOfHTTPRequestOperations:progressBlock:completionBlock:
// that handles errors and loading views
[self startOperations:#[op] completionBlock:^(NSArray *operations) {
// getBgQueue = return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(getBgQueue(), ^{
NSArray *data = [MyParserClass parseJSON:op.responseJSON inContext:self.localContext];
[self.localContext MR_saveToPersistentStoreWithCompletion:^(BOOL success, NSError *error) {
// this is executed on main thread
if(completion) completion(...);
}];
});
}];
}
(AFNetworking 1.x)
The above code works very fine, but it's a pain to setup and write. And often the whole method content is wrapped inside another block to fetch some required data first... basically the blocks just pile up and makes ugly code
I'm using enqueueBatchOfHTTPRequestOperations and not individual completion blocks on AFJSONRequestOperation because batch completion block would sometimes fire before all individual operations completion blocks... (I also read somewhere that Mattt discouraged doing this)
Any pointers on how to do better than this?
I'm not sure what you want here, but just like "longcat is long", it's somewhat inherent in the pattern: 'continuation-passing style is continuation-passing style'. If you want to flatten things out a bit, you could make local block variables, but to a certain degree, you're stuck because you need the completion for -MR_saveToPersistentStoreWithCompletion to close over data in order to pass it to the -getSomeDataWithParameters... completion, but data won't exist until the -startOperations completion is executed.
You could probably achieve a less-nested appearance by using a bunch of __block variables, and splitting the code into several local blocks, but to me that feels kind of like cutting off your nose to spite your face. This code is readily understandable the way it is.
By the way... I notice that you're closing over op in the -startOperations completion block. This is fine because you're enqueuing op by doing -startOperations: #[op] ... but it would arguably be cleaner to get op from the operations parameter to the completion. I tightened this up as much as seemed reasonable:
- (void)getSomeDataWithParameters:(...)parameters completion:(void (^)(NSArray *data))completion
{
NSURLRequest *req = ...;
AFJSONRequestOperation *op = [[AFJSONRequestOperation alloc] initWithRequest:req];
[self startOperations:#[op] completionBlock:^(NSArray *operations) {
for (AFJSONRequestOperation *op in operations) {
dispatch_async(getBgQueue(), ^{
NSArray *data = [MyParserClass parseJSON:op.responseJSON inContext:self.localContext];
void (^mrSaveCompletion)(BOOL, NSError*) = completion ? ^(BOOL success, NSError *error) { completion(data); } : nil;
[self.localContext MR_saveToPersistentStoreWithCompletion: mrSaveCompletion];
});
}
}];
}
This will fan out each response potentially to a different thread. If you want all responses to execute on a single background thread, just swap the nesting of the for loop and the dispatch_async.
From there, the only really "superfluous" code is the dispatch_async. You could eliminate that by making -startOperations:... take a queue parameter where you would pass in the queue you wanted the completion to be called. Maybe like this:
- (void)startOperations: (NSArray*)ops completionQueue: (dispatch_queue_t)queue completionBlock: (void (^)(NSArray*))completion
{
void (^completionWrapper)(NSArray*) = !completion ? nil : ^(NSArray* ops) {
if (queue)
dispatch_async(queue, ^{ completion(ops); });
else
completion(ops);
};
[self startOperations: ops completionBlock: completionWrapper];
}
- (void)getSomeDataWithParameters:(...)parameters completion:(void (^)(NSArray *data))completion
{
NSURLRequest *req = ...;
AFJSONRequestOperation *op = [[AFJSONRequestOperation alloc] initWithRequest:req];
[self startOperations:#[op] completionQueue: getBgQueue() completionBlock:^(NSArray *operations) {
for (AFJSONRequestOperation *op in operations) {
NSArray *data = [MyParserClass parseJSON:op.responseJSON inContext:self.localContext];
void (^mrSaveCompletion)(BOOL, NSError*) = !completion ? nil : ^(BOOL success, NSError *error) { completion(data); };
[self.localContext MR_saveToPersistentStoreWithCompletion: mrSaveCompletion];
});
}];
}
I have an "UIImage" return type method named "ComLog". I want to return a Image from this method. In "ComLog" method i use GCD to get the image value from an array. I use the following code, the "NSLog(#"qqqqqqqqqqq %#", exiIco)" print the 'image' value but NSLog(#"qqqqqqqqqqq %#", exiIco);" don't.
Here is the details :
-(UIImage*) ComLog
{
ExibitorInfo *currentExibitor100 = [[ExibitorInfo alloc] init];
currentExibitor100 = [self.exibitorsArray objectAtIndex:0];
imageQueueCompanyLogo = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(imageQueueCompanyLogo, ^
{
UIImage *imageCompanyLogo = [UIImage imageWithData:[NSData dataWithContentsOfURL: [NSURL URLWithString:[currentExibitor100 companyLogoURL]]]];
dispatch_async(dispatch_get_main_queue(), ^
{
self.exibitorIcoImageView.image = imageCompanyLogo;
exiIco = imageCompanyLogo;
NSLog(#"qqqqqqqqqqq %#", exiIco);
});
});
return exiIco;
}
- (void)viewDidLoad
{
[super viewDidLoad];
UIImage *a = [self ComLog];
NSLog(#"It should be a image %#", a);
}
Here all the properties are declared globally(In "Myclass.h" file). I am new in Objective C. Please give reply if you know the answer.
Thanks in Advance.
There's so much wrong in your code snippet that it is difficult to decide where to start.
I would suggest to leave GCD for now, and take a look at it later when you are more experienced.
Basically, you want to load an image from a remote server. NSURLConnection provides a convenient method for this which is sufficient for very simple use cases:
+ (void)sendAsynchronousRequest:(NSURLRequest *)request queue:(NSOperationQueue *)queue completionHandler:(void (^)(NSURLResponse*, NSData*, NSError*))handler;
You can find the docs here: NSURLConnection Class Reference.
The recommended approach to load remote resources is using NSURLConnection in asynchronous mode implementing the delegates. You can find more info here:
URL Loading System Programming Guide - Using NSURL Connection
I would also recommend to read Conventions.
Here is a short example how to use sendAsynchronousRequest:
NSURL* url = [NSURL URLWithString:[currentExibitor100 companyLogoURL]];
NSMutableURLRequest* urlRequest = [NSURLRequest requestWithURL:url];
NSOperationQueue* queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:urlRequest
queue:queue
completionHandler:^(NSURLResponse* response,
NSData* data,
NSError* error)
{
if (data) {
// check status code, and optionally MIME type
if ( [(NSHTTPURLResponse*)(response) statusCode] == 200 /* OK */) {
UIImage* image = [UIImage imageWithData:data];
if (image) {
dispatch_async(dispatch_get_main_queue(), ^{
self.exibitorIcoImageView.image = image;
});
} else {
NSError* err = [NSError errorWithDomain: ...];
[self handleError:err]; // execute on main thread!
}
}
else {
// status code indicates error, or didn't receive type of data requested
NSError* err = [NSError errorWithDomain:...];
[self handleError:err]; // execute on main thread!
}
}
else {
// request failed - error contains info about the failure
[self handleError:error]; // execute on main thread!
}
}];
First of all, I would recommend you to read about blocks in Objective C. The dispatch_async block you are using inside your function is async and thus it returns immediately after you use it, as it runs in it's own pool. For proper use, you can call another method to return the image processes inside the block, or post NSNotification when your image is ready. like this:
-(void) ComLog
{
ExibitorInfo *currentExibitor100 = [[ExibitorInfo alloc] init];
currentExibitor100 = [self.exibitorsArray objectAtIndex:0];
imageQueueCompanyLogo = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(imageQueueCompanyLogo, ^
{
UIImage *imageCompanyLogo = [UIImage imageWithData:[NSData dataWithContentsOfURL: [NSURL URLWithString:[currentExibitor100 companyLogoURL]]]];
dispatch_async(dispatch_get_main_queue(), ^
{
self.exibitorIcoImageView.image = imageCompanyLogo;
exiIco = imageCompanyLogo;
NSLog(#"qqqqqqqqqqq %#", exiIco);
[self imageIsReady:exiIco];
});
});
// return exiIco;
}
- (void)imageIsReady:(uiimage *)image
{
//do whatever you want with the image
NSLog(#"Image is here %#", image);
}
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.