Touch ID causing app to become non-responsive - ios

I Added ios-8's new touchID API to my app.
It usually works as expected, BUT when entering app while my finger is already on home-button - API's success callback is called but pop-up still appears on screen. after pressing CANCEL UI becomes non-responsive.

I also encountered the same issue, and the solution was to invoke the call to the Touch ID API using a high priority queue, as well as a delay:
// Touch ID must be called with a high priority queue, otherwise it might fail.
// Also, a dispatch_after is required, otherwise we might receive "Pending UI mechanism already set."
dispatch_queue_t highPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.75 * NSEC_PER_SEC), highPriorityQueue, ^{
LAContext *context = [[LAContext alloc] init];
NSError *error = nil;
// Check if device supports TouchID
if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error]) {
// TouchID supported, show it to user
[context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
localizedReason:#"Unlock Using Touch ID"
reply:^(BOOL success, NSError *error) {
if (success) {
// This action has to be on main thread and must be synchronous
dispatch_async(dispatch_get_main_queue(), ^{
...
});
}
else if (error) {
...
}
}];
}
});
When testing our app, we found a delay of 750ms to be optimal, but your mileage may vary.
Update (03/10/2015): Several iOS developers, like 1Password for example, are reporting that iOS 8.2 have finally fixed this issue.

Whilst using a delay can potentially address the issue, it masks the root cause. You need to ensure you only show the Touch ID dialog when the Application State is Active. If you display it immediately during the launch process (meaning the Application is still technically in an inactive state), then these sorts of display issues can occur. This isn't documented, and I found this out the hard way. Providing a delay seems to fix it because you're application is in an active state by then, but this isn't guarenteed.
To ensure it runs when the application is active, you can check the current application state, and either run it immediately, or when we receive the applicationDidBecomeActive notification. See below for an example:
- (void)setup
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(applicationDidBecomeActive:)
name:UIApplicationDidBecomeActiveNotification
object:nil];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// We need to be in an active state for Touch ID to play nice
// If we're not, defer the presentation until we are
if([UIApplication sharedApplication].applicationState == UIApplicationStateActive)
{
[self presentTouchID];
}
else
{
__weak __typeof(self) wSelf = self;
_onActiveBlock = ^{
[wSelf presentTouchID];
};
}
}
-(void)applicationDidBecomeActive:(NSNotification *)notif
{
if(_onActiveBlock)
{
_onActiveBlock();
_onActiveBlock = nil;
}
}
- (void)presentTouchID
{
_context = [[LAContext alloc] init];
_context.localizedFallbackTitle = _fallbackTitle;
[_context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
localizedReason:_reason
reply: ^(BOOL success, NSError *authenticationError)
{
// Handle response here
}];
}

This accepted answer does not address the underlying cause of the problem: invoking evaluatePolicy() twice, the second time while the first invocation is in progress. So the current solution only works sometimes by luck, as everything is timing dependent.
The brute-force, straightforward way to work around the problem is a simple boolean flag to prevent subsequent calls from happening until the first completes.
AppDelegate *delegate = [[UIApplication sharedApplication] delegate];
if ( NSClassFromString(#"LAContext") && ! delegate.touchIDInProgress ) {
delegate.touchIDInProgress = YES;
LAContext *localAuthenticationContext = [[LAContext alloc] init];
__autoreleasing NSError *authenticationError;
if ([localAuthenticationContext canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&authenticationError]) {
[localAuthenticationContext evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:kTouchIDReason reply:^(BOOL success, NSError *error) {
delegate.touchIDInProgress = NO;
if (success) {
...
} else {
...
}
}];
}

I started getting the "Pending UI mechanism already set." error mentioned as well, so I decided to see if other apps were affected. I have both Dropbox and Mint set up for Touch ID. Sure enough Touch ID wasn't working for them either and they were falling back to passcodes.
I rebooted my phone and it started working again, so it would seem the Touch ID can bug out and stop working. I'm on iOS 8.2 btw.
I guess the proper way to handle this condition is like those apps do and fallback to password / passcode.

Related

BGTaskScheduler: How do you continue an operation in queue in the background?

I have set up all the steps required for adding the capability for background process (identified the task, registered and calling as instructed in this video).
I wondered if I'm downloading a file and before the download ends, the app goes to the background – what can I do to keep the download running? I just don't know about the last part in this method where I need to call on my last item in a queue and keep running the download.
The code shown is in Objective-C, but any hints in Swift would be extremely helpful as well.
- (void) handelBackgroudProcessingTaskForDownloads:(BGProcessingTask *) task {
[self scheduleAppBGProcessingTaskForDownloads];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
task.expirationHandler = ^{
[queue cancelAllOperations];
};
__weak NSOperation* lastOperation = MyDownloadService.shared.queue.operations.lastObject;
if (lastOperation != nil) {
lastOperation.completionBlock = ^{
[task setTaskCompletedWithSuccess: !lastOperation.isCancelled];
};
} else {
[task setTaskCompletedWithSuccess: !lastOperation.isCancelled];
}
//this is for the case that I have more than one file in the queue which they have not started downloading yet
if ((! lastOperation.isExecuting) && (! lastOperation.isCancelled) ) {
[queue addOperation:MyDownloadService.shared.queue.operations.lastObject];
}
// My question: How can I keep downloading the single file when app goes in bg before download is done? I tried this line below and did not work!
if(lastOperation.isExecuting) {
[MyDownloadService.shared.queue waitUntilAllOperationsAreFinished];
}

How to get use BGTask in objective-c with iOS 13(Background Fetch)

So i want to build a iOS, i am pretty new to the world of objective-c and one feature i want to implement is the ability to send a API request and do a bit of background processing while the app is not "in focus/in background". I have researched for a couple days about this BGTask API for iOS 13 and have created a projected to see if i can get "background fetch" working. I have not be able to. Im pretty sure i have everything setup correctly but i can not get background fetch functionality to trigger on my iPhone, not even once over the past couple days.
I am using a actual iOS device to test this with iOS 13.4.1
"Permitted background task scheduler identifiers" is setup properly in Info.plist
App is signed
Background processing and Background fetch is checked in Background Modes
I waited the 15 minute interval as per Apples documentation
Here is my code. All this is just a blank iOS project using objective-c. I only edited AppDelegate.m and Info.plist
AppDelegate.m
#import "AppDelegate.h"
#import <BackgroundTasks/BackgroundTasks.h>
static NSString* TaskID = #"com.myapp.task";
#interface AppDelegate ()
#end
#implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[[BGTaskScheduler sharedScheduler] registerForTaskWithIdentifier:TaskID
usingQueue:nil
launchHandler:^(BGProcessingTask *task) {
[self handleAppRefreshTask:task];
}];
return YES;
}
#pragma mark - UISceneSession lifecycle
- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return [[UISceneConfiguration alloc] initWithName:#"Default Configuration" sessionRole:connectingSceneSession.role];
}
- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet<UISceneSession *> *)sceneSessions {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
-(void)handleAppRefreshTask:(BGProcessingTask *)task {
//do things with task
NSLog(#"Process started!");
task.expirationHandler = ^{
NSLog(#"WARNING: expired before finish was executed.");
};
NSString *targetUrl = #"https://webhook.site/1b274a6f-016f-4edf-8e31-4ed7058eaeac";
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
[request setHTTPMethod:#"GET"];
[request setURL:[NSURL URLWithString:targetUrl]];
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:
^(NSData * _Nullable data,
NSURLResponse * _Nullable response,
NSError * _Nullable error) {
NSString *myString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(#"Data received: %#", myString);
}] resume];
task.expirationHandler = ^{
NSLog(#"WARNING: expired before finish was executed.");
};
[task setTaskCompletedWithSuccess:YES];
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
NSLog(#"Entering background");
BGProcessingTaskRequest *request = [[BGProcessingTaskRequest alloc] initWithIdentifier:TaskID];
request.requiresNetworkConnectivity = true;
request.requiresExternalPower = false;
request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:60];
#try {
[[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:nil];
}
#catch(NSException *e){
NSLog(#" Unable to submit request");
}
}
#end
Is background fetch broken in iOS 13? Even clicking on the “Simulate background fetch" in Xcode debug menu does not work. It just closes the app and nothing happens. Can anybody help/give any advice?
A few observations:
The setTaskCompletedWithSuccess should be inside the network request’s completion handler. You don’t want to mark the task as complete until the request has had a chance to run and you’ve processed the result.
You are calling submitTaskRequest, but passing nil for the NSError reference. You have also wrapped that in an exception handler. But this API call doesn’t throw exceptions, but rather just passes back errors. But you have to supply it an error reference. E.g.
NSLog(#"Entering background");
BGProcessingTaskRequest *request = [[BGProcessingTaskRequest alloc] initWithIdentifier:TaskID];
request.requiresNetworkConnectivity = true;
request.requiresExternalPower = false;
request.earliestBeginDate = [NSDate dateWithTimeIntervalSinceNow:60];
NSError *error;
if (![[BGTaskScheduler sharedScheduler] submitTaskRequest:request error:&error]) {
NSLog(#"BGTaskScheduler failed: %#", error);
}
In your code, if it failed, you would never know.
You have placed this code in applicationDidEnterBackground. I.e., are you seeing this “Entering background” message at all? The reason I ask is that if you’ve supplied a scene delegate (common if you just created a new iOS 13 app), this method won’t be called, whereas sceneDidEnterBackground will.
You said that you tried “Simulate background fetch”. But you haven’t created a background fetch request (a BGAppRefreshTask). You created a background task (a BGProcessingTask), which is a different thing. To test background processing requests, refer to Starting and Terminating Tasks During Development.
There’s an interesting question as to how you know that the fetch request was processed. You’re just using NSLog (which presumes that you’re keeping your app attached to the Xcode debugger). I would suggest testing this without the app being attached to Xcode. There are a few options:
If you can watch your server logs for requests, that works.
I personally will often put in UserNotifications (and make sure to go into settings and turn on persistent notifications so I don’t miss them).
Another approach that I’ve done is to log these events in some table in my app’s persistent storage and then have some UI within the app to fetch this data so I can confirm what happened.
I’ll often use Unified Logging so that I can watch os_log statements issued by my device from the macOS Console even when Xcode is not running. This is very useful in logging app/scene methods. See WWDC 2016 Unified Logging and Activity Tracing
Whatever you do, for things like background processing, background app refresh, etc., I will program some mechanism so that I can check to see if the requests/tasks took place, even when not attached to Xcode. Being attached to the debugger can, in some cases, affect the app lifecycle, and I want to make sure I’ve got some way to confirm what was going on without the benefit of the console.
Likely obvious, but make sure you never “force quit” the app, as that will stop background processes from taking place.
For more information, See WWDC 2019 video Advances in App Background Execution.

CoreData Migration Local Store -> iCloud Store

I have the following problem.
I have successfully created my iCloud Magical Record Store and also works great if you filled it from zero :)
But now I have many data in the old Magical Record Local Store and I want to take this to the iCloud store.
But how i can migrate them.
if ([[NSFileManager defaultManager] ubiquityIdentityToken]) {
[self iCloudCoreDataSetup]}
else {
NSLog(#"iCloud not enabled");
[MagicalRecord setupAutoMigratingCoreDataStack];
}
Here i am checking iCloud is available. Before iCloud, i have always initialized with
[MagicalRecord setupAutoMigratingCoreDataStack];
Here is my code for the iCloud Installation
- (void)iCloudCoreDataSetup {
// containerID should contain the same string as your iCloud entitlements
NSString *containerID = [NSString stringWithFormat:#"iCloud.%#", [[NSBundle mainBundle] bundleIdentifier]];
[MagicalRecord setupCoreDataStackWithiCloudContainer:containerID
contentNameKey:#"App" // Must not contain dots
localStoreNamed:#"App.sqlite"
cloudStorePathComponent:#"Documents/CloudLogs" // Subpath within your ubiquitous container that will contain db change logs
completion:^{
// This gets executed after all the setup steps are performed
// Uncomment the following lines to verify
NSLog(#"%#", [MagicalRecord currentStack]);
// NSLog(#"%i events", [Event countOfEntities]);
}];
// NOTE: MagicalRecord's setup is asynchronous, so at this point the default persistent store is still probably NIL!
// Uncomment the following line if you want to check it.
NSLog(#"%#", [MagicalRecord currentStack]);
// The persistent store COORDINATOR is however fully setup and can be accessed
// Uncomment the following line if you want to check it.
NSLog(#"Store coordinator at this point %#", [NSPersistentStoreCoordinator MR_defaultStoreCoordinator]);
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Finally, store change notifications must be observed. Without these, your app will NOT function properly!
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// This notification is issued only once when
// 1) you run your app on a particular device for the first time
// 2) you disable/enable iCloud document storage on a particular device
// usually a couple of seconds after the respective event.
// The notification must be handled on the MAIN thread and synchronously
// (because as soon as it finishes, the persistent store is removed by OS).
// Refer to Apple's documentation for further details
[[NSNotificationCenter defaultCenter] addObserverForName:NSPersistentStoreCoordinatorStoresWillChangeNotification
object:[NSPersistentStoreCoordinator MR_defaultStoreCoordinator]
// queue:nil // Run on the posting (i.e. background) thread
queue:[NSOperationQueue mainQueue] // Run on the main thread
usingBlock:^(NSNotification *note) {
// For debugging only
NSLog(#"%s notificationBlockWillChange:%#, isMainThread = %i", __PRETTY_FUNCTION__, note, [NSThread isMainThread]);
// Disable user interface with setEnabled: or an overlay
// NOTE: Probably not crucial since the store switch is almost instantaneous.
// I only hint it here by changing the tint color to red.
self.window.tintColor = [UIColor redColor];
// Save changes to current MOC and reset it
if ([[NSManagedObjectContext MR_defaultContext] hasChanges]) {
[[NSManagedObjectContext MR_defaultContext] MR_saveToPersistentStoreAndWait];
}
[[NSManagedObjectContext MR_defaultContext] reset];
// TODO: Drop any managed object references here
}];
// This notification is issued couple of times every time your app starts
// The notification must be handled on the BACKGROUND thread and asynchronously to prevent deadlock
// Refer to Apple's documentation for further details
[[NSNotificationCenter defaultCenter] addObserverForName:NSPersistentStoreCoordinatorStoresDidChangeNotification
object:[NSPersistentStoreCoordinator MR_defaultStoreCoordinator]
queue:nil // Run on the posting (i.e. background) thread
usingBlock:^(NSNotification *note) {
// For debugging only
NSLog(#"%s notificationBlockDidChange:%#, isMainThread = %i", __PRETTY_FUNCTION__, note, [NSThread isMainThread]);
// This block of code must be executed asynchronously on the main thread!
dispatch_async(dispatch_get_main_queue(), ^{
// Recommended by Apple
[[NSManagedObjectContext MR_defaultContext] reset];
// Notify UI that the data has changes
// NOTE: I am using the same notification that MagicalRecord sends after merging changes
[[NSNotificationCenter defaultCenter] postNotificationName:kMagicalRecordPSCDidCompleteiCloudSetupNotification object:nil];
// Re-enable user interface with setEnabled: or removing the overlay
// NOTE: Probably not crucial since the store switch is almost instantaneous.
// I only hint it here by changing the tint color back to default.
self.window.tintColor = nil;
});
}];
}
But how can I take the old data from the local store to iCloud?
Thanks for your help.

iPhone - requesting Motion activity

I'm working on an iOS application in which I use Motion Activity Manager (in more detail - pedometer). When application launches I need to check is Motion Activity is allowed by user. I do this by doing
_motionActivityManager = [[CMMotionActivityManager alloc] init];
_pedometer = [[CMPedometer alloc] init];
[_pedometer queryPedometerDataFromDate : [NSDate date]
toDate : [NSDate date]
withHandler : ^(CMPedometerData *pedometerData, NSError *error) {
// BP1
if (error != nil) {
// BP2
}
else {
// BP3
}
}];
As discussed here ☛ iOS - is Motion Activity Enabled in Settings > Privacy > Motion Activity
In my understanding this code will trigger "alert window" asking user to opt-in/out.
What happens in my case is that when I run application first time (aka. all warnings are reset), application hangs before 'BP1' (callback is never executed) and then if I stop application with xCode or press home button "alert window" appears. And if I opt-in everything is good, on second run 'BP3' is reached (or 'BP2' if I opt-out).
What I tried do far:
I implemented another way of checking using async execution
[_pedometer queryPedometerDataFromDate : [NSDate date]
toDate : [NSDate date]
withHandler : ^(CMPedometerData *pedometerData, NSError *error) {
// Because CMPedometer dispatches to an arbitrary queue, it's very important
// to dispatch any handler block that modifies the UI back to the main queue.
dispatch_async(dispatch_get_main_queue(), ^{
authorizationCheckCompletedHandler(!error || error.code != CMErrorMotionActivityNotAuthorized);
});
}];
This doesn't hang the application, but "alert window" is never showed
I executed this "checking snippet" in later time in code - but again - application hangs
Essentially, use can first be sure that the alert view will not block your App, when the first view has appeared, ie. in onViewDidAppear.
For example do:
-(void) viewDidAppear:(BOOL)animated {
if ([MyActivityManager checkAvailability]) { // motion and activity availability checks
[myDataManager checkAuthorization:^(BOOL authorized) { // is authorized
dispatch_async(dispatch_get_main_queue(), ^{
if (authorized) {
// do your UI update etc...
}
else {
// maybe tell the user that this App requires motion and tell him about activating it in settings...
}
});
}];
}
}
This is what I do myself. I based my App on the Apple example code as well and noticed, that the example also has the problem you are describing.

Start something asynchronously, then wait on it later only if needed

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.

Resources