2

I am trying to achieve below image:

enter image description here

As you can see in the image, when a user types a number into the textfield, the other digits of the number still stand as a placeholder. (000 000 0000)

To do that, I placed a UILabel inside of the UITextField.

var placeHolderLabelString = "___ ___ ____"
var placeholderLabel = UILabel()
placeholderLabel.text = placeHolderLabelString
placeholderLabel.sizeToFit()
numberInputView.addSubview(placeholderLabel)
placeholderLabel.textColor = .gray
placeholderLabel.font = numberInputView.font

I want to delete each underscore when a user types a digit into the textfield. I have tried the below code:

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

    let rnd = Range(range, in: placeHolderLabelString)!
    placeHolderLabelString = placeHolderLabelString.replacingCharacters(in: rnd, with: " ")
    placeholderLabel.text = placeHolderLabelString

    return true
}

But this doesn't work. It adds a space char to the beginning of the placeHolderLabelString, not replacing it. How can I do that?

Tolgay Toklar
  • 4,151
  • 8
  • 43
  • 73

1 Answers1

1

The range passed to shouldChangeCharacters refers to a range of the text field's text, before the change. When you normally insert text using the keyboard, the range has a length of 0, and replacementString is the character that you are trying to insert. The idea is that you are replacing an empty range of the text, with the new character, hence "inserting" that character.

As a result, doing a replacement of that same range with " " to the placeholder string, will insert a space into the placeholder string.

Rather than using the given range on the placeholder string, you should use it on the text field's text. That is what the range means anyway.

You can compute the new text of the text field, after the insertion, by doing:

let newText = (textField.text! as NSString).replacingCharacters(in: range, with: string)

Then, replace the first newText.count characters of the placeholder string with spaces. Set this as the new text of the placeholder label.

// note that I changed this to a let, because I do not change the placeholder string
let placeHolderLabelString = "___ ___ ___"

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

    let newText = (textField.text! as NSString).replacingCharacters(in: range, with: string)

    // do not allow more text than the placeholder
    if newText.count > placeHolderLabelString.count {
        return false
    }
    
    let startIndex = placeHolderLabelString.startIndex
    let replaceableRange = startIndex..<placeHolderLabelString.index(startIndex, offsetBy: newText.count)
    let newPlaceholder = placeHolderLabelString.replacingCharacters(
        in: replaceableRange, with: String(repeating: " ", count: newText.count)
    )
    placeholderLabel.text = newPlaceholder
    return true
}

From the looks of it, you also want to automatically add spaces in some places.

To do that, you need to set textField.text:

// after the newText.count > placeHolderLabelString.count check
let spacesRemoved = newText.replacingOccurrences(of: " ", with: "")
textField.text = formatPhoneNumber(spacesRemoved)

where formatPhoneNumber is a method that inserts spaces appropriately. Its implementation is left as an exercise for the reader.

Then, the rest of the code should replace the first textField.text!.count characters of placeHolderLabelString, rather than newText.count. Because newText is now no longer the text in the text field.

let startIndex = placeHolderLabelString.startIndex

// note that I'm using textField.text instead of newText here,
// because *that* is actually the text of the text field now
let replaceableRange = startIndex..<placeHolderLabelString.index(startIndex, offsetBy: textField.text!.count)
let newPlaceholder = placeHolderLabelString.replacingCharacters(
    in: replaceableRange, with: String(repeating: " ", count: newText.count)
)
placeholderLabel.text = newPlaceholder

// the text field's text should not change, because we set it programmatically to the desired value already.
return false
Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • Wow, this is an amazing explanation. Thank you very much :) – Tolgay Toklar Apr 21 '23 at 10:25
  • FYI - the placeholder needs 4 underscores in the 3rd group, not 3. And this all assumes you only want to support US/Canadian phone numbers. `shouldChangeCharactersIn` also needs to ensure only digits are entered. Those pesky users might try to paste in all kinds of random text. – HangarRash Apr 21 '23 at 16:02
  • @HangarRash You’re right. I misread the placeholder, thinking there are only 9 digits. Ensuring only digits are entered is left as an exercise for the reader :). There are lots of posts regarding that already, and I felt like my answer is getting long. OP could have disabled paste for all we know. – Sweeper Apr 22 '23 at 00:08