11

I want the user to enter a Social Security Number in the format ••• •• ••••. The user types the first 3 numbers, then I append a space manually. Then they enter 2 more numbers and I manually append a space. Of course, even the spaces are being displayed as •. Is there a native way to change this behavior? I am currently using a funky manual implementation of this.

jscs
  • 63,694
  • 13
  • 151
  • 195
Zia
  • 14,622
  • 7
  • 40
  • 59
  • AFAIK, there is no native way of achieving this with secureTextEntry enabled but you can achieve the behaviour by overriding shouldChangeCharactersInRange of UITextFieldDelegate property but that would require you to disable secureTextEntry – ldindu Jul 15 '16 at 17:42
  • Jon, please consider accepting one of these because us future visitors can't tell which one helped you out (we don't want to waste time trying to use ones that don't work). – owlswipe Jul 19 '16 at 00:01

4 Answers4

8

What if instead of spaces, you use three different text entries? Then when each user enters the first three characters, you jump to the second text entry? When he types two more, you jump to the third text entry.

Here is an example:

EDIT: Now supports backspace (Thanks to @triple-s).

extension ViewController: UITextFieldDelegate {

    func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
        switch textField {
            // Jump forwards
        case self.textField1 where string.characters.count == 3 :
            self.textField2.becomeFirstResponder()
        case self.textField2 where string.characters.count == 2 :
            self.textField3.becomeFirstResponder()

            // Jump backwards
        case self.textField3 where string.characters.count == 0 :
            self.textField2.becomeFirstResponder()
        case self.textField2 where string.characters.count == 0 :
            self.textField1.becomeFirstResponder()
        default :
            break
        }

        return true
    }
}
Alonso Urbano
  • 2,266
  • 1
  • 19
  • 26
  • @JonSetting for back space you can check if the text box is empty then set the previous text box as `firstResponser` – triandicAnt Jul 15 '16 at 18:38
  • This is on the right track, but the `string` is just the new input: it will have length 1. You need to check the location of the proposed range. The second part is pretty much just wrong: `textField:shouldChangeCharactersInRange:replacementString:` _isn't called_ for a backspace at the beginning of a text field. – jscs Jul 15 '16 at 19:38
  • This is a great solution and will help others. But I just realized I can't use it because the text alignment has to be center. – Zia Jul 16 '16 at 04:19
  • Has anyone actually read or run this code?! **It doesn't work** as written. The idea is sound, but it's not implemented in this code. @JonSetting – jscs Jul 19 '16 at 18:49
  • Have you checked the instance member names? The class name? Have the appropriate delegates set? – Alonso Urbano Jul 19 '16 at 19:19
  • 1
    This is not the correct implementation for the proposed solution. – Zia Jul 22 '16 at 23:28
3

This can be achieve in one single textField as asked. I only tapped "1", in the gif.

enter image description here

  1. You select your keypad type to be number (0-9), which can ensure everything that will be input there is number only.

  2. Then you can adopt the textField delegate and implement the delegate method

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

    let  char = string.cStringUsingEncoding(NSUTF8StringEncoding)!
    let isBackSpace = { return strcmp(char, "\\b") == -92}
    
    if (textField.text?.characters.count == 3 && !isBackSpace()) || (textField.text?.characters.count == 6 && !isBackSpace()){
        textField.text = textField.text! + " "
    }
    
    if (textField.text?.characters.count) == 11 && !isBackSpace(){
        ssnString = textField.text!
        self.view.endEditing(true)
    
    }
    
    return true
    

    }

This includes the logic of adding space after third digit if you are not backspacing and same for the after 6th digit.

Also, after user input 11 digit, it will not allow user to input more number as the format of SSN, after 11 digit is input, the SSN is saved in ssnString, which will be used for you special masking.

  1. Because you don't want to mask space, we can not use secureTextEntry. So in the didEndEditing, I gave an condition only if the user enter the full SSN, we will mask it, which can be modified to any scenario if you want. But i think this makes more sense.

func textFieldDidEndEditing(textField: UITextField) { if textField.text?.characters.count == 11 { maskSSNTextField() } }

In the maskSSNTextField method, func maskSSNTextField() { textField.text = "••• •• ••••" }

  1. Finally, we need to unmask it when user come back to it, if they want to change the text

func textFieldDidBeginEditing(textField: UITextField) { if textField.text == "••• •• ••••"{ textField.text = ssnString } }

This fully fulfilled your requirement. Please let me know if you have other question.

Developer Sheldon
  • 2,140
  • 1
  • 11
  • 17
  • But it's supposed to be a secured text field, I can see clearly the whole number you enter. That's not the purpose of secure text. Secure text just shows the last digit you entered for a very short time. – Alonso Urbano Jul 15 '16 at 18:50
  • If you set it to be secured the space will be masked as well. You can manually do secure text in the didChangeInRange by replacing your second last digit character with "•", if string != " " – Developer Sheldon Jul 15 '16 at 18:55
2

I changed the didChangeInRange method to meet your new requirement, although I think my previous answer could work. Now it works as in the gif. If you want it to be still masked, you can change the code in textField did begin editing.

enter image description here

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

    let  char = string.cStringUsingEncoding(NSUTF8StringEncoding)!
    let isBackSpace = { return strcmp(char, "\\b") == -92}

    if (textField.text?.characters.count == 3 && !isBackSpace()) || (textField.text?.characters.count == 6 && !isBackSpace()){
        textField.text = textField.text! + " "
        ssnString = ssnString + " "
    }



    if isBackSpace() {

        ssnString = ssnString.substringToIndex(ssnString.endIndex.predecessor())

    }else {

        ssnString = ssnString + string
        print(ssnString)

        if ssnString.characters.count >= 2 {
            var starString = ""
            for i in 0...ssnString.characters.count-2 {

                if i==3 || i==6 {
                    starString = starString+" "
                }else {
                    starString = starString+"•"
                }
            }
            textField.text = ""
            print(ssnString.characters.last)
            textField.text = starString
        }
    }

    if (textField.text?.characters.count) == 11 && !isBackSpace(){
        self.view.endEditing(true)

    }
    return true
}
Developer Sheldon
  • 2,140
  • 1
  • 11
  • 17
1

The simple solution I have been using is to convert my input string to an NSAttributedString with text spacing (.kern) attributes added at the proper locations and keeping isSecureTextEntry set to true. Disabling isSecureTextEntry and doing it by hand in addition of being overly complex could have security implications at least if someone is using a third party keyboard.

var ssnText = "123456789"

let spacerPositions = [ 2, 4 ]
let spacingAmount: CGFloat = 5.0

let spacerRanges:[NSRange] = spacerPositions
                                .filter { $0 < ssnText.count - 1 }
                                .map { NSRange(location: $0, length: 1) }

let attributedString = NSMutableAttributedString(string: ssnText)
for range in spacerRanges {
    attributedString.addAttribute(.kern, value: spacingAmount, range: range)
}

textField.attributedText = attributedString

calling that stuff in textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String).

Sebastien Windal
  • 1,394
  • 12
  • 12