EDIT2 - Rewrote the question
I want to do some web service communication in the background. I am using Sudzc as the handler of HTTPRequests and it works like this:
SudzcWS *service = [[SudzcWS alloc] init];
[service sendOrders:self withXML:#"my xml here" action:#selector(handleOrderSending:)];
[service release];
It sends some XML to the webservice, and the response (in this one, a Boolean) is handled in the selector specified:
- (void)handleOrderSending:(id)value
{
//some controls
if ([value boolValue] == YES)
{
//my stuff
}
}
When I tried to use Grand Central Dispatch on my sendOrders:withXML:action: method, I noticed that the selector is not called. And I believe the reason for that is that NSURLConnection delegate messages are sent to the thread of which the connection is created But the thread does not live that long, it ends when the method finishes, killing any messages to the delegate.
Regards
EDIT1
[request send] method:
- (void) send {
//dispatch_async(backgroundQueue, ^(void){
// If we don't have a handler, create a default one
if(handler == nil) {
handler = [[SoapHandler alloc] init];
}
// Make sure the network is available
if([SoapReachability connectedToNetwork] == NO) {
NSError* error = [NSError errorWithDomain:#"SudzC" code:400 userInfo:[NSDictionary dictionaryWithObject:#"The network is not available" forKey:NSLocalizedDescriptionKey]];
[self handleError: error];
}
// Make sure we can reach the host
if([SoapReachability hostAvailable:url.host] == NO) {
NSError* error = [NSError errorWithDomain:#"SudzC" code:410 userInfo:[NSDictionary dictionaryWithObject:#"The host is not available" forKey:NSLocalizedDescriptionKey]];
[self handleError: error];
}
// Output the URL if logging is enabled
if(logging) {
NSLog(#"Loading: %#", url.absoluteString);
}
// Create the request
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL: url];
if(soapAction != nil) {
[request addValue: soapAction forHTTPHeaderField: #"SOAPAction"];
}
if(postData != nil) {
[request setHTTPMethod: #"POST"];
[request addValue: #"text/xml; charset=utf-8" forHTTPHeaderField: #"Content-Type"];
[request setHTTPBody: [postData dataUsingEncoding: NSUTF8StringEncoding]];
if(self.logging) {
NSLog(#"%#", postData);
}
}
//dispatch_async(dispatch_get_main_queue(), ^(void){
// Create the connection
conn = [[NSURLConnection alloc] initWithRequest: request delegate: self];
if(conn) {
NSLog(#" POST DATA %#", receivedData);
receivedData = [[NSMutableData data] retain];
NSLog(#" POST DATA %#", receivedData);
} else {
// We will want to call the onerror method selector here...
if(self.handler != nil) {
NSError* error = [NSError errorWithDomain:#"SoapRequest" code:404 userInfo: [NSDictionary dictionaryWithObjectsAndKeys: #"Could not create connection", NSLocalizedDescriptionKey,nil]];
[self handleError: error];
}
}
//});
//finished = NO;
// while(!finished) {
//
// [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
//
// }
//});
}
The parts that are commented out are the various things I tried. The last part worked but I'M not sure if that's a good way. In the NURLConnection delegate method of the class, here is what happens:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSError* error;
if(self.logging == YES) {
NSString* response = [[NSString alloc] initWithData: self.receivedData encoding: NSUTF8StringEncoding];
NSLog(#"%#", response);
[response release];
}
CXMLDocument* doc = [[CXMLDocument alloc] initWithData: self.receivedData options: 0 error: &error];
if(doc == nil) {
[self handleError:error];
return;
}
id output = nil;
SoapFault* fault = [SoapFault faultWithXMLDocument: doc];
if([fault hasFault]) {
if(self.action == nil) {
[self handleFault: fault];
} else {
if(self.handler != nil && [self.handler respondsToSelector: self.action]) {
[self.handler performSelector: self.action withObject: fault];
} else {
NSLog(#"SOAP Fault: %#", fault);
}
}
} else {
CXMLNode* element = [[Soap getNode: [doc rootElement] withName: #"Body"] childAtIndex:0];
if(deserializeTo == nil) {
output = [Soap deserialize:element];
} else {
if([deserializeTo respondsToSelector: #selector(initWithNode:)]) {
element = [element childAtIndex:0];
output = [deserializeTo initWithNode: element];
} else {
NSString* value = [[[element childAtIndex:0] childAtIndex:0] stringValue];
output = [Soap convert: value toType: deserializeTo];
}
}
if(self.action == nil) { self.action = #selector(onload:); }
if(self.handler != nil && [self.handler respondsToSelector: self.action]) {
[self.handler performSelector: self.action withObject: output];
} else if(self.defaultHandler != nil && [self.defaultHandler respondsToSelector:#selector(onload:)]) {
[self.defaultHandler onload:output];
}
}
[self.handler release];
[doc release];
[conn release];
conn = nil;
[self.receivedData release];
}
The delegate is unable to send messages because the thread it is dies when -(void)send finishes.
The method definition for sendOrders suggests that it is already designed to execute requests in an asynchronous fashion. You should have a look into the implementation of sendOrders: withXML: action: to find out if this is the case.
Without seeing your implementation using GCD or the code from SudzcWS it's hard to say what's going wrong. Despite the preceding caveats, the following might be of use.
It looks like you may be releasing SudzcWS *service before it is completed.
The following:
SudzcWS *service = [[SudzcWS alloc] init];
dispatch_async(aQueue, ^{
[sevice sendOrders:self withXML:xml action:#selector(handleOrderSending:)];
}
[service release];
could fail unless SudzcWS retains itself. You dispatch your block asynchronously, it gets put in a queue, and execution of the method continues. service is released and gets deallocated before the block executes or while service is waiting for a response from the webserver.
Unless otherwise specified, calling a selector will execute that selector on the same thread it is called on. Doing something like:
SudzcWS *service = [[SudzcWS alloc] init];
dispatch_async(aQueue, ^{
[sevice sendOrders:self withXML:xml action:#selector(handleOrderSending:)];
}
- (void)handleOrderSending:(id)value
{
//some controls
//your stuff
[service release];
}
should ensure that both the sendOrders: method and the handleOrderSending: are executed on the queue aQueue and that service is not released until it has executed the selector.
This will require you to keep a pointer to service so that handleOrderSending: can release it. You might also want to consider simply hanging onto a single SudzcWS instance instead of creating and releasing one each time you want to use it, this should make your memory management much easier and will help keep your object graph tight.
I've had help from both these links SO NURLConnection question and the original one.
It does not seem risky for my code and I will use it at my own risk. Thanks.
Any recommendations are still welcome of course.
Additional thanks to Pingbat for taking the time to try and help.
Related
i am new to iOS programming, still learning.
EDIT: !!!!!! Everything in my code works. My question is about the delegation pattern i use,
if i am generating problems in the background that i have no idea of, or if there is a better way to handle my situation in AFNetworking...
I have created an API for my app by subclassing AFHTTPSessionManager.
My API creates a singleton and returns it and supplies public functions for various requests. And those functions create parameter lists, and make GET requests on the server like this:
- (void)getCharacterListForKeyID:(NSString *)keyID vCode:(NSString *)vCode sender:(id)delegate
{
NSMutableDictionary *parameters = [NSMutableDictionary dictionary];
parameters[#"keyID"] = keyID;
parameters[#"vCode"] = vCode;
[self GET:#"account/Characters.xml.aspx" parameters:parameters success:^(NSURLSessionDataTask *task, id responseObject) {
self.xmlWholeData = [NSMutableDictionary dictionary];
self.errorDictionary = [NSMutableDictionary dictionary];
NSXMLParser *XMLParser = (NSXMLParser *)responseObject;
[XMLParser setShouldProcessNamespaces:YES];
XMLParser.delegate = self;
[XMLParser parse];
if ([delegate respondsToSelector:#selector(EVEAPIHTTPClient:didHTTPRequestWithResult:)]) {
[delegate EVEAPIHTTPClient:self didHTTPRequestWithResult:self.xmlWholeData];
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
if ([delegate respondsToSelector:#selector(EVEAPIHTTPClient:didFailWithError:)]) {
[delegate EVEAPIHTTPClient:self didFailWithError:error];
}
}];
}
I was using a normal protocol/delegate method earlier. But once i make calls this API more than once like this: (IT WAS LIKE THIS:)
EVEAPIHTTPClient *client = [EVEAPIHTTPClient sharedEVEAPIHTTPClient];
client.delegate = self;
[client getCharacterListForKeyID:self.keyID vCode:self.vCode];
Previous call's delegate was being overwritten by next. So i changed to above style. Passing sender as an argument in the function:
EVEAPIHTTPClient *client = [EVEAPIHTTPClient sharedEVEAPIHTTPClient];
[client getCharacterListForKeyID:self.keyID vCode:self.vCode sender:self];
And i pass this sender to GET request's success and failure blocks.
What i wonder is : "Is this a good programming practice ?". Passing objects to blocks like this should be avoided if possible ? Is there any other more elegant way in AFHTTPSessionManager to handle this type of work (making same GET request over and over with different parameters and returning results to the respective request owners) more elegantly ?
Delegation pattern falters when it comes to simplicity and asynchronous request processing. You should be using blocks, here's an example
Your server class:
static NSString *const kNews = #"user_news/"; // somewhere above the #implementation
- (NSURLSessionDataTask *)newsWithPage:(NSNumber *)page
lastNewsID:(NSNumber *)lastNewsID
completion:(void (^)(NSString *errMsg, NSArray *news, NSNumber *nextPage))completionBlock {
return [self GET:kNews
parameters:#{#"page" : page,
#"news_id" : lastNewsID
}
success:^(NSURLSessionDataTask *task, id responseObject) {
NSArray *news = nil;
NSNumber *nextPage = nil;
NSString *errors = [self errors:responseObject[#"errors"]]; // process errors
if ([responseObject[#"status"] boolValue]) {
news = responseObject[#"news"];
nextPage = responseObject[#"next_page"];
[self assignToken];
}
completionBlock(errors, news, nextPage);
}
failure:^(NSURLSessionDataTask *task, NSError *error) {
NSString *errors = [self errors:error];
completionBlock(errors, nil, nil);
}];
}
The caller
- (void)dealloc {
[_task cancel]; // you don't want this task to execute if user suddenly removes your controller from the navigation controller's stack
}
- (void)requestNews {
typeof(self) __weak wself = self; // to avoid the retain cycle
self.task = [[GSGServer sharedInstance] newsWithPage:self.page
lastNewsID:self.lastNewsID
completion:^(NSString *errMsg, NSArray *news, NSNumber *nextPage) {
if (errMsg) {
[GSGAppDelegate alertQuick:errMsg]; // shortcut for posting UIAlertView, uses errMsg for message and "Error" as a title
return;
}
[wself.news addObjectsFromArray:news];
wself.lastNewsID = [wself.news firstObject][#"id"];
wself.page = nextPage;
[wself.tableView reloadData];
}];
}
I use the following method to attempt to synchronously obtain an OAuth access token within 10 seconds, otherwise return nil. It works fine, however as an exercise I would like to convert my code to use a semaphore.
The Runloop version
- (NSString*)oAuthAccessToken
{
#synchronized (self)
{
NSString* token = nil;
_authenticationError = nil;
if (_authentication.accessToken)
{
token = [NSString stringWithFormat:#"Bearer %#", _authentication.accessToken];
}
else
{
[GTMOAuth2ViewControllerTouch authorizeFromKeychainForName:_keychainName authentication:_authentication];
[_authentication authorizeRequest:nil delegate:self didFinishSelector:#selector(authentication:request:finishedWithError:)];
for (int i = 0; i < 5; i++)
{
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
if (_authentication.accessToken)
{
token = [NSString stringWithFormat:#"Bearer %#", _authentication.accessToken];
break;
}
else if (_authenticationError)
{
break;
}
}
}
// LogDebug(#"Returning token: %#", token);
return token;
}
}
Semaphore Version
The semaphore version of the code goes a little something like this:
- (NSString*)oAuthAccessToken
{
#synchronized (self)
{
NSString* token = nil;
_authenticationError = nil;
if (_authentication.accessToken)
{
token = [NSString stringWithFormat:#"Bearer %#", _authentication.accessToken];
}
else
{
_authorizationSemaphore = dispatch_semaphore_create(0);
dispatch_async(_authorizationRequestQueue, ^(void)
{
[GTMOAuth2ViewControllerTouch authorizeFromKeychainForName:_keychainName authentication:_authentication];
[_authentication authorizeRequest:nil delegate:self didFinishSelector:#selector(authentication:request:finishedWithError:)];
});
dispatch_semaphore_wait(_authorizationSemaphore, DISPATCH_TIME_FOREVER);
if (_authentication.accessToken)
{
token = [NSString stringWithFormat:#"Bearer %#", _authentication.accessToken];
}
}
return token;
}
}
Gotcha!!! GTMOAuth2 sometimes returns immediately
When GTMOAuth2 needs to hit the network it calls back via a delegate method. In this method I signal my semaphore.
Sometimes GTMOAuth2 is able to return immediately. The problem is the method returns void.
How can I signal my semaphore in the latter case? If I add an observer to the authentication.assessToken will it be fired?
I'm not familiar with the GTMOAuth2 library but authentication.accessToken is a property, so and there doesn't seem to be anything that prevents it from being KVO compliant. Adding an observer should work for you in all cases, both for the async and the sync. Therefore, I'd consider only the async case.
If you want to make your solution even cleaner, then you should definitely try Reactive Cocoa.
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 trying to write a wrapper for RestKit, so that all requests call one function which in turn would trigger a request via RestKit.
Here's what I have so far:
A function would call my wrapper as follows:
NSDictionary *response = [Wrappers sendRequestWithURLString:url method:#"GET"];
And my wrapper methods:
+ (NSDictionary *)sendRequestWithURLString:(NSString *)request method:(NSString *)method
{
RKRequestDidFailLoadWithErrorBlock failBlock;
if ([method isEqualToString:#"GET"])
return [self sendGETRequestWithURLString:request withFailBlock:failBlock];
else if ([method isEqualToString:#"POST"])
return [self sendPOSTRequestWithURLString:request withFailBlock:failBlock];
return nil;
}
+ (NSDictionary *)sendGETRequestWithURLString:(NSString *)request withFailBlock:(RKRequestDidFailLoadWithErrorBlock)failBlock {
RKObjectManager *manager = [RKObjectManager sharedManager];
__block NSDictionary *responseDictionary;
[manager loadObjectsAtResourcePath:request usingBlock:^(RKObjectLoader *loader) {
loader.onDidLoadResponse = ^(RKResponse *response) {
[self fireErrorBlock:failBlock onErrorInResponse:response];
RKJSONParserJSONKit *parser = [RKJSONParserJSONKit new];
responseDictionary = [[NSDictionary alloc] initWithDictionary:[parser objectFromString:[response bodyAsString] error:nil]];
};
}];
return responseDictionary;
}
+ (void)fireErrorBlock:(RKRequestDidFailLoadWithErrorBlock)failBlock onErrorInResponse:(RKResponse *)response {
if (![response isOK]) {
id parsedResponse = [response parsedBody:NULL];
NSString *errorText = nil;
if ([parsedResponse isKindOfClass:[NSDictionary class]]) {
errorText = [parsedResponse objectForKey:#"error"];
}
if (errorText)
failBlock([self errorWithMessage:errorText code:[response statusCode]]);
}
}
+ (NSError *)errorWithMessage:(NSString *)errorText code:(NSUInteger)statusCode {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Error"
message:#"Please make sure you are connected to WiFi or 3G."
delegate:nil
cancelButtonTitle:#"OK"
otherButtonTitles:nil];
[alert show];
return nil;
}
The problem here is when responseDictionary returns, the value is nil since onDidLoadResponse would not have processed yet as it runs concurrently.
In this case, what would be the best approach in setting responseDictionary? I'm trying to avoid calling a setter method of another class. In this case, is my only option using delegates, which defeats the whole purpose of creating a wrapper class since RestKit calls require usage of delegate methods to return the response?
Would I be able to pass my wrapper a success block which would update some local ivar? How would I do that?
You pass a success block as you have said. Here is an example of how to do that:
.h
typedef void (^kServiceCompleteBlock)(NSDictionary* responseDictionary);
...
+ (NSDictionary *)sendGETRequestWithURLString:(NSString *)request withFailBlock:(RKRequestDidFailLoadWithErrorBlock)failBlock completion: (kServiceCompleteBlock) completion ;
...
.m
+ (NSDictionary *)sendGETRequestWithURLString:(NSString *)request withFailBlock:(RKRequestDidFailLoadWithErrorBlock)failBlock completion: (kServiceCompleteBlock) completion {
...
loader.onDidLoadResponse = ^(RKResponse *response) {
[self fireErrorBlock:failBlock onErrorInResponse:response];
RKJSONParserJSONKit *parser = [RKJSONParserJSONKit new];
responseDictionary = [[NSDictionary alloc] initWithDictionary:[parser objectFromString:[response bodyAsString] error:nil]];
if( completion )
completion(responseDictionary);
};
...
}
However, let me warn you of a potential design flaw. You should design your app so that the UI is driven by your data and not your web service. This means your UI should automatically update when your data model is updated and not when your web service returns. Be careful how you use this response dictionary. If it is to update UI then you are running down a dangerous road.
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!