52

I have an app in which the user has to type a four digit pin code. All digits have to be at a set distance from each other.

Is there a way to do this if the PinTextField is a subclass of UIView? I know in a ViewController you could use UITextFieldTextDidChangeNotification and set the attributed text for each change. Notifications don't seem to work in a UIView though.

Also I was wondering if there isn't a simpler way than making an attributed string for every update if you want to set the letter spacing of a UITextField text ?

Correct spacing:

TextField with correct spacing

Wrong spacing:

TextField with wrong spacing

KMV
  • 749
  • 1
  • 6
  • 16

7 Answers7

73

No need to go for attributedText, which to be honest, was a mess implementing with modified spacing. As soon as I closed the keyboard the spacing disappeared, which prompted me to dig further.

Every UITextField has a property called defaultTextAttributes, which according to Apple "returns a dictionary of text attributes with default values.". The Apple document also says that "this property applies the specified attributes to the entire text of the text field"

Just find a suitable place in your code, usually where the textfield is being initialized and then copy and paste the following.

Answered in Swift 3.0

textfield.defaultTextAttributes.updateValue(spacing, forKey: NSKernAttributeName)

where spacing is of CGFloat type. For example 2.0

This works for different fonts as well.

Cheers!!


The latest syntax seems to be:

 yourField.defaultTextAttributes.updateValue(36.0, 
     forKey: NSAttributedString.Key.kern)
Eric Aya
  • 69,473
  • 35
  • 181
  • 253
iOSer
  • 2,241
  • 1
  • 18
  • 26
  • 2
    @Fattie thank you for adding the latest syntax and for the comment as well. :) – iOSer Feb 05 '18 at 08:03
  • 1
    If the TextField is secure then this solution is not working. – Soumen Aug 06 '18 at 08:11
  • @Soumen sorry I have been busy with projects lately. But I will definitely look into this as soon as I get time. Thank btw for pointing that out – iOSer Aug 08 '18 at 04:36
  • How to remove space from last text. I have to only feed 4 digit. In that case it will wrong. – Ashu Nov 12 '19 at 11:32
  • @Ashu So you want three digits to be equally spaced and the last digit not so? Not sure what do you mean – iOSer Nov 13 '19 at 03:02
  • @iOSer yes, last digit should not have extra space – Ashu Nov 13 '19 at 07:38
  • Then Im afraid you'll have to use multiple texfields and take care of the spacing yourself. – iOSer Nov 14 '19 at 03:23
11

This is what eventually worked to set the kern for every change

    textField.addTarget(self, action: "textFieldDidChange", forControlEvents: .EditingChanged)

    func textFieldDidChange () {    
        let attributedString = NSMutableAttributedString(string: textField.text)
        attributedString.addAttribute(NSKernAttributeName, value: 5, range: NSMakeRange(0, count(textField.text)))
        attributedString.addAttribute(NSFontAttributeName, value: font, range: NSMakeRange(0, count(textField.text)))
        attributedString.addAttribute(NSForegroundColorAttributeName, value: UIColor.blackColor(), range: NSMakeRange(0, count(textField.text)))

        textField.attributedText = attributedString
    }

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

        if count(textField.text) < 4 {
            return true
            // Else if 4 and delete is allowed
        }else if count(string) == 0 {
            return true
            // Else limit reached
        }else{
            return false
        }
    }

The problem however remains because different numbers have different widths, I will just resort back to making a UITextField for every digit.

Cœur
  • 37,241
  • 25
  • 195
  • 267
KMV
  • 749
  • 1
  • 6
  • 16
  • 3
    The proper solution is to use monospace font. – Jurasic Nov 09 '17 at 10:33
  • Your entire second method can be nicely collapsed into one line: func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool { string.isEmpty || count(textField.text) < 4 } – rmp251 Jan 21 '22 at 20:11
4

Use the defaultTextAttributes property of UITextField. It will handle the conversion to NSAttributedString for you and apply the attributes you set. For example:

    NSMutableDictionary *attrs = [self.textField.defaultTextAttributes mutableCopy];
[attrs addEntriesFromDictionary:@{
    NSKernAttributeName: @18,
    NSUnderlineColorAttributeName: [UIColor grayColor],
    NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle | NSUnderlinePatternDash)
}];
self.textField.defaultTextAttributes = attrs;
Avi
  • 826
  • 6
  • 16
0

Not really sure about any other solution instead of using attributed string.

But for the notification part, you can set the textFields delegate to UIView and define below method in the view.

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string; 

The above method is called every time the text entered in the text field changes.

TheTiger
  • 13,264
  • 3
  • 57
  • 82
Scarlet
  • 56
  • 4
0

Try this code After setting the delegate to the textfield.Hope it will work.

- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
    NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:textField.text];
    [attributedString addAttribute:NSKernAttributeName
                             value:@(5.4)
                             range:NSMakeRange(0, textField.text.length)];
    textField.attributedText = attributedString;
    return YES;
}
Vishnuvardhan
  • 5,081
  • 1
  • 17
  • 33
  • `shouldChangeCharactersInRange` does not work for the first letter though, it only appears after you have input the second one. – KMV Aug 07 '15 at 13:23
  • 1
    check this link for first letter change, it may help you: http://stackoverflow.com/questions/7010547/uitextfield-text-change-event – Vishnuvardhan Aug 10 '15 at 03:31
0

This is working fine in Swift 2.2. Hope this will help you for letter spacing in text field

override func viewDidLoad() {
 // Do any additional setup after loading the view.
        NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(SignupVC.limitTextField(_:)), name: "UITextFieldTextDidChangeNotification", object: txtContactNumber)
}
 func limitTextField(Notif:NSNotification) {

    let limit=10;

    let attributedString = NSMutableAttributedString(string: txtContactNumber.text!)
    attributedString.addAttribute(NSKernAttributeName, value: 7, range: NSMakeRange(0, (txtContactNumber.text?.characters.count)!))
   // attributedString.addAttribute(NSFontAttributeName, value: font, range: NSMakeRange(0, count(textField.text)))
    attributedString.addAttribute(NSForegroundColorAttributeName, value: UIColor.blackColor(), range: NSMakeRange(0,(txtContactNumber.text?.characters.count)!))

    txtContactNumber.attributedText = attributedString
    if(txtContactNumber.text?.characters.count>limit)
    {
        txtContactNumber.text=txtContactNumber.text?.substringToIndex(limit)
    }
}
Arvind Kumar
  • 2,371
  • 1
  • 18
  • 25
0

Need to count the kern for each character and remove it for the last character. There is example on Swift 5.3

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    
    let maxLength = 6
    let symbolWidth = CGFloat(43)
    let font = UIFont.systemFont(ofSize: 30)
    
    if string == "" { // when user remove text
        return true
    }
        
    if textField.text!.count + string.count - range.length > maxLength { // when user type extra text
        return false
    }
    
    let currentText = NSMutableAttributedString(attributedString: textField.attributedText ?? NSMutableAttributedString())
    currentText.deleteCharacters(in: range) // delete selected text
    var newStringLength = 0 
    
    for char in string{
        let newSymbol = NSMutableAttributedString(string: String(char))
        newSymbol.addAttribute(.font, value: font, range: NSMakeRange(0, 1))
        let currentSymbolWidth = newSymbol.size().width
        let kern = symbolWidth - currentSymbolWidth
        newSymbol.addAttribute(.kern, value: kern, range: NSMakeRange(0,1))
        
        currentText.insert(newSymbol, at: range.location + newStringLength)
        newStringLength += 1
    }
    
    if currentText.length == maxLength{
        currentText.addAttribute(.kern, value: 0, range: NSMakeRange(maxLength - 1, 1))
    }
    
    textField.attributedText = currentText
    
    return false

}
Bodja4x
  • 1
  • 1