I have built a game using SpriteKit and I have it fully functional except for getting the user to input their name to store with their high scores. I'm using NSUserDefaults to store the scores as an array of their top 5 scores. I'd like to store their name as well because eventually I plan to move the storage to a server instead of NSUserDefaults to allow for players to compete for high scores.
My thought was to present a UIAlertController when the game first runs on the device, to grab their name via a text input field, then store it in NSUserDefaults. But no matter where I put the code for UIAlertController (GameScene.swift, GameViewController.swift, and even AppDelegate.swift) it doesn't pop up.
The code I'm using for the alert is:
let ac = UIAlertController(title: "Enter Name", message: nil, preferredStyle: .Alert)
ac.addTextFieldWithConfigurationHandler(nil)
ac.addAction(UIAlertAction(title: "OK", style: .Default) { [unowned self, ac] _ in
let playerName = ac.textFields![0]
})
ac.presentViewController(ac, animated: true, completion: nil)
This is the UPDATED CODE based on comments below, including the entire viewDidLoad function:
override func viewDidLoad() {
super.viewDidLoad()
let ac = UIAlertController(title: "Enter Name", message: nil, preferredStyle: .Alert)
ac.addTextFieldWithConfigurationHandler(nil)
ac.addAction(UIAlertAction(title: "OK", style: .Default) { [unowned self, ac] _ in
let playerName = ac.textFields![0]
})
self.presentViewController(ac, animated: true, completion: nil)
if let scene = GameScene(fileNamed:"GameScene") {
// Configure the view.
let skView = self.view as! SKView
skView.showsFPS = false
skView.showsNodeCount = false
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.scaleMode = .AspectFill
skView.presentScene(scene)
currentGame = scene
scene.viewController = self
}
}
Presenting a view controller won't generally work inside of viewDidLoadsince it is called after the view is loaded into memory, but before it is presented on the screen (see the view controller lifecycle here). You can set things up there (like your scenes) but any animations or interactive elements must be done later.
A safer place to put this would be in viewDidAppear (along with some logic to make sure it doesn't get repeatedly presented) or in response to a tap on a button.
Also, you have to call presentViewController from a view controller that is already on the screen for it to be shown. So, if you have this code somewhere in your GameViewController, you could change
ac.presentViewController(ac, animated: true, completion: nil)
to
self.presentViewController(ac, animated: true, completion: nil)
//1. Create the alert controller.
var alert = UIAlertController(title: "Some Title", message: "Enter a text", preferredStyle: .Alert)
//2. Add the text field. You can configure it however you need.
alert.addTextFieldWithConfigurationHandler({ (textField) -> Void in
textField.text = "Some default text."
})
//3. Grab the value from the text field, and print it when the user clicks OK.
alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: { (action) -> Void in
let textField = alert.textFields![0] as UITextField
println("Text field: \(textField.text)")
}))
// 4. Present the alert.
self.presentViewController(alert, animated: true, completion: nil)
is that's what you except?
Related
I need help presenting an alert view in the game scene. Im currently struggling to do so as GameScene.Swift isnt a standard ViewController. If it helps I need to do so as I need the user to input a value which is used as a coordinate for the ball Sprite Kit Node in my game. The input is only a standard integer so that isnt an issue. Any other idea of how I can do this which isnt through an alert view is also welcome.
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
let view = self.view as! SKView
if view.scene == nil {
view.showsFPS = false
view.showsNodeCount = false
let gameScene = GameScene(size: view.bounds.size)
gameScene.scaleMode = SKSceneScaleMode.AspectFill
view.presentScene(gameScene)
}
}
That is in the GameViewController file
var vc : GameViewController!
override init(size: CGSize) {
super.init(size: size)
let alertController = UIAlertController(title: "Bll Starting Position", message: "Please Enter a X Coordinate Value IN Range 0 to 345 ", preferredStyle: .Alert)
alertController.addTextFieldWithConfigurationHandler { (textField) in
textField.placeholder = "Value Must Be In Range 0 To 345"
textField.autocapitalizationType = UITextAutocapitalizationType.None
textField.autocorrectionType = UITextAutocorrectionType.No
textField.clearsOnBeginEditing = true
textField.clearsOnInsertion = true
textField.clearButtonMode = UITextFieldViewMode.Always
let cancelBtn = UIAlertAction(title: "Cancel", style: .Cancel, handler: nil)
let confirmBtn = UIAlertAction(title: "Confirm", style: .Default, handler: { (confirmView) in
if let field = alertController.textFields![0] as? UITextField {
}
})
alertController.addAction(confirmBtn)
alertController.addAction(cancelBtn)
self.vc.presentViewController(alertController, animated: true, completion: nil)
}
Thanks
You can show UIAlertControllers directly from SKScenes, simply show them on the rootViewController, which is probably the best place to show them anyway.
view?.window?.rootViewController?.present...
In general its not the best practice to reference the GameViewController in SKScenes and I never actually got to a point where I was forced to do so. NSNotificationCenter, delegation or protocol extensions are the better way.
I actually use a helper for Alerts I made using Swift 2's protocol extensions.
Just make a new .swift file and add this code
import SpriteKit
protocol Alertable { }
extension Alertable where Self: SKScene {
func showAlert(withTitle title: String, message: String) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .cancel) { _ in }
alertController.addAction(okAction)
view?.window?.rootViewController?.present(alertController, animated: true)
}
func showAlertWithSettings(withTitle title: String, message: String) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .cancel) { _ in }
alertController.addAction(okAction)
let settingsAction = UIAlertAction(title: "Settings", style: .default) { _ in
guard let url = URL(string: UIApplicationOpenSettingsURLString) else { return }
if #available(iOS 10.0, *) {
UIApplication.shared.open(url)
} else {
UIApplication.shared.openURL(url)
}
}
alertController.addAction(settingsAction)
view?.window?.rootViewController?.present(alertController, animated: true)
}
}
Now in your scenes you need to show alerts you simply conform to the protocol
class GameScene: SKScene, Alertable {
}
and call the methods like
showAlert(withTitle: "Alert title", message: "Alert message")
as if they are part of the scene itself.
Hope this helps
There may be the following options:
1) Quick solution. Do not use UIAlertController, use UIAlertView. Like that:
alert.show()
However, UIAlertView is deprecated so it's not quite safe to rely on it.
2) A better solution. Make your SKScene subclass hold a reference to the view controller which you use to present the scene and when you create the scene assign it the view controller:
myScene.viewController = self
And then you can use it.
self.viewController.presentViewController(alertController, animated: true, completion: nil)
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.”
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 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)
}
I have been trying to integrate a Pinboard bookmarks view (by parsing an RSS Feed and displaying it in a TableView) in my browser app. To get the username and API Token for the feed I have a UIAlertController in the Settings view of my app. The details entered are preserved through the session but if I force quit the app from the multitasking view, The details are deleted. How can I make them stay?
This is the code I'm using for the UIAlertController:
#IBAction func pinboardUserDetailsRequestAlert(sender: AnyObject) {
//Create the AlertController
var pinboardUsernameField :UITextField?
var pinboardAPITokenField :UITextField?
let pinboardUserDetailsSheetController: UIAlertController = UIAlertController(title: "Pinboard Details", message: "Please enter your Pinboard Username and API Token to access your bookmarks", preferredStyle: .Alert)
//Add a text field
pinboardUserDetailsSheetController.addTextFieldWithConfigurationHandler({(usernameField: UITextField!) in
usernameField.placeholder = "Username"
var parent = self.presentingViewController as! ViewController
usernameField.text = parent.pinboardUsername
pinboardUsernameField = usernameField
})
pinboardUserDetailsSheetController.addTextFieldWithConfigurationHandler({(apiTokenField: UITextField!) in
apiTokenField.placeholder = "API Token"
var parent = self.presentingViewController as! ViewController
apiTokenField.text = parent.pinboardAPIToken
pinboardAPITokenField = apiTokenField
})
pinboardUserDetailsSheetController.addAction(UIAlertAction(title: "Cancel", style: .Cancel, handler: nil))
pinboardUserDetailsSheetController.addAction(UIAlertAction(title: "Done", style: .Default, handler: { (action) -> Void in
// Now do whatever you want with inputTextField (remember to unwrap the optional)
var parent = self.presentingViewController as! ViewController
parent.pinboardAPIToken = pinboardAPITokenField?.text
parent.pinboardUsername = pinboardUsernameField?.text
}))
//Present the AlertController
self.presentViewController(pinboardUserDetailsSheetController, animated: true, completion: nil)
}
This question has been answered by Portland Runner in the comments to the question. The solution that worked was to save the text using NSUserDefaults.
Thanks Portland Runner! :)