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.
Related
i have written code to downloading data from server using NSOperationQueue and NSOperation. and now i want to show progress on UserInterface. i used UITableView and used NSOpeartionQueue as a datasource in tableview delegate
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [[[Downloadmanager sharedInstance] downloadOperationQueue] count];
}
and bind NSOperation`s properties to UITableViewCell.
1) Is this a fisible solution to sending NSOperationQueue as a datasource to tableview delegate ?
2) How to implement notification to reload tableview when NSOperation's state changes?
Thanks.
I don't think it's the proper way of showing progress using NSOperationQueue as a datasource to tableview. You can use networking library like AFNetworking for downloading data and use setDownloadProgressBlock: method for showing progress. Refer this link for the code download progress.
It's easy to reload tableview when the download completes, just call [tableView reloadData] in completionblock.
Here is the code which shows image downloading using AFNetworking which you can easily change for data download.(refer this gist)
- (void)downloadMultiAFN {
// Basic Activity Indicator to indicate download
UIActivityIndicatorView *loading = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
[loading startAnimating];
[self.imageView.superview addSubview:loading];
loading.center = self.imageView.center;
// Create a request from the url, make an AFImageRequestOperation initialized with that request
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:self.picUrl]];
AFImageRequestOperation *op = [[AFImageRequestOperation alloc] initWithRequest:request];
// Set a download progress block for the operation
[op setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
if ([op.request.URL.absoluteString isEqualToString:#"http://www.pleiade.org/images/hubble-m45_large.jpg"]) {
self.progressBar.progress = (float) totalBytesRead/totalBytesExpectedToRead;
} else self.progressBar2.progress = (float) totalBytesRead/totalBytesExpectedToRead;
}];
// Set a completion block for the operation
[op setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
self.imageView.image = responseObject;
self.image = responseObject;
if ([op.request.URL.absoluteString isEqualToString:#"http://www.pleiade.org/images/hubble-m45_large.jpg"]) {
self.progressBar.progress = 0;
} else self.progressBar2.progress = 0;
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {}];
// Start the image download operation
[op start];
// Remove the activity indicator
[loading stopAnimating];
[loading removeFromSuperview];
}
That is an interesting idea, but I don't think it's a good practice make such a "high coupling" - linking model so tightly to the view.
I'd approach it as - download the data on the background thread as you already do - with NSOperationQueue but save it to some kind of an object; say NSMutableArray that serves as the data source for the table view.
Every time a single operation ends (use completion handlers or KVO to get informed) - update the table view. The update can be done two ways - reloading or updating. I'll leave the choice up to you - you can read further discussion about that in this question.
I'm a bit confuse of how to take advantage of the new iOS 7 NSURLSession background transfers features and AFNetworking (versions 2 and 3).
I saw the WWDC 705 - What’s New in Foundation Networking session, and they demonstrated background download that continues after the app terminated or even crashes.
This is done using the new API application:handleEventsForBackgroundURLSession:completionHandler: and the fact that the session's delegate will eventually get the callbacks and can complete its task.
So I'm wondering how to use it with AFNetworking (if possible) to continue downloading in background.
The problem is, AFNetworking conveniently uses block based API to do all the requests, but if the app terminated or crashes those block are also gone. So how can I complete the task?
Or maybe I'm missing something here...
Let me explain what I mean:
For example my app is a photo messaging app, lets say that I have a PhotoMessage object that represent one message and this object has properties like
state - describe the state of the photo download.
resourcePath - the path to the final downloaded photo file.
So when I get a new message from the server, I create a new PhotoMessage object, and start downloading its photo resource.
PhotoMessage *newPhotoMsg = [[PhotoMessage alloc] initWithInfoFromServer:info];
newPhotoMsg.state = kStateDownloading;
self.photoDownloadTask = [[BGSessionManager sharedManager] downloadTaskWithRequest:request progress:nil destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
NSURL *filePath = // some file url
return filePath;
} completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
if (!error) {
// update the PhotoMessage Object
newPhotoMsg.state = kStateDownloadFinished;
newPhotoMsg.resourcePath = filePath;
}
}];
[self.photoDownloadTask resume];
As you can see, I use the completion block to update that PhotoMessage object according to the response I get.
How can I accomplish that with a background transfer? This completion block won't be called and as a result, I can't update the newPhotoMsg.
A couple of thoughts:
You have to make sure you do the necessary coding outlined in the Handling iOS Background Activity section of the URL Loading System Programming Guide says:
If you are using NSURLSession in iOS, your app is automatically relaunched when a download completes. Your app’s application:handleEventsForBackgroundURLSession:completionHandler: app delegate method is responsible for recreating the appropriate session, storing a completion handler, and calling that handler when the session calls your session delegate’s URLSessionDidFinishEventsForBackgroundURLSession: method.
That guide shows some examples of what you can do. Frankly, I think the code samples discussed in the latter part of the WWDC 2013 video What’s New in Foundation Networking are even more clear.
The basic implementation of AFURLSessionManager will work in conjunction with background sessions if the app is merely suspended (you'll see your blocks called when the network tasks are done, assuming you've done the above). But as you guessed, any task-specific block parameters that are passed to the AFURLSessionManager method where you create the NSURLSessionTask for uploads and downloads are lost "if the app terminated or crashes."
For background uploads, this is an annoyance (as your task-level informational progress and completion blocks you specified when creating the task will not get called). But if you employ the session-level renditions (e.g. setTaskDidCompleteBlock and setTaskDidSendBodyDataBlock), that will get called properly (assuming you always set these blocks when you re-instantiate the session manager).
As it turns out, this issue of losing the blocks is actually more problematic for background downloads, but the solution there is very similar (do not use task-based block parameters, but rather use session-based blocks, such as setDownloadTaskDidFinishDownloadingBlock).
An alternative, you could stick with default (non-background) NSURLSession, but make sure your app requests a little time to finish the upload if the user leaves the app while the task is in progress. For example, before you create your NSURLSessionTask, you can create a UIBackgroundTaskIdentifier:
UIBackgroundTaskIdentifier __block taskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^(void) {
// handle timeout gracefully if you can
[[UIApplication sharedApplication] endBackgroundTask:taskId];
taskId = UIBackgroundTaskInvalid;
}];
But make sure that the completion block of the network task correctly informs iOS that it is complete:
if (taskId != UIBackgroundTaskInvalid) {
[[UIApplication sharedApplication] endBackgroundTask:taskId];
taskId = UIBackgroundTaskInvalid;
}
This is not as powerful as a background NSURLSession (e.g., you have a limited amount of time available), but in some cases this can be useful.
Update:
I thought I'd add a practical example of how to do background downloads using AFNetworking.
First define your background manager.
//
// BackgroundSessionManager.h
//
// Created by Robert Ryan on 10/11/14.
// Copyright (c) 2014 Robert Ryan. All rights reserved.
//
#import "AFHTTPSessionManager.h"
#interface BackgroundSessionManager : AFHTTPSessionManager
+ (instancetype)sharedManager;
#property (nonatomic, copy) void (^savedCompletionHandler)(void);
#end
and
//
// BackgroundSessionManager.m
//
// Created by Robert Ryan on 10/11/14.
// Copyright (c) 2014 Robert Ryan. All rights reserved.
//
#import "BackgroundSessionManager.h"
static NSString * const kBackgroundSessionIdentifier = #"com.domain.backgroundsession";
#implementation BackgroundSessionManager
+ (instancetype)sharedManager {
static id sharedMyManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedMyManager = [[self alloc] init];
});
return sharedMyManager;
}
- (instancetype)init {
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:kBackgroundSessionIdentifier];
self = [super initWithSessionConfiguration:configuration];
if (self) {
[self configureDownloadFinished]; // when download done, save file
[self configureBackgroundSessionFinished]; // when entire background session done, call completion handler
[self configureAuthentication]; // my server uses authentication, so let's handle that; if you don't use authentication challenges, you can remove this
}
return self;
}
- (void)configureDownloadFinished {
// just save the downloaded file to documents folder using filename from URL
[self setDownloadTaskDidFinishDownloadingBlock:^NSURL *(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, NSURL *location) {
if ([downloadTask.response isKindOfClass:[NSHTTPURLResponse class]]) {
NSInteger statusCode = [(NSHTTPURLResponse *)downloadTask.response statusCode];
if (statusCode != 200) {
// handle error here, e.g.
NSLog(#"%# failed (statusCode = %ld)", [downloadTask.originalRequest.URL lastPathComponent], statusCode);
return nil;
}
}
NSString *filename = [downloadTask.originalRequest.URL lastPathComponent];
NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *path = [documentsPath stringByAppendingPathComponent:filename];
return [NSURL fileURLWithPath:path];
}];
[self setTaskDidCompleteBlock:^(NSURLSession *session, NSURLSessionTask *task, NSError *error) {
if (error) {
// handle error here, e.g.,
NSLog(#"%#: %#", [task.originalRequest.URL lastPathComponent], error);
}
}];
}
- (void)configureBackgroundSessionFinished {
typeof(self) __weak weakSelf = self;
[self setDidFinishEventsForBackgroundURLSessionBlock:^(NSURLSession *session) {
if (weakSelf.savedCompletionHandler) {
weakSelf.savedCompletionHandler();
weakSelf.savedCompletionHandler = nil;
}
}];
}
- (void)configureAuthentication {
NSURLCredential *myCredential = [NSURLCredential credentialWithUser:#"userid" password:#"password" persistence:NSURLCredentialPersistenceForSession];
[self setTaskDidReceiveAuthenticationChallengeBlock:^NSURLSessionAuthChallengeDisposition(NSURLSession *session, NSURLSessionTask *task, NSURLAuthenticationChallenge *challenge, NSURLCredential *__autoreleasing *credential) {
if (challenge.previousFailureCount == 0) {
*credential = myCredential;
return NSURLSessionAuthChallengeUseCredential;
} else {
return NSURLSessionAuthChallengePerformDefaultHandling;
}
}];
}
#end
Make sure app delegate saves completion handler (instantiating the background session as necessary):
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
NSAssert([[BackgroundSessionManager sharedManager].session.configuration.identifier isEqualToString:identifier], #"Identifiers didn't match");
[BackgroundSessionManager sharedManager].savedCompletionHandler = completionHandler;
}
Then start your downloads:
for (NSString *filename in filenames) {
NSURL *url = [baseURL URLByAppendingPathComponent:filename];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[[[BackgroundSessionManager sharedManager] downloadTaskWithRequest:request progress:nil destination:nil completionHandler:nil] resume];
}
Note, I don't supply any of those task related blocks, because those aren't reliable with background sessions. (Background downloads proceed even after the app is terminated and these blocks have long disappeared.) One must rely upon the session-level, easily recreated setDownloadTaskDidFinishDownloadingBlock only.
Clearly this is a simple example (only one background session object; just saving files to the docs folder using last component of URL as the filename; etc.), but hopefully it illustrates the pattern.
It shouldn't make any difference whether or not the callbacks are blocks or not. When you instantiate an AFURLSessionManager, make sure to instantiate it with NSURLSessionConfiguration backgroundSessionConfiguration:. Also, make sure to call the manager's setDidFinishEventsForBackgroundURLSessionBlock with your callback block - this is where you should write the code typically defined in NSURLSessionDelegate's method:
URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session. This code should invoke your app delegate's background download completion handler.
One word of advice regarding background download tasks - even when running in the foreground, their timeouts are ignored, meaning you could get "stuck" on a download that's not responding. This is not documented anywhere and drove me crazy for some time. The first suspect was AFNetworking but even after calling NSURLSession directly, the behaviour remained the same.
Good luck!
AFURLSessionManager
AFURLSessionManager creates and manages an NSURLSession object based on a specified NSURLSessionConfiguration object, which conforms to <NSURLSessionTaskDelegate>, <NSURLSessionDataDelegate>, <NSURLSessionDownloadDelegate>, and <NSURLSessionDelegate>.
link to documentation here documentation
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.
Problem: i want to download a file from my dropbox account and use quick look to visualize it.
First Solution:
1) use Dropbox API restClient:
[[self restClient] loadFile:fullpath intoPath:finalpath];
2) Once downloaded use QLPreviewController to preview the file.
The problem with this solution is that I don't know how to synchronize the download with the preview (to use quick look the file needs to be local, so I need to download it first).
The (ugly) workaround I came up with is to set up an alert ("Caching") and make it last an arbitrary length of time (let's say 12 sec, magic number...). At the same time I pause execution for 10-12 seconds (magic numbers):
[NSThread sleepForTimeInterval:12.0f];
...and hope at the end of this time interval the file is downloaded already so I can start the QLPreviewController.
Here is the code (ugly, I know....):
// Define Alert
UIAlertView *downloadAlert = [[UIAlertView alloc] initWithTitle:#"caching" message:nil delegate:nil cancelButtonTitle:nil otherButtonTitles:nil] ;
// If file does not exist alert downloading is ongoing
if(![[NSFileManager defaultManager] fileExistsAtPath:finalpath])
{
// Alert Popup
[downloadAlert show];
//[self performSelector:#selector(isExecuting) withObject:downloadAlert afterDelay:12];
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//Here your non-main thread.
if(![[NSFileManager defaultManager] fileExistsAtPath:finalpath])
{
[NSThread sleepForTimeInterval:12.0f];
}
dispatch_async(dispatch_get_main_queue(), ^{
// Dismiss alert
[downloadAlert dismissWithClickedButtonIndex: -1 animated:YES];
//Here we return to main thread.
// We use the QuickLook APIs directly to preview the document -
QLPreviewController *previewController = [[QLPreviewController alloc] init];
previewController.dataSource = self;
previewController.delegate = self;
// Push new viewcontroller, previewing the document
[[self navigationController] pushViewController:previewController animated:YES];
});
});
It does work (with small files and fast connection) but It's not the best solution... .
I think that the best solution would be integrate NSURLSession with dropbox restClient so to use this routine:
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration
delegate:nil
delegateQueue:[NSOperationQueue mainQueue]];
NSURLSessionDownloadTask *task;
task = [session downloadTaskWithRequest:request
completionHandler:^(NSURL *localfile, NSURLResponse *response, NSErr or *error) {
/* yes, can do UI things directly because this is called on the main queue */ }];
[task resume];
But I'm not sure how to use it with the DropBox API: any suggestion ?
Thanks,
dom
It looks like the API tells you about progress and completion:
- (void)restClient:(DBRestClient*)client loadedFile:(NSString*)destPath contentType:(NSString*)contentType metadata:(DBMetadata*)metadata;
- (void)restClient:(DBRestClient*)client loadProgress:(CGFloat)progress forFile:(NSString*)destPath;
No need to do any sleeping or gcd calls directly. Just change your UI to show busy when the download starts and use these to update UI with progress and completion.
I want to start a task that runs on another thread "just in case it is needed" to minimize the time the user will have to wait on it later. If there is time for it to complete, the user will not have to wait, but if it has not completed then waiting would be necessary.
Something like, opening a database in viewDidLoad: that will be needed when and if the user pushes a button on the screen. If I wait to open the database until the user actually pushes the button there is a lag. So I want to open it early. Since I don't know how long it will take to open and I don't know how long until the user hits the button, I need a way of saying, if that other task has not completed yet then wait, otherwise just go ahead.
For example:
#implementation aViewController
- (void) viewDidLoad {
[self.dbManager openOrCreateDbWithCompletionHandler: ^(NSError *err) {
if( err ) NSLog( #"There was a problem opening the database" );
}];
}
- (IBAction) goButtonTouched: (id) sender {
// Wait here until the database is open and ready to use.
if( ???DatabaseNotAvailableYet??? ) {
[self putSpinnerOnScreen];
???BlockProgressHereUntilDatabaseAvailable???
[self takeSpinnerOffScreen];
}
// Use the database...
NSManagedObjectContext *context = [self theDatabaseContext];
// Build the search request for the attribute desired
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName: NSStringFromClass([Destinations class])];
request.predicate = [NSPredicate predicateWithFormat: #"dId == %#", sender.tag];
request.sortDescriptors = nil;
// Perform the search
NSError *error = nil;
NSArray *matches = [context executeFetchRequest: request error: &error];
// Use the search results
if( !matches || matches.count < 1 ) {
NSLog( #"Uh oh, got a nil back from my Destination fetch request!" );
UIAlertView *alert = [[UIAlertView alloc] initWithTitle: #"No Info"
message: #"The database did not have information for this selection"
delegate: nil
cancelButtonTitle: #"OK"
otherButtonTitles: nil];
[alert show];
} else {
MyOtherViewController *movc = [[MyOtherViewContoller alloc] init];
movc.destDetails = [matches lastObject];
[self.navigationController pushViewController: movc animated: YES];
}
}
#end
My hope is that there is never a spinner on the screen and never any delay for the user but, since I don't know how long it will take for the database connection to be established, I have to be prepared for it not being ready when the user pushes the button.
I can't use the call back for when openOrCreateDbWithCompletionHandler: completes since I don't want to do anything then, only when the user pushes the button.
I thought about using a semaphore but it seems like I would only signal it once (in the completion handler of the openOrCreateDbWithCompletionHandler: call) but would wait on it every time a button was pushed. That seems like it would only work for the first button push.
I thought about using dispatch_group_async() for openOrCreateDbWithCompletionHandler: then dispatch_group_wait() in goButtonTouched: but since openOrCreateDbWithCompletionHandler: does its work on another thread and returns immediately, I don't think the wait state would be set.
I can simply set a my own flag, something like before the openOrCreateDbWithCompletionHandler:, self.notOpenYet = YES;, then in its completion handler do self.notOpenYet = NO;, then in goButtonTouched: replace ???DatabaseNotAvailableYet??? with self.notOpenYet, but then how do I block progress on its state? Putting in loops and timers seems kludgy since I don't know if the wait will be nanoseconds or seconds.
This seems like a common enough situation, I am sure that you have all done this sort of thing commonly and it is poor education on my side, but I have searched stackOverflow and the web and have not found a satisfying answer.
I think, blocking execution is a bad habit unless you are building your own event loop, which is rarely necessary. You also don't need to do any GCD stuff in your case. Just get a feeling for async.
The following should work:
#implementation aViewController
- (void) viewDidLoad {
self.waitingForDB = NO;
self.databaseReady = NO;
[self.dbManager openOrCreateDbWithCompletionHandler: ^(NSError *err) {
if( err ){
NSLog( #"There was a problem opening the database" )
}else{
[self performSelectorOnMainThread:#selector(handleDatabaseReady) withObject:nil waitUntilDone:NO];
};
}];
}
- (void)handleDatabaseReady{
self.databaseReady = YES;
if(self.waitingForDB){
[self takeSpinnerOffScreen];
[self go];
}
}
- (IBAction) goButtonTouched: (id) sender {
// Wait here until the database is open and ready to use.
if( !self.databaseReady ) {
self.waitingForDB = YES;
[self putSpinnerOnScreen];
else{
[self go];
}
}
-(void)go{
// Use the database...
NSManagedObjectContext *context = [self theDatabaseContext];
// Build the search request for the attribute desired
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName: NSStringFromClass([Destinations class])];
request.predicate = [NSPredicate predicateWithFormat: #"dId == %#", sender.tag];
request.sortDescriptors = nil;
// Perform the search
NSError *error = nil;
NSArray *matches = [context executeFetchRequest: request error: &error];
// Use the search results
if( !matches || matches.count < 1 ) {
NSLog( #"Uh oh, got a nil back from my Destination fetch request!" );
UIAlertView *alert = [[UIAlertView alloc] initWithTitle: #"No Info"
message: #"The database did not have information for this selection"
delegate: nil
cancelButtonTitle: #"OK"
otherButtonTitles: nil];
[alert show];
} else {
MyOtherViewController *movc = [[MyOtherViewContoller alloc] init];
movc.destDetails = [matches lastObject];
[self.navigationController pushViewController: movc animated: YES];
}
}
#end
Performing the call to handleDatabaseReady on the main thread guarantees that no race conditions in setting/reading your new properties will appear.
I'd go with the flag. You don't want to block the UI, just show the spinner and return from the goButtonTouched. However, you do need to cancel the spinner, if it is active, in openOrCreateDbWithCompletionHandler:.
This is rather a simple scenario. You make a method that does the stuff. Lets call it doStuff. From main thread, you call performSelectorInBackgroundThread:#selector(doStuff). Do not enable the button by default. Enable it at the end of doStuff so that user won't tap on it until you are ready. To make it more appealing, you can place a spinner in the place of the button and then replace it with the button when doStuff completes.
There are a number of classes and APIs you can use to achieve this kind of thing. You can use NSThread with synchronization primitives like semaphores and events to wait for it to finish when the user actually presses the button. You can use an NSOperation subclass (with an NSOperationQueue), and you can use GCD queues.
I would suggest you take a look at some the information in the Concurrency Programming Guide from Apple.
In your case you would probably be best served adding the operation to a GCD background queue using dispatch_async in combination with a semaphore which you can wait on when the user taps the button. You can check out the question "How do I wait for an asynchronously dispatched block to finish?" for an example.