Apple's iOS UX guidelines show the following example:
...where:
When the most likely button performs a nondestructive action, it
should be on the right in a two-button alert. The button that cancels
this action should be on the left.
When the most likely button performs a destructive action, it should
be on the left in a two-button alert. The button that cancels this
action should be on the right.
Source: https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/Alerts.html
However, it does not seem possible to implement this guidance with the current iOS APIs (testing on iOS 9 with Swift). Here's one attempt. But I've tried varying the order of adding the alert actions, and changing which action is .Default vs. .Cancel style. No combination appears to work.
#IBAction func showAlertAction(sender: AnyObject)
{
let title = "An Alert that Offers Two Alternatives Is Easy for People to Use"
let alertViewController = UIAlertController(title: title, message: nil, preferredStyle: UIAlertControllerStyle.Alert)
let cancelAction = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Cancel, handler: { (alert :UIAlertAction!) -> Void in
print("'Cancel' tapped")
})
alertViewController.addAction(cancelAction)
let safeChoiceAction = UIAlertAction(title: "Safe Choice", style: UIAlertActionStyle.Default, handler: { (alert :UIAlertAction!) -> Void in
print("'Safe Choice' tapped")
})
alertViewController.addAction(safeChoiceAction)
self.presentViewController(alertViewController, animated: true, completion: nil)
}
Result:
Related
I want to make a "rate app" alert, but for some reason it gets deallocated before showing it.
Here's the code:
func showAlert() {
if #available(iOS 8.0, *)
{
let alertController = UIAlertController(title: "Rate App", message: "Rate this app now", preferredStyle: .Alert)
let neverAction = UIAlertAction(title: "Never Show This Again", style: .Destructive, handler: { (action: UIAlertAction) in
self.userDefaults.setBool(true, forKey: "rateAlertRejected")
})
let rateAction = UIAlertAction(title: "Rate Now", style: .Default, handler: { (action: UIAlertAction) in
// Rate App
})
let remindAction = UIAlertAction(title: "Remind Me Later", style: .Cancel, handler: nil)
alertController.addAction(rateAction)
alertController.addAction(neverAction)
alertController.addAction(remindAction)
presentViewController(alertController, animated: true, completion: nil)
}
else
{
// Identical code (using UIAlertView) for iOS 7 which works perfectly
}
}
The method executes (in certain conditions, but for testing purposes it does it every time) after a custom unwind segue.
Why do I have this problem? I used UIAlertController before but I had no issues.
According to your comment you show showAlert in
a method that executes after a custom unwind segue.
unwind segue dismisses the view heriarchy and therefore your alert
does not get reference to a view controller to show from.
To solve this, show your alert in the View controller you unwind to or wait for the alert controller action to be completed before unwinding.
I'm trying to show a UIAlertController in my ViewController in a function that's been called via an NSNotification. However I'm getting the error:
Attempt to present <UIAlertController: 0x7fe013d05d40> on <submarine.ViewController: 0x7fe011f20370> whose view is not in the window hierarchy!
The NSNotification is posted from a completion block (callback I guess) from something else in my UI. Because it's a callback it's failing to display. Hence I thought I'd try NSNotificationCentre to get around the problem without using the rootViewController to display the alert.
My code is:
override func viewDidAppear(animated: Bool) {
// Handle onboarding
if needsOnboarding() {
handleOnboarding() // This create the completion block that posts the NSNotification
}
NSNotificationCenter.defaultCenter().addObserver(self, selector: "showTermsAlert:", name:"showTermsAlert", object: nil)
}
func showTermsAlert(notification: NSNotification) {
let termsAlert:UIAlertController = UIAlertController(title: "Terms And Conditions", message: "Please view the terms below before accepting them.", preferredStyle: UIAlertControllerStyle.Alert)
termsAlert.addAction(UIAlertAction(title: "View Terms", style: .Default, handler: { (action: UIAlertAction!) in
UIApplication.sharedApplication().openURL(NSURL(string: "my_terms_url")!)
}))
termsAlert.addAction(UIAlertAction(title: "I Agree to the Terms", style: .Default, handler: { (action: UIAlertAction!) in
self.onboardingFinished()
}))
self.presentViewController(termsAlert, animated: true, completion: nil)
}
Has anyone got an idea why this is happening? I don't see why it's not in the window hierarchy - it's being presented from the self viewController and is created in a top-level function inside the VC.
Thanks!
EDIT: original code inside the handleOnboarding():
Library used: Onboard
func handleOnboarding() {
let secondPage = OnboardingContentViewController(title: "What's going on?", body: "Submarine routes your data through our network, around any filters and restrictions, giving you unrestricted and unmonitored internet access.", image: UIImage(named: "back"), buttonText: "Next") { () -> Void in
// do something here when users press the button, like ask for location services permissions, register for push notifications, connect to social media, or finish the onboarding process
}
secondPage.movesToNextViewController = true
let thirdPage = OnboardingContentViewController(title: "Terms of Use", body: "You must agree to our Terms of Use to use Submarine.\nIf you don't, please close Submarine.", image: UIImage(named: "back"), buttonText: "View Terms") { () -> Void in
let termsAlert:UIAlertController = UIAlertController(title: "Terms And Conditions", message: "Please view the terms below before accepting them.", preferredStyle: UIAlertControllerStyle.Alert)
termsAlert.addAction(UIAlertAction(title: "View Terms", style: .Default, handler: { (action: UIAlertAction!) in
UIApplication.sharedApplication().openURL(NSURL(string: "my_policy_url")!)
}))
termsAlert.addAction(UIAlertAction(title: "I Agree to the Terms", style: .Default, handler: { (action: UIAlertAction!) in
self.onboardingFinished()
}))
self.presentViewController(termsAlert, animated: true, completion: nil)
// NSNotificationCenter.defaultCenter().postNotificationName("showTermsAlert", object: nil)
}
// Image
let onboardingVC = OnboardingViewController(backgroundImage: UIImage(named: "back"), contents: [secondPage, thirdPage])
self.navigationController?.presentViewController(onboardingVC, animated: false, completion: nil)
}
This happen when the presenting view controller is no longer part of the controller hierarchy, and it's view is no longer in the view hierarchy of any window. Most likely, the controller was dismissed or popped, but it heard the notification and attempted to present the alert controller.
You should manage your controller states more carefully. Perhaps remove observer when the controller is dismissed or popped from your controller hierarchy.
There are a few things i'd change in your code. Add a call to super in viewDidAppear:, and stop using the NSNotifications for your presentation. You don't know what thread showTermsAlert will get called on with this pattern. You can make your intent more explicit by calling showTermsAlert directly, and this will also guarantee you're on the main thread.
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// Handle onboarding
if needsOnboarding() {
self.showTermsAlert()
}
}
func showTermsAlert() {
let termsAlert:UIAlertController = UIAlertController(title: "Terms And Conditions", message: "Please view the terms below before accepting them.", preferredStyle: UIAlertControllerStyle.Alert)
termsAlert.addAction(UIAlertAction(title: "View Terms", style: .Default, handler: { (action: UIAlertAction!) in
UIApplication.sharedApplication().openURL(NSURL(string: "my_terms_url")!)
}))
termsAlert.addAction(UIAlertAction(title: "I Agree to the Terms", style: .Default, handler: { (action: UIAlertAction!) in
self.onboardingFinished()
}))
self.presentViewController(termsAlert, animated: true, completion: nil)
}
Edit: Thank you for the replies, yes I needed an alert and not an action sheet! I have implemented this new code and it works, but is there a reason why it segues to the next view before the user can enter a ride title?
Also it throws this message in the debug console, should I be concerned?
2016-02-16 12:30:21.675 CartoBike[687:128666] Presenting view controllers on detached view controllers is discouraged .
#IBAction func stopAction(sender: UIButton) {
let alert = UIAlertController(title: "Ride Stopped", message: "Give a title to your ride", preferredStyle: .Alert)
let saveAction = UIAlertAction(title: "Save", style: .Default,
handler: { (action:UIAlertAction) -> Void in
// Allow for text to be added and appended into the RideTableViewController
let textField = alert.textFields!.first
rideContent.append(textField!.text!)
})
let cancelAction = UIAlertAction(title: "Cancel",
style: .Default) { (action: UIAlertAction) -> Void in
}
alert.addTextFieldWithConfigurationHandler {
(textField: UITextField) -> Void in
}
alert.addAction(saveAction)
alert.addAction(cancelAction)
// Save the ride
saveRide()
// Automatically segue to the Ride details page
self.performSegueWithIdentifier("ShowRideDetail", sender: nil)
presentViewController(alert, animated: true, completion: nil)
timer.invalidate()
self.stopLocation()
}
Overview:
I am working my way through my first real app. The basic logic is a home screen to start a new ride or view previous rides. Starting a ride will open a new view with a map to record a bicycle ride based on the users location, this can be stopped and save the ride and immediately switch to a new view to see a map of the ride. Alternatively from the home screen the user can select previous rides and view a list of their old rides in a table view and select one and transition to a detailed view with a map of their ride.
Problem: When adding a UIAlertAction I would like there to be a save and cancel feature. In addition, I would like the user to be able to add a custom title by typing it in via a text field. The input from the text field will be appended to a global variable called rideContent that is tied to the creation of new cells in the table view to store multiple bike rides by unique title.
Research:
I have reviewed the questions titled "How to add a TextField to UIAlertView in Swift" & "Writing handler for UIAlertAction" and still can't seem to discern what I have done wrong. Ideally the alert within this screenshot of an app from the raywenderlich site is what I would like it to look like. I am not sure if what I am trying to do is even possible since there a so many view controllers involved. Granted I am new to swift and I'm sure I am missing something obvious!
Currently getting an error of
"Type of expression is ambiguous without more context", see screenshot: here
Here is the UIAlertController code:
// The timer pauses & the location stops updating when the the stop button is pressed
#IBAction func stopAction(sender: UIButton) {
var inputTextField: UITextField?
let actionSheetController = UIAlertController (title: "Ride Stopped", message: "Add a title to your ride", preferredStyle: UIAlertControllerStyle.ActionSheet)
// Add a cancel action
actionSheetController.addAction(UIAlertAction(title: "Cancel", style: UIAlertActionStyle.Cancel, handler: nil))
// Add a save action
actionSheetController.addAction(UIAlertAction(title: "Save", style: UIAlertActionStyle.Default, handler: {
(actionSheetController) -> Void in
//Add a text field --- Getting an error on this line
actionSheetController.addTextFieldWithConfigurationHandler { textField -> Void in
// you can use this text field
inputTextField = textField
// Append the ride title to the table view
rideContent.append(textField!.text!)
// Update when user saves a new ride to store the ride in the table view for permanent storage
NSUserDefaults.standardUserDefaults().setObject(rideContent, forKey: "rideContent")
// Save the ride
self.saveRide()
// Automatically segue to the Ride details page
self.performSegueWithIdentifier("ShowRideDetail", sender: nil)
}}))
//present actionSheetController
presentViewController(actionSheetController, animated: true, completion: nil)
timer.invalidate()
self.stopLocation()
}
Thank you stack overflow for any help you may offer!
You're using an .ActionSheet while the tutorial you showed is using an .Alert
Action Sheets can have buttons but not text fields.
"Alerts can have both buttons and text fields, while action sheets only support buttons."
NSHipster
Use your tableview Array when Add Button is pressed and "Add alertview textfirld to tableview array and then reload tableview".
#IBAction func btnAdd(_ sender: Any) {
let alertController = UIAlertController(title: "Add Category", message: "", preferredStyle: .alert)
let saveAction = UIAlertAction(title: "Add", style: .default, handler: { alert -> Void in
let firstTextField = alertController.textFields![0] as UITextField
self.categories.add(firstTextField.text!)
self.tblCatetgory.reloadData()
})
let cancelAction = UIAlertAction(title: "Cancel", style: .default, handler: {
(action : UIAlertAction!) -> Void in })
alertController.addTextField { (textField : UITextField!) -> Void in
textField.placeholder = "Enter Category!!!"
}
alertController.addAction(saveAction)
alertController.addAction(cancelAction)
self.present(alertController, animated: true, completion: nil)
}
}
I have a button that causes a UIAlertController to appear. I want to show a couple choices to the user and allow them to select a subset of the options.
[A +]
[B ]
[C +]
[D ]
[----]
[DONE]
I need to give an array to fill the options and then let the users select their subset (+ symbols in this example -> likely more common to use check marks). When they click done then it would let the code know their selection. Here is my current code from the button segue.
#IBAction func insideButton(sender: AnyObject) {
var refreshAlert = UIAlertController(title: "Refresh", message: "All data will be lost.", preferredStyle: UIAlertControllerStyle.Alert)
refreshAlert.addAction(UIAlertAction(title: "Ok", style: .Default, handler: { (action: UIAlertAction!) in
print("Handle Ok logic here")
}))
refreshAlert.addAction(UIAlertAction(title: "Cancel", style: .Default, handler: { (action: UIAlertAction!) in
print("Handle Cancel Logic here")
}))
presentViewController(refreshAlert, animated: true, completion: nil)
}
How would I do this?
From the UIAlertController docs: "The UIAlertController class is intended to be used as-is and does not support subclassing. The view hierarchy for this class is private and must not be modified."
You'll have to make your own custom alert and present it.
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")
}
}