What can cause UIAlertController actions to never be called? - ios

In crash reports I'm seeing NSInternalInconsistencyException get thrown from the WKWebView delegate methods for prompting JavaScript alerts, ie. "Completion handler passed to -[MyClass webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:completionHandler:] was not called".
Every UIAlertAction calls the WKWebView completion handler from its handler. The only explanation is that the alert is being canceled without invoking any action. UIAlertView had delegate methods for cases like this, but UIAlertController does not offer that level of control.
Has anyone devised a solution for this?
I've considered using the same technique Apple uses in CompletionHandlerCallChecker (in WebKit) to capture my own callback's failure to be invoked, and invoke the WKWebView's handler to prevent the spurious exception. Seems awfully kludgy, and I'm not yet sure it would work. I'd rather prevent this from happening in the first place.
Edit: I know that programmatically dismissing the alert controller produces this behavior, which is unfortunate, but I do not know the condition where iOS decides to dismiss the controller without user interaction.
Edit 2: For those requesting the code, it is really the minimum implementation:
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler {
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message message:nil preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:#"OK" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
completionHandler();
}]];
[self presentViewController:alertController animated:YES completion:nil];
}

Here's my kludgy but working solution:
class CompletionHandlerCallCheckerDefeater: NSObject {
private var calledCompletionHandler: Bool = false
private var fallbackHandler: () -> Void
init(fallbackHandler: () -> Void) {
self.fallbackHandler = fallbackHandler
}
deinit {
if (!calledCompletionHandler) {
fallbackHandler()
}
}
func didCallCompletionHandler() {
calledCompletionHandler = true
}
}
In your delegate callback, use it like this:
let defeater = CompletionHandlerCallCheckerDefeater(fallbackHandler: completionHandler)
let alertController = UIAlertController(title: title, message: message, preferredStyle: .Alert)
alertController.addAction(UIAlertAction(title:"OK", style: .Default) { (action) in
completionHandler()
defeater.didCallCompletionHandler()
})
presentViewController(alertController, animated: true, completion: nil)
Assign whatever is necessary to the fallbackHandler, it doesn't have to be the block passed in, it just needs to invoke the completionHandler with appropriate parameters. The "defeater" is destroyed when all references are released, that is, when the alert is destroyed, and it will call the fallbackHandler (which calls completionHandler) if didCallCompletionHandler was never called, thus avoiding the exception.

Related

ios Keyboard must not show up automatically for a UITextField in UIAlertController

I am trying to show UIAlertController with text field. When it launches keyfoard automatically shows up as textfield get the focus automatically. How can I show alert with textfield without keyboard (it should show up only when user clicks on textfield).
Here is my code
UIAlertController* alert = [UIAlertController alertControllerWithTitle:#"Collect Input" message:#"input message"
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:#"Submit" style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
//use alert.textFields[0].text
}];
UIAlertAction* cancelAction = [UIAlertAction actionWithTitle:#"Cancel" handler:^(UIAlertAction * action) {
//cancel action
}];
[alert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
// A block for configuring the text field prior to displaying the alert
//[textField resignFirstResponder];
}];
[alert addAction:defaultAction];
[alert addAction:cancelAction];
[self presentViewController:alert animated:NO completion:nil];
There are a few solutions.
You can get a reference to the text field in the addTextFieldWithConfigurationHandler callback and store it in a local variable. Then in the completion handler of presentViewController you can call resignFirstResponder on the text field. But this solution is far from ideal because the keyboard will appear and then immediately be dismissed.
Better yet is to set the text field's delegate and implement the shouldBeginEditing delegate method. Add an instance variable to act as a flag. The first time shouldBeginEditing is called, not the flag isn't set, set it, and return NO. Then each time after, check the flag and return YES.
Here's the implementation for option 2:
Indicate that your class conforms to the UITextFieldDelegate protocol in the .m file:
#interface YourClassHere () <UITextFieldDelegate>
#end
Add an instance variable for the flag:
BOOL showKeyboard = NO;
Update your alert setup code to set the text field's delegate:
[alert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
textField.delegate = self;
}];
Implement the text field delegate method:
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
if (showKeyboard) {
return YES;
} else {
showKeyboard = YES;
return NO;
}
}
This prevents the initial display of the keyboard but allows it any time after that.

UIAlertController is nil when handler block executes

Here's the problem up front: I have a UIAlertController that has a textfield. I want to save the content of that textfield as an NSString when the user touches a "Confirm" button in the alert. When the Confirm action block is executed, however, the alert is nil (presumably already dismissed and deallocated at that point), and thus so is its textfield, meaning I cannot save the textfield's text.
I am using a series of UIAlertControllers to allow a user to create a passcode for my app, such that any time the app comes to the foreground, the user is prompted for the code before the app can be used.
I created a category of UIAlertController with several convenience methods that return preconfigured alerts that I need to use. Here's one of them:
+ (UIAlertController*)passcodeCreationAlertWithConfirmBehavior:(void(^)())confirmBlock andCancelBehavior:(void(^)())cancelBlock {
UIAlertController *passcodeCreationAlert = [UIAlertController alertControllerWithTitle:#"Enter a passcode"
message:nil
preferredStyle:UIAlertControllerStyleAlert];
[passcodeCreationAlert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
textField.keyboardType = UIKeyboardTypeNumberPad;
}];
UIAlertAction* cancelAction = [UIAlertAction actionWithTitle:#"Cancel"
style:UIAlertActionStyleCancel
handler:^(UIAlertAction * action) {
if (cancelBlock) {
cancelBlock();
}
}];
UIAlertAction* confirmAction = [UIAlertAction actionWithTitle:#"Confirm"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
if (confirmBlock) {
confirmBlock();
}
}];
[passcodeCreationAlert addAction:cancelAction];
[passcodeCreationAlert addAction:confirmAction];
passcodeCreationAlert.preferredAction = confirmAction;
confirmAction.enabled = NO;
return passcodeCreationAlert;
}
This method returns a UIAlertController that allows the user to enter their desired passcode into a textfield. When I call this method in my view controller, I pass blocks as parameters which are used as the UIAlertAction handlers:
- (void)presentCreatePasscodeAlert {
UIAlertController *alert = [UIAlertController passcodeCreationAlertWithConfirmBehavior:^{
firstPasscode = alert.textFields[0].text;
[self presentConfirmPasscodeAlert];
} andCancelBehavior:^{
[self presentEnablePasscodeAlert];
}];
alert.textFields[0].delegate = self;
[self presentViewController:alert animated:YES completion:nil];
}
To reiterate the problem now that there is more context: When the action block is entered at the line:
firstPasscode = alert.textFields[0].text;
the alert is nil, and so is its textfield, meaning I cannot save the textfield's text.
In a separate project, however, I tried getting the same functionality without using the category and custom convenience methods, and that works as desired:
- (void) createPassword {
UIAlertController *createPasswordAlert = [UIAlertController alertControllerWithTitle:#"Enter a password"
message:nil
preferredStyle:UIAlertControllerStyleAlert];
__weak ViewController *weakSelf = self;
[createPasswordAlert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
textField.delegate = weakSelf;
textField.keyboardType = UIKeyboardTypeNumberPad;
}];
UIAlertAction* confirmAction = [UIAlertAction actionWithTitle:#"Confirm"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
self.password = createPasswordAlert.textFields[0].text;
[self confirmPassword];
}];
UIAlertAction* cancelAction = [UIAlertAction actionWithTitle:#"Cancel"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
[self promptPasswordCreation];
}];
[createPasswordAlert addAction:confirmAction];
[createPasswordAlert addAction:cancelAction];
confirmAction.enabled = NO;
[self presentViewController:createPasswordAlert animated:YES completion:nil];
}
Sure enough, in the above code the alert exists when the Confirm block is entered, and I can save the text just fine.
Have I done something screwy by passing blocks as parameters to my convenience methods?
One of your problems is that when you create the block and send it into the initializer you are referencing a nil object inside the block. Then the block is saved with that nil reference and passed as a block to your handler.
UIAlertController *alert = [UIAlertController passcodeCreationAlertWithConfirmBehavior:^{
firstPasscode = alert.textFields[0].text;
[self presentConfirmPasscodeAlert];
} andCancelBehavior:^{
[self presentEnablePasscodeAlert];
}];
In there alert is not initialized yet when you create that block and send it to be saved. An option to fix that would be to use a standard initializer and then categorize a method to add the block functions that you want. Another option might be to try to add the __block modifier to the alert object, although I don't think that will help.

iOS 9 - Keyboard pops up after UIAlertView dismissed

I have a strange visual bug that only affects iOS 9 devices:
My app’s login UIViewController runs off and gets an OAuth token when you hit the button, much like you’d expect. If the response from my API returns a specific status code, I pop up a UIAlertView saying they need to reset their password (this is if they’ve been flagged as such on the server end). The email and password fields for login resignFirstResponder once you hit the button, standard stuff.
On iOS 9 only, if you hit the reset path, the second you tap OK on that alert view, the keyboard pops back up, for maybe 800ms, then dismisses again. It’s almost as if something was queued to present it, but the presence of the alert blocked it until you hit OK – it’s absolutely instantaneous after hitting okay on the alert.
It seems a really tricky one to debug. I’ve added symbolic breakpoints to becomeFirstResponder and it isn’t called anywhere near this process occurring.
Any other ideas for how I can look at debugging / fixing this? It doesn’t affect iOS 7 and iOS 8, only iOS 9.
I faced this problem about 30 minutes ago.
The UIAlertView has been deprecated since iOS9 was released.
We solved this issue by using the UIAlertController, like this:
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:#"Alert Title!" message:#"This is an alert message." preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* ok = [UIAlertAction actionWithTitle:#"OK" style:UIAlertActionStyleDefault handler:nil];
[alertController addAction:ok];
[self presentViewController:alertController animated:NO completion:nil];
This should fix your problem.
If animated = YES, you may get the same issue as before. This is a bug with iOS9.
Let me know how it goes, and if this fixes your problem.
Here is an extension to handle this in swift 3
extension UIViewController {
func presentOk(with title: String, and message: String, handler: ((UIAlertAction) -> Void)?) {
let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertControllerStyle.alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.default, handler: handler))
OperationQueue.main.addOperation {
self.view.endEditing(true)
self.present(alert, animated: true, completion: nil)
}
}
}
The key is to hide the keyboard and present the controller in the main queue.
Usage
presentOk(with: "My app title", and: "this is the alert message", handler: nil)

How to cancel UIActivityItemProvider and don't show activity?

I'm using UIActivityItemProvider subclass to provide custom data. But sometimes getting data fails and I don't want to present activity (e.g. message composer). Tried [self cancel] and return nil; in item method, but message composer still shows (with empty message).
If you dismiss the UIActivityViewController before returning from -(id)item it will not present the users chosen activity.
To do this you first need to grab the activityViewController in activityViewControllerPlaceholderItem. In -(id)item run code in a dispatch_async to update progress and dismiss on complete / error which I'm doing using a promise lib.
In your subclass of UIActivityItemProvider do something similar to the example below.
-(id) activityViewControllerPlaceholderItem:(UIActivityViewController *)activityViewController
{ self.avc = activityViewController;
return NSURL;
}
-(id)item
{ __block BOOL fileProcDone = NO;
dispatch_async(dispatch_get_main_queue(), ^
{ self.pvc = [[ProgressAlertVC alloc] init];
[self.vc presentViewController:self.pvc animated:YES completion:nil];
[[[[self promiseMakeFile]
progressed:^(float progress)
{ self.pvc.progress = progress;
}]
fulfilled:^(id result)
{ [self.pvc dismissViewControllerAnimated:YES completion:^
{ fileProcDone = YES;
}];
}]
failed:^(NSError *error)
{ [self.pvc dismissViewControllerAnimated:YES completion:^
{ [self.vc dismissViewControllerAnimated:YES completion:^
{ fileProcDone = YES;
}];
}];
}];
});
while (!fileProcDone)
{ [NSThread sleepForTimeInterval:0.1];
};
return NSURL;
}
This will result in a console log message from activity extensions but as long as they deal correctly with errors things should be fine. If you return nil from -(id)activityViewController: itemForActivityType: you don't get console errors but will get the users chosen activity even if you dismiss the UIActivityViewController at this point.
You simply need to call the cancel method of UIActivityItemProvider. Since UIActivityItemProvider is an NSOperation, calling cancel will mark the operation cancelled.
At that point, you have a few options to actually stop the long running task, depending on the structure of your task. You could override the cancel method and do your cancellation there, just be sure to call [super cancel] as well. The second option is the check the value of isCancelled within the item method.
An example item provider
import UIKit
import Dispatch
class ItemProvider: UIActivityItemProvider {
override public var item: Any {
let semaphore = DispatchSemaphore(value: 0)
let message = "This will stop the entire share flow until you press OK. It represents a long running task."
let alert = UIAlertController.init(title: "Hello", message: message, preferredStyle: .alert)
let action = UIAlertAction.init(title: "OK", style: .default, handler:
{ action in
semaphore.signal()
})
let cancel = UIAlertAction.init(title: "CANCEL", style: .destructive, handler:
{ [weak self] action in
self?.cancel()
semaphore.signal()
})
alert.addAction(action)
alert.addAction(cancel)
//Truly, some hacking to for the purpose of demonstrating the solution
DispatchQueue.main.async {
UIApplication.shared.delegate?.window??.rootViewController?.presentedViewController!.present(alert, animated: true, completion: nil)
}
// We can block here, because our long running task is in another queue
semaphore.wait()
// Once the item is properly cancelled, it doesn't really matter what you return
return NSURL.init(string: "blah") as Any
}
}
In the view controller, start a share activity like this.
let provider = ItemProvider.init(placeholderItem: "SomeString")
let vc = UIActivityViewController.init(activityItems: [provider], applicationActivities: nil)
self.present(vc, animated: true, completion: nil)

How to implement a pop-up dialog box in iOS?

After a calculation, I want to display a pop up or alert box conveying a message to the user. Does anyone know where I can find more information about this?
Yup, a UIAlertView is probably what you're looking for. Here's an example:
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"No network connection"
message:#"You must be connected to the internet to use this app."
delegate:nil
cancelButtonTitle:#"OK"
otherButtonTitles:nil];
[alert show];
[alert release];
If you want to do something more fancy, say display a custom UI in your UIAlertView, you can subclass UIAlertView and put in custom UI components in the init method. If you want to respond to a button press after a UIAlertView appears, you can set the delegate above and implement the - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex method.
You might also want to look at the UIActionSheet.
Different people who come to this question mean different things by a popup box. I highly recommend reading the Temporary Views documentation. My answer is largely a summary of this and other related documentation.
Alert (show me an example)
Alerts display a title and an optional message. The user must acknowledge it (a one-button alert) or make a simple choice (a two-button alert) before going on. You create an alert with a UIAlertController.
It is worth quoting the documentation's warning and advice about creating unnecessary alerts.
Notes:
See also Alert Views, but starting in iOS 8 UIAlertView was deprecated. You should use UIAlertController to create alerts now.
iOS Fundamentals: UIAlertView and UIAlertController (tutorial)
Action Sheet (show me an example)
Action Sheets give the user a list of choices. They appear either at the bottom of the screen or in a popover depending on the size and orientation of the device. As with alerts, a UIAlertController is used to make an action sheet. Before iOS 8, UIActionSheet was used, but now the documentation says:
Important: UIActionSheet is deprecated in iOS 8. (Note that UIActionSheetDelegate is also deprecated.) To create and manage action sheets in iOS 8 and later, instead use UIAlertController with a preferredStyle of UIAlertControllerStyleActionSheet.
Modal View (show me an example)
A modal view is a self-contained view that has everything it needs to complete a task. It may or may not take up the full screen. To create a modal view, use a UIPresentationController with one of the Modal Presentation Styles.
See also
Presenting View Controllers from Other View Controllers
Modal Contexts
Popover (show me an example)
A Popover is a view that appears when a user taps on something and disappears when tapping off it. It has an arrow showing the control or location from where the tap was made. The content can be just about anything you can put in a View Controller. You make a popover with a UIPopoverPresentationController. (Before iOS 8, UIPopoverController was the recommended method.)
In the past popovers were only available on the iPad, but starting with iOS 8 you can also get them on an iPhone (see here, here, and here).
See also
View Controllers: Popovers
Notifications
Notifications are sounds/vibrations, alerts/banners, or badges that notify the user of something even when the app is not running in the foreground.
See also
Local and Remote Notification Programming Guide
Simple, interactive notifications in iOS 8
A note about Android Toasts
In Android, a Toast is a short message that displays on the screen for a short amount of time and then disappears automatically without disrupting user interaction with the app.
People coming from an Android background want to know what the iOS version of a Toast is. Some examples of these questions can he found here, here, here, and here. The answer is that there is no equivalent to a Toast in iOS. Various workarounds that have been presented include:
Make your own with a subclassed UIView
Import a third party project that mimics a Toast
Use a buttonless Alert with a timer
However, my advice is to stick with the standard UI options that already come with iOS. Don't try to make your app look and behave exactly the same as the Android version. Think about how to repackage it so that it looks and feels like an iOS app.
Since the release of iOS 8, UIAlertView is now deprecated; UIAlertController is the replacement.
Here is a sample of how it looks in Swift 5:
let alert = UIAlertController(title: "Hello!", message: "Message", preferredStyle: .alert)
let alertAction = UIAlertAction(title: "OK!", style: .default) { (sender: UIAlertAction) -> Void in
// ... Maybe handle "OK!" being tapped.
}
alert.addAction(alertAction)
// Show.
present(alert, animated: true) { () -> Void in
// ... Maybe do something once showing is complete.
}
As you can see, the API allows us to implement callbacks for both the action and when we are presenting the alert, which is quite handy!
For older Swift version:
let alert = UIAlertController(title: "Hello!", message: "Message", preferredStyle: UIAlertControllerStyle.alert)
let alertAction = UIAlertAction(title: "OK!", style: UIAlertActionStyle.default)
{
(UIAlertAction) -> Void in
}
alert.addAction(alertAction)
present(alert, animated: true)
{
() -> Void in
}
Since iOS 8.0, you will need to use UIAlertController as the following:
-(void)alertMessage:(NSString*)message
{
UIAlertController* alert = [UIAlertController
alertControllerWithTitle:#"Alert"
message:message
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* defaultAction = [UIAlertAction
actionWithTitle:#"OK" style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {}];
[alert addAction:defaultAction];
[self presentViewController:alert animated:YES completion:nil];
}
Where self in my example is a UIViewController, which implements "presentViewController" method for a popup.
For Swift 3 & Swift 4 :
Since UIAlertView is deprecated, there is the good way for display Alert on Swift 3
let alertController = UIAlertController(title: NSLocalizedString("No network connection",comment:""), message: NSLocalizedString("connected to the internet to use this app.",comment:""), preferredStyle: .alert)
let defaultAction = UIAlertAction(title: NSLocalizedString("Ok", comment: ""), style: .default, handler: { (pAlert) in
//Do whatever you want here
})
alertController.addAction(defaultAction)
self.present(alertController, animated: true, completion: nil)
Deprecated :
This is the swift version inspired by the checked response :
Display AlertView :
let alert = UIAlertView(title: "No network connection",
message: "You must be connected to the internet to use this app.", delegate: nil, cancelButtonTitle: "Ok")
alert.delegate = self
alert.show()
Add the delegate to your view controller :
class AgendaViewController: UIViewController, UIAlertViewDelegate
When user click on button, this code will be executed :
func alertView(alertView: UIAlertView, clickedButtonAtIndex buttonIndex: Int) {
}
Although I already wrote an overview of different kinds of popups, most people just need an Alert.
How to implement a popup dialog box
class ViewController: UIViewController {
#IBAction func showAlertButtonTapped(_ sender: UIButton) {
// create the alert
let alert = UIAlertController(title: "My Title", message: "This is my message.", preferredStyle: UIAlertController.Style.alert)
// add an action (button)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: nil))
// show the alert
self.present(alert, animated: true, completion: nil)
}
}
My fuller answer is here.
Here is C# version in Xamarin.iOS
var alert = new UIAlertView("Title - Hey!", "Message - Hello iOS!", null, "Ok");
alert.Show();

Resources