I have an game app where players count cows on road trip. I have a button that comes up with an alert that allow players to enter however many cows they see. I had an error where if they pressed "add" without entering any cows, it would come up nil and crash. So I set the alert text field to equal "0." However, now if they add, say, 25 cows, the text field would read 025 (it still enters it as only 25 and doesn't cause problems with the math). I would like to clear the text field when players start adding numbers, but I'm not sure how to do that with a text field within an alert. I successfully did it in other areas of the app, so I'm familiar with how it works. Just not this specific instance (if it is in fact even different).
Here's the creation of the alert (I cut out all the math inside as I don't think that's relevant, but let me know if you think it's needed.):
#IBAction func playerOneSawCows(_ sender: UIButton) {
var textField = UITextField()
let alert = UIAlertController(title: "Cows Spotted!", message: "How many cows did you see?", preferredStyle: .alert)
let actionCancel = UIAlertAction(title: "False Alarm", style: .cancel)
let action = UIAlertAction(title: "Add Cows", style: .default) {(action) in
//MARK: - first cows
var totalCows = self.totalCowsOne
var additionalCows = 0
let newCows = textField.text!
let cowsNumber = Int(newCows)
var cowArrayOne: [String] = []
if self.numberOfCowsPlayerOne.text == "0 cows"{
...
}
}
alert.addTextField { (alertTextField) in
alertTextField.placeholder = ""
alertTextField.text = "0"
textField = alertTextField
textField.keyboardType = UIKeyboardType.numberPad
}
alert.addAction(action)
alert.addAction(actionCancel)
present (alert, animated: true, completion: nil)
}
And here's my previous instance of clearing a text field that works correctly:
func textFieldDidBeginEditing(_ textField: UITextField) {
if (textField == playerOneNameText) {
playerOneNameText.text = ""
}
else if (textField == playerTwoNameText) {
playerTwoNameText.text = ""
}else {
print("No players")
}
}
I tried adding another if statement there, but it didn't seem to trigger it. I feel like it's an easy solution, but I could be wrong. Any help would be great.
Related
So my goal is to have the label text always revert back to the original price if the alert view controller is dismissed.
Here is a clip of the issue so you guys can understand it more, it would be too long to explain over text...
So as you can see, when the user cancels the alert prompt, I want everything to revert back as if they never pressed it in the first place. The stepper value reverts, the number label text
reverts, but the event cost does not and I can't figure out why.
Here is the #IBAction function I use with the stepper:
#IBAction func guestsCount(_ sender: UIStepper) {
guestNumberCount.text = String(Int(sender.value))
let totalCost = Decimal(sender.value) * cost
let formatted = totalCost.formattedAmount
var textfield = UITextField()
var textfield2 = UITextField()
actualCostOfEvent.text = "$\(formatted ?? "")"
if(Int(sender.value) > sampleStepperValueForIncrement){
print("increasing")
sampleStepperValueForIncrement += 1
let alert = UIAlertController(title: "Add A Guest", message: "Type full name the of guest you want to add below", preferredStyle: .alert)
let addGuest = UIAlertAction(title: "Add", style: .default) { (add) in
guard let user = Auth.auth().currentUser else { return }
db.document("student_users/\(user.uid)/guestTickets/guestDetails").setData(["guests": FieldValue.arrayUnion([textfield.text ?? "Nil"])], merge: true) { (error) in
if let error = error {
print("There was an error adding the name: \(error)")
} else {
print("name added successfully")
}
}
}
let dismiss = UIAlertAction(title: "Dismiss", style: .cancel) { (cancel) in
self.dismiss(animated: true, completion: nil)
self.guestNumberCount.text = String(Int(sender.value) - 1)
self.stepperValue.value -= 1
}
alert.addTextField { (alerttextfield) in
textfield = alerttextfield
alerttextfield.placeholder = "Guest Name"
alerttextfield.clearButtonMode = .whileEditing
}
alert.addAction(dismiss)
alert.addAction(addGuest)
present(alert, animated: true, completion: nil)
}
else {
print("decreasing")
sampleStepperValueForIncrement = sampleStepperValueForIncrement - 1
let alert = UIAlertController(title: "Remove A Guest", message: "Type the full name of the guest you want to remove. The guest name is case-sensitive and must equal the name you added.", preferredStyle: .alert)
let dismiss = UIAlertAction(title: "Dismiss", style: .cancel) { (cancel) in
self.dismiss(animated: true, completion: nil)
self.guestNumberCount.text = String(Int(sender.value) + 1)
self.stepperValue.value += 1
}
let removeGuest = UIAlertAction(title: "Remove", style: .destructive) { (remove) in
guard let user = Auth.auth().currentUser else { return }
db.document("student_users/\(user.uid)/guestTickets/guestDetails").updateData(["guests": FieldValue.arrayRemove([textfield2.text ?? "Nil"])]) { (error) in
if let error = error {
print("There was an error removing the name: \(error)")
} else {
print("name removed successfully")
}
}
}
alert.addTextField { (alerttextfield2) in
textfield2 = alerttextfield2
alerttextfield2.placeholder = "Guest Name"
alerttextfield2.clearButtonMode = .whileEditing
}
alert.addAction(dismiss)
alert.addAction(removeGuest)
present(alert, animated: true, completion: nil)
}
}
The problems both stem from the dismiss actions in both the if statement and the else statement. I tried possibly adding Decimal(sender.value) - 1 in one dismiss action and then Decimal(sender.value) + 1 in the other, but those didn't make a difference. The exact thing happens when you already have a guest added and you decide to remove, but end up dismissing the alert vc, the price also doesn't revert as well. The odd thing is that if I add all the way to the max value, which is 6 and then remove guests all the way back down to 1 again, the price will revert back to the original price.
Any help would be appreciated, thanks.
You only set actualCostOfEvent.text before the alert controller is displayed.
In the event that it is cancelled, you'll need to set it again, with the recalculated value once you've decremented the stepper again.
I'd suggest moving that code into a helper function so that you aren't repeating it:
func setLabelToFormattedTotalCost(multiplier: Int) {
let totalCost = Decimal(multiplier) * cost
let formatted = totalCost.formattedAmount
actualCostOfEvent.text = "$\(formatted ?? "")"
}
Then, call that with the stepper value (that would be the multiplier parameter) at the beginning of guestsCount and then again once you've decremented the value if the alert is dismissed.
Its because in dismiss you reset stepper value and guest number count but dont update the cost value, so in both dismiss action you can have
let dismiss = UIAlertAction(title: "Dismiss", style: .cancel) { (cancel) in
//all other code of yours and finally
let totalCost = Decimal(self.stepperValue.value) * cost
let formatted = totalCost.formattedAmount
actualCostOfEvent.text = "$\(formatted ?? "")"
}
You can obviously move this to a function and call the function from both dismiss action as
func updateCostValue() {
let totalCost = Decimal(self.stepperValue.value) * cost
let formatted = totalCost.formattedAmount
actualCostOfEvent.text = "$\(formatted ?? "")"
}
let dismiss = UIAlertAction(title: "Dismiss", style: .cancel) { (cancel) in
self.dismiss(animated: true, completion: nil)
self.guestNumberCount.text = String(Int(sender.value) - 1)
self.stepperValue.value -= 1
self.updateCostValue()
}
let dismiss = UIAlertAction(title: "Dismiss", style: .cancel) { (cancel) in
self.dismiss(animated: true, completion: nil)
self.guestNumberCount.text = String(Int(sender.value) + 1)
self.stepperValue.value += 1
self.updateCostValue()
}
So I'm using Swift 5 and working with core data. I'm trying to add validation to the textfields through the UIAlertView.
To give clarification to what my code already does:
User presses a button which the iOS Photo Library Interface Pops up.
Once photo is selected a UIAlertViewpops up and asks me two enter texts in two textfields.
Once that is done, the data is submitted to the database and displays the image and the texts that I entered onto a TableViewCell
The issue?
When submitting a blank text field, there is no prompt to tell the user to enter texts. It will just display the image only in the UITableViewCell without any texts which is the obvious result when inputting no strings.
What do I want to achieve?
To add a validation message to the user when they enter no text and cancel the process of submitting to the database.
What I've already tried?
Please see below for code. Note: Submitting an image and texts to the database already works, my issue is with textfield validation
func createBrandItem (with image:UIImage){
let brandItem = Brand(context: managedObjectContext)
brandItem.image = NSData(data: image.jpegData(compressionQuality: 0.3)!) as Data
let inputAlert = UIAlertController(title: "New Brand", message: "Enter an item and a brand.", preferredStyle: .alert)
inputAlert.addTextField { (textfield:UITextField) in
textfield.placeholder = "Item"
}
inputAlert.addTextField { (textfield:UITextField) in
textfield.placeholder = "Brand"
}
inputAlert.addAction(UIAlertAction(title: "Save", style: .default, handler: { (action:UIAlertAction) in
let itemTextField = inputAlert.textFields?.first
let productTextField = inputAlert.textFields?.last
if (itemTextField?.text!.isEmpty)! || (productTextField?.text!.isEmpty)! {
let alertBlankInput = UIAlertController(title: "Blank Input", message: "Please don't leave the textfields empty.", preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: UIAlertAction.Style.cancel)
alertBlankInput.addAction(okAction)
self.present(alertBlankInput, animated: true, completion: nil)
}
if itemTextField?.text != "" && productTextField?.text != "" {
brandItem.item = itemTextField?.text
brandItem.brand = productTextField?.text
do{
try self.managedObjectContext.save()
self.loadData()
}
catch{
print("Could not save data \(error.localizedDescription)")
}
}
}))
inputAlert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
if self.presentedViewController == nil {
self.present(inputAlert, animated: true, completion: nil)
}
else {
self.dismiss(animated: false, completion: nil)
self.present(inputAlert, animated: true, completion: nil)
}
}
Breaking the code down
The snippet below is what I tried adding to the createBrandItem function. When I add this ifstatement when debugging the app, it uses this conditional statement but also completes the conditional statement where it adds to the database.
if (itemTextField?.text!.isEmpty)! || (productTextField?.text!.isEmpty)! {
let alertBlankInput = UIAlertController(title: "Blank Input", message: "Please don't leave the textfields empty.", preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: UIAlertAction.Style.cancel)
alertBlankInput.addAction(okAction)
self.present(alertBlankInput, animated: true, completion: nil)
}
The code below is the snippet which adds the data to the database.
let brandItem = Brand(context: managedObjectContext)
brandItem.image = NSData(data: image.jpegData(compressionQuality: 0.3)!) as Data
if itemTextField?.text != "" && productTextField?.text != "" {
brandItem.item = itemTextField?.text
brandItem.brand = productTextField?.text
do{
try self.managedObjectContext.save()
self.loadData()
}
catch{
print("Could not save data \(error.localizedDescription)")
}
}
Summary
How would I go if the textfields are empty and stops the process of submitting to the database?
I'd recommend creating your own child view controller which you present as a model dialogue, which would be more elegant and give you far greater control over the process flow. However if you want to continue with an alert controller, you could make it work by creating the OK action in a disabled state:
let okAction = UIAlertAction(title: "OK", style: UIAlertAction.Style.cancel)
okActions.isEnabled = false
inputAlert.addAction(okAction)
and then use textFields' delegates to validate the content of the text fields (you'd need to have a means to coordinate between the two textfields - you could use the alertController's textfields? array to cross-check, or use local variables as flags) and when you're happy with the content of both
inputAlert.actions.filter({$0.title == "OK" }).first?.isEnabled = true
I am working on a checklist app. If the user wants to add a new point on his checklist, he can push the "Add" button and an alert will appear with a UITextField to put his new task in.
alert.addAction(UIAlertAction(title: "Add", style: .default, handler: { (action) in
let textf = alert.textFields![0] as UITextField
if(textf.text != "")
{
checklist.append(textf.text!.capitalized)
}
self.myTableView.reloadData()
}))
self.present(alert, animated: true, completion: nil)
}
Doing it like this will capitalize the words after hitting "Add". But I want that only the first word to be capitalized. Also, I want it to be on the keyboard when it appears so that the first word is capitalized right away. How can I do that?
let alert = UIAlertController(title: "Add new task", message: nil, preferredStyle: .alert)
alert.addTextField { (alertInputTextField) in
}
Use the autocapitalizationType property of the textfield and set it to sentences when you add it...
alert.addTextField { textField in
textField.text = "Some default text"
textField.autocapitalizationType = .sentences
}
Always refer to the documentation, it explains this quite well.
If you need to do this to a String directly just uppercase the first letter of the String.
You can use any of the below function:-
public mutating func capitalizeFirst() {
guard self.count > 0 else { return }
self.replaceSubrange(startIndex...startIndex, with: String(self[startIndex]).capitalized)
}
or
public func capitalizedFirst() -> String {
guard self.count > 0 else { return self }
var result = self
result.replaceSubrange(startIndex...startIndex, with: String(self[startIndex]).capitalized)
return result
}
I encountered a strange issue and maybe it is only a lack of knowledge about Swift 3.0 / iOS 10 so hopefully you can guide me into the right direction or explain to me what I'm doing wrong.
HOW IT CURRENTLY WORKS
I am trying to create a UIAlertController with style .alert so I can get a user text input for my app. My requirements are that the text must not be empty and if there was a text there before, it must be different.
I can use the following code to achieve what I want:
//This function gets called by a UIAlertController of style .actionSheet
func renameDevice(action: UIAlertAction) {
//The AlertController
let alertController = UIAlertController(title: "Enter Name",
message: "Please enter the new name for this device.",
preferredStyle: .alert)
//The cancel button
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
//The confirm button. Make sure to deactivate on first start
let confirmAction = UIAlertAction(title: "Ok", style: .default, handler: { action in
self.renameDevice(newName: alertController.textFields?.first?.text)
})
//Configure the user input UITextField
alertController.addTextField { textField in
log.debug("Setting up AlertDialog target")
textField.placeholder = "Enter Name"
textField.text = self.device.getName()
textField.addTarget(self, action: #selector(self.textFieldDidChange(_:)), for: .editingChanged)
}
//Disable the OK button so that the user first has to change the text
confirmAction.isEnabled = false
self.confirmAction = confirmAction
//Add the actions to the AlertController
alertController.addAction(cancelAction)
alertController.addAction(confirmAction)
present(alertController, animated: true, completion: nil)
}
var confirmAction: UIAlertAction?
func textFieldDidChange(_ textField: UITextField){
log.debug("IT CHAGNED!=!=!=!=!")
if let text = textField.text {
if !text.isEmpty && text != self.device.getName() {
confirmAction?.isEnabled = true
return
}
}
confirmAction?.isEnabled = false
}
//Finally this code gets executed if the OK button was pressed
func renameDevice(newName: String?){ ... }
HOW I WANT IT TO WORK
So far so good but I'm going to ask the user for a text input at various places so I want to use a utility class to handle all this stuff for me. The final call shall look like this:
func renameDevice(action: UIAlertAction) {
MyPopUp().presentTextDialog(title: "Enter Name",
message: "Please enter the new name for this device.",
placeholder: "New Name",
previousText: self.device.getName(),
confirmButton: "Rename",
cancelButton: "Cancel",
viewController: self){ input: String in
//do something with the input, e. g. call self.renameDevice(newName: input)
}
WHAT I CAME UP WITH
So I implemented everything in this little class:
class MyPopUp: NSObject {
var confirmAction: UIAlertAction!
var previousText: String?
var textField: UITextField?
func presentTextDialog(title: String, message: String?, placeholder: String?, previousText: String?, confirmButton: String, cancelButton: String, viewController: UIViewController, handler: ((String?) -> Swift.Void)? = nil) {
//The AlertController
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
//The cancel button
let cancelAction = UIAlertAction(title: cancelButton, style: .cancel)
//The confirm button. Make sure to deactivate on first start
confirmAction = UIAlertAction(title: confirmButton, style: .default, handler: { action in
handler?(alertController.textFields?.first?.text)
})
//Configure the user input UITextField
alertController.addTextField { textField in
log.debug("Setting up AlertDialog target")
self.textField = textField
}
//Set placeholder if necessary
if let placeholder = placeholder {
self.textField?.placeholder = placeholder
}
//Set original text if necessary
if let previousText = previousText {
self.textField?.text = previousText
}
//Set the target for our textfield
self.textField?.addTarget(self, action: #selector(textChanged), for: .editingChanged)
log.debug("It appears that our textfield \(self.textField) has targets: \(self.textField?.allTargets)")
//Store the original text for a later comparison with the new entered text
self.previousText = previousText
//Disable the OK button so that the user first has to change the text
confirmAction.isEnabled = false
//Add the actions to the AlertController
alertController.addAction(cancelAction)
alertController.addAction(confirmAction)
viewController.present(alertController, animated: true, completion: nil)
}
func textChanged() {
if let text = textField?.text {
if !text.isEmpty && text != previousText {
confirmAction.isEnabled = true
return
}
}
confirmAction.isEnabled = false
}
}
THE PROBLEM
My problem is that no matter where I try to set the target for the UITextField of the UIAlertController, it never executes my target. I tried setting the TextFields delegate in alertController.addTextField{} as well as setting the target there. The issue which confuses me the most is that setting the placeholder and original text works just fine but delegate or target functions are never called. Why does the same code works when executed in a UIViewController but does not work when executed in a utility class?
THE SOLUTION (UPDATE)
Apparently I made a mistake. In my viewcontroller, I create an instance of MyPopUp and call the present() function on it.
MyPopUp().presentTextDialog(title: "Enter Name",
message: "Please enter the new name for this device.",
placeholder: "New Name",
previousText: self.device.getName(),
confirmButton: "Rename",
cancelButton: "Cancel",
viewController: self)
In the presentTextDialog() I thought setting the current instance of MyPopUp as the delegate/target would be enough but it seems that the MyPopUp instance is released immediately and therefore never called. My very simple workaround is to create the MyPopUp instance in an instance variable and call the present method whenever I need to.
let popup = MyPopUp()
func renameDevice(action: UIAlertAction) {
popup.presentTextDialog(...){ userinput in
renameDevice(newName: userinput)
}
}
Okay so here's exactly what I did wrong.
I created a utility class which I had to instantiate
The class itself was basically empty and it's only purpose was to be the target or delegate of the UITextField
I instantiated the class and immediately called the presentation function without keeping a reference around
By not keeping a reference to my instance, the object must have gotten released immediately after presenting the UIAlertController in my viewcontroller.
Solution: Just keep a reference around in your viewcontroller. Of course a local variable won't do. I store the reference in an instance variable of my viewcontroller but this doesn't feel very "swifty". I'm still a beginner in swift and maybe my mind is "damaged" by other languages (java, c#). I will probably start by making my utility class a singleton or creating an extension for UIViewController to present the alert. If you have other good ideas feel free to teach them to me :)
Instead of presenting dialogue in NSObject class. You must use delegates and protocol to present an alert. Replace your code with this. In View Controller we have a function named renameDevice. You can present alert here.
MyPopUp.swift
import UIKit
class MyPopUp: NSObject {
var confirmAction: UIAlertAction!
var previousText: String?
var textField: UITextField?
var delegate : MyPopUpDelegate!
func textChanged() {
if let text = textField?.text {
if !text.isEmpty && text != previousText {
confirmAction.isEnabled = true
return
}
}
confirmAction.isEnabled = false
}
}
protocol MyPopUpDelegate {
func renameDevice(action: UIAlertAction)
}
ViewController.swift
import UIKit
class ViewController: UIViewController,MyPopUpDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func renameDevice(action: UIAlertAction) {
// Add dialogue that you wnat to create.
}
}
In your MyPopUp class first you need to conform to UITextFieldDelegate method like this
class MyPopUp:NSObject,UITextFieldDelegate {
then while adding your UITextField to alert you need to set delegate to that UITextField like this
alertController.addTextField { textField in
log.debug("Setting up AlertDialog target")
textField.delegate = self
self.textField = textField
}
then you need to implement UITextField Delegate method to get the change in your UITextField like this
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
This will solve your problem.Also check this
I have programmatically added UITextView's and a class object that gets some part of it changed upon calling:
textViewShouldEndEditing(textView: UITextView)
I handle saving the object fields for the user as a batch save, but the problem is that the last edited UITextView value doesn't ever make it to the save, even though the textViewShouldEndEditing function gets called. The only other complication is that the batch saving is part of a confirmation message via UIAlertAction.
To clarify, the class object that the UIAlertAction accesses appears to be out of date even though all functions are called in the correct order. I hope this is clear.
var tmpGuest:VIG = VIG()
func textViewShouldEndEditing(textView: UITextView) -> Bool {
let saveTxt:String = (textView.text == "--" ? "" : textView.text)
//SET SOME tmpGuest VALUES HERE
return true
}
#IBAction func editInfo(sender: AnyObject) {
let actionSheetController: UIAlertController = UIAlertController(title: "Are you sure you wish to save?", message: "Changes are permanent.", preferredStyle: .Alert)
let okAction: UIAlertAction = UIAlertAction(title: "Yes", style: .Default) { action -> Void in
//SAVE
self.setGuest.setRecords(guestMainTmp: self.tmpGuest)
}
actionSheetController.addAction(okAction)
self.presentViewController(actionSheetController, animated: true, completion: nil)
}