Hi I’m experiencing a strange behavior I really don’t understand.
I present a touch ID identification to the user and if he’s authorized I
call a [self performSegueWithIdentifier: #"callCustomSegue" sender:self];
inside the block in this way:
[myContext evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
localizedReason:myLocalizedReasonString
reply:^(BOOL success, NSError *error) {
if (success) {
[self performSegueWithIdentifier: #"callCustomSegue" sender:self];
Then the app stops for several seconds (at least 3-4) then next ViewController is presented.
The perform called by “callCustomSegue” does this:
- (void) perform {
src = (UIViewController *) self.sourceViewController;
dst = (UIViewController *) self.destinationViewController;
[src.view addSubview:dst.view];
}
I don’t understand what’s happening between the identification on touch ID and the performSegueWithIdentifier
and why the app stops.
If I bypass the touch ID and just call the performSegueWithIdentifier works immediately as I would expect.
If I put in the touch ID block:
[myContext evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
localizedReason:myLocalizedReasonString
reply:^(BOOL success, NSError *error) {
if (success) {
authenticated = YES;
[self showMessage:#"Authentication is successful" withTitle:#"Success"];
}
where showMessage does this:
UIAlertController * alert= [UIAlertController
alertControllerWithTitle:title
message:message
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* cancel = [UIAlertAction
actionWithTitle:#"OK"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action)
{
[alert dismissViewControllerAnimated:YES completion:nil];
if (authenticated) {
[self performSegueWithIdentifier: #"callCustomSegue" sender:self];
}
if (!authenticated) {
[self touchID];
}
}];
after tapping OK the next ViewController is called immediately.
So the question is: why I can’t call performSegue within the touch ID block and get an immediate response?
Any idea where I’m wrong?
Thank you so much.
You should perform all UI related activities on the main queue. The reply block for the touchID process is not guaranteed to be executing on the main queue. In fact you can almost guarantee that it won't be.
You should have -
if (success) {
authenticated = YES;
dispatch_async(dispatch_get_main_queue(), ^{
[self performSegueWithIdentifier: #"callCustomSegue" sender:self];
});
Related
I am following the following tutorial
https://developers.braintreepayments.com/guides/drop-in/ios/v4
In this I am finding issues when I post the payment nuance to server. The drop in UI remains after I enter the card details. And the options are still enabled so while the payment nuance is in progress user can select another option
Here is my code
BTDropInRequest *request = [[BTDropInRequest alloc] init];
BTDropInController *dropIn = [[BTDropInController alloc] initWithAuthorization:client_TOK request:request handler:^(BTDropInController * _Nonnull controller, BTDropInResult * _Nullable result, NSError * _Nullable error) {
if (error != nil) {
NSLog(#"ERROR");
} else if (result.cancelled) {
NSLog(#"CANCELLED");
[self dismissViewControllerAnimated:YES completion:NULL];
} else {
[self performSelector:#selector(dismiss_BT)
withObject:nil
afterDelay:0.0];
[self postNonceToServer:result.paymentMethod.nonce];
}
}];
[self presentViewController:dropIn animated:YES completion:nil];
In this above image I want to hide the Braintree drop in after I enter the card detail / Paypal
Thanks in advance
How do I avoid callback hell in the following or similar cases,
[self saveSomethingToTheServerWithCompletion:^(BOOL saveSucceeded) {
dispatch_async(dispatch_get_main_queue(), ^{
[self dismissViewControllerAnimated:YES completion:^{
dispatch_async(dispatch_get_main_queue(), ^{
if (saveSucceeded) {
[self showAlertControllerWithTitle:#"Message" message:#"Save successful."];
} else {
[self showAlertControllerWithTitle:#"Message" message:#"Save failed."];
}
});
}];
});
}];
My understanding is that dismissViewControllerAnimated:completion: and showAlertControllerWithTitle:message: must be executed on the main thread.
The question was fairly vague, so I'm taking a best guess at what you're asking. The only thing I can think of is that you're not happy with the nested calls. You can tidy that up with methods to an extent. e.g. you know that showing an alert always needs to be done on the main thread, so move the main thread code to your alert method:
[self saveSomethingToTheServerWithCompletion:^(BOOL saveSucceeded) {
dispatch_async(dispatch_get_main_queue(), ^{
[self dismissViewControllerAnimated:YES completion:^{
NSString *message = saveSucceeded ? #"Save successful." : #"Save failed.";
[self showAlertControllerWithTitle:#"Message" message:message];
}];
});
}];
- (void)showAlertControllerWithTitle:(NSString *)title message:(NSString *)message {
dispatch_async(dispatch_get_main_queue(), ^{
//Show alert;
}
}
Additionally, you could consider removing the completion block from the dismissViewControllerAnimated call. i.e. does your alert being shown really depend on whether or not the view controller has dismissed?
[self saveSomethingToTheServerWithCompletion:^(BOOL saveSucceeded) {
dispatch_async(dispatch_get_main_queue(), ^{
NSString *message = saveSucceeded ? #"Save successful." : #"Save failed.";
[self showAlertControllerWithTitle:#"Message" message:message];
[self dismissViewControllerAnimated:YES completion:nil];
});
}];
Ultimately though, sometimes you just have to nest callbacks. There's nothing really wrong with that.
Edit: I also replaced your if statement with a ternary one without really thinking about it. Not sure if you count that as "callback hell".
You can try something like this
[self saveSomethingToTheServerWithCompletion:^(BOOL saveSucceeded) {
dispatch_async(dispatch_get_main_queue(), ^{
NSString *strMsg = saveSucceeded ? #"Save successful." : #"Save failed.";
UIAlertAction *okAction = [UIAlertAction actionWithTitle:#"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
dispatch_async(dispatch_get_main_queue(), ^{
[self dismissViewControllerAnimated:YES completion:^{
}];
});
}];
[self showAlertWithTitle:#"Message" message:strMsg actions:#[okAction]];
});
}];
- (void)showAlertWithTitle:(NSString * _Nonnull)title message:(NSString * _Nonnull)msg actions:(NSArray * _Nonnull)actions {
UIAlertController *alertVC = [UIAlertController alertControllerWithTitle:title message:msg preferredStyle:UIAlertControllerStyleAlert];
for (UIAlertAction *action in actions) {
[alertVC addAction:action];
}
[self presentViewController:alertVC animated:true completion:nil];
}
So on callback it will show alert with message and on ok button top alert will dismiss and also dismiss the presented view controller. So it will be on main thread.
You don't have to wrap dismissViewControllerAnimated: with callback in main thread because it's already in main thread, also try to avoid duplicated code:
[self saveSomethingToTheServerWithCompletion:^(BOOL saveSucceeded) {
dispatch_async(dispatch_get_main_queue(), ^{
[self dismissViewControllerAnimated:YES completion:^{
NSString *messageContent = saveSucceeded ? #"Save successful." : #"Save failed.";
[self showAlertControllerWithTitle:#"Message" message:messageContent];
}];
});
}];
The problem only happens when I have using mobile data to upload photo to Parse that I will get "Upload Failure - Request Body Stream exhausted" error. That means if my phone is using WiFi mode, everything goes smooth.
Anyone could help me to solve the problem?
Thanks,
Kenny
My code for saving image is attached below:
self.pfProfile[#"logo"] = imageFile;
// Upload Profile to Parse
[self.pfProfile saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
[indicator stopAnimating];
if (succeeded) {
NSLog(#"Update Profile Successfully");
// Notify table view to reload the recipes from Parse cloud
//[[NSNotificationCenter defaultCenter] postNotificationName:#"refreshTable" object:self];
} else {
UIAlertController *alertController = [UIAlertController
alertControllerWithTitle:#"Upload Failure" message:[error localizedDescription]
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *okayAction = [UIAlertAction actionWithTitle:#"OK"
style:UIAlertActionStyleDefault handler:nil];
[alertController addAction:okayAction];
[self presentViewController:alertController animated:YES completion:nil];
}
}];
I'm currently calling storeViewController loadProductWithParameters via dispatch_async . Is it possible to set a timeout value so it only tries to fetch the results for X seconds and then gives up?
I implemented my own timeout by using with the class method below instead of calling loadProductWithParameters directly. It times out thanks to a dispatch_after and __block variable.
+ (void)loadProductViewControllerWithTimeout:(NSTimeInterval)timeout
storeKitViewController:(SKStoreProductViewController *)storeKitViewController
parameters:(NSDictionary *)parameters
completionHandler:(void (^)(BOOL result, NSError *error))completionHandler {
__block BOOL hasReturnedOrTimedOut = NO;
[storeKitViewController loadProductWithParameters:parameters completionBlock:^(BOOL result, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (!hasReturnedOrTimedOut) {
hasReturnedOrTimedOut = YES;
if (completionHandler) completionHandler(result, error);
}
});
}];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (!hasReturnedOrTimedOut) {
hasReturnedOrTimedOut = YES;
if (completionHandler) completionHandler(NO, nil); // Or add your own error instead of |nil|.
}
});
}
My latest app update got rejected by Apple because loadProductWithParameters never called its completionBlock and stopped my users from buying songs on iTunes... Hope this helps.
I have acomplished it like so:
__block BOOL timeoutOrFinish = NO;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(30 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if(!timeoutOrFinish) {
timeoutOrFinish = YES;
[self dismissAndShowError];
}
});
[storeViewController loadProductWithParameters:parameters completionBlock:^(BOOL result, NSError * _Nullable error) {
if(timeoutOrFinish) {
return;
}
timeoutOrFinish = YES;
//[[NetworkManager sharedManager] showNetworkActivityIndicator:NO];
if(error) {
[self dismissAndShowError];
}
}];
[self.view.window.rootViewController presentViewController:storeViewController animated:YES completion:nil];
where dismissAndShowError method runs dismissViewControllerAnimated and shows alert with an error.
Basically, you have a separate timer (30 seconds in my case) that switches a flag. After that time, if store has still not been loaded, I close it and display an error. Otherwise, completion is called (on cancel, finish and error) and handles all actions according to the status.
I have the following code where i show a MBProgress view and then run code in a separate thread. I then get a handle to the main thread and dismiss the spinner which works and then i show a UIAlertView. The UIAlertView loads fine however i can not click any of the buttons. If the alert view is outside of the dispatch block it works fine. Any ideas?
[MBProgressHUD showHUDAddedTo:self.view animated:YES];
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
// Do something...
GamePlayManager *gameManager = [GamePlayManager alloc];
Session *sess = [Session sharedInstance];
//Add the last actor to the end of the list
NSMutableDictionary *connections = sess.connections;
[connections setObject:sess.secondActor forKey:[NSString stringWithFormat:#"%d",kLastFieldtag]];
BOOL result = [gameManager areAnswersCorrect:sess.connections startingActor:sess.firstActor endingActor:sess.secondActor];
NSString *display = #"Sorry incorrect. Please recheck your answers.";
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Result"
message:display
delegate:self
cancelButtonTitle:#"Cancel"
otherButtonTitles:#"OK", nil];
if (result)
{
display = #"You are correct! You Won!";
if (sess.isMutiplayerGame)
{
[_gameCenterController endGame];
[self showGameOverScreen:YES isMultiplayer:YES];
}
else
{
[self showGameOverScreen:YES isMultiplayer:NO];
}
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUD hideHUDForView:self.view animated:YES];
[alert show];
});
}
else
{
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUD hideHUDForView:self.view animated:YES];
[alert show];
});
}
});
This is likely an issue caused by a clash between the MBProgressHUD's animation and the UIAlertView's animation.
I've never used MBProgressHUD, but looking at the code on GitHub it seems they've already solved your problem. MBProgressHUD has a completionBlock property.
Code like this should work: (Warning: Untested)
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUD HUDForView:self.view].completionBlock = ^{
[alert show];
};
[MBProgressHUD hideHUDForView:self.view animated:YES];
});
MBProgressHUD fires its completionBlock after the view has finished its animation, so there should no longer be a conflict.
As a side note the MBProgressHUD method:
- (void)showAnimated:(BOOL)animated
whileExecutingBlock:(dispatch_block_t)block
onQueue:(dispatch_queue_t)queue
completionBlock:(MBProgressHUDCompletionBlock)completion;
seems like it would be a better fit for your code.
Declare the alert view outside the threads with block:
__block UIAlertView *alert;