Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to achieve that placeholder text disappears character by character in UITextField

Can you please help me.

In UITextField when we provide a placeholder text its placeholder string will be gone when we enter any character. How can I achieve that only entered character will be gone not a full string? Meaning that if I type in 3 characters, only the first 3 characters of placeholder will be gone.

enter image description here

#EDIT 1

Also, newly entered character text color will change and other remaining character text color remains same.

Thanks in advance.

like image 417
iamVishal16 Avatar asked Sep 05 '17 06:09

iamVishal16


4 Answers

I don't believe the default behavior of the placeholder is editable, but what you are trying to accomplish can be done using NSAttributedString to simulate the placeholder value.

I'm sure this can be optimized, but here I have created a handler class that acts as the delegate for a given UITextField, manipulating the string the user inputs to achieve the desired effect. You init the handler with your desired placeholder string, so you can make any text field work this way.

Custom Placeholder Text Field

import UIKit

class CustomPlaceholderTextFieldHandler: NSObject {
    let placeholderText: String
    let placeholderAttributes = [NSForegroundColorAttributeName : UIColor.lightGray]
    let inputAttributes = [NSForegroundColorAttributeName : UIColor(red: 255/255, green: 153/255, blue: 0, alpha: 1.0)]
    var input = ""

    init(placeholder: String) {
        self.placeholderText = placeholder
        super.init()
    }

    func resetPlaceholder(for textField: UITextField) {
        input = ""
        setCombinedText(for: textField)
    }

    fileprivate func setCursorPosition(for textField: UITextField) {
        guard let cursorPosition = textField.position(from: textField.beginningOfDocument, offset: input.characters.count)
            else { return }

        textField.selectedTextRange = textField.textRange(from: cursorPosition, to: cursorPosition)
    }

    fileprivate func setCombinedText(for textField: UITextField) {
        let placeholderSubstring = placeholderText.substring(from: input.endIndex)
        let attributedString = NSMutableAttributedString(string: input + placeholderSubstring, attributes: placeholderAttributes)
        attributedString.addAttributes(inputAttributes, range: NSMakeRange(0, input.characters.count))
        textField.attributedText = attributedString
    }
}

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

        if string == "" {
            if input.characters.count > 0 {
                input = input.substring(to: input.index(before: input.endIndex))
            }
        } else {
            input += string
        }

        if input.characters.count <= placeholderText.characters.count {
            setCombinedText(for: textField)
            setCursorPosition(for: textField)
            return false
        }
        return true
    }

    func textFieldDidBeginEditing(_ textField: UITextField) {
        setCursorPosition(for: textField)
    }
}

Here's a the way I initialized the gif above:

class ViewController: UIViewController {

    @IBOutlet weak var textField: UITextField!
    let placeholderHandler = CustomPlaceholderTextFieldHandler(placeholder: "_2_-__-__A")

    override func viewDidLoad() {
        super.viewDidLoad()
        textField.delegate = placeholderHandler
        placeholderHandler.resetPlaceholder(for: textField)
    }
}

This could be expanded to take color parameters, fonts, etc. at initialization, or you may find it cleaner to subclass UITextField and make it its own delegate. I also haven't really tested this for selecting/deleting/replacing multiple characters.

The input variable will return the text the user has input at any given point. Also, using a fixed-width font would remove the jitteriness as the user types and replaces the placeholder text.

like image 65
JAB Avatar answered Nov 01 '22 05:11

JAB


Instead of using placeholder text, use a UILabel below your textfield and give the same font style to both. the label text should be like "- - - - -"

And when user starts typing on textfield give a space after each character press.

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

    if textField.text?.characters.count == 0 && string.characters.count != 0 {
        textField.text = textField.text! + " "
    }
     else {
        return false
    }

    if textField.text?.characters.count == 1 && string.characters.count != 0 {
        textField.text = textField.text! + " "
    }
    else {
        return false
    }

    if textField.text?.characters.count == 2 && string.characters.count != 0 {
        textField.text = textField.text! + " "
    }
    else {
        return false
    }
    if textField.text?.characters.count == 3 && string.characters.count != 0 {
        textField.text = textField.text! + " "
    }
    else {
        return false
    }

    return true
}
like image 21
Saranjith Avatar answered Nov 01 '22 06:11

Saranjith


There is my solution using UITextField text property

+(BOOL)shouldChangeCharactersInRange:(NSRange)range
                   replacementString:(NSString *)string
                           textField:(UITextField *)textField
                                mask:(NSString *)mask withMaskTemplate:(NSString *)maskTemplate{

    NSString * alreadyExistString = @"";
    if (string.length == 0) {
        alreadyExistString = textField.text;
        for (int i = range.location; i >= 0; i--) {
            unichar  currentCharMask = [maskTemplate characterAtIndex:i];
            unichar  currentChar = [alreadyExistString characterAtIndex:i];
            if (currentCharMask == currentChar) {// fixed value and _
                continue;
            }else{
                alreadyExistString = [alreadyExistString stringByReplacingCharactersInRange:NSMakeRange(i, 1) withString:@"_"];
                break;
            }
        }
        textField.text = alreadyExistString;
        return NO;
    }else{
        alreadyExistString = [textField.text stringByReplacingCharactersInRange:NSMakeRange(range.location, 1) withString:string];
    }

    NSMutableString * validText = [[NSMutableString alloc] init];

    int last = 0;
    BOOL append = NO;
    for (int i = 0; i < alreadyExistString.length; i++) {
        unichar  currentCharMask = [mask characterAtIndex:i];
        unichar  currentChar = [alreadyExistString characterAtIndex:i];
        BOOL isLetter = [[NSCharacterSet alphanumericCharacterSet] characterIsMember: currentChar];
        BOOL isDigit  = [[NSCharacterSet decimalDigitCharacterSet] characterIsMember: currentChar];
        if ((isLetter && currentCharMask == '#') || (isDigit && currentCharMask == '9')) {
            [validText appendString:[NSString stringWithFormat:@"%c",currentChar]];
        }else{
            if (currentCharMask == '#' || currentCharMask == '9') {
                break;
            }
            if ((isLetter && currentCharMask!= currentChar)|| (isDigit && currentCharMask!= currentChar)) {
                append = YES;
            }
            [validText appendString:[NSString stringWithFormat:@"%c",currentCharMask]];
        }
        last = i;
    }

    for (int i = last+1; i < mask.length; i++) {
        unichar currentCharMask = [mask characterAtIndex:i];
        if (currentCharMask != '#' && currentCharMask != '9') {
            [validText appendString:[NSString stringWithFormat:@"%c",currentCharMask]];
        }
        if (currentCharMask == '#' || currentCharMask == '9') {
            break;
        }
    }
    if (append) {
        [validText appendString:string];
    }

    NSString *placeHolderMask = textField.text;
    NSString *sub = [validText substringWithRange:NSMakeRange(range.location, 1)];
    placeHolderMask = [placeHolderMask stringByReplacingCharactersInRange:NSMakeRange(range.location, 1) withString:sub];
    textField.text = placeHolderMask;
    return NO;
}

@property (nonatomic,strong) NSString * maskTemplate;// like: _2_-__-__A
@property (nonatomic,strong) NSString * mask;// like: #2#-99-##A
  • Initially set text field text to mask template
  • Then when user enters input shouldChangeCharactersInRange invoked and it does a great job

#Edit 1 There is also some more code I have implemented which move the cursor to the underscore location. If someone need help. Please comment I will help you.

#Edit 2

Problems I have facing using this approached

  • Not able to change individual text color. The color will be same for both the string like @"_" underscore and input characters has the same color.
  • If user not providing any input then I have to provide a check to send blank as a string.
  • Tracking for individual inputs.

Thanks, Still waiting for if there is any other workaround using Placeholder String.

like image 2
iamVishal16 Avatar answered Nov 01 '22 05:11

iamVishal16


I guess this is just what you are looking for. Create your UITextField object with the text _2_-__-__A (not placeholder text). Then, use its view controller as delegate, and add that to the view controller:

-(BOOL)textField:(UITextField*)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString*)string{
    if (range.length>1) return NO; // Avoids removing multiple characters at once
    if (range.location==1) range.location++;  // '2' index
    if (range.location==3) range.location++;  // '-' index
    if (range.location==6) range.location++;  // '-' index
    if (range.location==9)  return NO; // 'A' index
    if (range.location==10) return NO; // String end
    if ([string isEqualToString:@""]) return NO; //Avoids removing characters

    if (range.length==0) {
        range.length++;
        UITextPosition *beginning = textField.beginningOfDocument;
        UITextPosition *start = [textField positionFromPosition:beginning offset:range.location];
        UITextPosition *end = [textField positionFromPosition:start offset:range.length];
        UITextRange *textRange = [textField textRangeFromPosition:start toPosition:end];
        [textField setSelectedTextRange:textRange];
    }

    return YES;
}
-(void)textFieldDidBeginEditing:(UITextField*)textField{
    UITextPosition *beginning = textField.beginningOfDocument;
    UITextPosition *start = [textField positionFromPosition:beginning offset:0];
    UITextPosition *end = [textField positionFromPosition:start offset:0];
    UITextRange *textRange = [textField textRangeFromPosition:start toPosition:end];
    [textField setSelectedTextRange:textRange];
}
-(BOOL)textFieldShouldReturn:(UITextField*)textField{
    [passwordInput resignFirstResponder];
    return YES;
}

It should work as intended.

like image 1
VitorMM Avatar answered Nov 01 '22 05:11

VitorMM