Implementing an async task in iOS - ios

I have a View controller where I have a button which brings up the front camera. The user then takes a photo, the camera controller is dismissed and I show the picture taken in an Image View in the View Controller. Now, I start uploading the server to Amazon S3.
It does take some time for the uploading to complete and the user has to wait for that time before moving to the next view.
I would ideally like to move the user to the next view and let the uploading complete in the background. Is there some way I could do this uploading task in the background ? I know their is something called dispatch_queue which could be used to do this but I am not sure how. If I put the uploading code inside a queue in the view controller file and then move to the next view controller, will not the reference of the previous one be lost.
EDIT
I tried doing the following -
Making a new class which would be responsible for uploading to Amazon S3.
This new class is a delegate to AmazonServiceRequest which should be called when the uploading to Amazon S3 is complete.
On completion uploading to S3, I make a call to my server to save the URL in the database.
I make an object of this class inside the queue block like follows -
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_async(queue, ^{
RIDEUploadPhotoService *uploadPhotoServiceObj = [[RIDEUploadPhotoService alloc] init;
[uploadPhotoServiceObj uploadAndSaveImage:imageToSave];
});
It seems to me that the control never comes inside the request complete which should get called when uploading to Amazon S3 is complete.

Just use one of the asynchronous upload-mechanisms of NSURLConnection/NSURLSession, so you don't have to mess around with background threads. For example:
NSURL *fileURL = [NSURL fileURLWithPath:...];
NSURLRequest *request = [NSURLRequest requestWithURL:...];
// modern way: NSURLSession
NSURLSessionUploadTask *uploadTask = [[NSURLSession sharedSession] uploadTaskWithRequest:request fromFile:fileURL completionHandler:...];
[uploadTask resume];
// old way: NSURLConnection
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:...];
// another old way: synchronous NSURLRequest executed in a background queue
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSURLResponse *response;
NSError *error;
NSData *responseData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
});

No. The reference will not be lost.
You can use - (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg to perform the method aSelector in the background. If you have any delegates after the completion of the task, they will still be called.

Related

Object dealloc is not called if NSURLSessionTask resume is called

I have a NSObject class 'DataDownload_A' for downloading data. Inside I use NSURLSession and NSURLSessionTask. Now the problem is when I set this object to nil, the dealloc won't be called. Here is a segment code of DataDownload_A:
NSString *urlString = [self URL];
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *theRequest = [NSURLRequest requestWithURL:url
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0];
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
self.session = [NSURLSession sessionWithConfiguration:configuration
delegate:self
delegateQueue:nil];
self.receivedData = [NSMutableData data];
self.task = [self.session dataTaskWithRequest:theRequest];
[self.task resume];
And if I comment the last two lines 'self.task = [self.session dataTaskWithRequest:theRequest];' and '[self.task resume];', dealloc will be called. Therefore I assume there must be some issues when I use them. I tried to called '[self.task cancel]', 'self.task = nil', '[self.session invalidateAndCancel];' and '[self.session resetWithCompletionHandler:nil];' before I set my object to nil. But didn't help.
So does anyone know what the mistake I made ?? Would be grateful letting me know. Thank you in advance.
It looks like the URLSession holds a strong reference to its delegate. I found this line in the docs on URLSession:
Important: The session object keeps a strong reference to the delegate
until your app explicitly invalidates the session. If you do not
invalidate the session, your app leaks memory.
That bit makes it sound like a URLSession holds a strong reference to its delegate from the moment it's set up until it's invalidated or your app is terminated, but it may be that it only creates a strong reference when a data (or other) task is started.
It sounds like you need to invalidate the URLSession in order to get it to release it's delegate. I suggest searching for that text in the URLSession docs in Xcode. Also search on "invalidate"

How to improve the speed while using NSURLSessionDownloadTask?

I need to download some images and a video from server:
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];
for (int i = 0; i < _collectionArr.count; ++i) {
NSURL *URL = [NSURL URLWithString:URL_ADDRESS(portNumber,_collectionArr[i])];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:nil destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
NSURL *documentsDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
return [documentsDirectoryURL URLByAppendingPathComponent:[response suggestedFilename]];
} completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
NSLog(#"File downloaded to: %#", filePath);
if (i == _collectionArr.count - 1) {
[SVProgressHUD showSuccessWithStatus:#"Done"];
}
}];
[downloadTask resume];
}
I found that it is so slow! The speed is about 200K/s and the time with Android is 2/3 less than iPhone.
When I download a mp4 about 4.6M, it takes 15s.
2014-11-29 13:46:35.071 testDownload[2105:47825] Begin
2014-11-29 13:46:51.740 testDownload[2105:47825] File downloaded to: file:///Users/apple/Library/Developer/CoreSimulator/Devices/259F2CB8-01FD-47C2-A38F-6100A2FF350A/data/Containers/Data/Application/71E553BC-F85D-4BFA-8937-FE9026FDF65C/Documents/VTS_01_audi.mp4
But when I using other app to download movies it can be 2M/s. Am I wrong when I use afnetworking ? How it happens and what can I do to deal with it.
Another question is that I know it's wrong to monitor the last request with if (i == _collectionArr.count - 1){[SVProgressHUD showSuccessWithStatus:#"Done"];} But I don't know the right answer.
Thanks for your help.
A couple of thoughts:
The removal of the HUD when i hits count - 1 is not correct. You actually may remove it before they're all done. If you start two downloads, one huge one and one tiny one, the HUD will be dismissed when the second one finishes, but the other one might not be done yet. These run concurrently, so you have to wait until they're all done, not just when the last submitted one is done.
One way to do this is to use a dispatch group (which you enter as you submit the requests, leave in the completion block, and then add a dispatch group notification that removes the HUD).
You're not doing anything else outside of this code that might be blocking the main thread are you? AFNetworking dispatches its completion blocks to the main queue, and if you block the main thread for anything, it will affect this performance. You could either use Instruments to identify waiting threads, or, for testing purposes only, temporarily change the completionQueue of the manager to be some custom queue of your own creation.
For example, before your for loop, do something like:
manager.completionQueue = dispatch_queue_create("com.example.app.netCompletionQueue", NULL);
That would take main thread blocking out of the equation (though this strikes me as unlikely unless you're doing something extraordinary in the main thread). Once you confirm this is not the issue, though, comment out that line, because you really do want to use the main thread for your completion blocks.
Once you confirm the perf problem is not result on main thread contention issue, I'd suggest doing some benchmarking of alternative techniques (e.g. your own NSURLSession, etc.). This could help diagnose whether the problem is AFNetworking, rather than something like simulator vs device performance, caching, etc. Frankly, I find it unlikely that AFNetworking is, itself, the problem, though.

Remotely Loading Images in an iPhone App

An app that I am making will contain mass amounts of images. So obviously it would be really bad to store them all on the users phone and take up all the disk space. As I am new to iOS Development, how would I best go about loading an image remotely into a UIImageView. What are my options?
Just to let you know I would only ever be loading a maximum of 2 images at a time, if it helps.
Any advise would be very much appreciated. Thank you in advance!
You can download image Asynchronously by unblocking the main thread using the below code
NSURLRequest* request=[NSURLRequest requestWithURL:[NSURL URLWithString:#"Your imageURL"]];
[NSURLConnection sendAsynchronousRequest:request queue:[[NSOperationQueue alloc]init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
if (error==nil && [data length]>0)
{
UIImage* img=[UIImage imageWithData:data];
// set the downloaded image to imageview here
}
else
{
NSLog(#"%#",error);
}
});
Use SDImageView or similar open sources, another options if you are loading images in table, use Lazy Loading by Apple

UITableView scrolls slow after downloading cell images

I am trying to download some thumbnail from server and then display them in each cell :
// displaying image...
dictionary = [newsFeed objectAtIndex:indexPath.row];
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[dictionary objectForKey:#"image"]]]];
cell.imageView.image = [image imageScaledToSize:CGSizeMake(55, 55)];
but after downloading and displaying images, the table view scrolls slowly!
The main problem is that [NSData dataWithContentsOfURL:] method is a synchronous request. It blocks the thread where is running until the request is done.
You should avoid this type of interaction if you run this on the main thread. This thread will be frozen and the user will be disappointed ;)
You have many solutions for this. A simple one is to use third library like SDWebImage or AFNetworking.
Both have categories around UIImageView class that allows to use them in a simple manner. For example, using UIImageView+AFNetworking you should do like the following:
[cell.imageView setImageWithURL:yourURL placeholderImage:yourPlaceholderImage];
I think You are trying to say this.
NSURL* url = [NSURL URLWithString:#"http://www.YourImageUrl.com"];
NSURLRequest* request = [NSURLRequest requestWithURL:url];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse * response,
NSData * data,
NSError * error) {
if (!error){
UIImage* image = [[UIImage alloc] initWithData:data];
// Now workout with the image
}
}];
This will make the asynchronous call, and load the images after tableview loaded i.e when the image load complete the image will show but the table will be loaded when the table view is needed to load.
Directly use like this
UIImage *image = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[dictionary objectForKey:#"image"]]]];
It takes the main thread, so you can't scroll befor download your Image.So You add NSURLConnectionDataDelegate, NSURLConnectionDelegate add this protocol and download image by
Insert these lines of coding where you want to download Your Image
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString]];
NSURLConnection *connection = [NSURLConnection connectionWithRequest:request delegate:self];
received_data = [[NSMutableData alloc]init];
[connection start];
These lines triggers these delegates to download data.
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
[received_data setLength:0];
}
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[received_data appendData:data];
}
-(void)connectionDidFinishLoading:(NSURLConnection *)connection
{
//Use your download image
imageView.image = [UIImage imageWithData:received_data];
}
Using AFNetworking will get you a better feel. There is a UIImageView category as part of the project that will insert a temporary image, while switching to a background thread to download the real image.
That will keep the image download from locking the main thread. Once the image is downloaded the temporary image will be swapped out for you.
Here's a nice tutorial that includes some pointers on using the UIImageView category AFNetworking crash course.
I have written an answer here:
https://stackoverflow.com/a/14624875/1375695
which explains how to use Grand Central Despatch to keep your table scrolling smoothly while your images are downloading in the background. That answer doesn't mandate the use of a 3rd party library. Although - as Andrew and Flexaddicted have mentioned - there are libraries which will provide a solution for you - it might be worth reading so that you can understand what needs to happen under the hood.
Note that in my proposed solution, you could still use your synchronous method to actually get the data from the network:
UIImage *image = [UIImage imageWithData:
[NSData dataWithContentsOfURL:
[NSURL URLWithString:
[dictionary objectForKey:#"image"]]]];
as it would be called on an asynchronous background thread.
I highly recommend the AFNetworking framework as #flexaddicted has recommended (UIImageView+AFNetworking). Also, check out the Sensible TableView framework. It should be able to automatically fetch all data from the web service and display them in the table view, and will also automatically handle all asynch image downloading. Should save you a bunch of work.
For More convenience to download image use EGOImageLoader/EGOImageView
Download this class by this link
https://github.com/enormego/EGOImageLoading

What is the best way to load a remote image?

I've been researching and haven't found any answer to this question - sendAsynchronousRequest vs. dataWithContentsOfURL.
Which is more efficient? more elegant? safer? etc.
- (void)loadImageForURLString:(NSString *)imageUrl
{
self.image = nil;
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
NSURLRequest * request = [NSURLRequest requestWithURL:[NSURL URLWithString:imageUrl]];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse * response, NSData * data, NSError * connectionError)
{
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
if (data) {
self.image = [UIImage imageWithData:data];
}
}];
}
OR
- (void)loadRemoteImage
{
self.image = nil;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSData * imageData = [NSData dataWithContentsOfURL:self.URL];
if (imageData)
self.image = [UIImage imageWithData:imageData];
dispatch_async(dispatch_get_main_queue(), ^{
if (self.image) {
[self setupImageView];
}
});
});
}
So I've come up with an answer for my own question:
Currently there are 3 main ways to load images async.
NSURLConnection
GCD
NSOperationQueue
Choosing the best way is different for every problem.
For example, in a UITableViewController, I would use the 3rd option (NSOperationQueue) to load an image for every cell and make sure the cell is still visible before assigning the picture. If the cell is not visible anymore that operation should be cancelled, if the VC is popped out of stack then the whole queue should be cancelled.
When using NSURLConnection + GCD we have no option to cancel, therefore this should be used when there is no need for that (for example, loading a constant background image).
Another good advice is to store that image in a cache, even it's no longer displayed, and look it up in cache before launching another loading process.
sendAsynchronousRequest is better, elegant and whatever you call it. But, personally, I prefer creating separate NSURLConnection and listen to its delegate and dataDelegate methods. This way, I can: 1. Set my request timeout. 2. Set image to be cached using NSURLRequest's cache mechanism (it's not reliable, though). 2. Watch download progress. 3. Receive NSURLResponse before actual download begins (for http codes > 400). etc... And, also, it depends on cases like, image size, and some other requirements of your app. Good luck!

Resources