Why doesn't NSOperationQueue execute a block submitted with addOperationWithBlock? - ios

Here is the thing, I got some code, that does not execute (the compiler run the code but does not do anything)... Here is the code... Using NSURLSessionDelegate and [NSOperationQueue mainQueue] addOperationWithBlock
#interface tablaPosViewController : UIViewController <NSURLSessionDelegate>
#end
#implementation tablaPosViewController ()
- (void)viewDidLoad
{
//some code to set the view, labels and stuff
[self downloadTheHTMLdata] // this code download some data from WWW
}
- (void)downloadTheHMTLdata
{
//some code to set the session using an object
[object.downloadTask resume]; //Begins the download
}
- (void)toFixTablaPosiciones
{
//some code to do with the downloaded data from WWW (and HTML sheet)
//here, parse the HTML Sheet, and put some data into an Arrays, and another public vars
//call another method
[self PutTheDataInLabel];
}
- (void)PutTheDataInLabel
{
//some code to put all the data in every label
//take the public vars that was set in the previous method, and do some code with it
//and put the info into the labels
//call another method
[self MoreStuff];
}
- (void)MoreStuff
{
//some code..
}
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
//When the download finish this method fire up!
//this is to copy file from tmp folder to Docs folder
if ([fileManager fileExistsAtPath:[destinationURL path]])
{
[fileManager removeItemAtURL:destinationURL error:nil];
}
BOOL success = [fileManager copyItemAtURL:location //TMP download folder
toURL:destinationURL //Documents folder
error:&error];
//HERES COMES THE TROUBLE!!!
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self toFixTablaPosiciones]; //Call this method, that has other method calls!
}];
}
#end
UPDATE
This another code put methods in the queue...
-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session{
AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
if ([downloadTasks count] == 0) {
if (appDelegate.backgroundTransferCompletionHandler != nil) {
void(^completionHandler)() = appDelegate.backgroundTransferCompletionHandler;
appDelegate.backgroundTransferCompletionHandler = nil;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
completionHandler();
}];
}
}
}];
}
The issue is when the download file ends, calls -(void)URLSession:(NSURLSession *)session downloadTask:... method and I wait to [[NSOperationQueue mainQueue] addOperationWithBlock:^{... runs everything... but it does not execute anythig of [self toFixTablaPosiciones]!!.
I ran the code step by step, and I see how the compiler runs all the code, method over method... but the view never updates, runs but not executes, simply does do anything, and I have an Activity Indicator, and want to stop it, but the lablels stills with de dummie data and the Activity Indicator never stops and never disappears.
In a previous view, I download another file using a similiar class, and downloads very quickly. Going to this view/class try to perform the download and this is the thing...
Hoping any body can help me and send me any advice. Thanks!

One technique I use, as do most experienced developers, is to use asserts and also NSLog (which you can comment on and off) to verify that assumptions you are making about your code are in fact true. Try cutting and pasting the below code and see what happens - it should help. In the future, don't bang your head on a wall - start adding asserts and logs. At some point you will find that some assumption is untrue.
-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
NSLog(#"Received URLSessionDidFinishEventsForBackgroundURLSession: application state is %d", [UIApplication sharedApplication] applicationState];
AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
NSLog(#"Download task count %d", [downloadTasks count]);
#warning "This looks like incorrect logic, but I don't know this background code. Wouldn't you see "1", or more? Won't you get an array of the tasks you submitted???
if ([downloadTasks count] == 0) {
assert(appDelegate.backgroundTransferCompletionHandler); // if this is nil, your app will be stuck in some odd state forever, so treat this like a fatal error
void(^completionHandler)() = appDelegate.backgroundTransferCompletionHandler;
appDelegate.backgroundTransferCompletionHandler = nil;
assert([NSOperationQueue mainQueue]); // why not?
assert(![NSOperationQueue mainQueue].isSuspended); // I looked at the class description, the queue **could** be suspended
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(#"Completion Operation called!");
assert(completionHandler); // perhaps an ARC bug, unlikely, but then you cannot get this code to work, right?
completionHandler();
}];
}
}];
}

Related

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.

AFNetworking and background transfers

I'm a bit confuse of how to take advantage of the new iOS 7 NSURLSession background transfers features and AFNetworking (versions 2 and 3).
I saw the WWDC 705 - What’s New in Foundation Networking session, and they demonstrated background download that continues after the app terminated or even crashes.
This is done using the new API application:handleEventsForBackgroundURLSession:completionHandler: and the fact that the session's delegate will eventually get the callbacks and can complete its task.
So I'm wondering how to use it with AFNetworking (if possible) to continue downloading in background.
The problem is, AFNetworking conveniently uses block based API to do all the requests, but if the app terminated or crashes those block are also gone. So how can I complete the task?
Or maybe I'm missing something here...
Let me explain what I mean:
For example my app is a photo messaging app, lets say that I have a PhotoMessage object that represent one message and this object has properties like
state - describe the state of the photo download.
resourcePath - the path to the final downloaded photo file.
So when I get a new message from the server, I create a new PhotoMessage object, and start downloading its photo resource.
PhotoMessage *newPhotoMsg = [[PhotoMessage alloc] initWithInfoFromServer:info];
newPhotoMsg.state = kStateDownloading;
self.photoDownloadTask = [[BGSessionManager sharedManager] downloadTaskWithRequest:request progress:nil destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
NSURL *filePath = // some file url
return filePath;
} completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
if (!error) {
// update the PhotoMessage Object
newPhotoMsg.state = kStateDownloadFinished;
newPhotoMsg.resourcePath = filePath;
}
}];
[self.photoDownloadTask resume];
As you can see, I use the completion block to update that PhotoMessage object according to the response I get.
How can I accomplish that with a background transfer? This completion block won't be called and as a result, I can't update the newPhotoMsg.
A couple of thoughts:
You have to make sure you do the necessary coding outlined in the Handling iOS Background Activity section of the URL Loading System Programming Guide says:
If you are using NSURLSession in iOS, your app is automatically relaunched when a download completes. Your app’s application:handleEventsForBackgroundURLSession:completionHandler: app delegate method is responsible for recreating the appropriate session, storing a completion handler, and calling that handler when the session calls your session delegate’s URLSessionDidFinishEventsForBackgroundURLSession: method.
That guide shows some examples of what you can do. Frankly, I think the code samples discussed in the latter part of the WWDC 2013 video What’s New in Foundation Networking are even more clear.
The basic implementation of AFURLSessionManager will work in conjunction with background sessions if the app is merely suspended (you'll see your blocks called when the network tasks are done, assuming you've done the above). But as you guessed, any task-specific block parameters that are passed to the AFURLSessionManager method where you create the NSURLSessionTask for uploads and downloads are lost "if the app terminated or crashes."
For background uploads, this is an annoyance (as your task-level informational progress and completion blocks you specified when creating the task will not get called). But if you employ the session-level renditions (e.g. setTaskDidCompleteBlock and setTaskDidSendBodyDataBlock), that will get called properly (assuming you always set these blocks when you re-instantiate the session manager).
As it turns out, this issue of losing the blocks is actually more problematic for background downloads, but the solution there is very similar (do not use task-based block parameters, but rather use session-based blocks, such as setDownloadTaskDidFinishDownloadingBlock).
An alternative, you could stick with default (non-background) NSURLSession, but make sure your app requests a little time to finish the upload if the user leaves the app while the task is in progress. For example, before you create your NSURLSessionTask, you can create a UIBackgroundTaskIdentifier:
UIBackgroundTaskIdentifier __block taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^(void) {
// handle timeout gracefully if you can
[[UIApplication sharedApplication] endBackgroundTask:taskId];
taskId = UIBackgroundTaskInvalid;
}];
But make sure that the completion block of the network task correctly informs iOS that it is complete:
if (taskId != UIBackgroundTaskInvalid) {
[[UIApplication sharedApplication] endBackgroundTask:taskId];
taskId = UIBackgroundTaskInvalid;
}
This is not as powerful as a background NSURLSession (e.g., you have a limited amount of time available), but in some cases this can be useful.
Update:
I thought I'd add a practical example of how to do background downloads using AFNetworking.
First define your background manager.
//
// BackgroundSessionManager.h
//
// Created by Robert Ryan on 10/11/14.
// Copyright (c) 2014 Robert Ryan. All rights reserved.
//
#import "AFHTTPSessionManager.h"
#interface BackgroundSessionManager : AFHTTPSessionManager
+ (instancetype)sharedManager;
#property (nonatomic, copy) void (^savedCompletionHandler)(void);
#end
and
//
// BackgroundSessionManager.m
//
// Created by Robert Ryan on 10/11/14.
// Copyright (c) 2014 Robert Ryan. All rights reserved.
//
#import "BackgroundSessionManager.h"
static NSString * const kBackgroundSessionIdentifier = #"com.domain.backgroundsession";
#implementation BackgroundSessionManager
+ (instancetype)sharedManager {
static id sharedMyManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedMyManager = [[self alloc] init];
});
return sharedMyManager;
}
- (instancetype)init {
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:kBackgroundSessionIdentifier];
self = [super initWithSessionConfiguration:configuration];
if (self) {
[self configureDownloadFinished]; // when download done, save file
[self configureBackgroundSessionFinished]; // when entire background session done, call completion handler
[self configureAuthentication]; // my server uses authentication, so let's handle that; if you don't use authentication challenges, you can remove this
}
return self;
}
- (void)configureDownloadFinished {
// just save the downloaded file to documents folder using filename from URL
[self setDownloadTaskDidFinishDownloadingBlock:^NSURL *(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, NSURL *location) {
if ([downloadTask.response isKindOfClass:[NSHTTPURLResponse class]]) {
NSInteger statusCode = [(NSHTTPURLResponse *)downloadTask.response statusCode];
if (statusCode != 200) {
// handle error here, e.g.
NSLog(#"%# failed (statusCode = %ld)", [downloadTask.originalRequest.URL lastPathComponent], statusCode);
return nil;
}
}
NSString *filename = [downloadTask.originalRequest.URL lastPathComponent];
NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *path = [documentsPath stringByAppendingPathComponent:filename];
return [NSURL fileURLWithPath:path];
}];
[self setTaskDidCompleteBlock:^(NSURLSession *session, NSURLSessionTask *task, NSError *error) {
if (error) {
// handle error here, e.g.,
NSLog(#"%#: %#", [task.originalRequest.URL lastPathComponent], error);
}
}];
}
- (void)configureBackgroundSessionFinished {
typeof(self) __weak weakSelf = self;
[self setDidFinishEventsForBackgroundURLSessionBlock:^(NSURLSession *session) {
if (weakSelf.savedCompletionHandler) {
weakSelf.savedCompletionHandler();
weakSelf.savedCompletionHandler = nil;
}
}];
}
- (void)configureAuthentication {
NSURLCredential *myCredential = [NSURLCredential credentialWithUser:#"userid" password:#"password" persistence:NSURLCredentialPersistenceForSession];
[self setTaskDidReceiveAuthenticationChallengeBlock:^NSURLSessionAuthChallengeDisposition(NSURLSession *session, NSURLSessionTask *task, NSURLAuthenticationChallenge *challenge, NSURLCredential *__autoreleasing *credential) {
if (challenge.previousFailureCount == 0) {
*credential = myCredential;
return NSURLSessionAuthChallengeUseCredential;
} else {
return NSURLSessionAuthChallengePerformDefaultHandling;
}
}];
}
#end
Make sure app delegate saves completion handler (instantiating the background session as necessary):
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
NSAssert([[BackgroundSessionManager sharedManager].session.configuration.identifier isEqualToString:identifier], #"Identifiers didn't match");
[BackgroundSessionManager sharedManager].savedCompletionHandler = completionHandler;
}
Then start your downloads:
for (NSString *filename in filenames) {
NSURL *url = [baseURL URLByAppendingPathComponent:filename];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[[[BackgroundSessionManager sharedManager] downloadTaskWithRequest:request progress:nil destination:nil completionHandler:nil] resume];
}
Note, I don't supply any of those task related blocks, because those aren't reliable with background sessions. (Background downloads proceed even after the app is terminated and these blocks have long disappeared.) One must rely upon the session-level, easily recreated setDownloadTaskDidFinishDownloadingBlock only.
Clearly this is a simple example (only one background session object; just saving files to the docs folder using last component of URL as the filename; etc.), but hopefully it illustrates the pattern.
It shouldn't make any difference whether or not the callbacks are blocks or not. When you instantiate an AFURLSessionManager, make sure to instantiate it with NSURLSessionConfiguration backgroundSessionConfiguration:. Also, make sure to call the manager's setDidFinishEventsForBackgroundURLSessionBlock with your callback block - this is where you should write the code typically defined in NSURLSessionDelegate's method:
URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session. This code should invoke your app delegate's background download completion handler.
One word of advice regarding background download tasks - even when running in the foreground, their timeouts are ignored, meaning you could get "stuck" on a download that's not responding. This is not documented anywhere and drove me crazy for some time. The first suspect was AFNetworking but even after calling NSURLSession directly, the behaviour remained the same.
Good luck!
AFURLSessionManager
AFURLSessionManager creates and manages an NSURLSession object based on a specified NSURLSessionConfiguration object, which conforms to <NSURLSessionTaskDelegate>, <NSURLSessionDataDelegate>, <NSURLSessionDownloadDelegate>, and <NSURLSessionDelegate>.
link to documentation here documentation

download task is null for the first request when when in backgroundfetch IOS

I was wondering if you have seen this or might have some ideas as to why I see the following behavior in my code: I have an NSURLsession with background config. I initiate periodic download task when the program runs in the Foreground, and everything works. WhenI simulate backgroundfetch (in xcode), my task gets a null value (eventhough the request and the session are not null). of course in this case, my session delegate never gets fired to do completionhandler. if I simulate subsequent background fetches, they all work afterward. at this point, if I bring the app to the foreground in the simulator, and I simulate another backgroundfetch, the symptoms star all over. I am using this code in my appdelegate class.
your help is greatly appreciated.
- (NSURLSession *)FlickrSession
{
if(!_FlickrSession)
{
NSLog(#"setting new FlickrSession");
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:FLICKR_SESSION];
configuration.allowsCellularAccess = NO;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_FlickrSession = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
_FlickrSession.sessionDescription = FLICKR_SESSION;
NSLog(#" new self is %#", _FlickrSession);
NSLog(#"queue in session %#", dispatch_get_current_queue());
});
}
return _FlickrSession;
}
-(void) startFlickrFetch
{
// initialize session config and the background session
[self.FlickrSession getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks)
{
if(![downloadTasks count])
{
NSLog(#"new downloadtask session %#", self.FlickrSession.sessionDescription);
NSURLRequest *request = [NSURLRequest requestWithURL:[FlickrFetcher URLforRecentGeoreferencedPhotos]];
// NSURLSessionDownloadTask *task = [self.FlickrSession downloadTaskWithURL:[FlickrFetcher URLforRecentGeoreferencedPhotos]];
NSURLSessionDownloadTask *task = [self.FlickrSession downloadTaskWithRequest:request];
task.taskDescription = FLICKR_DOWNLOAD_TASK;
NSLog(#"new request %#", request);
NSLog(#"new downloadtask %#", task);
[task resume];
//task?[task resume]:[self fireBackgroungFetchCompletionHandler];;
//[self fireBackgroungFetchCompletionHandler];
NSLog(#"queue in task %#", dispatch_get_current_queue());
}
else
{
NSLog(#"resuming old downloadtask %d", [downloadTasks count]);
for(NSURLSessionDownloadTask *task in dataTasks) [task resume];
}
}];
NSLog(#"queue outside the block %#", dispatch_get_current_queue());
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions: (NSDictionary *)launchOptions
{
// Override point for customization after application launch.
// open the file, if there is no managedContext. this is the case wjere the application was launched directly by user
// it did not come from the bckground state;
//need to enable background fetch
[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
//[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(documentChangedState) name:UIDocumentStateChangedNotification object: self.document];
NSLog(#"in application didfinishlaunching");
[self openDatabaseFile];
[self startFlickrFetch];
return YES;
}
-(void) application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
self.backgroundFetchCompletionHandler = completionHandler;
if(self.document.documentState == UIDocumentStateNormal)
{
//[self openDatabaseFile];
NSLog(#"in performFetchWithCompletionHandler");
[self startFlickrFetch];
}
}
- (void) application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler
{
self.completionhandler = completionHandler;
NSLog(#"handle event for backgroundURLsession***********");
}
This could have something to do with a strange interaction of the background fetch and a background session, in which case I have no advice.
However, in the background, iOS doesn't know to wait for async calls, e.g. getTasksWithCompletionHandler. You can solve this by wrapping those calls with a UIBackgroundTaskIdentifier†-based [UIApplication] begin/end task (in this case, with the "end (app) task" inside the "get (session) tasks completion handler" block).
But if all you need is a count, here's what I did, which I think is simpler:
Create an ivar:
NSMutableSet *activeTaskIDs;
When you create a task, add it to the set:
[activeTaskIDs addObject:#(task.taskIdentifier)];
When the task completes, remove it.
You can get your count from there, no async.
† Confusingly, a different kind of task. I differentiate with the terms "app tasks" vs. "session tasks".

sendAsynchronousRequest makes UI freezes

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 :)

Run multiple instances of NSOperation with NSURLConnection?

We have a large project that needs to sync large files from a server into a 'Library' in the background. I read subclassing NSOperation is the most flexible way of multithreading iOS tasks, and attempted that. So the function receives a list of URLs to download & save, initialises an instance of the same NSOperation class and adds each to an NSOperation queue (which should download only 1 file at a time).
-(void) LibSyncOperation {
// Initialize download list. Download the homepage of some popular websites
downloadArray = [[NSArray alloc] initWithObjects:#"www.google.com",
#"www.stackoverflow.com",
#"www.reddit.com",
#"www.facebook.com", nil];
operationQueue = [[[NSOperationQueue alloc]init]autorelease];
[operationQueue setMaxConcurrentOperationCount:1]; // Only download 1 file at a time
[operationQueue waitUntilAllOperationsAreFinished];
for (int i = 0; i < [downloadArray count]; i++) {
LibSyncOperation *libSyncOperation = [[[LibSyncOperation alloc] initWithURL:[downloadArray objectAtIndex:i]]autorelease];
[operationQueue addOperation:libSyncOperation];
}
}
Now, those class instances all get created fine, and are all added to the NSOperationQueue and begin executing. BUT the issue is when it's time to start downloading, the first file never begins downloading (using an NSURLConnection with delegate methods). I've used the runLoop trick I saw in another thread which should allow the operation to keep running until the download is finished. The NSURLConnection is established, but it never starts appending data to the NSMutableData object!
#synthesize downloadURL, downloadData, downloadPath;
#synthesize downloadDone, executing, finished;
/* Function to initialize the NSOperation with the URL to download */
- (id)initWithURL:(NSString *)downloadString {
if (![super init]) return nil;
// Construct the URL to be downloaded
downloadURL = [[[NSURL alloc]initWithString:downloadString]autorelease];
downloadData = [[[NSMutableData alloc] init] autorelease];
NSLog(#"downloadURL: %#",[downloadURL path]);
// Create the download path
downloadPath = [NSString stringWithFormat:#"%#.txt",downloadString];
return self;
}
-(void)dealloc {
[super dealloc];
}
-(void)main {
// Create ARC pool instance for this thread.
// NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init]; //--> COMMENTED OUT, MAY BE PART OF ISSUE
if (![self isCancelled]) {
[self willChangeValueForKey:#"isExecuting"];
executing = YES;
NSURLRequest *downloadRequest = [NSURLRequest requestWithURL:downloadURL];
NSLog(#"%s: downloadRequest: %#",__FUNCTION__,downloadURL);
NSURLConnection *downloadConnection = [[NSURLConnection alloc] initWithRequest:downloadRequest delegate:self startImmediately:NO];
// This block SHOULD keep the NSOperation from releasing before the download has been finished
if (downloadConnection) {
NSLog(#"connection established!");
do {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
} while (!downloadDone);
} else {
NSLog(#"couldn't establish connection for: %#", downloadURL);
// Cleanup Operation so next one (if any) can run
[self terminateOperation];
}
}
else { // Operation has been cancelled, clean up
[self terminateOperation];
}
// Release the ARC pool to clean out this thread
//[pool release]; //--> COMMENTED OUT, MAY BE PART OF ISSUE
}
#pragma mark -
#pragma mark NSURLConnection Delegate methods
// NSURLConnectionDelegate method: handle the initial connection
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSHTTPURLResponse*)response {
NSLog(#"%s: Received response!", __FUNCTION__);
}
// NSURLConnectionDelegate method: handle data being received during connection
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[downloadData appendData:data];
NSLog(#"downloaded %d bytes", [data length]);
}
// NSURLConnectionDelegate method: What to do once request is completed
-(void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(#"%s: Download finished! File: %#", __FUNCTION__, downloadURL);
NSFileManager *fileManager = [NSFileManager defaultManager];
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *docDir = [paths objectAtIndex:0];
NSString *targetPath = [docDir stringByAppendingPathComponent:downloadPath];
BOOL isDir;
// If target folder path doesn't exist, create it
if (![fileManager fileExistsAtPath:[targetPath stringByDeletingLastPathComponent] isDirectory:&isDir]) {
NSError *makeDirError = nil;
[fileManager createDirectoryAtPath:[targetPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:&makeDirError];
if (makeDirError != nil) {
NSLog(#"MAKE DIR ERROR: %#", [makeDirError description]);
[self terminateOperation];
}
}
NSError *saveError = nil;
//NSLog(#"downloadData: %#",downloadData);
[downloadData writeToFile:targetPath options:NSDataWritingAtomic error:&saveError];
if (saveError != nil) {
NSLog(#"Download save failed! Error: %#", [saveError description]);
[self terminateOperation];
}
else {
NSLog(#"file has been saved!: %#", targetPath);
}
downloadDone = true;
}
// NSURLConnectionDelegate method: Handle the connection failing
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(#"%s: File download failed! Error: %#", __FUNCTION__, [error description]);
[self terminateOperation];
}
// Function to clean up the variables and mark Operation as finished
-(void) terminateOperation {
[self willChangeValueForKey:#"isFinished"];
[self willChangeValueForKey:#"isExecuting"];
finished = YES;
executing = NO;
downloadDone = YES;
[self didChangeValueForKey:#"isExecuting"];
[self didChangeValueForKey:#"isFinished"];
}
#pragma mark -
#pragma mark NSOperation state Delegate methods
// NSOperation state methods
- (BOOL)isConcurrent {
return YES;
}
- (BOOL)isExecuting {
return executing;
}
- (BOOL)isFinished {
return finished;
}
NOTE: If that was too unreadable, I set up a QUICK GITHUB PROJECT HERE you can look through. Please note I'm not expecting anyone to do my work for me, simply looking for an answer to my problem!
I suspect it has something to do with retaining/releasing class variables, but I can't be sure of that since I thought instantiating a class would give each instance its own set of class variables. I've tried everything and I can't find the answer, any help/suggestions would be much appreciated!
UPDATE: As per my answer below, I solved this problem a while ago and updated the GitHub project with the working code. Hopefully if you've come here looking for the same thing it helps!
In the interests of good community practice and helping anyone else who might end up here with the same problem, I did end up solving this issue and have updated the GitHub sample project here that now works correctly, even for multiple concurrent NSOperations!
It's best to look through the GitHub code since I made a large amount of changes, but the key fix I had to make to get it working was:
[downloadConnection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
This is called after the NSURLConnection is initialized, and just before it is started. It attaches the execution of the connection to the current main run loop so that the NSOperation won't prematurely terminate before the download is finished. I'd love to give credit to wherever first posted this clever fix, but it's been so long I've forgotten where, apologies. Hope this helps someone!

Resources