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];
}
Related
I have a method that returns a string usually locally, but with a backup from the Web. I was retrieving some JSON using dataWithContentsOfUrl but want to switch to using a Session object which is better for the UI and also--if I am not mistaken--allows the server to set a sessionId on the phone, however, I'm struggling with the async issue.
With the old code, I just returned the JSON but I'm struggling with how to do this for the asynchronous result. I can't change the calling method which returns a string. What can I do with the asynchronous Api call to use the data that is retrieved?
async:
-(void)getAsyncAnswerFor:(NSString*) str {
NSString *surl = [NSString stringWithFormat: #"https://~.com//api.php?q=%#",str];
NSURL *url = [NSURL URLWithString:surl];
NSURLSessionDataTask *downloadTask = [[NSURLSession sharedSession]
dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
//HOW DO I PASS THIS BACK TO THE CALLING METHOD OR IS THAT IMPOSSIBLE
}];
[downloadTask resume];
}
sync
-(NSString*)getAnswerFor:(NSString*) str {
NSError *error;
NSString *surl = [NSString stringWithFormat: #"https://~.com//api.php?q=%#",str];
NSData *data = [NSData dataWithContentsOfURL: [NSURL URLWithString:surl]];
NSMutableArray *json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
//process JSON
if (error) {
return #"";
}
return #"processed JSON";
}
Would appreciate any suggestions.
If what I want to do is totally impossible, is it possible to set a sessionID on the phone without the Session object? I know setting a session ID is is not the greatest approach, but I'm trying to avoid a lot of authentication overhead.
You can pass a block to your asynchronous function and then call it when the url session completion handler is called. This is a trivial example:
- (void)doSomethingWithBlock:(void (^)(double, double))block {
...
block(21.0, 2.0);
}
I lifted this ^^ from the Apple Docs but you might be able to do something like this: (Note: I didn't check this in a compiler!)
-(void)getAsyncAnswerFor:(NSString*) str completion:(void (^)(NSData, NSURLResponse, NSError))block {
NSString *surl = [NSString stringWithFormat: #"https://~.com//api.php?q=%#",str];
NSURL *url = [NSURL URLWithString:surl];
NSURLSessionDataTask *downloadTask = [[NSURLSession sharedSession]
dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
block(data, response, error);
}];
[downloadTask resume];
}
You'll need to be careful if you try to reference self anywhere in the blocks.
I am trying to download a file using AFNetworking (2.5.4). The download completes, the completion handler is called, with error set to nil, everything seeming fine, but the destination file does not exist:
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
NSString *fullPath = [valid path from my apps local manager]
NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:req progress:nil destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
return [NSURL URLWithString:fullPath];
} completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
NSLog(#"Saved file to %#.", filePath);
*** [[NSFileManager defaultManager] fileExistsAtPath:filePath.absoluteString] returns NO here ***
}];
[cell.progressView setProgressWithDownloadProgressOfTask:downloadTask animated:YES];
[downloadTask resume];
The file path is a regular path that my app has write access to:
/var/mobile/Containers/Data/Application/APP-GUID-REDACTED/Documents/FILE-NAME-REDACTED.docx
I was using a different method before AFNetworking, and it could write to the exact same path just fine. HTTP response headers show everything perfectly (status 200, correct content length etc.) and if I curl the download URL it downloads the file with no issues. There's no problem with the file.
Why is my destination file not written in completion handler despite no errors?
UPDATE: I've also tried AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]]; but it changes nothing. I've also tried creating an NSProgress pointer and sending that for the progress argument, but no avail.
Use [NSURL fileURLWithPath:] (not URLWithString).
return [NSURL fileURLWithPath:fullPath];
The problem here is wrong file path or invalid file path. I had same problem here.
Create path like given below :
NSURL *documentsDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
NSURL *filePathURL = [documentsDirectoryURL URLByAppendingPathComponent:[NSString stringWithFormat:#"your file name here",i]];
Now use above path :
NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:req progress:nil destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
return filePathURL;
}
Idea
I'm building files download manager using AFNetworking and I'm using AFURLSessionManager class. the app is suppose to download mp3 files from the server.
I was concerned about memory consuming, so I'm trying to limit the number of simultaneous downloads to 1.
I know that there is a NSOperationQueue property in AFURLSessionManager called operationQueue and it's limited to 1 operation at a time by default.so I'm adding my NSURLSessionDownloadTask to operationQueue.
the problem
the code isn't working. files is being downloaded simultaneously instead of one after another.
the code
// 1. build sessionManager and prepare some vars
// note: by testing i found that it's better to init NSURLSessionConfiguration with backgroundSessionConfigurationWithIdentifier for memory issues
NSURLSessionConfiguration *conf = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:#"special_Identifier"];
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:conf];
NSURL *urlDocs = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory
inDomain:NSUserDomainMask
appropriateForURL:nil
create:NO
error:nil];
__block NSProgress *progress = Nil;
// 2. open sessionManager operation Queue and add this new download
[manager.operationQueue addOperationWithBlock:^{
// 2.1 init new download request
NSURLRequest *request = [[NSURLRequest alloc]initWithURL:[NSURL URLWithString:fileLink]];
// 2.2 creat a NSURLSessionDownloadTask
NSURLSessionDownloadTask *downloadTask = [self.downloadManager downloadTaskWithRequest:request progress:&progress
destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
return [urlDocs URLByAppendingPathComponent:fileName];
} completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
if (!error) {
NSLog(#"done: %#", filePath);
}else{
NSLog(#"error %#",error);
}
}];
// 2.3 start downloading
[downloadTask resume];
// 2.4 track downloading progress using KVO
[progress addObserver:self
forKeyPath:NSStringFromSelector(#selector(fractionCompleted))
options:NSKeyValueObservingOptionNew
context:(__bridge void *)(fileLink)];
}];
In AFNetworking 2 (and AFNetworking 3), you can init your AFHTTPSessionManager with an NSURLSessionConfiguration (use AFHTTPSessionManager initWithBaseURL:sessionConfiguration:). There you can specify the number of connections per host (HTTPMaximumConnectionsPerHost).
Sample:
NSURL *url = [NSURL URLWithString:#"myurl.net"];
NSURLSessionConfiguration *configuration = NSURLSessionConfiguration.defaultSessionConfiguration;
configuration.HTTPMaximumConnectionsPerHost = 1;
AFHTTPSessionManager *sessionManager = [[AFHTTPSessionManager alloc] initWithBaseURL:url sessionConfiguration:sessionConfiguration];
Documentation:
AFHTTPSessionManager: http://cocoadocs.org/docsets/AFNetworking/3.0.4/Classes/AFHTTPSessionManager.html#//api/name/initWithBaseURL:sessionConfiguration:
NSURLSessionConfiguration: https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSessionConfiguration_class/#//apple_ref/occ/instp/NSURLSessionConfiguration/HTTPMaximumConnectionsPerHost
I am trying to fetch a JSON-feed but somehow the command is never executed. I have placed a NSLog just before the session gets called and that actually gets output on the console. The NSLog later "test" never gets output. I can't find out where the problem is. Another JSON request works just fine. Here is the code:
NSLog(#"fetchClassified started!");
// connect to webserver and ask for the feed
NSURL *url = [NSURL URLWithString:#"http://test.server/services/rest/v1/interface2?id=22"];
NSURLRequest *req = [NSURLRequest requestWithURL:url];
// create a task that transfers the feed from the server
NSURLSessionTask *dataTask = [self.session dataTaskWithRequest:req
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSDictionary *jsonObject = [NSJSONSerialization JSONObjectWithData:data
options:0 error:nil];
NSLog(#"test %#", jsonObject);
self.classified = jsonObject[#"tasks"];
NSLog(#"%#", self.classified);
// put the output on the main queue (UI has to run always on main thread)
dispatch_async(dispatch_get_main_queue(), ^{
self.textView.text =self.classified;
});
}
];
[dataTask resume];
Thank you in advance for any hint on this!
JoeFryer solved it. self.session was nil.
I'm relatively new to iOS development but I'm working on an application to get a better understanding of development. I'm working with a web service and want to check the credentials a user enters. To do this I am making a simple get request with their credentials and then checking the http status for 200. Here is my code below:
-(BOOL)checkCredentials:(NSString *)username withPassword:(NSString *)password{
NSString *requestString = #"SOME URL";
NSURL *url = [NSURL URLWithString:requestString];
NSURLRequest *req = [NSURLRequest requestWithURL:url];
NSData *userPasswordData = [[NSString stringWithFormat:#"%#:%#", username, password] dataUsingEncoding:NSUTF8StringEncoding];
NSString *base64EncodedCredential = [userPasswordData base64EncodedStringWithOptions:0];
NSString *authString = [NSString stringWithFormat:#"Basic %#", base64EncodedCredential];
NSURLSessionConfiguration *sessionConfig=[NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.HTTPAdditionalHeaders=#{#"Authorization":authString};
self.session=[NSURLSession sessionWithConfiguration:sessionConfig];
__block BOOL success = NO;
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if(!error){
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
if (httpResp.statusCode == 200) {
success = YES;
}
}
NSMutableDictionary *jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(#"%#", jsonObject);
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
[dataTask resume];
return success;
}
I was going to use a semaphore to wait for the block to complete so I can check the status code and then return. But first it seems like my code just hangs, and I think that because I don't have a release, but that's not allowed with ARC. I'm not sure why it's hanging. Is there a better way to wait for the block to complete (without a semaphore) so I can return whether my credentials are valid?
Also is there a better way to pass the username and password so that it's not possible for someone to spoof the username and password?
Any help would be greatly appreciated.
Think simple!
Make your own completionHandler so that you won't deal with the return anymore, the caller will take the responsibility of result verification instead.
There's one thing you need to keep in mind, that if you want to modify anything related to UI (User Interface), you need to dispatch your completion block to main queue or you will get unexpected behavior, see more detail here.
Change your return type to void and add a completion block:
-(void)checkCredentials:(NSString *)username withPassword:(NSString *)password completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))myCompletion
{
NSString *requestString = #"http://google.com";
NSURL *url = [NSURL URLWithString:requestString];
NSURLRequest *req = [NSURLRequest requestWithURL:url];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// Here you return exactly what the NSURLSessionDataTask downloaded
// and pass it to the caller as an another completion block
myCompletion(data, response, error);
}];
[dataTask resume];
}
Caller's code, I assume that self is the caller:
[self checkCredentials:#"" withPassword:#"" completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if(!error){
// Result verification's here
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
if (httpResp.statusCode == 200) {
NSLog(#"SUCESS");
}
}
}];
You code stops waiting for a semaphore and [dataTask resume] is never executed.
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); <=== waits here
[dataTask resume]; <=== never reached
I'd suggest not using the semaphore here. Do the work in your block instead.
As to username/password. If you worry about spoofing then SSL layer on top of HTTP is the answer.
This is a really dangerous pattern, because this call is going to block until the network request completes. If this is on the main thread, your app will stop responding and the watchdog may kill you.
That warning aside, the reason the block doesn't complete is because the network task is never started. You trap on your semaphore before you call resume, so your task never runs. I would also, personally use a dispatch_group to do the waiting.
To make it better, you would need to rewrite it asynchronously. Basically have your app continue to function, maybe disable the inputs, until the call completes, then run a block to re-enable them, or show an error:
// Assume your login button and whatever are exposed as properties here
self.loginButton.enabled = NO;
NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:req completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if(!error){
NSHTTPURLResponse *httpResp = (NSHTTPURLResponse*) response;
if (httpResp.statusCode == 200) {
success = YES;
}
}
NSMutableDictionary *jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(#"%#", jsonObject);
// Need to be back on the main queue, the call is complete
self.loginButton.enabled = YES;
}];
[dataTask resume];
Or, just to keep it the way you have it, but resolve the immediate issue, re-order your trap so that it happens after the task resumes:
[dataTask resume];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); // might want to time out here instead of waiting forever
return success;