I'm developing a library that gets json data from a server, and I'm using NSURLSessionDataTask. In order to test my library I created a new project that calls this library method.
typedef void (^CompletionBlock)();
// More stuff...
- (void)downloadAllPodcastsMetadataWithCompletionHandler:(CompletionBlock)block{
NSURL *url = [NSURL URLWithString:#"MyServerURL"];
NSURLSessionDataTask *dataTask = [self.session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// Connection and response handling stuff...
// When my data is saved, executes the block parameter
dispatch_async(dispatch_get_main_queue(), ^{
block();
});
}
[dataTask resume];
}
EDIT: I forgot to put [dataTask resume] here, but it's in my code. Sorry.
EDIT 2: So, as some of you have said, i changed dispatch_sync with dispatch_async. But the result is the same :(
In my test project i call this method like this.
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
[manager downloadAllPodcastsMetadataWithCompletionHandler:^{
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
}];
But the network activity Indicator never shows off. It's like the block parameter never executes.
It's because it executes the NSURLSessionDataTask logic inside a library and then I should use something else instead dispatch_sync?
I already checked NSURLSessionDataTask not executing the completion handler block and I think I do the same. If it helps, manager is a Singleton. Any thoughts?
Thank you.
You need to start the download with [dataTask resume];.
You need to add [dataTask resume]; and also add this in completion handler
dispatch_async(dispatch_get_main_queue(), ^{
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
});
I tested some code.
Dispatch_sync won't work.
dispatch_sync(dispatch_get_main_queue(), ^{
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
});
Dispatch_async worked for me. But I had to put the code on veiwWillAppear.
Not sure what's going to happen using it inside a block...
dispatch_async(dispatch_get_main_queue(), ^{
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
});
Well, i'm dumb.
There was an error in my previous code i didn't notice, so the completion block war never executing. That is what happens when someone has too much confidence in his code.
Sorry for bothering you.
Related
I'm new to queues and threading. Below I have the following code that seeks to run various methods within an asynchronous queue. Each item in the queue will update a count when finished, and when all the items are done in the queue, the update count will be complete and ready for return.
The problem is that when this code runs the last line is called and then, it hangs with no errors and I cannot continue to step through the trace.
__block int masterUpdateCount = 0;
dispatch_group_t group = dispatch_group_create();
dispatch_group_enter(group);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self updateStuffWithCompletionCount:^(int updatedItemsCount) {
masterUpdateCount += updatedItemsCount;
}];
[self updateMoreStuffWithCompletionCount:^(int updatedItemsCount) {
masterUpdateCount += updatedItemsCount; // Never gets called
}];
}); // <------ APPLICATION HANGS HERE after calling dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
return masterUpdateCount;
Something I noticed is that the completion blocks never get called, which could very well be why it hangs forever, but my question is why? If it helps, inside the updateStuffWithCompletionCount type methods I'm actually initializing an NSURLSession with an NSURLSessionDataTask and I am in fact running the task by calling [task resume];, so I don't see why the completion wouldn't be called.
Here is what it looks like inside the updateStuffWithCompletionCount method:
- (void) updateStuffWithCompletionCount: (void (^)(int)) completionResult
{
__block int updateCount = 0;
NSString *someURL = #"www.someplace.com";
NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];
NSURLSessionDataTask * getDataTask = [defaultSession dataTaskWithURL:[NSURL someURL] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
{
if(error == nil)
{
// do stuff, if updated, add to count
updateCount ++;
if(updateCount > 0) {
completionResult(updateCount); // Invoke the completion handler
} else {
completionResult(0); // Invoke the completion handler
}
}
else {
NSLog(#"Error: %#", error);
completionResult(-1);
}
}];
[getDataTask resume];
}
Hopefully an experienced eye can point it out. Appreciated.
Include code for your dataTask please. This line masterUpdateCount += updatedItemsCount; // Never gets called , because its in the completion block which means it doesn't get ran until your datatask is complete, Did you set a breakpoint within your datatask and make sure something its jumping in there? Do you have a NSLog(%#, error.description) line in there in case your getting one? Looks like you may need to do more debugging there.
I have a for loop containing three asynchronous methods, and I want to make some treatment after this 3 async methods are finished.
-(void)getAllUsersInformations{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for(User *user in users){
[self getUserInfo:user];
}
//Here, I want to reload the table view for example, after finishing the for loop (executing the whole three methods).
});
}
-(void)getUserInfo:(User*)user{
[self getInformations:user];
[self getExperiences:user];
[self getEducation:user];
}
Do you have any technic to have this result?
Thank you very much.
One GCD approach is to use dispatch_group. So, before you start an asynchronous task, call dispatch_group_enter, and then when the asynchronous task finishes, call dispatch_group_leave, and you can then create a dispatch_group_notify which will be called when the asynchronous tasks finish. You can marry this with a completion block pattern (which is a good idea for asynchronous methods, anyway):
If getInformations, getExperiences and getEducation are, themselves, all asynchronous methods, the first thing you need is some mechanism to know when they're done. A common solution is to implement a completion block pattern for each. For example:
// added completionHandler parameter which will be called when the retrieval
// of the "informations" is done.
- (void)getInformations:(User*)user completionHandler:(void (^)(void))completionHandler {
// do whatever you were before, but in the asynchronous task's completion block, call this
// completionHandler()
//
// for example
NSURLRequest *request;
[NSURLConnection sendAsynchronousRequest:request queue:nil completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
// handle the request here
// the important thing is that the completion handler should
// be called _inside_ the this block
if (completionHandler) {
completionHandler();
}
}];
}
Repeat this process for getExperiences and getEducation, too.
Then, you can use a dispatch group to notify you of when each of these three requests are done done, calling a completion block in getUserInfo when that takes place:
// added completion handler that will be called only when `getInformations`,
// `getExperiences` and `getEducation` are all done.
//
// this takes advantage of the completion block we added to those three
// methods above
- (void)getUserInfo:(User*)user completionHandler:(void (^)(void))completionHandler {
dispatch_group_t group = dispatch_group_create();
// start the three requests
dispatch_group_enter(group);
[self getInformations:user completionHandler:^{
dispatch_group_leave(group);
}];
dispatch_group_enter(group);
[self getExperiences:user completionHandler:^{
dispatch_group_leave(group);
}];
dispatch_group_enter(group);
[self getEducation:user completionHandler:^{
dispatch_group_leave(group);
}];
// this block will be called asynchronously only when the above three are done
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
if (completionHandler) {
completionHandler();
}
});
}
And you then repeat this process at the getAllUsersInformations:
// call new getUserInfo, using dispatch group to keep track of whether
// all the requests are done
-(void)getAllUsersInformations {
dispatch_group_t group = dispatch_group_create();
for(User *user in users){
dispatch_group_enter(group);
[self getUserInfo:user completionHandler:^{
dispatch_group_leave(group);
}];
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
}
Two final thoughts:
Having outlined all of that, I must confess that I would probably wrap these requests in concurrent/asynchronous custom NSOperation subclasses instead of using dispatch groups. See the "Configuring Operations for Concurrent Execution" section of the Concurrency Programming Guide. This is a more radical refactoring of the code, so I won't tackle that here, but it lets you constrain the number of these requests that will run concurrently, mitigating potential timeout issues.
I don't know how many of these user requests are going on, but you might want to consider updating the UI as user information comes in, rather than waiting for everything to finish. This is, again, a more radical refactoring of the code, but might lead to something that feels more responsive.
Try to do a block with completion, you can't do this with a for loop if the methods are async. you have to call getUserInfo one by one after the completion of the previous. I think this gonna be solved your problem.
-(void)getAllUsersInformations{
[self registerUserAtIndex:0];
}
- (void) registerUserAtIndex: (NSInteger ) userIndex
{
RegisterOperation *op = [[RegisterOperation alloc] initWithUser:[users objectAtIndex:userIndex]];
[RegisterOperation setResultCompletionBlock:^(BOOL *finished, NSInteger userIndex) {
dispatch_async(dispatch_get_main_queue(), ^{
if (userIndex++ < [users count] {
[self registerUserAtIndex:userIndex++];
} else {
[myTableView reloadData];
}
}];
[[NSOperationQueue mainQueue] addOperation:op];
}
Hope this will help you.
Rop Answer with swift:
func processData()
{
let group: dispatch_group_t = dispatch_group_create()
for item in data as! Object {
dispatch_group_enter(group)
item.process(completion: {() -> (Void) in
dispatch_group_leave(group)
})
}
dispatch_group_notify(group, dispatch_get_main_queue(), {
//Do whatever you want
})
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Background work
for(User *user in users){
[self getUserInfo:user];
}
dispatch_async(dispatch_get_main_queue(), ^{
//reload tableview , this is on main thread.
});
});
In my modal UI there is a "DONE" button linked with IBAction -done:, it will upload a text to (lets say Dropbox server). Its code looks like this
- (IBAction)done:(id)sender {
// must contain text in textview
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
if (![_textView.text isEqualToString:#""]) {
// check to see if we are adding a new note
if (!self.note) {
DBFile *newNote = [[DBFile alloc] init];
newNote.root = #"dropbox";
self.note = newNote;
}
_note.contents = _textView.text;
_note.path = _filename.text;
// - UPLOAD FILE TO DROPBOX - //
NSLog(#"Initializing URL...");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
NSURL *url = [Dropbox uploadURLForPath:self.note.path];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setHTTPMethod:#"PUT"];
NSData *noteContents = [self.note.contents dataUsingEncoding:NSUTF8StringEncoding];
NSLog(#"Creating session task...");
NSURLSessionUploadTask *uploadTask = [self.session uploadTaskWithRequest:request
fromData:noteContents
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSHTTPURLResponse *resp = (NSHTTPURLResponse *) response;
if (!error && resp.statusCode == 200) {
NSLog(#"OK");
dispatch_async(dispatch_get_main_queue(), ^{
[self.delegate noteDetailsViewControllerDoneWithDetails:self];
});
} else {
NSLog(#"Status code: %d", resp.statusCode);
}
}];
[uploadTask resume];
});
} else {
UIAlertView *noTextAlert = [[UIAlertView alloc] initWithTitle:#"No text"
message:#"Need to enter text"
delegate:nil
cancelButtonTitle:#"Ok"
otherButtonTitles:nil];
[noTextAlert show];
}
}
The delegate method noteDetailsViewControllerDoneWithDetails: of this class is look like this
-(void)noteDetailsViewControllerDoneWithDetails:(NoteDetailsViewController *)controller{
// refresh to get latest
[self dismissViewControllerAnimated:YES completion:nil];
[self notesOnDropbox];}
(notesOnDropbox is a time-consuming task). When DONE button is tapped, I expect this modal VC/UI to dismiss immediately and it fetches data on background (by notesOnDropbox method). However, when I try tapping DONE button, my UI stop responding for about seconds, after that the modal UI is dismissed. I cannot figure out where I misuse the GCD. Please help me.
if you want to dismiss your modal VC/UI immediately, just ask the delegate to dismiss,
like is:
- (IBAction)done:(id)sender {
[self.delegate noteDetailsViewControllerDoneWithDetails:self];
// ...
}
In your sample code,
you do the dismiss action after the upload task completed, but the upload task is asynchronous.
and you ask the delegate to dismiss use GCD dispatch_async, this is asynchronous task, too.
After all, you have to consider the what time to do upload, who to do upload task and what time to invoke notesOnDropbox.
First, if notesOnDropbox is a time-consuming task, then you should not be performing it on the main thread (as you are doing). If it is sufficiently time-consuming and you do it on the main thread, the WatchDog process will kill your app dead right before the user's eyes.
Second, there is no need to get off the main thread to do an upload. If you use NSURLSession correctly, it will be asynchronous.
Your code only calls noteDetailsViewControllerDoneWithDetails when the whole upload task is completed, because that's how you wrote your code. Actually, the situation seems worse. If the upload task has any kinds of problems, noteDetailsViewControllerDoneWithDetails will never be called.
You need to call noteDetailsViewControllerDoneWithDetails as soon as possible, and then think about what you are going to do when the upload fails - which might easily happen a long time later.
I am using AFURLSessionManager, and set the manager as a singleton instance.
- (AFURLSessionManager *)backgroundSession
{
static AFURLSessionManager *backgroundSession = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfiguration:#"com.testBackground.BackgroundDownload.BackgroundSession1234"];
backgroundSession = [[AFURLSessionManager alloc]initWithSessionConfiguration:config];
[backgroundSession setDownloadTaskDidWriteDataBlock:^(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite){
NSLog(#"i am downloading my id = %d progress= %f",downloadTask.taskIdentifier, totalBytesWritten*1.0/totalBytesExpectedToWrite);
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
}];
[backgroundSession setDownloadTaskDidFinishDownloadingBlock:^NSURL *(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, NSURL *location){
NSLog(#"download finished");
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
return location;
}];
});
return backgroundSession;
}
//assign a download task
NSURLSessionDownloadTask *task = [manager1 downloadTaskWithRequest:request progress:&progress destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
return targetPath;
} completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
NSLog("%download success");
}];
[task resume];
I found that when I switch the app to the background the download task is running but when it was finished, the system call handleEventsForBackgroundURLSession will never be called.I am feeling that I have missed some setting or options. Any idea will be useful for me, thanks a lot.
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler;
You probably have figured this out already but anyway, the exact same thing has happened to me while writing some test code to play with the background transfer service APIs (without AFNetworking). The solution was simply to change the identifier string for my background session configuration. Somehow the one I was using got bugged and the system wouldn't trigger the handleEventsForBackgroundURLSession callback. And not even restarting the device fixes it... however just changing the identifier does.
My theory is that my code created multiple instances of NSURLSession with the same configuration, which Apple clearly advices against (they say in the documentation that it has an undefined behavior if you do). I was obtaining the NSURLSessionConfiguration in the view controller's viewDidLoad method without a dispatch_once block, so it's certainly plausible that that happened.
As stated by Apple:
If an iOS app is terminated by the system and relaunched, the app can use the same identifier to create a new configuration object and session and retrieve the status of transfers that were in progress at the time of termination. This behavior applies only for normal termination of the app by the system. If the user terminates the app from the multitasking screen, the system cancels all of the session’s background transfers. In addition, the system does not automatically relaunch apps that were force quit by the user. The user must explicitly relaunch the app before transfers can begin again.
Hope it helps.
Stefan
I was testing with it the other day and it was working. Today, after calling this method, it never calls the completion block.
Is there a case where the completion block is not called? I remember Apple said that it's always called regardless of what happened.
Here's the code block:
dispatch_async (dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
// Get ubiquitous url
NSURL *ubiq = [[NSFileManager defaultManager]
URLForUbiquityContainerIdentifier:nil];
NSURL *ubiquitousPackage = [[ubiq URLByAppendingPathComponent:
#"Documents"] URLByAppendingPathComponent:fileName];
// Create new CloudFile
CloudFile *file = [[CloudFile alloc]initWithFileURL:ubiquitousPackage];
file.data = data;
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(onDocumentStateChanged:) name:UIDocumentStateChangedNotification object:file];
dispatch_async (dispatch_get_main_queue (), ^(void) {
[file saveToURL:file.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success) {
NSLog(#"test");
if (success) {
[self sendEvent:ICLOUD_FILE_SAVE_SUCCESSFUL object:file];
} else {
[self sendEvent:ICLOUD_FILE_SAVE_FAILED];
NSLog(#"kaiCloud: Saving failed. (%#)",fileName);
}
}];
});
});
EDIT: Note that I added NSLog(#"test") and I'm not seeing it getting logged.
I encountered the same problem (which is how I found your question).
The reason is simple. The test code is not waiting for the completion handler to be called and certainly not for it to finish executing.
Your test may work sometimes if they are long enough or happen to get stalled. But that's not reliable.
Use the WAIT macros provided and explained here and problem solved.
https://github.com/hfossli/AGAsyncTestHelper