📜 ⬆️ ⬇️

Why NSURLSession is Better than NSURLConnection



iOS 7 officially came out in September, then Apple provided developers with a new way to work with the network - NSURLSession. This is quite a fundamental thing, because if you need to support iOS 6 and below, parallelizing the code relative to the system version will be extremely problematic. But nevertheless, time is ticking, and now, according to various data, from 75 to 85 percent of users switched to the latest iOS, so I would advise you to try the NSURLSession in the next project.

As conceived by Apple, NSURLSession should change the NSURLConnection, and then the question really arises: “why is all this necessary?” Because immediately, the advantages compared with NSURLConnection:
  1. Loading and sending data in the background
  2. Ability to stop and continue downloading
  3. We can use blocks and delegates at the same time, for example, we use blocks to get data and handle errors, and the delegate method can be used to pass authentication.
  4. The session has a special configuration container in which you can put all the necessary properties for all tasks (requests) in the session, as well as, for example, headers for all requests in the session
  5. You can use private storage for cookies, caches and other things.
  6. We get a more rigorous and structured code, as opposed to a messy NSURLConnection set


')
I will show that the new method is not at all scary and that it really should be used. So let's get started, the key class is NSURLSession, as the name implies, it creates a certain session for downloading / uploading data via HTTP. There are three types of session: default is what NSURLConnection used to do, ephemeral - nothing is cached in it and everything is stored in RAM (like a private browser mode), download - the result is presented as files.

NSURLSessionConfiguration


The session properties are controlled by the NSURLSessionConfiguration class, in which there are a huge number of parameters, in addition to the choice of session type: the ability to download via the mobile network, cookies, cache, proxy, security. There is one interesting feature of discretionary - it allows you to give the download to the discretion of the system (when there is wi-fi and a lot of battery power).

NSURLSession


Having set the session configuration, we create the session itself, taking the configuration in the constructor. We get the data in the usual two ways: set the delegate or catch the data in the completion block (about them later).

NSURLTask


It is a minimal task, that before this was an NSURLConnection. The class itself is abstract, but it has 3 subclasses: NSURLSessionDataTask, NSURLSessionUploadTask (a subclass of the first) and NSURLSessionDownloadTask, however, they do not have their own constructor. All of them are created by the session itself with or without a completion block (it is quite logical that in the first case the session delegate is not needed). It all looks somewhat exotic:
NSURLSessionDownloadTask *downloadTask = [ourSession downloadTaskWithRequest:simpleNSURLRequest]; 


Blocks and delegates


In general, the download process itself is very similar to working with NSURLConnection, quickly consider two ways to work with sessions.

Through delegates:
Sessions ask the delegate during creation.
 [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil]; 

Then all delegate methods (including tasks) are called in the delegate.

Through the blocks:
It is enough to create Tasks using
  -(NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURL *location, NSURLResponse *response, NSError *error))completionHandler 

Again, nothing new, all this is familiar to us from NSURLConnection -sendAsynchronousRequest: queue: completionHandler:
In this case, we can add a delegate method to pass authentication if necessary.

Examples


Understood with the general scheme, we will postpone the theory, time to look at examples!

Stop / continue loading.

The whole scheme rather closely resembles the work through NSURLConnection, but, unlike it, we can simply cancel any download task. Also, when canceled, the delegate method URLSession: task: didCompleteWithError: will be called, so that all the necessary UI manipulations can be performed there. And you can not only cancel, but just stop.
  [self.resumableTask cancelByProducingResumeData:^(NSData *resumeData) { partialDownload = resumeData; self.resumableTask = nil; }]; //        if(partialDownload) { self.resumableTask = [inProcessSession downloadTaskWithResumeData:partialDownload]; } else { ... } [self.resumableTask resume]; 

By stopping the task, you can save all the received data, and after that you can give it to the new download task.

Upload to file

Another thing that I would like to disassemble is download task. Let me remind you, they allow the downloaded immediately put in the file.

through the unit:
 NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig]; NSURL* downloadTaskURL = [NSURL URLWithString:@"http://upload.wikimedia.org/wikipedia/commons/1/14/Proton_Zvezda_crop.jpg"]; [[session downloadTaskWithURL: downloadTaskURL completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { NSFileManager *fileManager = [NSFileManager defaultManager]; NSArray *urls = [fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask]; NSURL *documentsDirectory = [urls objectAtIndex:0]; NSURL *originalUrl = [NSURL URLWithString:[downloadTaskURL lastPathComponent]]; NSURL *destinationUrl = [documentsDirectory URLByAppendingPathComponent:[originalUrl lastPathComponent]]; NSError *fileManagerError; [fileManager removeItemAtURL:destinationUrl error:NULL]; // ! [fileManager copyItemAtURL:location toURL:destinationUrl error:&fileManagerError]; }] resume]; 


via delegate method:
 NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil]; NSURL* downloadTaskURL = [NSURL URLWithString:@"http://upload.wikimedia.org/wikipedia/commons/1/14/Proton_Zvezda_crop.jpg"]; [[session downloadTaskWithURL:downloadTaskURL] resume]; //    - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location { //  } 


I must say that we get the address on our device to the location variable:
file: /// private / var / mobile / Applications / {appUUID} /tmp/CFNetworkDownload_fileID.tmp, then save the file to a safer place, in the example file: /// var / mobile / Applications / {appUUID} / Documents /Proton_Zvezda_crop.jpg

We send a finite number of requests at once

Sometimes we need to limit the number of simultaneous requests, for example - 5. In this case, we just need to specify the maximum number of connections:
 sessionConfig.HTTPMaximumConnectionsPerHost = 5; 

Further there will be an example to try, it is better to pick up more files, I advise you also to simulate the download via 3g (Settings -> Developer -> Network link conditioner -> Choose a profile -> 3g -> Enable)

 - (void) methodForNSURLSession{ NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; _tasksArray = [[NSMutableArray alloc] init]; sessionConfig.HTTPMaximumConnectionsPerHost = 5; sessionConfig.timeoutIntervalForResource = 0; sessionConfig.timeoutIntervalForRequest = 0; NSURLSession* session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil]; // download tasks // [self createDataTasksWithSession:session]; // data tasks [self createDownloadTasksWithSession:session]; } - (void) createDownloadTasksWithSession:(NSURLSession *)session{ for (int i = 0; i < 10; i++) { NSURLSessionDownloadTask *sessionDownloadTask = [session downloadTaskWithURL: [NSURL URLWithString:@"https://discussions.apple.com/servlet/JiveServlet/showImage/2-20930244-204399/iPhone%2B5%2BProblem2.jpg"]]; [_tasksArray addObject:sessionDownloadTask]; [sessionDownloadTask addObserver:self forKeyPath:@"countOfBytesReceived" options:NSKeyValueObservingOptionOld context:nil]; [sessionDownloadTask resume]; } } - (void) createDataTasksWithSession:(NSURLSession *)session{ for (int i = 0; i < 10; i++) { NSURLSessionDataTask *sessionDataTask = [session dataTaskWithURL: [NSURL URLWithString:@"https://discussions.apple.com/servlet/JiveServlet/showImage/2-20930244-204399/iPhone%2B5%2BProblem2.jpg"]]; [_tasksArray addObject:sessionDataTask]; [sessionDataTask addObserver:self forKeyPath:@"countOfBytesReceived" options:NSKeyValueObservingOptionOld context:nil]; [sessionDataTask resume]; } } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{ if([[change objectForKey:@"old"] integerValue] == 0){ NSLog(@"task %d: started", [_tasksArray indexOfObject: object]); } } - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{ NSLog(@"task %d: finished!", [_tasksArray indexOfObject:task]); } 


The example is quite simple and transparent, but I will focus your attention on one point:
 sessionConfig.timeoutIntervalForResource = 0; sessionConfig.timeoutIntervalForRequest = 0; 

According to the documentation:
timeoutIntervalForRequest - the time that is allotted for loading each task
timeoutIntervalForResource - the time allotted for downloading all requests
and here we have a problem, the fact is that at the moment when we start the task ([task resume]) the timeoutIntervalForRequest counter started ticking, and nobody cares that we have 100 tasks, and at the same time only 5 can work. the reason it turns out that the values ​​of these parameters must be the same, because the tasks that will be caused by the latter may end up without getting a data bit.

Therefore, we have no choice but to set both variables to the same values, it is also possible to set it to 0, in this case the counter will go to infinity.

Yes, of course, you can invent a bicycle and independently monitor the number of tasks, but you want the “out of the box” option. Here, in my opinion, Apple engineers are not completely thought out.

Download tracking

The download task has a special delegate method:
 - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { double progress = (double)totalBytesWritten / (double)totalBytesExpectedToWrite; NSLog(@"download: %@ progress: %f", downloadTask, progress); dispatch_async(dispatch_get_main_queue(), ^{ self.progressView.progress = progress; }); } *) session downloadTask: (NSURLSessionDownloadTask *) downloadTask didWriteData: (int64_t) bytesWritten totalBytesWritten: (int64_t) totalBytesWritten totalBytesExpectedToWrite: (int64_t) totalBytesExpectedToWrite - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { double progress = (double)totalBytesWritten / (double)totalBytesExpectedToWrite; NSLog(@"download: %@ progress: %f", downloadTask, progress); dispatch_async(dispatch_get_main_queue(), ^{ self.progressView.progress = progress; }); } 

For the rest of the tasks, you can use KVO as in the previous example.

Loading in the background

Well, in the end we will deal with the example of loading in the background, the example repeats the demo from wwdc'13 705. Personally, the demo shook me. We start loading the picture, exit the application, come back - the picture is loaded and already laid out, and this can be seen even in the multitask menu (the one that by double pressing the home button). But more than that, if we drop the application at the time of download, the download will end and everything will return as if nothing happened! Moreover, after loading, our UI is updated right in the background, and the snapshot in the multitasking menu changes. The only case where the download does not end is when the user himself kills the application, but there's nothing you can do, the owner is the master.

Why does this “magic” work? The thing is that when an application starts a background process, the system creates a daemon, which transfers data to the application. It is logical, we need something that will live independently of the application. For this reason, we are not afraid of either stopping or crashing the application. After the download is complete, the daemon “wakes up” the application, after which we can restore the session and get all the data. Creating a new session with the old identifier will “connect” us to the existing background of the session.

Now let's look at the main points, the test project itself can be collected here .

First, in the singleton style, create a session:
 - (NSURLSession *)backgroundSession{ static NSURLSession *session = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ //         ,      NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfiguration:@"com.dev.BackgroundDownloadTest.BackgroundSession"]; [config setAllowsCellularAccess:YES]; session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil]; }); return session; } 

We start the download (there should be no questions here):
  self.downloadTask = [[self backgroundSession] downloadTaskWithURL:[NSURL URLWithString:@"https://discussions.apple.com/servlet/JiveServlet/showImage/2-20930244-204399/iPhone%2B5%2BProblem2.jpg"]]; [self.downloadTask resume]; 

In the delegate method for the background task, save the image and display it:
 - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location { // save image //   //... // set image if (success) { dispatch_async(dispatch_get_main_queue(), ^{ self.imageView.image = [UIImage imageWithContentsOfFile:[destinationPath path]]; [self.progressView setHidden:YES]; }); } } 


In the delegate method, to end all tasks, we catch the end of the load (in this case, both this and the previous methods will be called)
 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { if (error) { NSLog(@"error: %@ - %@", task, error); } else { NSLog(@"success: %@", task); } self.downloadTask = nil; //  ,     [self callCompletionHandlerIfFinished]; } 


Now let's move to AppDelegate.m
We need to catch messages from the system when the download is complete:
 - (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler { //    ,    UILocalNotification* locNot = [[UILocalNotification alloc] init]; locNot.fireDate = [NSDate dateWithTimeIntervalSinceNow:1]; locNot.alertBody = [NSString stringWithFormat:@"still alive!"]; locNot.timeZone = [NSTimeZone defaultTimeZone]; [[UIApplication sharedApplication] scheduleLocalNotification:locNot]; //     -   ,     , //   UI        . //      self.backgroundSessionCompletionHandler = completionHandler; } 


We return to the main controller.
Restore the session if necessary:
 - (void)viewDidLoad { [super viewDidLoad]; [self backgroundSession]; } 


The method that is called at the very end:
 - (void)callCompletionHandlerIfFinished { NSLog(@"call completion handler"); [[self backgroundSession] getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) { NSUInteger count = [dataTasks count] + [uploadTasks count] + [downloadTasks count]; if (count == 0) { //    //       //      UI NSLog(@"all tasks ended"); AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate]; if (appDelegate.backgroundSessionCompletionHandler == nil) return; void (^comletionHandler)() = appDelegate.backgroundSessionCompletionHandler; appDelegate.backgroundSessionCompletionHandler = nil; comletionHandler(); } }]; } 


I’ll add that if we don’t call this handler, we’ll get a warning to the log:
 Warning: Application delegate received call to - application:handleEventsForBackgroundURLSession:completionHandler: but the completion handler was never called. 


Also, if we open a multitasking menu, we will not see our updated interface. Actually, this example demonstrates one of the parties to the multi-tasking "UI", which Apple told us about.

That's all, I hope this article will encourage you to use NSURLSession in the next projects!

Source: https://habr.com/ru/post/209736/


All Articles