I have several "proxy" classes, which inherit from a "base proxy". These classes connect to my server and pass data to their delegates. In event of 0 status code, I want to handle these different requests in the same way.
For 0 status codes, I want to retry the method in 5 seconds, hoping the user's internet connection has improved.
SomethingProxy.m
- (void)fetchSomething {
NSString *fullPath = [NSString stringWithFormat:#"%#/route/index.json",MY_BASE_URL];
NSURL *url = [NSURL URLWithString:fullPath];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation = [[AFJSONRequestOperation alloc] initWithRequest:request];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSDictionary *d = (NSDictionary *)responseObject;
[self.delegate fetchedPolicy:d[#"keypath"]];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[self handleOperationFailed:operation action:^{
[self fetchSomething];
}];
}];
NSOperationQueue *q = [[NSOperationQueue alloc] init]; [q addOperation:operation];
}
MyBaseProxy.m
- (bool)shouldRetryOperation:(AFHTTPRequestOperation *)o {
return self.retries < [self maxRetries];
}
- (void)handleOperationFailed:(AFHTTPRequestOperation *)o action:(ActionBlock)block {
NSInteger statusCode = o.response.statusCode;
if (statusCode == 0) {
if ([self shouldRetryOperation:o]) {
double delayInSeconds = 5.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
self.retries++;
block();
});
} else {
self.retries = 0;
[SVProgressHUD showErrorWithStatus:#"Please check your internet connection and try again"];
return;
}
}
self.retries = 0;
What's a better way to handle the request failure? Should I subclass AFHTTPRequestOperation?
EDIT: Removed confusing text. When I meant "same way", I meant per request Eg. handle all 500s the same, handle all 403s the same. I'm specifically asking for handling status code 0 - no internet connection.
The key situation you should be concerned with, IMHO, is when the network, itself, is not available to the device. In that case, you'd generally use Reachability to have a notification posted to your app when the network becomes available. No point in repeatedly retrying when (a) you know the network is unavailable; and (b) you easily can be notified when the network becomes available again.
If you're concerned about server-specific issues, I would be careful about send a request in 5 seconds. Let's say your server is overwhelmed and cannot respond to all the client requests. Having large numbers of clients proceeding to retry every 5 seconds might not improve the situation. It sort of depends upon why your server is not responding. You just want to make sure that the attempts to connect again will never make the situation worse.
(As an aside, the description for 403 says, "The server understood the request, but is refusing to fulfill it. Authorization will not help and the request SHOULD NOT be repeated.")
If you're going to employ a retry process, I'd specify some reasonable maximum number of retries (which, judging by your revised answer, you are doing). And perhaps you'd want something that when the app returns to foreground, you reinitiate your "retry three times, once every 15 seconds" logic (or whatever you settle upon). The scenario I'm thinking about is "user started app, it failed after exceeding max retries, and hit the home button on their device, and a few minutes/hours later, they tap on the app again (which might still be running)."
Related
I have run into a bit of a conundrum with a service I am working on in objective-c. The purpose of the service is to parse through a list of core-data entities and download a corresponding image file for each object. The original design of the service was choking my web-server with too many simultaneous download requests. To get around that, I moved the code responsible for executing the download request into a recursive method. The completion handler for each download request will call the method again, thus ensuring that each download will wait for the previous one to complete before dispatching.
Where things get tricky is the code responsible for actually updating my core-data model and the progress indicator view. In the completion handler for the download, before the method recurses, I make an asynchronous call the a block that is responsible for updating the core data and then updating the view to show the progress. That block needs to have a variable to track how many times the block has been executed. In the original code, I could simply have a method-level variable with block scope that would get incremented inside the block. Since the method is recursive now, that strategy no longer works. The method level variable would simply get reset on each recursion. I can't simply pass the variable to the next level either thanks to the async nature of the block calls.
I'm at a total loss here. Can anyone suggest an approach for dealing with this?
Update:
As matt pointed out below, the core issue here is how to control the timing of the requests. After doing some more research, I found out why my original code was not working. As it turns out, the timeout interval starts running as soon as the first task is initiated, and once the time is up, any additional requests would fail. If you know exactly how much time all your requests will take, it is possible to simply increase the timeout on your requests. The better approach however is to use an NSOperationQueue to control when the requests are dispatched. For a great example of how to do this see: https://code-examples.net/en/q/19c5248
If you take this approach, keep in mind that you will have to call the completeOperation() method of each operation you create on the completion handler of the downloadTask.
Some sample code:
-(void) downloadSkuImages:(NSArray *) imagesToDownload onComplete:(void (^)(BOOL update,NSError *error))onComplete
{
[self runSerializedRequests:imagesToDownload progress:weakProgress downloaded:0 index:0 onComplete:onComplete ];
}
-(void)runSerializedRequests:(NSArray *) skuImages progress:(NSProgress *) progress downloaded:(int) totalDownloaded index:(NSUInteger) index onComplete:(void (^)(BOOL update,NSError *error))onComplete
{
int __block downloaded = totalDownloaded;
TotalDownloadProgressBlock totalDownloadProgressBlock = ^BOOL (SkuImageID *skuImageId, NSString *imageFilePath, NSError *error) {
if(error==nil) {
downloaded++;
weakProgress.completedUnitCount = downloaded;
//save change to core-data here
}
else {
downloaded++;
weakProgress.completedUnitCount = downloaded;
[weakSelf setSyncOperationDetail:[NSString stringWithFormat:#"Problem downloading sku image %#",error.localizedDescription]];
}
if(weakProgress.totalUnitCount==weakProgress.completedUnitCount) {
[weakSelf setSyncOperationIndicator:SYNC_INDICATOR_WORKING];
[weakSelf setSyncOperationDetail:#"All product images up to date"];
[weakSelf setSyncOperationStatus:SYNC_STATUS_SUCCESS];
weakProgress.totalUnitCount = 1;
weakProgress.completedUnitCount = 1;
onComplete(false,nil);
return true;
}
return false;
};
NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:nil destination:nil
completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
NSLog(#"finished download %u of %lu", index +1, (unsigned long)skuImages.count);
if(error != nil)
{
NSLog(#"Download failed for URL: %# with error: %#",skuImage.url, error.localizedDescription);
}
else
{
NSLog(#"Download succeeded for URL: %#", skuImage.url);
}
dispatch_async(dispatch_get_main_queue(), ^(void){
totalDownloadProgressBlock(skuImageId, imageFilePath, error);
});
[self runSerializedRequests:manager skuImages:skuImages progress:progress downloaded:downloaded index:index+1 onComplete:onComplete ];
}];
NSLog(#"Starting download %u of %lu", index +1, (unsigned long)skuImages.count);
[downloadTask resume];
}
The original design of the service was choking my web-server with too many simultaneous download requests. To get around that, I moved the code responsible for executing the download request into a recursive method.
But that was never the right way to solve the problem. Use a single persistent custom NSURLSession with your own configuration, and set the configuration's httpMaximumConnectionsPerHost.
I'm trying to update an old Mac OS program I wrote in ASOC (mostly Applescript, but some ObjC objects for things like web service access). I used a synchronous connection:
NSData *resultsData = [NSURLConnection sendSynchronousRequest: req returningResponse: &response error: &err];
The server credentials were embedded in the URL. This worked fine for me since the program really could not continue to do anything while the data was being fetched. A change to the server authentication method however has forced the need for changes to this application. I have tried all the usual workarounds with a NSURLCredential but that still does not work with this service.
So it looks like I will need to change to the asynchronous call:
[[NSURLConnection alloc] initWithRequest:request
delegate:self
startImmediately:YES];
I have this working with the appropriate delegate methods, most importantly:
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
Although I'd love to just use some form of delay loop to check for when the data has finished loading (essentially making it synchronous again), I have not found a way to do this that does not actually block the connection.
I am able to use a NSTimer to wait for the data before continuing:
set theJobListTimer to current application's NSTimer's scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(0.05, me, "jobListTimerFired:", "", true)
on jobListTimerFired_(theTimer)
(jobListData as list) & count of jobListData
if count of jobListData ≠ 0 then
log "jobListTimerFired_ done"
tell theTimer to invalidate()
setUpJobList(jobListData)
end if
end jobListTimerFired_
but this is clumsy and does not work while I'm in a modal dialog:
set buttonReturned to current application's NSApp's runModalForWindow_(collectionWindow)
(I have a drop down in the dialog that needs to be updated with the results of the web service call). Right now, the delegate methods are blocked until the modal is dismissed.
Is there no simple way to emulate the synchronous call using the async methods?
Trying to use semaphore, I changed code to:
- (void) startConnection:(int)reqType :(NSMutableURLRequest *)request {
requestType = [NSNumber numberWithInt:reqType];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
// This could be any block that is run asynchronously
void (^myBlock)(void) = ^(void) {
self.connection = [[NSURLConnection alloc] initWithRequest:request
delegate:self
startImmediately:YES];
myBlock();
if (self.connection) {
// create an object to hold the received data
self.receivedData = [NSMutableData data];
NSLog(#"connection started %#", requestType);
}
dispatch_time_t timeOut = dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC);
dispatch_semaphore_wait(semaphore, timeOut);
dispatch_release(semaphore);
semaphore = NULL;
}
then in the connection handler:
- (void) connectionDidFinishLoading:(NSURLConnection *)connection
{
NSLog(#"connectionDidFinishLoading %#", requestType);
NSString *returnData = [[NSString alloc] initWithData:receivedData
encoding:NSUTF8StringEncoding] ;
// NSLog(#"connectionDidFinishLoading %#", returnData);
[self handleData:requestType :returnData];
[self terminate];
if(semaphore) {
dispatch_semaphore_signal(semaphore);
}
}
However, the connectionDidFinishLoading handler (and for that matter the didReceiveResponse and didReceiveData handlers) do not get called until after the 10 second dispatch timeout. What am I missing here?
You can use dispatch_semaphore_wait to make any asynchronous API into a synchronous one again.
Here's an example:
__block BOOL accessGranted = NO;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
// This could be any block that is run asynchronously
ABAddressBookRequestAccessWithCompletion(addressBook, ^(bool granted, CFErrorRef error) {
accessGranted = granted;
if(semaphore) {
dispatch_semaphore_signal(semaphore);
}
});
// This will block until the semaphore has been signaled
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_release(semaphore);
semaphore = NULL;
return accessGranted;
Found the answer here:
iOS, NSURLConnection: Delegate Callbacks on Different Thread?
I knew the connection was running on a different thread and tried various other while loops to wait for it to finish. But this was REALLY the magic line:
while(!self->finished]){
//This line below is the magic!
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
I'm calling a method that will enumerate through an array, create an NSURL, and call an NSURLSessionDataTask that returns JSON. The loop typically runs about 10 times but can vary depending on the day.
I need to wait for the for loop and all NSURLSessionDataTasks to complete before I can start processing the data.
I'm having a hard time figuring out when all the work is complete. Could anyone recommend any ways or logic to know when the entire method is complete (for loop and data tasks)?
-(void)findStationsByRoute{
for (NSString *stopID in self.allRoutes) {
NSString *urlString =[NSString stringWithFormat:#"http://truetime.csta.com/developer/api/v1/stopsbyroute?route=%#", stopID];
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if(httpResponse.statusCode == 200){
NSError *jsonError = [[NSError alloc]init];
NSDictionary *stopLocationDictionary = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&jsonError];
NSArray *stopDirectionArray = [stopLocationDictionary objectForKey:#"direction"];
for (NSDictionary * _stopDictionary in stopDirectionArray) {
NSArray *stop = [_stopDictionary objectForKey:#"stop"];
[self.arrayOfStops addObject:stop];
}
}
}];
[task resume];
}
}
There are a number of options. The fundamental issue is that these individual data tasks run asynchronously, so you need some way to keep track of these asynchronous tasks and establish some dependency on their completion.
There are several possible approaches:
The typical solution is to employ a dispatch group. Enter the group before you start the request with dispatch_group_enter, leave the group with dispatch_group_leave inside the completion handler, which is called asynchronously, and then, at the end of the loop, supply a dispatch_group_notify block that will be called asynchronously when all of the "enter" calls are offset by corresponding "leave" calls:
- (void)findStationsByRoute {
dispatch_group_t group = dispatch_group_create();
for (NSString *stopID in self.allRoutes) {
NSString *urlString = [NSString stringWithFormat:#"http://truetime.csta.com/developer/api/v1/stopsbyroute?route=%#", stopID];
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
dispatch_group_enter(group); // enter group before making request
NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if(httpResponse.statusCode == 200){
NSError *jsonError; // Note, do not initialize this with [[NSError alloc]init];
NSDictionary *stopLocationDictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
NSArray *stopDirectionArray = [stopLocationDictionary objectForKey:#"direction"];
for (NSDictionary *stopDictionary in stopDirectionArray) {
NSArray *stop = [stopDictionary objectForKey:#"stop"];
[self.arrayOfStops addObject:stop];
}
}
dispatch_group_leave(group); // leave group from within the completion handler
}];
[task resume];
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// do something when they're all done
});
}
A more sophisticated way to handle this is to wrap the NSSessionDataTask in a NSOperation subclass and you can then use dependencies between your data task operations and your final completion operation. You'll want to ensure your individual data task operations are "concurrent" ones (i.e. do not issue isFinished notification until the asynchronous data task is done). The benefit of this approach is that you can set maxConcurrentOperationCount to constrain how many requests will be started at any given time. Generally you want to constrain it to 3-4 requests at a time.
Note, this can also address timeout issues from which the dispatch group approach can suffer from. Dispatch groups don't constrain how many requests are submitted at any given time, whereas this is easily accomplished with NSOperation.
For more information, see the discussion about "concurrent operations" in the Operation Queue section of the Concurrency Programming Guide.
For an example of wrapping NSURLSessionTask requests in asynchronous NSOperation subclass, see a simple implementation the latter half NSURLSession with NSBlockOperation and queues. This question was addressing a different topic, but I include a NSOperation subclass example at the end.
If instead of data tasks you used upload/download tasks, you could then use a [NSURLSessionConfiguration backgroundSessionConfiguration] and URLSessionDidFinishEventsForBackgroundURLSession: of your NSURLSessionDelegate would then get called when all of the tasks are done and the app is brought back into the foreground. (A little annoyingly, though, this is only called if your app was not active when the downloads finished: I wish there was a rendition of this delegate method that was called even if the app was in the foreground when the downloads finished.)
While you asked about data tasks (which cannot be used with background sessions), using background session with upload/download tasks enjoys a significant advantage of background operation. If your process really takes 10 minutes (which seems extraordinary), refactoring this for background session might offer significant advantages.
I hate to even mention this, but for the sake a completeness, I should acknowledge that you could theoretically just by maintain an mutable array or dictionary of pending data tasks, and upon the completion of every data task, remove an item from that list, and, if it concludes it is the last task, then manually initiate the completion process.
Look at dispatch groups, for your example it will look roughly like this:
create group
for (url in urls)
enter group
start_async_task
when complete leave group
wait on group to finish or supply a block to be run when completed
Using AFNetworking, I need to download ~100 images in the background and store them to disk while ensuring any other network connectivity in my app takes precedence.
~~~~~~~
I've got an application that has 4 tabs. Each tab basically does the same thing: Pulls down a JSON response from a server, and displays a thumbnail grid of images, pulling down each image on demand (using AF's ImageView category). Tapping on a thumbnail takes you to a detail view controller where you see a larger image. The response and images are different for each tab.
There's a new requirement to fetch all of the images for the 4th tab ahead of time, so that theoretically by the time the user taps on the 4th tab, the JSON data and images are being read from disk.
I've got this working more or less right now, with the 4th tab pre-fetching and saving to disk being performed on a background thread so the main thread doesn't lock up. However, the network requests being kicked off when a user is on the 1st, 2nd or 3rd tab are being blocked by the pre-fetching network requests.
I'm using AFNetworking, and here is the code I'm using when Tab 1, 2 or 3 loads:
// this network request ends up getting blocked by the network request that
// is fired upon the application becoming active
- (void)getAllObjectDataWithBlock:(AFCompletionBlockWrapper)block
{
[[[MyAPIClient] sharedClient] getPath:#"" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
block(operation, responseObject, nil);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
block(operation, nil, error);
}];
}
And here is the code I'm using when my application becomes active, to pre-fetch content for the 4th tab:
// this network request runs in the background, but still blocks requests
// that should have a higher priority
- (void)applicationDidBecomeActive:(UIApplication *)application
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
NSOperationQueue *imageQueue = [[NSOperationQueue alloc] init];
[imageQueue setMaxConcurrentOperationCount:8];
for (NSString *imageURL in self.images) {
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL imageURL]];
AFImageRequestOperation *operation = [[AFImageRequestOperation alloc] initWithRequest:request];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"success");
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"fail");
}];
[imageQueue addOperation:smallOperation];
}
});
}
How can I structure things so any network requests kicked off from the main thread interrupt those that were kicked off in the background thread?
I don't know that you can easily interrupt running operations, unless you want to send them a cancel--but you'd have to look at whether AFImageRequestOperation pays attention to isCancelled.
Have you tried using setQueuePriority? You could start all the pre-fetching requests with a low priority, then add current tab requests with a higher priority. I believe running operations would complete, but once they complete, your higher-priority operations would get scheduled ahead of queued low-priority ones.
Everywhere I look, I see people explicitly using queues or run loops to do network operations. Should I still do that in iOS 5, or should I use NSURLConnection sendAsynchronousRequest:queue:completionHandler: instead? Is this the preferred method of doing network operations in iOS >= 5?
I can't answer for others' preference, but I rolled my own < os5, and I strongly prefer the block operation. a) I'm never interested in intermediate results of the network operation, or the repetitive code to handle them, b) the block is retained, so I get fewer race conditions where some aspect of the delegate gets prematurely released, and c) I never get mixed up about what code is running when a particular operation finishes.
In short, it's a huge improvement in the NSURLConnection interface, IMO.
It depends. For simple things, the new block-based API makes life a lot easier. However, compared to implementing the NSURLConnectionDelegate methods, you lose some control.
For example, it's not possible to cancel connections that have been initiated with this method, or to handle the response (e.g. for parsing headers) before all data is downloaded.
You can do something similar to this with iOS 4 as well using GCD.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
NSData *data = [NSURLConnection sendSynchronousRequest:blah returningResponse:response error:blah];
//process response body here
dispatch_async(dispatch_get_main_queue(), ^{
//Update UI Code here
self.textView.text = [[NSString alloc] initWithData:data];
});
});
The problem with this code and the code OP posted is that, once the connection is made, you cannot cancel them.
Canceling a request on viewDidDisappear goes a long way in improving the performance of your application. I talk extensively about this on the book iOS PTL
Second reason why you need a third party networking framework like MKNetworkKit or RestKit or the now defunct ASIHTTP is for authentication. Most web service require that you authenticate using a NSURLCredential (HTTP Basic or HTTP Digest or Windows NTLM or oAuth)
This alone will take a couple of days to do if you roll out your own code. Not that you shouldn't do it. But there is no need for one as all these third party frameworks are extensively used and the chances or bugs or performance issues in them is less compared to your own code.
I've written a blog post that compares several approaches including NSURLConnection, AFNetworking, and ReactiveCocoa.
ReactiveCocoa approach
If you want to get really fancy with asynchronous network calls, you can try out ReactiveCocoa. ReactiveCocoa allows you to define signals that represent each of your network requests. And it allows you to chain multiple dependent network calls.
#import <ReactiveCocoa/ReactiveCocoa.h>
// ...
-(RACSignal*)signalGetNetworkStep1 {
NSURL* url = ...;
return [RACSignal createSignal:^(RACDisposable *(id subscriber) {
NSURLRequest *request1 = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation1 = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
[subscriber sendNext:JSON];
[subscriber sendCompleted];
} failure:^(AFHTTPRequestOperation *operation, NSError *error){
[subscriber sendError:error];
}];
[operation1 start];
return nil;
}
}
-(void) yourMethod {
[[self signalGetNetworkStep1] subscribeNext:^(id *x) {
// perform your custom business logic
}];
}