Text Input and Delegation via UITextField iOS 10.05.2020

UITextField is part of UIKit framework and is used to display an area to collect text input from the user using the onscreen keyboard. When a text field is tapped, the keyboard automatically slides up onto the screen. (You will see why this happens later in this chapter.) The keyboard’s appearance is determined by a set of the UITextField’s properties called the UITextInputTraits. One of these properties is the type of keyboard that is displayed.

Initialize text field

let frame = CGRect(x: 0, y: 0, width: 100, height: 100)
let textField = UITextField(frame: frame)

The default event for text fields is .editingDidBegin, which is triggered when the field is tapped. This is not the event you are interested in. Instead, you are interested in the .editingChanged event, which is triggered when a change is made to the field.

Currently, there is no way to . Let’s add that functionality. One common way of dismissing the keyboard is by detecting when the user taps the Return key and using that action to dismiss the keyboard.

When the text field is tapped, the method becomeFirstResponder() is called on it. This is the method that, among other things, causes the keyboard to appear. To dismiss the keyboard, you call the method resignFirstResponder() on the text field.

Implement an action method that will dismiss the keyboard when called.

@IBAction func dismissKeyboard(_ sender: UITapGestureRecognizer) {
    textField.resignFirstResponder()
}

Now you need a way of triggering the method you implemented. You will use a gesture recognizer to accomplish this.

A gesture recognizer is a subclass of UIGestureRecognizer that detects a specific touch sequence and calls an action on its target when that sequence is detected. There are gesture recognizers that detect taps, swipes, long presses, and more.

In Main.storyboard, find Tap Gesture Recognizer in the library. Drag this object onto the background view for the view controller. You will see a reference to this gesture recognizer in the scene dock, the row of icons above the scene in the canvas.

Control-drag from the gesture recognizer in the scene dock to the view controller and connect it to the dismissKeyboard method.

When the user types into a text field, that text field will ask its delegate if it wants to accept the changes that the user has made. The first step is enabling instances of the ViewController class to perform the role of UITextField delegate by declaring that ViewController conforms to the UITextFieldDelegate protocol.

class MovieAddViewController: UIViewController, UITextFieldDelegate { }

Open Main.storyboard and Control-drag from the text field to the view controller. Choose delegate from the panel to connect the delegate property of the text field to the ViewController.

Next, you are going to implement the UITextFieldDelegate method that you are interested in textField(_:shouldChangeCharactersIn:replacementString:). Because the text field calls this method on its delegate, you must implement it in ViewController.swift.

func textField(_ textField: UITextField,
               shouldChangeCharactersIn range: NSRange,
               replacementString string: String) -> Bool {
    let text = (tfOutlet.text! as NSString).replacingCharacters(in: range, with: string)

    print("Current text: \(textField.text)")
    print("Replacement text: \(string)")
    print("New text: \(text)")

    let existingTextHasDecimalSeparator = textField.text?.range(of: ".")
    let replacementTextHasDecimalSeparator = string.range(of: ".")

    if existingTextHasDecimalSeparator != nil,
        replacementTextHasDecimalSeparator != nil {
        return false
    } else {
        return true
    }

    //return true
}

Character limit on UITextField

The first thing that needs to get done is to make ViewController adopt and conform to UITextFieldDelegate. Let's do that now.

class ViewController: UIViewController, UITextFieldDelegate { 
    func textField(_ textField: UITextField,
                   shouldChangeCharactersIn range: NSRange,
                   replacementString string: String) -> Bool {
        return self.textLimit(existingText: textField.text,
                              newText: string,
                              limit: 10)
    }

    private func textLimit(existingText: String?,
                           newText: String,
                           limit: Int) -> Bool {
        let text = existingText ?? ""
        let isAtLimit = text.count + newText.count <= limit
        return isAtLimit
    }    
}

Input accessory view toolbar

Add an accessory view above the keyboard. This is commonly used for adding next/previous buttons, or additional buttons like Done/Submit (especially for the number/phone/decimal pad keyboard types which don’t have a built-in return key).

let textField = UITextField()
let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 44.0)

let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .FlexibleSpace, target: nil, action: nil)
let doneButton = UIBarButtonItem(barButtonSystemItem: .Done, target: self, action: Selector("done"))
let items = [flexibleSpace, doneButton]  // pushes done button to right side

toolbar.setItems(items, animated: false)
toolbar.sizeToFit()

textField.inputAccessoryView = toolbar

Padding in UITextField

extension UITextField {
    func setLeftPadding(padding: CGFloat) {
        let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: padding, height: self.frame.size.height))
        self.leftView = paddingView
        self.leftViewMode = .always
    }

    func setRightPadding(padding: CGFloat) {
        let paddingView = UIView(frame: CGRect(x: 0, y: 0, width: padding, height: self.frame.size.height))
        self.rightView = paddingView
        self.rightViewMode = .always
    }
}

Usage

myTextField.setLeftPadding(padding: 20)

Change placeholder color and font

We can change the style of the placeholder by setting attributedPlaceholder (a NSAttributedString).

var placeholderAttributes = [String: AnyObject]()
placeholderAttributes[NSAttributedString.Key.foregroundColor] = UIColor.red
placeholderAttributes[NSFontAttributeName] = font

if let placeholder = textField.placeholder {
    let newAttributedPlaceholder = NSAttributedString(string: placeholder, attributes: placeholderAttributes)
    textField.attributedPlaceholder = newAttributedPlaceholder
}

In this example we change only the color and font. You could change other properties such as underline or strikethrough style. Refer to NSAttributedString for the properties that can be changed.

Custom UITextField for Filtering Input Text

Here is an example of custom UITextField that takes only numerical text and discards all other.

class NumberTextField: UITextField {   
    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        registerForTextFieldNotifications()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
    }

    override func awakeFromNib() {
        super.awakeFromNib()
        keyboardType = .numberPad//useful for iPhone only
    }

    private func registerForTextFieldNotifications() {
        NotificationCenter.default.addObserver(self, selector: #selector(NumberTextField.textDidChange), name: NSNotification.Name(rawValue: "UITextFieldTextDidChangeNotification"), object: self)
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    func textDidChange() {
        text = filteredText()
    }
    private func filteredText() -> String {
        let inverseSet = CharacterSet(charactersIn:"0123456789").inverted
        let components = text!.components(separatedBy: inverseSet)
        return components.joined(separator: "")
    }
}

For filtering you can override shouldChangeCharactersIn method

let allowedCharacters = CharacterSet(charactersIn:"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvxyz").inverted    
func textField(_ textField: UITextField, 
    shouldChangeCharactersIn range: NSRange, 
    replacementString string: String) -> Bool {

    let components = string.components(separatedBy: allowedCharacters)
    let filtered = components.joined(separator: "")

    if string == filtered {    
        return true
    } else {
        return false
    }
}

Add UIDatePicker to UITextField

We wil use inputView property of UItextField. inputView property, as per apple documentation

If the value in this property is nil, the text field displays the standard system keyboard when it becomes first responder. Assigning a custom view to this property causes that view to be presented instead.

So, if we specify UIDatePicker as inputview to UItextField then system will not present keyboard to user and will show custom view, which in our case is UIDatePicker. We will use extension class to add UIDatePicker to UITextField.

extension UITextField {
    func setDatePickerAsInputViewFor(target:Any, selector:Selector) {
        let screenWidth = UIScreen.main.bounds.width
        let datePicker = UIDatePicker(frame: CGRect(x: 0.0, y: 0.0, width: screenWidth, height: 200.0))
        datePicker.datePickerMode = .date
        self.inputView = datePicker

        let toolBar = UIToolbar(frame: CGRect(x: 0.0, y: 0.0, width: screenWidth, height: 40.0))
        let cancel = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(tapCancel))
        let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
        let done = UIBarButtonItem(title: "Done", style: .done, target: nil, action: selector)
        toolBar.setItems([cancel,flexibleSpace, done], animated: false)
        self.inputAccessoryView = toolBar
    }

    @objc func tapCancel() {
        self.resignFirstResponder()
    }
}

Usage

override func viewDidLoad() {
    super.viewDidLoad()
    self.txtDate.setDatePickerAsInputViewFor(target: self, selector: #selector(dateSelected))
}

@objc func dateSelected() {
    if let datePicker = self.txtDate.inputView as? UIDatePicker {
        let dateFormatter = DateFormatter()
        dateFormatter.dateStyle = .medium
        self.txtDate.text = dateFormatter.string(from: datePicker.date)
    }
    self.txtDate.resignFirstResponder()
}

OTPView

OTPView.swift file.

import UIKit

class OTPView: UIStackView {

    var textFieldArray = [OTPTextField]()
    var numberOfOTPdigit = 4

    override init(frame: CGRect) {
        super.init(frame: frame)

        setupStackView()
        setTextFields()
    }

    required init(coder: NSCoder) {
        super.init(coder: coder)
        setupStackView()
        setTextFields()
    }

    private func setupStackView() {
        self.backgroundColor = .clear
        self.isUserInteractionEnabled = true
        self.translatesAutoresizingMaskIntoConstraints = false
        self.contentMode = .center
        self.distribution = .fillEqually
        self.spacing = 5
    }

    private func setTextFields() {
        for i in 0..<numberOfOTPdigit {
            let field = OTPTextField()

            textFieldArray.append(field)
            addArrangedSubview(field)
            field.delegate = self
            field.backgroundColor = .lightGray
            field.layer.opacity = 0.5
            field.textAlignment = .center
            field.layer.shadowColor = UIColor.black.cgColor
            field.layer.shadowOpacity = 0.1

            i != 0 ? field.previousTextField = textFieldArray[i-1] : nil
            i != 0 ? textFieldArray[i-1].nextTextFiled = textFieldArray[i] : nil
        }
    }
}

extension OTPView: UITextFieldDelegate {
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        guard let field = textField as? OTPTextField else {
            return true
        }
        if !string.isEmpty {
            field.text = string
            field.resignFirstResponder()
            field.nextTextFiled?.becomeFirstResponder()
            return true
        }
        return true
    }
}

class OTPTextField: UITextField {
    var previousTextField: UITextField?
    var nextTextFiled: UITextField?

    override func deleteBackward() {
        text = ""
        previousTextField?.becomeFirstResponder()
    }
}

ViewController.swift file.

import UIKit
import SnapKit

class ViewController: UIViewController {
    lazy var otpView: OTPView = {
        let v = OTPView()
        return v
    }()

    lazy var btn: UIButton = {
        let btn = UIButton(type: .system)
        btn.contentEdgeInsets = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16)
        btn.setTitle("Verify", for: .normal)
        btn.layer.cornerRadius = 8.0
        btn.tintColor = .white
        btn.backgroundColor = .red
        btn.addTarget(self, action: #selector(onVerifyTapped), for: .touchUpInside)
        return btn
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }

    @objc func onVerifyTapped(sender: UIButton!) {
    }

    func setupUI() {
        self.view.addSubview(otpView)
        self.view.addSubview(btn)

        otpView.snp.makeConstraints { (make) -> Void in
            make.size.equalTo(CGSize(width: 200, height: 50))
            make.center.equalTo(self.view)
        }

        btn.snp.makeConstraints { (make) -> Void in
            make.top.equalTo(otpView.snp.bottom).offset(16)
            make.centerX.equalTo(self.view)
        }
    }
}