How can I get NSURLSession to process completionHandler when running as a command-line tool - objective-c

I am somewhat new to Objective-C. I have some PERL scripts that download files from a site that requires client certificate authentication. I would like to port those scripts from PERL to Objective-C command line tools that I can then run from Terminal. Based on my research I have concluded that NSURLSession should work for what I need to do. However, I have been unable to get NSURLSession to work in my command line tool. It seems to build fine with no errors, but does not return anything from the completionHandler. Further, I have put this same code into a Mac OS X App tool and it seems to work fine.
Here is my main.m file:
#import <Foundation/Foundation.h>
#import "RBC_ConnectDelegate.h"
int main(int argc, const char * argv[])
{
#autoreleasepool {
// insert code here...
NSURL *myURL =[NSURL URLWithString:#"http://google.com"];
//[[[RBC_Connect alloc] init] connectGetURLSynch];
RBC_ConnectDelegate *myConnect = [[RBC_ConnectDelegate alloc] init];
[myConnect GetURL2: myURL];
}
return 0;
}
Here is my Implementation File:
#import <Foundation/Foundation.h>
#interface RBC_ConnectDelegate : NSObject
- (void)GetURL2:(NSURL *)myURL;
#property(nonatomic,assign) NSMutableData *receivedData;
//<==== note use assign, not retain
//do not release receivedData in a the dealloc method!
#end
And here is my implementation file:
#import "RBC_ConnectDelegate.h"
#implementation RBC_ConnectDelegate
- (void)GetURL2:(NSURL *)myURL2{
//create semaphore
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
// Create the request.
NSLog(#"Creating Request");
NSURLRequest *theRequest =
[NSURLRequest requestWithURL:myURL2
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:10.0];
NSLog(#"Creating Session");
NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate: nil delegateQueue: [NSOperationQueue mainQueue]];
NSLog(#"Initializing Data Task");
NSURLSessionDataTask * dataTask = [defaultSession dataTaskWithRequest:theRequest
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSLog(#"CompletionHandler");
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
NSInteger myStatusCode = [(NSHTTPURLResponse *) response statusCode];
NSLog(#"Status Code: %ld", (long)myStatusCode);
}
if(error == nil)
{
NSString * text = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
NSLog(#"Data = %#",text);
}
else
{
NSLog(#"Error");
}
dispatch_semaphore_signal(semaphore);
}];
NSLog(#"Resuming Data Task");
[dataTask resume];
}
#end
As you can see, I am trying to get something very simple working here first, with the idea that I can then build on it. Everything I have looked at suggests this may be related to the fact that NSURLSession runs asynchronously but I have been unable to find a solution that speaks specifically to how to address this issue when building a command-line tool. Any direction anyone can provide would be appreciated.
cheers,

NSOperationQueue's reference says that for +mainQueue, the main thread's run loop controls the execution of operations, so you need a run loop. Add this to the end of your main:
while (1)
{
SInt32 res = 0;
#autoreleasepool
{
res = CFRunLoopRunInMode(kCFRunLoopDefaultMode, DBL_MAX, false);
}
if (kCFRunLoopRunStopped == res || kCFRunLoopRunFinished == res)
break;
}
To call -GetURL2:, use -[NSOperationQueue addOperation:] or dispatch_async. You can stop execution with CFRunLoopStop. I tested with GCD instead of NSOperations but this should work anyway.
Also, you need to call dispatch_semaphore_wait if you want to synchronize after the URL session is complete.

Related

Objective C: replacing a synchronous request

I'm having problem replacing this code.
-(NSMutableArray *) GetPrices: {
NSError *error;
NSURLResponse *response;
NSData *tw_result = [NSURLConnection
sendSynchronousRequest:urlRequest
returningResponse:&response error:&error];
The problem I have is that the function that calls this code process the url and then returns data to a method that calls it.
Previously I used this like so.
ViewController calls a function to gather data by creating an operation queue (so that the UI & main thread are available)
NSOperationQueue *myQueue = [NSOperationQueue new];
NSInvocationOperation *operation = [[NSInvocationOperation alloc]
initWithTarget:self selector:#selector(loadDataWithOperation) object:nil];
[myQueue addOperation:operation];
[operation release];
[myQueue release];
The function in the operation queue calls the method to get data on an object and that method then runs the synchronous URLrequest.
-(void)loadDataWithOperation {
self.sectionPriceArray = [self.myObject GetPrices];
So myObject would return a price array.
I have tried using NSSession but but I can't figure out how to pass the result back as the method terminates prior to getting tw_result from the completion Handler.
Any thoughts I have to do this in Objective C as I don't have permission from the client to convert to swift.
EDIT of Question with more details:
Inside my GetPrices method I have tried
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithRequest:urlRequest
completionHandler:^(NSData *tw_result,
NSURLResponse *response,
NSError *error) {
result = [[NSString alloc] initWithData:tw_result encoding:NSUTF8StringEncoding];
NSArray *resultArray = [result componentsSeparatedByString:#"\n"];
}) resume];
But I cannot figure out how to make this work one level up at the calling level.
As #maddy mentioned you're going to want to use a completion block for your getPrices method instead of a return -- returns + async don't mix.
This would be the general form to convert your getPrices method to:
- (void)_getPricesWithCompletion:(void(^)(NSMutableArray *sectionPriceArray))priceCompletion;
This site: http://goshdarnblocksyntax.com has some of the common block syntax declaration usages.
Typically you'd call this async method and then set your iVar in the completion block and reload your associated UI elements after receiving the new data. Something along these lines:
[self _getPricesWithCompletion:^(NSMutableArray *sectionPriceArray) {
self.sectionPriceArray = sectionPriceArray;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// reload any UI elements dependent on your sectionPriceArray here
}];
}];
Now in the example code you show, it seems like you're using an NSOperationQueue to queue up different operations. Things can get a bit more complicated here. Subsequent queue'd operations won't wait on your async operations to finish before executing. So for example, if you have an operation after the getPrices operation which utilizes the result of the fetch of the prices, the iVar will almost definitely not contain the correct data at that point. In this case you'd need to use some sort of semaphore to handle waiting for the async operation to complete before continuing to the operation which depends upon it.
Here's an example of what I mean:
NotProperlyWaiting.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
#interface NotProperlyWaiting : NSObject
#property (strong, nullable) NSMutableArray *sectionPriceArray;
- (void)callOperations;
- (void)fakeServerCallWithCompletion:(void(^)(NSData *tw_result, NSURLResponse *response, NSError *error))completion;
#end
NotProperlyWaiting.m
#import "NotProperlyWaiting.h"
#interface NotProperlyWaiting()
- (void)_getPricesWithCompletion:(void(^)(NSMutableArray *sectionPriceArray))priceCompletion;
- (void)_printPricesArray;
#end
#implementation NotProperlyWaiting
- (instancetype)init {
self = [super init];
if (self) {
_sectionPriceArray = [NSMutableArray array];
}
return self;
}
- (void)callOperations {
// setup our completion block to be passed in (this is what will eventually set the self.sectionPricesArray
void (^pricesCompletion)(NSMutableArray *) = ^ void (NSMutableArray *sectionPricesArrayFromCompletion){
self.sectionPriceArray = sectionPricesArrayFromCompletion;
};
NSOperationQueue *myQueue = [NSOperationQueue new];
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(_getPricesWithCompletion:) object:pricesCompletion];
NSInvocationOperation *printOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(_printPricesArray) object:nil];
[myQueue addOperation:operation];
[myQueue addOperation:printOperation];
}
- (void)_getPricesWithCompletion:(void(^)(NSMutableArray *sectionPricesArray))priceCompletion {
[self fakeServerCallWithCompletion:^(NSData *tw_result, NSURLResponse *response, NSError *error) {
// check the error or response or whatever else to verify that the data is legit from your server endpoint here
// then convert the data to your mutable array and pass it through to our completion block
NSString *stringData = [[NSString alloc] initWithData:tw_result encoding:NSUTF8StringEncoding];
NSMutableArray *tempPricesArray = [NSMutableArray arrayWithArray:[stringData componentsSeparatedByString:#"\n"]];
// now our completion block passing in the result prices array
priceCompletion(tempPricesArray);
}];
}
- (void)_printPricesArray {
NSLog(#"NotWaiting -- Prices array : %#", self.sectionPriceArray);
}
// this is a fake version of NSURLSession
- (void)fakeServerCallWithCompletion:(void(^)(NSData *tw_result, NSURLResponse *response, NSError *error))completion {
NSString *fakeServerResponse = #"FirstThing\nSecondThing\nThirdThing";
NSData *fakeData = [fakeServerResponse dataUsingEncoding:NSUTF8StringEncoding];
NSURLResponse *fakeResponse = [[NSURLResponse alloc] init];
NSError *fakeError = [NSError errorWithDomain:#"FakeErrorDomain" code:33 userInfo:nil];
// never call sleep in your own code, this is just to simulate the wait time for the server to return data
sleep(3);
completion(fakeData,fakeResponse,fakeError);
}
NS_ASSUME_NONNULL_END
ProperlyWaiting.h (Subclass of NotProperlyWaiting.h to re-use callOperation and fakeServerCallWithCompletion:)
#import "NotProperlyWaiting.h"
NS_ASSUME_NONNULL_BEGIN
#interface ProperlyWaiting : NotProperlyWaiting
#end
NS_ASSUME_NONNULL_END
ProperlyWaiting.m
#import "ProperlyWaiting.h"
#interface ProperlyWaiting()
- (void)_getPricesWithCompletion:(void(^)(NSMutableArray *sectionPricesArray))priceCompletion;
- (void)_printPricesArray;
#property dispatch_semaphore_t semaphore;
#end
#implementation ProperlyWaiting
- (void)callOperations {
self.semaphore = dispatch_semaphore_create(0);
[super callOperations];
}
// identical implementations to NotProperlyWaiting, but this time we'll use a semaphore to ensure the _printPricesArray waits for the async operation to complete before continuing
- (void)_getPricesWithCompletion:(void(^)(NSMutableArray *sectionPricesArray))priceCompletion {
[self fakeServerCallWithCompletion:^(NSData *tw_result, NSURLResponse *response, NSError *error) {
// check the error or response or whatever else to verify that the data is legit from your server endpoint here
// then convert the data to your mutable array and pass it through to our completion block
NSString *stringData = [[NSString alloc] initWithData:tw_result encoding:NSUTF8StringEncoding];
NSMutableArray *tempPricesArray = [NSMutableArray arrayWithArray:[stringData componentsSeparatedByString:#"\n"]];
// now our completion block passing in the result prices array
priceCompletion(tempPricesArray);
// signal our semaphore to let it know we're done
dispatch_semaphore_signal(self.semaphore);
}];
}
- (void)_printPricesArray {
// wait for the semaphore signal before continuing (so we know the async operation we're waiting on has completed)
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
NSLog(#"Waiting -- Prices array : %#", self.sectionPriceArray);
}
#end
With example calls of the class like this:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NotProperlyWaiting *notWaiting = [[NotProperlyWaiting alloc] init];
[notWaiting callOperations];
ProperlyWaiting *waiting = [[ProperlyWaiting alloc] init];
[waiting callOperations];
}
The output in the log will be:
NotWaiting -- Prices array : (
)
And then 3 seconds later:
Waiting -- Prices array : (
FirstThing,
SecondThing,
ThirdThing
)
Some additional links to helpful documentation related to this topic:
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithBlocks/WorkingwithBlocks.html
https://developer.apple.com/documentation/dispatch/1452955-dispatch_semaphore_create

Data transfer between NSOperations

I would like to obtain the following: I have two NSOperations in a NSOperationQueue. The firs is a download from a website (gets some json data) the next is parsing that data. This are dependent operations.
I don't understand how to link them together. If they are both allocated and in the queue, how do I transfer the json string to the operation that parses it? Is it a problem if this queue is inside another NSOperationQueue that executes an NSOperation that consists of the two mentioned previously?
All I could find is transfers of data to a delegate on the main thread (performSelectorOnMainThread), but I need all this operations to execute in the background.
Thanks.
Code:
NSDownload : NSOperation
- (instancetype)initWithURLString:(NSString *)urlString andDelegate:(id<JSONDataDelegate>)delegate
{
self = [super init];
if (self) {
_urlStr = urlString;
_delegate = delegate; /// this needs to be a NSOPeration
_receivedData = [NSMutableData dataWithCapacity:256];
}
return self;
}
#pragma mark - OVERRIDE
- (void)main
{
#autoreleasepool {
if (self.isCancelled) {
return;
}
NSURL *url = [NSURL URLWithString:self.urlStr];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
self.urlConnection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES];
}
}
#pragma mark - NSURLConnectionDataDelegate
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
if (self.isCancelled) {
[connection cancel];
self.receivedData = nil;
return;
}
[self.receivedData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
if (self.isCancelled) {
self.receivedData = nil;
return;
}
// return data to the delegate
NSDictionary *responseDict = #{JSON_REQUESTED_URL : self.urlStr,
JSON_RECEIVED_RESPONSE : self.receivedData};
[(NSObject *)self.delegate performSelectorOnMainThread:#selector(didReceiveJSONResponse:) withObject:responseDict waitUntilDone:NO]; // ok to uses performSelector as this data is not for use on the main thread ???
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
// return error to the delegate
[(NSObject *)self.delegate performSelectorOnMainThread:#selector(didFailToReceiveDataWithError:) withObject:error waitUntilDone:NO];
}
#user1028028:
Use the following approach.
1) Maintain the operation queue reference that you are using to add DownloadOperation.
2) In connectionDidFinishLoading method, create ParseOperation instance, set the json data and add it to operation queue. Maintain ParseOperation strong reference variable in DownloadOperation and handling of cancelling of parsing operation through DownloadOperation interface.
3) After completed parsing call the UI functionality in main thread.
I hope this helps.
As lucianomarisi notes, it would usually be best to just have the first operation generate the second operation. This is usually simpler to manage. Operation dependencies aren't really that common in my experience.
That said, it's of course possible to pass data between operations. For instance, you could create a datasource property on the second operation. That would be the object to ask for its data; that object would be the first operation. This approach may require locking, though.
You can also create a nextOp property on the first operation. When it completes, it would call setData: on the second operation before exiting. You probably wouldn't need locking for this, but you might. In most cases it would be better for the first operation to just schedule the nextOp at this point (which again looks like lucianomarisi's answer).
The point is that an operation is just an object. It can have any methods and properties you want on it. And you can pass one operation to another.
Keep in mind that since an operation runs in the background, there's no reason you need to use the asynchronous interface to NSURLConnection. The synchronous API (sendSynchronousRequest:returningResponse:error: is fine for this, and much simpler to code. You could even use a trivial NSBlockOperation. Alternately, you can use the asynchronous NSURLConnection interface, but then you really don't need an NSOperation.
I also notice:
_receivedData = [NSMutableData dataWithCapacity:256];
Is it really such a small piece of JSON data? It's hard to believe that this complexity is worth it to move such a small parsing operation to the background.
(As a side note, unless you know precisely the size of the memory, there's not usually much benefit to specifying a capacity manually. Even then it's not always clear that it's a benefit. I believe NSURLConnection is using dispatch data under the covers now, so you're actually requesting a memory block that will never be used. Of course Cocoa also won't allocate it because it optimizes that out... the point is that you might as well just use [NSMutableData data]. Cocoa is quite smart about these kinds of things; you generally can only get in the way of its optimizations.)
As Rob said, unless you have any particular reason to use operations use the synchronized call. Then perform the selector on MainThread or on any other thread you need. Unless you want to separate the retrieval and parsing in separate operations or thread (explicitly).
Here is the code I was using for json retrieval and parsing:
-(BOOL) loadWithURL:(NSString*) url params: (NSDictionary*) params andOutElements:(NSDictionary*) jElements
{
NSError *reqError = nil;
NSString* urlStr = #"";//#"http://";
urlStr = [urlStr stringByAppendingString:url];
NSURL* nsURL = [NSURL URLWithString:urlStr];
//Private API to bypass certificate ERROR Use only for DEBUG
//[NSURLRequest setAllowsAnyHTTPSCertificate:YES forHost:[nsURL host]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL: nsURL
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
[request setHTTPMethod:#"POST"];
NSString *postString = #"";
if(params!=nil) {
NSEnumerator* enumerator = params.keyEnumerator;
NSString* aKey = nil;
while ( (aKey = [enumerator nextObject]) != nil) {
NSString* value = [params objectForKey:aKey];
//Use our own encoded implementation instead of above Apple one due to failing to encode '&'
NSString* escapedUrlString =[value stringByAddingPercentEscapesUsingEncoding:NSASCIIStringEncoding];
//Required to Fix Apple bug with not encoding the '&' to %26
escapedUrlString = [escapedUrlString stringByReplacingOccurrencesOfString: #"&" withString:#"%26"];
//this is custom append method. Please implement it for you -> the result should be 'key=value' or '&keyNotFirst=value'
postString = [self appendCGIPairs:postString key:aKey value:escapedUrlString isFirst:false];
}
}
//************** Use custom enconding instead !!!! Error !!!!! **************
[request setHTTPBody:[postString dataUsingEncoding:NSUTF8StringEncoding]];
NSData *response = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:&reqError];
if(reqError!=nil) {
NSLog(#"SP Error %#", reqError);
return NO;
}
NSString *json_string = [[NSString alloc] initWithData:response encoding:NSUTF8StringEncoding];
//Handles Server Errors during execution of the web service that handles the call.
if([json_string hasPrefix:#"ERROR"] == YES){
NSLog(#"SP Error %#", lastError);
return NO;
}
//Very Careful!!!!!! Will stop for any reason.!!!!!!
//Handles errors from IIS Server that serves teh request.
NSRange range = [json_string rangeOfString:#"Runtime Error"];
if(range.location != NSNotFound) {
NSLog(#"SP Error %#", lastError);
return NO;
}
//Do the parsing
jElements = [[parser objectWithString:json_string error:nil] copy];
if([parser error] == nil) {
NSLog(#"Parsing completed");
} else {
jElements = nil;
NSLog(#"Json Parser error: %#", parser.error);
NSLog(#"Json string: %#", json_string);
return NO;
}
//Parsed JSON will be on jElements
return YES;
}

Downloading multiple files in batches in iOS

I have an app that right now needs to download hundreds of small PDF's based on the users selection. The problem I am running into is that it is taking a significant amount of time because every time it has to open a new connection. I know that I could use GCD to do an async download, but how would I go about doing this in batches of like 10 files or so. Is there a framework that already does this, or is this something I will have to build my self?
This answer is now obsolete. Now that NSURLConnection is deprecated and NSURLSession is now available, that offers better mechanisms for downloading a series of files, avoiding much of the complexity of the solution contemplated here. See my other answer which discusses NSURLSession.
I'll keep this answer below, for historical purposes.
I'm sure there are lots of wonderful solutions for this, but I wrote a little downloader manager to handle this scenario, where you want to download a bunch of files. Just add the individual downloads to the download manager, and as one finishes, it will kick off the next queued one. You can specify how many you want it to do concurrently (which I default to four), so therefore there's no batching needed. If nothing else, this might provoke some ideas of how you might do this in your own implementation.
Note, this offers two advantages:
If your files are large, this never holds the entire file in memory, but rather streams it to persistent storage as it's being downloaded. This significantly reduces the memory footprint of the download process.
As the files are being downloaded, there are delegate protocols to inform you or the progress of the download.
I've attempted to describe the classes involved and proper operation on the main page at the Download Manager github page.
I should say, though, that this was designed to solve a particular problem, where I wanted to track the progress of downloads of large files as they're being downloaded and where I didn't want to ever hold the entire in memory at one time (e.g., if you're downloading a 100mb file, do you really want to hold that in RAM while downloading?).
While my solution solves those problem, if you don't need that, there are far simpler solutions using operation queues. In fact you even hint at this possibility:
I know that I could use GCD to do an async download, but how would I go about doing this in batches of like 10 files or so. ...
I have to say that doing an async download strikes me as the right solution, rather than trying to mitigate the download performance problem by downloading in batches.
You talk about using GCD queues. Personally, I'd just create an operation queue so that I could specify how many concurrent operations I wanted, and download the individual files using NSData method dataWithContentsOfURL followed by writeToFile:atomically:, making each download it's own operation.
So, for example, assuming you had an array of URLs of files to download it might be:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4;
for (NSURL* url in urlArray)
{
[queue addOperationWithBlock:^{
NSData *data = [NSData dataWithContentsOfURL:url];
NSString *filename = [documentsPath stringByAppendingString:[url lastPathComponent]];
[data writeToFile:filename atomically:YES];
}];
}
Nice and simple. And by setting queue.maxConcurrentOperationCount you enjoy concurrency, while not crushing your app (or the server) with too many concurrent requests.
And if you need to be notified when the operations are done, you could do something like:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4;
NSBlockOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self methodToCallOnCompletion];
}];
}];
for (NSURL* url in urlArray)
{
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSData *data = [NSData dataWithContentsOfURL:url];
NSString *filename = [documentsPath stringByAppendingString:[url lastPathComponent]];
[data writeToFile:filename atomically:YES];
}];
[completionOperation addDependency:operation];
}
[queue addOperations:completionOperation.dependencies waitUntilFinished:NO];
[queue addOperation:completionOperation];
This will do the same thing, except it will call methodToCallOnCompletion on the main queue when all the downloads are done.
By the way, iOS 7 (and Mac OS 10.9) offer URLSession and URLSessionDownloadTask, which handles this quite gracefully. If you just want to download a bunch of files, you can do something like:
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSFileManager *fileManager = [NSFileManager defaultManager];
for (NSString *filename in self.filenames) {
NSURL *url = [baseURL URLByAppendingPathComponent:filename];
NSURLSessionTask *downloadTask = [session downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
NSString *finalPath = [documentsPath stringByAppendingPathComponent:filename];
BOOL success;
NSError *fileManagerError;
if ([fileManager fileExistsAtPath:finalPath]) {
success = [fileManager removeItemAtPath:finalPath error:&fileManagerError];
NSAssert(success, #"removeItemAtPath error: %#", fileManagerError);
}
success = [fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:finalPath] error:&fileManagerError];
NSAssert(success, #"moveItemAtURL error: %#", fileManagerError);
NSLog(#"finished %#", filename);
}];
[downloadTask resume];
}
Perhaps, given that your downloads take a "significant amount of time", you might want them to continue downloading even after the app has gone into the background. If so, you can use backgroundSessionConfiguration rather than defaultSessionConfiguration (though you have to implement the NSURLSessionDownloadDelegate methods, rather than using the completionHandler block). These background sessions are slower, but then again, they happen even if the user has left your app. Thus:
- (void)startBackgroundDownloadsForBaseURL:(NSURL *)baseURL {
NSURLSession *session = [self backgroundSession];
for (NSString *filename in self.filenames) {
NSURL *url = [baseURL URLByAppendingPathComponent:filename];
NSURLSessionTask *downloadTask = [session downloadTaskWithURL:url];
[downloadTask resume];
}
}
- (NSURLSession *)backgroundSession {
static NSURLSession *session = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:kBackgroundId];
session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
});
return session;
}
#pragma mark - NSURLSessionDownloadDelegate
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *finalPath = [documentsPath stringByAppendingPathComponent:[[[downloadTask originalRequest] URL] lastPathComponent]];
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL success;
NSError *error;
if ([fileManager fileExistsAtPath:finalPath]) {
success = [fileManager removeItemAtPath:finalPath error:&error];
NSAssert(success, #"removeItemAtPath error: %#", error);
}
success = [fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:finalPath] error:&error];
NSAssert(success, #"moveItemAtURL error: %#", error);
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes {
// Update your UI if you want to
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
// Update your UI if you want to
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (error)
NSLog(#"%s: %#", __FUNCTION__, error);
}
#pragma mark - NSURLSessionDelegate
- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(NSError *)error {
NSLog(#"%s: %#", __FUNCTION__, error);
}
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
AppDelegate *appDelegate = (id)[[UIApplication sharedApplication] delegate];
if (appDelegate.backgroundSessionCompletionHandler) {
dispatch_async(dispatch_get_main_queue(), ^{
appDelegate.backgroundSessionCompletionHandler();
appDelegate.backgroundSessionCompletionHandler = nil;
});
}
}
By the way, this assumes your app delegate has a backgroundSessionCompletionHandler property:
#property (copy) void (^backgroundSessionCompletionHandler)();
And that the app delegate will set that property if the app was awaken to handle URLSession events:
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
self.backgroundSessionCompletionHandler = completionHandler;
}
For an Apple demonstration of the background NSURLSession see the Simple Background Transfer sample.
If all of the PDFs are coming from a server you control then one option would be to have a single request pass a list of files you want (as query parameters on the URL). Then your server could zip up the requested files into a single file.
This would cut down on the number of individual network requests you need to make. Of course you need to update your server to handle such a request and your app needs to unzip the returned file. But this is much more efficient than making lots of individual network requests.
Use an NSOperationQueue and make each download a separate NSOperation. Set the maximum concurrent operations property on your queue to however many downloads you want to be able to run simultaneously. I'd keep it in the 4-6 range personally.
Here's a good blog post that explains how to make concurrent operations.
http://www.dribin.org/dave/blog/archives/2009/05/05/concurrent_operations/
What came as a big surprise is how slow dataWithContentsOfURL is when downloading multiple files!
To see it by yourself run the following example:
(you don't need the downloadQueue for downloadTaskWithURL, its there just for easier comparison)
- (IBAction)downloadUrls:(id)sender {
[[NSOperationQueue new] addOperationWithBlock:^{
[self download:true];
[self download:false];
}];
}
-(void) download:(BOOL) slow
{
double startTime = CACurrentMediaTime();
NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration];
static NSURLSession* urlSession;
if(urlSession == nil)
urlSession = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:nil];
dispatch_group_t syncGroup = dispatch_group_create();
NSOperationQueue* downloadQueue = [NSOperationQueue new];
downloadQueue.maxConcurrentOperationCount = 10;
NSString* baseUrl = #"https://via.placeholder.com/468x60?text=";
for(int i = 0;i < 100;i++) {
NSString* urlString = [baseUrl stringByAppendingFormat:#"image%d", i];
dispatch_group_enter(syncGroup);
NSURL *url = [NSURL URLWithString:urlString];
[downloadQueue addOperationWithBlock:^{
if(slow) {
NSData *urlData = [NSData dataWithContentsOfURL:url];
dispatch_group_leave(syncGroup);
//NSLog(#"downloaded: %#", urlString);
}
else {
NSURLSessionDownloadTask* task = [urlSession downloadTaskWithURL:url completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
//NSLog(#"downloaded: %#", urlString);
dispatch_group_leave(syncGroup);
}];[task resume];
}
}];
}
dispatch_group_wait(syncGroup, DISPATCH_TIME_FOREVER);
double endTime = CACurrentMediaTime();
NSLog(#"Download time:%.2f", (endTime - startTime));
}
There is nothing to "build". Just loop through the next 10 files each time in 10 threads and get the next file when a thread finishes.

Objective-C: Async/Background POST without using delegate method?

I need to make some POST calls to my server, but I need to not block the main thread. As I understand, NSMutableURLRequest and NSURLConnection are not thread safe, so it is best to use the async method of NSURLConnection.
My question about this is, how I can package it up nicely into a method, instead of having to use the delegate method? I would prefer to do:
NSData *returnedData = [Utility postDataToURL:#"some string of data"];
This is how it is easy done with the following method:
[NSURLConnection sendSynchronousRequest:serviceRequest returningResponse:&serviceResponse error:&serviceError];
It is so nice keeping everything within one method, then just having my data returned from it!
Are there any block based methods for this? It becomes an issue when I need to write methods for about 50 different calls and each one needs to use the same delegate method. Am I going about this the wrong way?
This will only need to be for iOS5.
iOS 5 adds sendAsynchronousRequest:queue:completionHandler: which does what I think you want. I've got my code set up to use that if available, but to fall back on performing a synchronous fetch on a background GCD queue and hopping onto the main thread with the result if it doesn't. The latter will be less power efficient but it's just to maintain legacy support.
if([NSURLConnection respondsToSelector:#selector(sendAsynchronousRequest:queue:completionHandler:)])
{
// we can use the iOS 5 path, so issue the asynchronous request
// and then just do whatever we want to do
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:
^(NSURLResponse *response, NSData *data, NSError *error)
{
[self didLoadData:data];
}];
}
else
{
// fine, we'll have to do a power inefficient iOS 4 implementation;
// hop onto the global dispatch queue...
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
// ... perform a blocking, synchronous URL retrieval ...
NSError *error = nil;
NSURLResponse *urlResponse = nil;
NSData *responseData =
[NSURLConnection sendSynchronousRequest:request returningResponse:&urlResponse error:&error];
// ... and hop back onto the main queue to handle the result
dispatch_async(dispatch_get_main_queue(),
^{
[self didLoadData:responseData];
});
});
}
In production code you'd actually check the errors and HTTP response codes (as a server 404 response is probably just as much an error from your point of view as a connection failure), obviously.
iOS 5.0 > you can use sendAsynchronousRequest method look at NSURLConnection Class and it uses blocks. If you want to support iOS 4.0 > too then you have to write one of your own block based Asynchronous URL loading which is fairly easy to write. You are better off by using MKNetworkKit.
but I need to not block the main thread. As I understand, NSMutableURLRequest and NSURLConnection are not thread safe, so it is best to use the async method of NSURLConnection.
You don't want to do Synchronous network connection it blocks thread whichever it is called from (its even worse if its main thread). You can do Asynchronous network connection on main thread. If you want to do call NSURLConnection on non-main thread then have to create a RunLoop on that thread (if you don't then the delegate methods of NSURLConnection never gets called).
I had this problem pre-5.0 so I made a little class to handle the NSURLConnection delegate protocol and offer callers an interface with closures:
BetterNSURLConnection.h
#property (retain, nonatomic) NSURLRequest *request;
BetterNSURLConnection.m
#property (retain, nonatomic) NSURLConnection *connection;
#property (retain, nonatomic) NSHTTPURLResponse *response;
#property (retain, nonatomic) NSMutableData *responseData;
#property (copy, nonatomic) void (^completionBlock)(id, NSHTTPURLResponse *);
#property (copy, nonatomic) void (^errorBlock)(NSError *);
... You can add typedefs to make those block signatures prettier...then:
#synthesize connection = _connection;
#synthesize response = _response;
#synthesize responseData = _responseData;
#synthesize completionBlock = _completionBlock;
#synthesize errorBlock = _errorBlock;
#synthesize request=_request;
- (void)startWithCompletion:(void (^)(id, NSHTTPURLResponse *))completionBlock error:(void (^)(NSError *))errorBlock {
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
self.completionBlock = completionBlock;
self.errorBlock = errorBlock;
self.responseData = [NSMutableData data];
NSURLConnection *connection = [NSURLConnection connectionWithRequest:self.request delegate:self];
self.connection = connection;
[self.connection start];
[connection release];
}
... then do the delegate like this:
- (void)connection:(NSURLConnection *)aConnection didReceiveResponse:(NSHTTPURLResponse *)response {
[self.responseData setLength:0];
self.response = response;
}
- (void)connection:(NSURLConnection *)aConnection didReceiveData:(NSData *)data {
[self.responseData appendData:data];
}
- (void)connection:(NSURLConnection *)aConnection didFailWithError:(NSError *)error {
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
self.errorBlock(error);
self.connection = nil;
}
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection {
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
if (self.response.statusCode >= 400) {
self.errorBlock(error);
} else {
// i do json requests, and call a json parser here, but you might want to do something different
id result = [self parseResponse:self.responseData];
self.completionBlock(result, self.response);
}
self.connection = nil;
}
I use a facade method to op-queue an internal worker which issues the synchronous call. Depending on the rate at which you send the calls, it might work. Example:
// Presented in #interface
-(void)sendPostRequest {
// Last chance to update main thread/UI
NSInvocationOperation *op = [[[NSInvocationOperation alloc] initWithTarget:self selector:#selector(sendPostRequest_internal) object:nil] autorelease];
[opQueue addOperation:op];
}
// Hidden in #implementation
-(void)sendPostRequest_internal {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSURLRequest *request = // yadda, you might use NSURLMutableRequest
NSURLResponse *response = nil;
NSError *error = nil;
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:response error:error];
// process data, retain things as needed, post results using performSelectorOnMainThread:
[pool release];
}
It works pretty well for my purposes but you might need to delve a little deeper in to the async stuff, which really isn't too bad.

HTTP server works in Cocoa application but not test case -- run loop issue?

I'm trying to add a GHUnit test case to this SimpleHTTPServer example. The example include a Cocoa application that works fine for me. But I can't duplicate the behavior in a test case.
Here is the test class:
#import <GHUnit/GHUnit.h>
#import "SimpleHTTPServer.h"
#interface ServerTest : GHTestCase
{
SimpleHTTPServer *server;
}
#end
#implementation ServerTest
-(void)setUpClass
{
[[NSRunLoop currentRunLoop] run];
}
- (NSString*)requestToURL:(NSString*)urlString error:(NSError**)error
{
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:1];
NSURLResponse *response = nil;
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:error];
NSString *page = nil;
if (error == nil)
{
NSStringEncoding responseEncoding = CFStringConvertEncodingToNSStringEncoding(CFStringConvertIANACharSetNameToEncoding((CFStringRef)[response textEncodingName]));
page = [[NSString alloc] initWithData:data encoding:responseEncoding];
[page autorelease];
}
return page;
}
- (void)testPortReuse
{
unsigned int port = 50001;
NSError *error = nil;
NSString *path, *url;
server = [[SimpleHTTPServer alloc] initWithTCPPort:port delegate:self];
sleep(10);
path = #"/x/y/z";
url = [NSString stringWithFormat:#"http://localhost:%u%#", port, path];
[self requestToURL:url error:&error];
GHAssertNil(error, #"%# : %#", url, error);
[server release];
}
- (void)processURL:(NSURL *)path connection:(SimpleHTTPConnection *)connection
{
NSLog(#"processURL");
}
- (void)stopProcessing
{
NSLog(#"stopProcessing");
}
#end
I've tried sending requests via NSURLRequest and also (during the sleep) via a web browser. The delegate methods -processURL and -stopProcessing are never called. The problem seems to be that [fileHandle acceptConnectionInBackgroundAndNotify] in SimpleHTTPServer -initWithTCPPort:delegate: is not causing any NSFileHandleConnectionAcceptedNotifications to reach the NSNotificationCenter -- so I suspect a problem involving run loops.
The problem seems to be with the NSFileHandle, not the NSNotificationCenter, because when [nc postNotificationName:NSFileHandleConnectionAcceptedNotification object:nil] is added to the end of initWithTCPPort:delegate:, the NSNotificationCenter does get the notification.
if (error == nil)
That should be:
if (data != nil)
error here is the passed-in pointer to an NSError* - it will only be nil if the caller passed nil instead of a reference to an NSError* object, which isn't what your -testPortReuse method does.
It would also be incorrect to dereference it (as in if (*error == nil)), because error arguments are not guaranteed to be set to nil upon error. The return value indicates an error condition, and the value returned in the error argument is only meaningful or reliable if there is an error. Always check the return value to determine if an error happened, then check the error parameter for details only if something did in fact go wrong.
In other words, as it's written above, your -requestToURL:error: method is incapable of handling success. Much like Charlie Sheen. :-)