2

I am writing an application which needs inputs of volumes and prices, I want to limit the volume to a 4 digit number, and the price to a 4 digit number with a decimal in place between the 4 digits.

How do I go about encoding this into my project?

I have searched multiple pages and found this: (http://www.globalnerdy.com/2016/05/24/a-better-way-to-program-ios-text-fields-that-have-maximum-lengths-and-accept-or-reject-specific-characters/) I still keep getting an error towards the end of my code for the TextFieldMaxLength.Swift file.

@objc func limitLength(textField: UITextField) {
    guard let prospectiveText = textField.text, prospectiveText.count > maxLength else {
            return
    }

    // If the change in the text field's contents will exceed its maximum length,
    // allow only the first [maxLength] characters of the resulting text.
    let selection = selectedTextRange
    text = prospectiveText.substringWith(
        Range<String.Index>(prospectiveText.startIndex ..< prospectiveText(maxLength))
    )
    selectedTextRange = selection
}

I am expecting to be able to limit the individual textFields to different numbers. In the interface building there is a section that can be entered to limit this. But this error keeps popping up: "Cannot call value of non-function type String", in the section which depicts:

text = prospectiveText.substringWith(
            Range<String.Index>(prospectiveText.startIndex ..< prospectiveText(maxLength))
        )
birdy
  • 943
  • 13
  • 25
Cubespike
  • 23
  • 3

1 Answers1

1

What you should do is implement the UITextFieldDelegate method textField(_:shouldChangeCharactersIn:replacementString:).

This delegate method is called before the text updates within the UITextField. Within it, you can check the length of the current string, and return false if the current length is 4 characters and the added character isn't a backspace, and true otherwise. Similarly, you can change the textField's text and return false, effectively processing the updating character, and returning the appropriate string.

I've created a github repository that I used to test this code here: Github Repository

First, be sure that you set the Keyboard Type of both UITextFields to Number Pad in the interface builder.

Next, add the following extension to the string class.

extension String {
    func stringByRemovingAll(characters: [Character]) -> String {
        var stringToReturn = self
        for character in characters {
            stringToReturn = stringToReturn.replacingOccurrences(of: String(character), with: "")
        }
        return stringToReturn
    }

    subscript (i: Int) -> Character {
        return self[index(startIndex, offsetBy: i)]
    }
}

And now, actually addressing the issue:

import UIKit

class ViewController: UIViewController {

    @IBOutlet var volumeTextField: UITextField!
    @IBOutlet var priceTextField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()
        volumeTextField.delegate = self
        priceTextField.delegate = self
    }
}


extension ViewController: UITextFieldDelegate {
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {

        //An empty string, i.e. "", is the passed text when the user enters a backspace
        if textField == volumeTextField {

            if textField.text!.count == 4 && string != "" {
                return false
            } else {
                return true
            }
        } else {

            let currentText = textField.text!.stringByRemovingAll(characters: ["$","."])
            print(currentText)

            var newText: String!
            if string == "" {
                newText = String(currentText.dropLast())
            } else if currentText.count == 0 {
                newText = string
            } else {
                newText = "\(currentText)\(string)"
            }


            while newText.count != 0 && newText[0] == "0" {
                newText.remove(at: newText.startIndex)
            }

            switch newText.count {

            case 0:
                textField.text = "$00.00"
            case 1:
                textField.text = "$00.0\(newText!)"

            case 2:
                textField.text = "$00.\(newText[0])\(newText[1])"

            case 3:
                textField.text = "$0\(newText[0]).\(newText[1])\(newText[2])"

            case 4:
                textField.text = "$\(newText[0])\(newText[1]).\(newText[2])\(newText[3])"

            default:
                break
            }
            return false
        }
    }
}

Within this delegate method, you can also put a check on the character passed, and return false if it isn't one of the few characters you want allowed.

End result:

enter image description here

Reference: Apple docs textField(_:shouldChangeCharactersIn:replacementString:) reference

David Chopin
  • 2,780
  • 2
  • 19
  • 40
  • Apologies, the original answer addressed this as dealing with a `UITextView` and not a `UITextField`. I've made changes to correct this. – David Chopin Sep 09 '19 at 21:25
  • Sorry but this code is incorrect. It's looking only at the current length of the text in the text field. It needs to base the decision on what the new length would become from this change. – rmaddy Sep 09 '19 at 21:29
  • Isn't this method called before every character is added to the textField's text? You could easily analyze the new string by combining `textField.text` (the text before updating) with `text` (the character being appended). The end result is the combined string. – David Chopin Sep 09 '19 at 21:33
  • Additionally, the added text size is always either 1 (normal characters) or -1 (in the case of a backspace). Either way all scenarios can be handled within this method. – David Chopin Sep 09 '19 at 21:33
  • Yes, it's called before the actual change. If it returns false then the change isn't made. You need to remember that the user may have selected some of the text and the user may be deleting several characters or may be pasted in multiple characters (so your assumption about a length of 1 or -1 is wrong). You need to build a new string properly based on the current text and the values of `range` and `text`. Then look at the length of that new string. If the new string will be too long, then reject the change, otherwise allow it. Checking just the current length isn't sufficient. – rmaddy Sep 09 '19 at 21:36
  • Valid points. Let me take a look at the other delegate methods and some of my own code and make a change to the answer. I know I’ve done something similar to this before. – David Chopin Sep 09 '19 at 21:39
  • So I've added your code and reverted some of my old code. ''' let selection = selectedTextRange text = prospectiveText.substringWith( Range(prospectiveText.startIndex ..< prospectiveText.startIndex.advancedBy(maxLength)) ) ''' produces the error: Value of type 'String.Index' has no member 'advancedBy' your code has not produced any errors so far... – Cubespike Sep 09 '19 at 21:58
  • Cubespike, I'm currently editing my former answer to more closely match your scenario. – David Chopin Sep 09 '19 at 21:59
  • Ah thank you David, I'm just trying to figure this out as am new to swift and haven;t fully grasped the language. – Cubespike Sep 09 '19 at 22:06
  • Hey Cubespike, I updated the answer and added a github repo that will hopefully help you :) I'm sure the code could be more concise, but I have class in thirty minutes and cannot spend forever on this code – David Chopin Sep 09 '19 at 22:33
  • David, you are an absolute gem, it is midnight here so I am going to bed but will check this in the morning and hope it works out perfectly. You have helped me so much and I wish you the best. – Cubespike Sep 09 '19 at 23:00
  • Good luck pal :) let me know tomorrow if it works for you! – David Chopin Sep 09 '19 at 23:01