Alright, I've looked up a lot of documentation on how the UIAlertController's work and I have a rather unique scenario. My application is being designed to work with a HID Bluetooth scanner. When I use:
preferredStyle: UIAlertControllerStyle.Alert
After I generate an alert that the item I have scanned is incorrect. Cool, alert is happening, problem is if I scan again ( which emulates keyboard input ), the return key is being sent to the alert and the alert is running the dismiss action.
If I use:
preferredStyle: UIAlertControllerStyle.ActionSheet
Then the alert is staying where it should be and ignoring scans while the alert window is up just how I want it.
So my question is, how do I capture the return key and prevent the Alert from calling the dismiss action? I'm a bit new with swift and I have a feeling the answer is simple, but I've tried half a dozen things that just isn't working.
If there is a setting to prevent all user input to the alert window or anything solution, I'm all for any method. I just rather not use the ActionSheet, and prefer to use the iOS alerts instead of creating my own screen. If this is not possible, I'm sure I can build my own 'alerts' window.
Code, that I'm calling from a simple Alerts class I made.
import UIKit
class Alerts {
var controller: UIViewController
var message: String
var title: String
init?(title: String, message: String, controller: UIViewController){
if title.isEmpty || message.isEmpty {
return nil
}
self.title = title
self.message = message
self.controller = controller
}
func save_alert(input_field:UITextField, callable: (Bool)->Void ){
let action = UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.Default) {
UIAlertAction in
callable(false)
input_field.enabled = true
input_field.becomeFirstResponder()
print("DISMISS CALLED")
}
let alert = UIAlertController(title: self.title,message:self.message,preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(action)
self.controller.presentViewController(alert,animated:true, completion: nil)
}
}
Try something like this.
func save_alert(input_field:UITextField, callable: (Bool)->Void ){
let action = UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.Default) {
UIAlertAction in
callable(false)
input_field.enabled = true
input_field.becomeFirstResponder()
print("DISMISS CALLED")
showAlert()
}
showAlert()
}
func showAlert() {
let alert = UIAlertController(title: self.title,message:self.message,preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(action)
self.controller.presentViewController(alert,animated:true, completion: nil)
}
Related
I have a simple Parent Gateway for child friendly game using a UIAlertController.
When the user answers the question incorrectly - I DONT want the alert view to be dismissed, at the moment I have the device vibrate and the screen shake but I can't get the alert view to stay on the screen.
The answers provided are in Obj-C or involve disabling the button, which obviously won't work and the other answers do not work - so I was hoping there would be an updated answer.
func parentGate () {
var val1 = [1,2,3,4]
var val2 = [5,6,7,8]
let valIndex1 = Int(arc4random_uniform(UInt32(val1.count-1)))
let valIndex2 = Int(arc4random_uniform(UInt32(val2.count-1)))
accessAnswer = val1[valIndex1]+val2[valIndex2]
let alertController = UIAlertController (title: "Parental Control", message: "This section is for Parents only, \nplease answer the question below: \nWhat is \(val1[valIndex1]) + \(val2[valIndex2])?", preferredStyle: .alert)
let testAnswer = UIAlertAction(title: "OK", style: .default, handler: answerHandler)
let cancelAnswer = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
alertController.addTextField(configurationHandler: answerTextFieldResult)
alertController.addAction(testAnswer)
alertController.addAction(cancelAnswer)
self.present(alertController, animated: true, completion: nil)
}
and then the testAnswer handler looks like this:
func answerHandler (alert: UIAlertAction) {
guard let suppliedAnswer = answerTextField?.text,
let answer = Int(suppliedAnswer) else {
return
}
if answer == accessAnswer {
print ("Good")
} else {
print ("Bad")
AudioServicesPlayAlertSound(SystemSoundID(kSystemSoundID_Vibrate))
self.view.shake(count: 3, for: 0.3, withTranslation: 3)
}
}
If you want to see what the textField handler looks like at the moment:
func answerTextFieldResult(textfield: UITextField) {
answerTextField = textfield
answerTextField?.placeholder = "Answer"
}
You can't do this.
It's system behaviour - when you click on button alert closes and you can't prevent this.
Only one solution is to create a custom view controller that will look like native UIAlertController.
i’m using UIAlertController for asking user enter a string, and this string should not be empty. For this purpose i'm adding handler for every editing event on UITextField located on UIAlertController. This handler checks, if the textField is empty, it makes «OK» button (located on UIAlertController) disabled, and, if the textField is not empty, it makes «OK» button enabler. «OK» button also located on UIAlertController.
I have a problem with access to the «OK» button from this
handler function:
There is no opportunity to pass the button as a parameter directly to the handler function because i’m calling it using #selector.
I’ve also tried to access UIAlertController through the UITextField.superview from handler function, but it didn’t work: it returned UIView, which can’t be downcasted to UIAlertController. And it’s not clear why it is so: UITextField is subview of UIAlertController and consequently UITextField.superview must return UIAlertController.
I’ve also tried the easiest way: declare UIAlertController as property in my ViewController class: this allows me to access the button from handler function without passing it directly to function. But in this case i’ve collided with another problem: at the runtime Xcode shows this warning in debug area: Attempting to load the view of a view controller while it is deallocating is not allowed and may result in undefined behavior ()
And my question is how to get access to «OK» button from my handler function?
class TVC_ProgramsList: UITableViewController {
var enterNameAC = UIAlertController()
#IBAction func addpush(sender: UIBarButtonItem) {
addProgram()
}
func addProgram() {
enterNameAC = UIAlertController(title: "Adding program", message: "Enter program name", preferredStyle: UIAlertControllerStyle.Alert)
enterNameAC.addAction(UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Cancel, handler: nil))
enterNameAC.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil))
enterNameAC.addTextFieldWithConfigurationHandler { (textField) -> Void in
textField.placeholder = "Program name"
textField.addTarget(self, action: #selector(TVC_ProgramsList.enterProgramNameAC_editingTextField), forControlEvents: UIControlEvents.AllEditingEvents)
}
self.presentViewController(enterNameAC, animated: true, completion: nil)
}
func enterProgramNameAC_editingTextField() {
let programNameString = enterNameAC.textFields![0].text!
if programNameString.characters.count > 0 {
enterNameAC.actions[1].enabled = true
} else {
enterNameAC.actions[1].enabled = false
}
}
}
Why don't you make an attribute of the UIAlertAction? I've changed your code:
class TVC_ProgramsList: UITableViewController {
var enterNameAC = UIAlertController()
let okAction = UIAlertAction(title: "OK", style: UIAlertActionStyle.Default, handler: nil)
#IBAction func addpush(sender: UIBarButtonItem) {
addProgram()
}
func addProgram() {
enterNameAC = UIAlertController(title: "Adding program", message: "Enter program name", preferredStyle: UIAlertControllerStyle.Alert)
enterNameAC.addAction(UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Cancel, handler: nil))
enterNameAC.addAction(self.okAction)
enterNameAC.addTextFieldWithConfigurationHandler { (textField) -> Void in
textField.placeholder = "Program name"
textField.addTarget(self, action: #selector(TVC_ProgramsList.enterProgramNameAC_editingTextField), forControlEvents: UIControlEvents.AllEditingEvents)
}
self.presentViewController(enterNameAC, animated: true, completion: nil)
}
func enterProgramNameAC_editingTextField() {
let programNameString = enterNameAC.textFields![0].text!
// use okAction here
self.okAction.enabled = programNameString.characters.count > 0
}
}
Passing it with addTarget won't be possible. But it's a valid approach to make a class attribute out of it and therefore making it accessible in other parts of your ViewController.
Also i found another solution of this problem: it's enough to make lazy declaration of enterNameAC to avoid alert message in debug area at runtime:
lazy var enterNameAC = UIAlertController()
But it's not all: All i said it was about Swift 2, but yesterday i updated to Swift 3, and, it's miracle: it works without any debug area messages, even without lazy declaration!
This is how I simply create UIAlertController and present it on the screen:
private class func showAlertWithTitle(title: String, message: String) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .Alert)
//alert.accessibilityLabel = "my string here" //doesnt work
let action = UIAlertAction(title: "OK", style: .Default) { action in
alert.dismissViewControllerAnimated(true, completion: nil)
}
alert.addAction(action)
UIStoryboard.topViewController()?.presentViewController(alert, animated: true, completion: nil)
}
and this is how I access it under UITests:
emailAlert = app.alerts["First Name"] //for title "First Name"
but I would like to set there custom identifier and access this by firstName like this:
emailAlert = app.alerts["firstName"]
Is it possible?
This is an old thread but someone might use this.
I was able to set the accessibility identifier like this:
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.view.accessibilityIdentifier = "custom_alert"
alert.view.accessibilityValue = "\(title)-\(message)"
alert.addAction(
UIAlertAction(
title: "ALERT_BUTTON_OK".localized,
style: .default,
handler: handler
)
)
present(alert, animated: true)
That way I can access the alert by accessibility identifier and check its contents in accessibility value.
It is not perfect of course, but it works - at least for my testing using Appium.
The only way I figured out to do this was to use Apple's private APIs. You call valueForKey on the UIAlertAction object with this super secret key: "__representer" to get whats called a _UIAlertControllerActionView.
let alertView = UIAlertController(title: "This is Alert!", message: "This is a message!", preferredStyle: .Alert)
let okAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
alertView.addAction(okAction)
self.presentViewController(alertView, animated: true, completion: {
let alertButton = action.valueForKey("__representer")
let view = alertButton as? UIView
view?.accessibilityIdentifier = "okAction_AID"
})
This has to be done in the completion handler because that that _UIAlertControllerActionView won't exist until the view is presented. On a side note in my project I used these following extensions to make things easier / more readable:
extension UIAlertController {
func applyAccessibilityIdentifiers()
{
for action in actions
{
let label = action.valueForKey("__representer")
let view = label as? UIView
view?.accessibilityIdentifier = action.getAcAccessibilityIdentifier()
}
}
}
extension UIAlertAction
{
private struct AssociatedKeys {
static var AccessabilityIdentifier = "nsh_AccesabilityIdentifier"
}
func setAccessibilityIdentifier(accessabilityIdentifier: String)
{
objc_setAssociatedObject(self, &AssociatedKeys.AccessabilityIdentifier, accessabilityIdentifier, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
func getAcAccessibilityIdentifier() -> String?
{
return objc_getAssociatedObject(self, &AssociatedKeys.AccessabilityIdentifier) as? String
}
}
So the above code would be rewritten:
let alertView = UIAlertController(title: NSLocalizedString("NMN_LOGINPAGECONTROLLER_ERROR_TITLE", comment: ""), message: message as String, preferredStyle:.Alert)
let okAction = UIAlertAction(title: NSLocalizedString("NMN_OK", comment: ""), style: .Default, handler: nil)
okAction.setAccessibilityIdentifier(InvalidLoginAlertView_AID)
alertView.addAction(okAction)
self.presentViewController(alertView, animated: true, completion: {
alertView.applyAccessibilityIdentifiers()
})
My first attempt involved trying to navigate the view hierarchy but that became difficult since UIAlertControllerActionView was not a part of the public API. Anyway I'd probably would try to ifdef out the valueForKey("__representer") for builds submitted for the app store or Apple might give you a spanking.
Right now I have a UIAlertAction called addCamera and I'm just doing:
addCamera.accessibilityLabel = "camera-autocomplete-action-photo"
That allows me to tap it in UI Tests as follows:
app.sheets.buttons["camera-autocomplete-action-photo"].firstMatch.tap()
From Apple docs...
https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/UIKitUICatalog/UIAlertView.html
Making Alert Views Accessible
Alert views are accessible by default.
Accessibility for alert views pertains to the alert title, alert message, and button titles. If VoiceOver is activated, it speaks the word “alert” when an alert is shown, then speaks its title followed by its message if set. As the user taps a button, VoiceOver speaks its title and the word “button.” As the user taps a text field, VoiceOver speaks its value and “text field” or “secure text field.”
I am having a couple of issues with alert controllers in swift. I have two functions for displaying activity indicators. 1 with animation, and one without. The reason for creating the second one without an animation was because.. I am displaying an activity on a view controller when a user clicks on a table view cell and is segued to a new controller. This controller calls a webservice and populates a second table view.
My problem was that the web service was returning a response so quick that the activity indicator wasn't up on screen when I was trying to dismiss it i.e. in the repsonse of the webservice call. I was presenting this indicator in the viewdidload and then calling the web service function in the view did load after.
The only way i could get around this was to create an activity alert which did not have an animation as it seemed as though the animation was slowing it down a bit. But when I set the animation property to false, the alert controller did not have a backgroundColor. When I try to add a background color to the alert controller, the width changes to full screen.
So I'm looking for:
A) a way around dismissing the regular alert controller when the web service returns too quickly
or
B) to reduce the size of the second alert controller which has no animation.
Thanks in advance. I was having a lot of trouble with dismissing these alert controllers in that when I was attempting to dismiss them, my actual view controller was being dismissed so I tried to check the class of the presentedController and only dismissing if the class was alertController but I don't think this is actually the right way to go around it at all.
Code below:
func displayActivityAlert(title: String, #ViewController: UIViewController)
{
let pending = UIAlertController(title: "\n\n\n"+title, message: nil, preferredStyle: .Alert)
//create an activity indicator
let indicator = UIActivityIndicatorView(frame: pending.view.bounds)
indicator.autoresizingMask = .FlexibleWidth | .FlexibleHeight
indicator.color = UIColor(rgba: Palette.accent)
//add the activity indicator as a subview of the alert controller's view
pending.view.addSubview(indicator)
//pending.view.backgroundColor = UIColor.whiteColor()
indicator.userInteractionEnabled = false // required otherwise if there buttons in the UIAlertController you will not be able to press them
indicator.startAnimating()
ViewController.presentViewController(pending, animated: true, completion: nil)
}
and
func displayActivityAlertNoAnim(title: String, #ViewController: UIViewController)
{
let pending = UIAlertController(title: "\n\n\n"+title, message: nil, preferredStyle: .Alert)
//create an activity indicator
let indicator = UIActivityIndicatorView(frame: pending.view.bounds)
indicator.autoresizingMask = .FlexibleWidth | .FlexibleHeight
indicator.color = UIColor(rgba: Palette.accent)
//add the activity indicator as a subview of the alert controller's view
pending.view.addSubview(indicator)
pending.view.backgroundColor = UIColor.whiteColor()
// this line cause the alert controller to become full width of the screen
indicator.userInteractionEnabled = false // required otherwise if there buttons in the UIAlertController you will not be able to press them
indicator.startAnimating()
ViewController.presentViewController(pending, animated: **false**, completion: nil)
}
Code for checking class and dismissing:
if self.presentedViewController!.isKindOfClass(UIAlertController){
self.dismissViewControllerAnimated(true, completion: nil)
}
You need to make use of the completion parameter in presentViewController(). This is a closure which will get executed exactly after the UIAlertController has become visible on the screen.
Now, I can only provide you with some pseudocode since you haven't provided any code on how you download or the callback you receive after downloading, but try something like the following:
func displayActivityAlert(title: String, #ViewController: UIViewController) {
let pending = UIAlertController(title: "\n\n\n"+title, message: nil, preferredStyle: .Alert)
...
ViewController.presentViewController(pending, animated: true) { () -> Void in
// Start downloading from webservice
}
}
And dismissing:
if self.presentedViewController!.isKindOfClass(UIAlertController){
self.dismissViewControllerAnimated(true) { () -> Void in
// Perform segue to tableview
}
}
UPDATE 1:
Updated pseudocode based on OP's architecture.
If you have factorised the code for your alerts into a separate file, then simply pass in the completion handler as a parameter like so:
func displayActivityAlert(title: String, #ViewController: UIViewController, completionHandler: ()->() ) {
let pending = UIAlertController(title: "\n\n\n"+title, message: nil, preferredStyle: .Alert)
...
self.presentViewController(pending, animated: true, completion: completionHandler)
}
And then whenever you call displayActivityAlert, then simply specify the callback, for example like so:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
displayActivityAlert("Hello", ViewController: self) { () -> () in
// Download from webservice
}
}
simple code modify as you need
put this code inside a function or inside a button of action
will have a single button "OK"
let alertView = UIAlertController(title: "Your ERROR Heading!", message: "Your error message here", preferredStyle: .Alert)
let OKAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
alertView.addAction(OKAction)
self.presentViewController(alertView, animated: true, completion: nil)
class func alertController(_ title:String, message: String, okTitle: String,cancelTitle: String? = nil,cancelCompletion:(() ->Void)? = nil, okCompletion :(() -> Void)?) {
let alertController = UIAlertController.init(title: title as String, message: message as String, preferredStyle: UIAlertControllerStyle.alert)
let okAction = UIAlertAction.init(title: okTitle as String, style: UIAlertActionStyle.default) { (alertAction :UIAlertAction) in
if okCompletion != nil{
okCompletion!()
}
}
alertController.addAction(okAction)
if cancelTitle != nil && !(cancelTitle?.isEmpty)!{
let cancelAction = UIAlertAction.init(title: cancelTitle, style: UIAlertActionStyle.cancel) { (alertAction : UIAlertAction) in
if cancelCompletion != nil{
cancelCompletion!()
}
}
alertController.addAction(cancelAction)
}
Constant.Common.APPDELObj.navVC?.visibleViewController?.present(alertController, animated: true, completion: nil)
}
Couldn't find a clear and informative explanation for this.
After searching a while on a subject I didn't
find a clear explanation , even in it's class reference
UIAlertController Reference
It is ok, but not clear enough for me.
So after collecting some peaces I decided to make my own explanation
(Hope it helps)
So here it goes:
UIAlertView is deprecated as pointed out :
UIAlertView in Swift
UIAlertController should be used in iOS8+
so to create one first we need to instantiate it,
the Constructor(init) gets 3 parameters:
2.1 title:String -> big-bold text to display on the top of alert's dialog box
2.2 message:String -> smaller text (pretty much explains it's self)
2.3 prefferedStyle:UIAlertControllerStyle -> define the dialog box style, in most cases: UIAlertControllerStyle.Alert
Now to actually show it to the user, we can use showViewController or presentViewController and pass our alert as parameter
To add some interaction with a user we can use:
4.1
UIAlertController.addAction to create buttons
4.2
UIAlertController.addTextField to create text fields
Edit note: code examples below, updated for swift 3 syntax
Example 1: Simple Dialog
#IBAction func alert1(sender: UIButton) {
//simple alert dialog
let alert=UIAlertController(title: "Alert 1", message: "One has won", preferredStyle: UIAlertControllerStyle.alert);
//show it
show(alert, sender: self);
}
Example 2: Dialog with one input textField & two buttons
#IBAction func alert2(sender: UIButton) {
//Dialog with one input textField & two buttons
let alert=UIAlertController(title: "Alert 2", message: "Two will win too", preferredStyle: UIAlertControllerStyle.alert);
//default input textField (no configuration...)
alert.addTextField(configurationHandler: nil);
//no event handler (just close dialog box)
alert.addAction(UIAlertAction(title: "No", style: UIAlertActionStyle.cancel, handler: nil));
//event handler with closure
alert.addAction(UIAlertAction(title: "Yes", style: UIAlertActionStyle.default, handler: {(action:UIAlertAction) in
let fields = alert.textFields!;
print("Yes we can: "+fields[0].text!);
}));
present(alert, animated: true, completion: nil);
}
Example 3: One customized input textField & one button
#IBAction func alert3(sender: UIButton) {
// one input & one button
let alert=UIAlertController(title: "Alert 3", message: "Three will set me free", preferredStyle: UIAlertControllerStyle.alert);
//configured input textField
var field:UITextField?;// operator ? because it's been initialized later
alert.addTextField(configurationHandler:{(input:UITextField)in
input.placeholder="I am displayed, when there is no value ;-)";
input.clearButtonMode=UITextFieldViewMode.whileEditing;
field=input;//assign to outside variable(for later reference)
});
//alert3 yesHandler -> defined in the same scope with alert, and passed as event handler later
func yesHandler(actionTarget: UIAlertAction){
print("YES -> !!");
//print text from 'field' which refer to relevant input now
print(field!.text!);//operator ! because it's Optional here
}
//event handler with predefined function
alert.addAction(UIAlertAction(title: "Yes", style: UIAlertActionStyle.default, handler: yesHandler));
present(alert, animated: true, completion: nil);
}
Hope It helps, and good luck ;-)
An instance of the UIAlertController can be presented modally on screen just as any other UIViewController using the presentViewController:animated:completion: method. What makes the UIAlertController instance differentiate between working as an ActionSheet or as an AlertView is the style parameter you pass when creating it.
No more delegation
If you have used a UIActionSheet or UIAlertView, you know that the way to get a callback from it is for a class (in most cases the ViewController) to implement the UIActionSheetDelegate or UIAlertViewDelegate protocol. There are some open source projects that replaced this delegation pattern with block based callbacks, but the official APIs were never updated. UIAlertController does not use delegation. Instead, it has a collection of UIAlertAction items, that use closures (or blocks if you are using Objective-C) to handle user input.
For Action Sheet
#IBAction func showActionSheet(sender: AnyObject) {
//Create the AlertController
let actionSheetController: UIAlertController = UIAlertController(title: "Action Sheet", message: "Swiftly Now! Choose an option!", preferredStyle: .ActionSheet)
//Create and add the Cancel action
let cancelAction: UIAlertAction = UIAlertAction(title: "Cancel", style: .Cancel) { action -> Void in
//Just dismiss the action sheet
}
actionSheetController.addAction(cancelAction)
//Create and add first option action
let takePictureAction: UIAlertAction = UIAlertAction(title: "Take Picture", style: .Default) { action -> Void in
//Code for launching the camera goes here
}
actionSheetController.addAction(takePictureAction)
//Create and add a second option action
let choosePictureAction: UIAlertAction = UIAlertAction(title: "Choose From Camera Roll", style: .Default) { action -> Void in
//Code for picking from camera roll goes here
}
actionSheetController.addAction(choosePictureAction)
//Present the AlertController
self.presentViewController(actionSheetController, animated: true, completion: nil)
}
For AlertView with Text Field
#IBAction func showAlert(sender: AnyObject) {
//Create the AlertController
let actionSheetController: UIAlertController = UIAlertController(title: "Alert", message: "Swiftly Now! Choose an option!", preferredStyle: .Alert)
//Create and add the Cancel action
let cancelAction: UIAlertAction = UIAlertAction(title: "Cancel", style: .Cancel) { action -> Void in
//Do some stuff
}
actionSheetController.addAction(cancelAction)
//Create and an option action
let nextAction: UIAlertAction = UIAlertAction(title: "Next", style: .Default) { action -> Void in
//Do some other stuff
}
actionSheetController.addAction(nextAction)
//Add a text field
actionSheetController.addTextFieldWithConfigurationHandler { textField -> Void in
//TextField configuration
textField.textColor = UIColor.blueColor()
}
//Present the AlertController
self.presentViewController(actionSheetController, animated: true, completion: nil)
}
Some of the syntax has changed since the original responses. Here is some sample code that alerts the user if they are not signed in to iCloud.
CKContainer.default().accountStatus { (accountStatus, error) in
switch accountStatus {
case .available:
print("iCloud Available")
case .noAccount:
print("No iCloud account")
//simple alert dialog
let alert=UIAlertController(title: "Sign in to iCloud", message: "This application requires iClound. Sign in to your iCloud account to write records. On the Home screen, launch Settings, tap iCloud, and enter your Apple ID. Turn iCloud Drive on. If you don't have an iCloud account, tap Create a new Apple ID", preferredStyle: UIAlertControllerStyle.alert);
//no event handler (just close dialog box)
alert.addAction(UIAlertAction(title: "Cancel", style: UIAlertActionStyle.cancel, handler: nil));
//show it
self.present(alert, animated: true, completion: nil)
case .restricted:
print("iCloud restricted")
case .couldNotDetermine:
print("Unable to determine iCloud status")
}
}