0

I have a code below that will have an UIAlertController with an UITextField that will only accept numbers.

class ViewController: UIViewController, UITextFieldDelegate {

    let priceText = UILabel()
    static let DefaultText = "Click to enter price"
    static let DefaultPlaceHolder = "In dollar"

    override func viewDidLoad() {
        super.viewDidLoad()
        priceText.text = ViewController.DefaultText
        view.addSubview(priceText)
        priceText.translatesAutoresizingMaskIntoConstraints = false
        priceText.backgroundColor = .yellow
        NSLayoutConstraint.activate([
            priceText.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            priceText.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        ])
        priceText.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(openEnterPriceDialog)))
        priceText.isUserInteractionEnabled = true
    }

    @objc private func openEnterPriceDialog() {
        let alert = UIAlertController(
            title: "Enter Price",
            message: "The value you want to sell",
            preferredStyle: UIAlertController.Style.alert)

        alert.addTextField {
            $0.placeholder = ViewController.DefaultPlaceHolder
            $0.keyboardType = .numberPad
            $0.delegate = self
        }
        alert.addAction(UIAlertAction(
            title: "Cancel",
            style: UIAlertAction.Style.default,
            handler: nil))
        alert.addAction(UIAlertAction(
            title: "Submit",
            style: UIAlertAction.Style.default,
            handler: nil }))
        self.present(alert, animated: true, completion: nil)
    }

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        let invalidCharacters = CharacterSet(charactersIn: "0123456789").inverted
        return string.rangeOfCharacter(from: invalidCharacters) == nil
    }
}

extension UITextField {
    open override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
        return action == #selector(UIResponderStandardEditActions.cut) || action == #selector(UIResponderStandardEditActions.copy)
    }
}

However, it is still somehow allow key like 000 or even 01230, which is not a nice number. I would be okay with 0 or 1230, but not something start with 0.

How could I achieve that?

ps: most of stackoverflow answer like How can I declare that a text field can only contain an integer? only check to ensure digit from 0123456789 can be accepted, but still allow things like 00, or 01230.

Iulian Onofrei
  • 9,188
  • 10
  • 67
  • 113
Elye
  • 53,639
  • 54
  • 212
  • 474

3 Answers3

1

Here is how you need to implement textField(_:shouldChangeCharactersIn:replacementString:) method,

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    let invalidCharacters = CharacterSet(charactersIn: "0123456789").inverted
    if (string.rangeOfCharacter(from: invalidCharacters) == nil) {
        if let text = textField.text {
             var str = (text as NSString).replacingCharacters(in: range, with: string)
             if Set(str) == ["0"] {
                 textField.text = "0"
                 return false
             } else if str.first == "0" {
                 str.removeFirst()
                 textField.text = str
                 return false
             }
         }
         return true
    }
    return false
}
Elye
  • 53,639
  • 54
  • 212
  • 474
PGDev
  • 23,751
  • 6
  • 34
  • 88
1

There is no need to override canPerformAction and/or to set your view controller as the text field delegate. You simply addTarget to your text field for UIControl.Event's .editingChanged and filter all non digits in the editingChanged selector method. You can also control the maximum value allowed and format the final price on the fly:


First create your alert controller as a property of your view controller and add all properties that your textField might need as well:

private let alert = UIAlertController(title: "Enter Price",
                              message: "The value you want to sell in dollars.",
                              preferredStyle: .alert)

private var price: Decimal = 0
private var maximum: Decimal = 999_999
private var lastValue: String?

Then customize your text field and add the target for editingChanged events inside viewDidLoad method:

override func viewDidLoad() {
    super.viewDidLoad()

    alert.addTextField { textField in
        textField.addTarget(self, action: #selector(self.editingChanged), for: .editingChanged)
        textField.keyboardType = .numberPad
        textField.textAlignment = .right
        textField.sendActions(for: .editingChanged)
    }
    alert.addAction(.init(title: "Submit", style: .default) { _ in
        print("Price:", Formatter.custom.string(for: self.price)!)
    })
    alert.addAction(.init(title: "Cancel", style: .default))
}

Next you can create your method to deal with the edit changed events as needed:

@objc func editingChanged(textField: UITextField) {
    guard let newValue = textField.text?.decimal, newValue <= maximum else {
        textField.text = lastValue
        return
    }
    price = newValue        
    let text = Formatter.custom.string(for: price)!
    print("Price:", text)
    textField.text = text
    lastValue = text
}

You will need to add a custom NumberFormatter to your view controller file and also a helper to filter the non digits from your string:

private extension Formatter {
    static let custom: NumberFormatter = {
        let formatter = NumberFormatter()
        formatter.locale = Locale(identifier: "en_US")
        formatter.numberStyle = .currency
        formatter.minimumFractionDigits = 0
        formatter.maximumFractionDigits = 0
        return formatter
    }()
}

private extension String {
    var decimal: Decimal { Decimal(string: filter { $0.isWholeNumber }) ?? 0 }
}

Now you can just present your alert when needed:

present(alert, animated: true)
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • Thanks Leo. Upvote this as well. But it is doing more than what the question asked for, beside, it needs the `lastValue` to reset it, which is not as nice. I've did a little more understanding around delegate vs .editingChange, post it up there https://medium.com/@elye.project/differentiate-uitextfield-delegate-and-editingchange-usage-c7abe7439faa. Feel free to let me know if it miss anything important. Thanks. – Elye Mar 01 '20 at 07:17
  • 1
    No problem. I prefer using another object than casting as NSString. I don’t like using shouldChangeCharactersIn either. Just personal preference. As long as it works as you expect that’s fine. – Leo Dabus Mar 01 '20 at 07:38
  • Still learning. Don't know what's the benefit of NSString or other strange ObjC type that is so unfamiliar from the Android world. Once I learn more, I might appreciate it more. Thanks!! – Elye Mar 01 '20 at 07:39
  • 1
    You are welcome. You will realize on the long run that editing changed is much easier to use – Leo Dabus Mar 01 '20 at 07:40
  • 1
    Don’t forget to change `if let text = textField.text {`to `let text = textField.text!` – Leo Dabus Mar 01 '20 at 07:42
  • Could I use `textField.text!` directly instead of storing it in temporary `text`? – Elye Mar 01 '20 at 07:45
  • 1
    Sure no problem at all – Leo Dabus Mar 01 '20 at 07:46
0

Make a slightly shorter version using Decimal to avoid manually extracting the initial 0 from the String

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        let invalidCharacters = CharacterSet(charactersIn: "0123456789").inverted
        if (string.rangeOfCharacter(from: invalidCharacters) == nil) {
            if let text = textField.text {
                let str = (text as NSString).replacingCharacters(in: range, with: string)
                if let number = Decimal(string: str.filter { $0.isWholeNumber }) {
                    if (number <= Decimal(ViewController.maxValue)) { textField.text = "\(number)" }
                    return false
                }
            }
            return true
        }
        return false
    }
Elye
  • 53,639
  • 54
  • 212
  • 474
  • 1
    `textField` text property default value is an empty string. It will never return nil. You can safely force unwrap its value. It is Swift naming convention to name al your variables starting with a lowercase letter and you shouldn't use underscore in Swift. I wonder why don't you use UIControlEvent editing changed instead of shouldChangeCharactersIn method. It would take care handling unwanted characters if pasted by the user. – Leo Dabus Feb 28 '20 at 01:46
  • Thanks mate. I'm new to iOS. Came from Android background. Let me change my answer accordingly. – Elye Mar 01 '20 at 07:06