What's the use of reachability in AFNetworking? - ios

When in the middle of download connection is lost or there was no connection initially, completionHandler is fired with error and I have no chance to resume after connection restored. What is the proper way to handle resumable downloading with AFNetworking/reachability? Do I have to create another task because this one is already expired due to network failure or there is a way to revive it?
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
AFURLSessionManager *man = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];
NSURL *URL = [NSURL URLWithString:#"http://my_server.com/video/2.mp4"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
NSURLSessionDownloadTask *downloadTask = [man 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: %#, error: %#", filePath, error);
}];
[man.reachabilityManager startMonitoring];
[man.reachabilityManager setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
switch (status) {
case AFNetworkReachabilityStatusReachableViaWWAN:
case AFNetworkReachabilityStatusReachableViaWiFi:
[downloadTask resume];
break;
case AFNetworkReachabilityStatusNotReachable:
default:
[downloadTask suspend];
break;
}
}];

What is the proper way to handle resumable downloading with AFNetworking/reachability? Do I have to create another task because this one is already expired due to network failure or there is a way to revive it?
What you are asking for is how to resume download after a connection failure. You start an NSURLSessionDownloadTask, and during the download the connection fails with an appropriate NSError describing the failure. You want to retry the connection, reusing any previously downloaded data, when the network interface reachability changes.
NSURLSessionDownloadTask can reuse the previously downloaded data if your application grabs the resume data and later passes it to the task that retries the connection. This is documented here and here.
In the case of a network failure, tthe NSError returned will have a userInfo dictionary populated with the NSURLSessionDownloadTaskResumeData key. This will have the data you need to hold on to. When your application attempts to download the data again, the download task should be created using either downloadTaskWithResumeData: or downloadTaskWithResumeData:completionHandler:, passing in the resume data.
In your case, after a network failure that returns an NSError with the NSURLErrorDomain, an appropriate error code, and a userInfo dictionary with a populated NSURLSessionDownloadTaskResumeData key, you would hold on to the NSData value for the NSURLSessionDownloadTaskResumeData key and start monitoring for reachability changes. If the network interface comes back, in the reachability change notification handler you would create a new download task using downloadTaskWithResumeData: or downloadTaskWithResumeData:completionHandler: and pass the NSData you retreived from the NSError's userInfo dictionary.

Take a look to this answer.
AFNetworking + Pause/ Resume downloading big files
Another way to do it is setting the HTTPHeaderField:#"Range" from your NSMutableURLRequest. to set this header use a formatted string that looks like: [NSString stringWithFormat:#"bythes=%lld-", downloadedBytes]
But as you are using AFNetworking instead of NSMutableURLRequest then just follow the instructions in the link I posted.
Worth to mention that if you use NSMutableURLRequest you will have to go to the place where you are writing the file and check the size of it in order to set the header and the server can provide you the remaining of the file from the last downloaded byte.

Related

Http/2 server_push with NSURLSession

I have implemented a program to communicate http2 using NSURLSession of iOS9, And it can communicate with my server in http2.
However, I'm having a problem with receive server_push.
I found ENABLE_PUSH value is 0 in their settings and there's no delegate in receive server push in NSURLSession...
・I think NSURLSession doesn't support server_push. Is this right?
・If it support server_push,how to use?
/**
It WORKS for post data and get response.
I don't know the code should be added here
in order to get the server_push.
I suspect that NSURLSession itself cannot receive the server_push(;_;)
**/
- (void) postData
{
NSString *urlstr = self.urlArea.text;
NSURL * url = [NSURL URLWithString:urlstr];
NSDictionary *params = #{#"data":#""};
//json to query
NSData *query = [self buildQueryWithDictionary: params];
//make request
NSMutableURLRequest *request = [NSMutableURLRequest
requestWithURL:url
cachePolicy: NSURLRequestReloadIgnoringCacheData
timeoutInterval: 10.0];
[request setHTTPMethod: #"POST"];
[request setHTTPBody: query];
//prepare session
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
//resume
[[session dataTaskWithRequest: request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
{
if (response && ! error) {
NSLog(#"Data: %#", [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding]);
}else {
NSLog(#"ERR: %#", error);
}
}] resume];
}
I read this here:
The HTTP2 push mechanism is not a generic server push mechanism like
websocket or server sent events.
It is designed for a specific optimisation of HTTP conversations.
Specifically when a client asks for a resource (eg index.html) the
server can guess that it is going to next ask for a bunch of
associated resources (eg theme.css, jquery.js, logo.png, etc. etc.)
Typically a webpage can have 10s of such associated requests.
With HTTP/1.1, the server had to wait until the client actually sends
request for these associated resources, and then the client is limited
by connections to only ask for approx 6 at a time. Thus it can take
many round trips before all the associated resources that are needed
by a webpage are actually sent.
With HTTP/2, the server can send in the response to the index.html GET
push promises to tell the client that it is going to also send
theme.css, jquery.js, logo.png, etc. as if the client had requested
them. The client can then cancel those pushes or just wait for them to
be sent without incurring the extra latency of multiple round trips.
ere is a blog about the push API for HTTP2 and SPDY in jetty:
https://webtide.com/http2-push-with-experimental-servlet-api/
Solved
I received following reply from support.
iOS does not currently support this.
update
(#vin25 comment)
ios10 supports it.

returning a value from asynchronous call using semaphores

I need to use NSURLSession to make network calls. On the basis of certain things, after I receive the response, I need to return an NSError object.
I am using semaphores to make the asynchronous call behave synchronously.
The problem is, the err is set properly inside call, but as soon as semaphore ends (after
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
), the err becomes nil.
Please help
Code:
-(NSError*)loginWithEmail:(NSString*)email Password:(NSString*)password
{
NSError __block *err = NULL;
// preparing the URL of login
NSURL *Url = [NSURL URLWithString:urlString];
NSData *PostData = [Post dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
// preparing the request object
NSMutableURLRequest *Request = [[NSMutableURLRequest alloc] init];
[Request setURL:Url];
[Request setHTTPMethod:#"POST"];
[Request setValue:postLength forHTTPHeaderField:#"Content-Length"];
[Request setHTTPBody:PostData];
NSMutableDictionary __block *parsedData = NULL; // holds the data after it is parsed
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.TLSMinimumSupportedProtocol = kTLSProtocol11;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:nil];
NSURLSessionDataTask *task = [session dataTaskWithRequest:Request completionHandler:^(NSData *data, NSURLResponse *response1, NSError *err){
if(!data)
{
err = [NSError errorWithDomain:#"Connection Timeout" code:200 userInfo:nil];
}
else
{
NSString *formattedData = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"%#", formattedData);
if([formattedData rangeOfString:#"<!DOCTYPE"].location != NSNotFound || [formattedData rangeOfString:#"<html"].location != NSNotFound)
{
loginSuccessful = NO;
//*errorr = [NSError errorWithDomain:#"Server Issue" code:201 userInfo:nil];
err = [NSError errorWithDomain:#"Server Issue" code:201 userInfo:nil];
}
else
{
parsedData = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&err];
NSMutableDictionary *dict = [parsedData objectForKey:#"User"];
loginSuccessful = YES;
}
dispatch_semaphore_signal(semaphore);
}];
[task resume];
// but have the thread wait until the task is done
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return err;
}
Rob's answer tells you how to do it right, but not what mistake you made:
You have two variables named err, which are totally unrelated. It seems that you haven't turned on some important warnings, otherwise your code wouldn't even have compiled.
The parameter err that is passed to your completion block is the error from the URL request. You replace it without thinking with a timeout error - so the true error is now lost. Consider that timeout is not the only error.
But all the errors that you set only set the local variable err which was passed to you in the completion block; they never touch the variable err in the caller at all.
PS. Several serious errors in your JSON handling. JSON can come in UTF-16 or UTF-32, in which case formattedData will be nil and you incorrectly print "Server Issue". If the data isn't JSON there is no guarantee that it contains DOCTYPE or html, that test is absolute rubbish. Your user with the nickname JoeSmith will hate you.
Passing NSJSONReadingAllowFragments to NSJSONSerialization is nonsense. dict is not mutable; if you try to modify it your app will crash. You don't check that the parser returned a dictionary, you don't check that there is a value for the key "User", and you don't check that the value is a dictionary. That's lots of ways how your app can crash.
I would suggest cutting the Gordian knot: You should not use semaphores to make an asynchronous method behave synchronously. Adopt asynchronous patterns, e.g. use a completion handler:
- (void)loginWithEmail:(NSString *)email password:(NSString*)password completionHandler:(void (^ __nonnull)(NSDictionary *userDictionary, NSError *error))completionHandler
{
NSString *post = ...; // build your `post` here, making sure to percent-escape userid and password if this is x-www-form-urlencoded request
NSURL *url = [NSURL URLWithString:urlString];
NSData *postData = [post dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:#"POST"];
// [request setValue:postLength forHTTPHeaderField:#"Content-Length"]; // not needed to set length ... this is done for you
[request setValue:#"application/x-www-form-urlencoded" forHTTPHeaderField:#"Content-Type"]; // but it is best practice to set the `Content-Type`; use whatever `Content-Type` appropriate for your request
[request setValue:#"text/json" forHTTPHeaderField:#"Accept"]; // and it's also best practice to also inform server of what sort of response you'll accept
[request setHTTPBody:postData];
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.TLSMinimumSupportedProtocol = kTLSProtocol11;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:nil];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *err) {
if (!data) {
dispatch_async(dispatch_get_main_queue(), ^{
completionHandler(nil, [NSError errorWithDomain:#"Connection Timeout" code:200 userInfo:nil]);
});
} else {
NSError *parseError;
NSDictionary *parsedData = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&parseError];
dispatch_async(dispatch_get_main_queue(), ^{
if (parsedData) {
NSDictionary *dict = parsedData[#"User"];
completionHandler(dict, nil);
} else {
completionHandler(nil, [NSError errorWithDomain:#"Server Issue" code:201 userInfo:nil]);
}
});
}
}];
[task resume];
}
And then call it like so:
[self loginWithEmail:userid password:password completionHandler:^(NSDictionary *userDictionary, NSError *error) {
if (error) {
// do whatever you want on error here
} else {
// successful, use `userDictionary` here
}
}];
// but don't do anything reliant on successful login here; put it inside the block above
Note:
I know you're going to object to restoring this back to asynchronous method, but it's a really bad idea to make this synchronous. First it's a horrible UX (the app will freeze and the user won't know if it's really doing something or whether it's dead) and if you're on a slow network you can have all sorts of problems (e.g. the watchdog process can kill your app if you do this at the wrong time).
So, keep this asynchronous. Ideally, show UIActivityIndicatorView before starting asynchronous login, and turn it off in the completionHandler. The completionHandler would also initiate the next step in the process (e.g. performSegueWithIdentifier).
I don't bother testing for HTML content; it is easier to just attempt parse JSON and see if it succeeds or not. You'll also capture a broader array of errors this way.
Personally, I wouldn't return my own error objects. I'd just go ahead and return the error objects the OS gave to me. That way, if the caller had to differentiate between different error codes (e.g. no connection vs server error), you could.
And if you use your own error codes, I'd suggest not varying the domain. The domain should cover a whole category of errors (e.g. perhaps one custom domain for all of your app's own internal errors), not vary from one error to another. It's not good practice to use the domain field for something like error messages. If you want something more descriptive in your NSError object, put the text of the error message inside the userInfo dictionary.
I might suggest method/variable names to conform to Cocoa naming conventions (e.g. classes start with uppercase letter, variables and method names and parameters start with lowercase letter).
There's no need to set Content-Length (that's done for you), but it is good practice to set Content-Type and Accept (though not necessary).
You need to let the compiler know that you will be modifying err. It needs some special handling to preserve that beyond the life of the block. Declare it with __block:
__block NSError *err = NULL;
See Blocks and Variables in Blocks Programming Topics for more details.

Cookies handling with JSONModel/JSONHTTPClient in iOS

In my current project I am using, +(void)postJSONFromURLWithString:(NSString*)urlString params:(NSDictionary*)params completion:(JSONObjectBlock)completeBlock; method to create account and log in my application for the very first time . For second time and onwards, log in call is not there, application directly opens the user's profile screen. But when I am updating user profile (name, contact number etc.), I am getting response status code 403 from by the statement
NSLog(#"Response status code = %i", (int)response.statusCode);
added in implementation of method
+(NSData*)syncRequestDataFromURL:(NSURL*)url method:(NSString*)method requestBody:(NSData*)bodyData headers:(NSDictionary*)headers etag:(NSString**)etag error:(JSONModelError**)err
403 is generally invoked due to authorization failure in server side.
Is there any way to see what are the cookies are going to server side while I am making an API call with
+(void)postJSONFromURLWithString:(NSString*)urlString params:(NSDictionary*)params completion:(JSONObjectBlock)completeBlock;?
JSONModel's built-in networking support is intentionally very basic/primitive/limited. It does not support custom authentication, cookies, etc.
You'd be best making a request with NSURLSession to get the data, then use JSONModel just for the deserialization - rather than for the whole request.
e.g.:
NSURLSession *session = [NSURLSession sharedSession];
NSURL *url = [NSURL URLWithString:#"https://example.com/mydata.json"];
NSURLSessionDataTask *task = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
MyModel *obj = [[MyModel alloc] initWithData:data error:nil];
}];
[task resume];

AFURLSessionManager downloadTaskWithRequest completionHandler not Asynchronous

While working with the AFNetworking library I am running into an issue where after downloading JSON data into a file using the AFURLSessionManager downloadTaskWithRequest's destination param code block asynchronously, I am wanting to perform the remaining operations asynchronously as well in its completionHandler block. The problem is the completionHandler block does not seem to run asynchronously.
Would there be a need to setup a new session manager and/or download task to accomplish this. Is there perhaps a better way to do this so the operations can be performed away from the main thread in the completionHandler block.
The reason for wanting to accomplish this is to avoid tying up the main thread in case there's a huge amount of data which needs to be assigned to the self.googleResults array or rather in a for loop using a custom class containing properties for specific key data which would eventually be added as elements to an array.
Here's the code so far...
- (void)viewDidLoad
{
[super viewDidLoad];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
NSURL *url = [NSURL URLWithString:#"https://ajax.googleapis.com/ajax/services/search/web?v=1.0&q=json"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:nil destination:^NSURL *(NSURL *targetPath, NSURLResponse *response)
{
// NOTE: This code block runs asynchronously
NSURL *docPathURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
return [docPathURL URLByAppendingPathComponent:[response suggestedFilename]];
} completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error)
{
// NOTE: This code block does not run asynchronously
// Would there be a need to create a new session and/or download task here to get the data from the filePath asynchronously?
// Or is there another way to this for the following code?
NSError *jsonSerializationErr;
NSData *jsonData = [NSData dataWithContentsOfURL:filePath];
NSDictionary *reponseDictionary = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&jsonSerializationErr];
// self.googleResults is an instance of (NSArray *)
self.googleResults = [[reponseDictionary objectForKey:#"responseData"] objectForKey:#"results"];
NSLog(#"%#", self.googleResults);
}];
[downloadTask resume];
}

How to recognize file download

I need to download files from internet inside the app using internal web browser and save them to some custom path.
I think I should use UIWebViewDelegate and intercept lick clicks in
- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType;
But here's my question: how can I know when the file is being downloaded (not just other webpage being opened).
I thought I could parse the link and determine if the extension is png, doc or something. But the problem is I need to be able to download the files of any type.
Thank you for any help.
UPDATE:
How can I recognize that this is link is actually for downloading. E.g. in this case -> etextlib.ru/Book/DownLoadPDFFile/19036 <- the link does not have recognizable extension.
You only have two ways to go about this in my opinion, and both involve implementing shouldStartLoadWithRequest:
Define which file types you want to download and check for them in the tapped link.
On any tap the user makes on a link, do the following
If the link is to a known file type, download it.
If the link is a web page (htm, html, asp, ...), just open it
Any other case: Ask user whether he wants to download the link as a file.
AFNetworking
Creating a Download Task
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];
NSURL *URL = [NSURL URLWithString:#"http://example.com/download.zip"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:nil destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
NSURL *documentsDirectoryPath = [NSURL fileURLWithPath:[NSSearchPathForDirectorie
sInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]];
return [documentsDirectoryPath URLByAppendingPathComponent:[response suggestedFilename]];
} completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
NSLog(#"File downloaded to: %#", filePath);
}];
[downloadTask resume];
just check [request.URL.absoluteString pathExtension] to see the type of link and do what you want

Resources