Swift 4
Inspired by @Anurag Soni and @Varun Naharia answers
Variant A
extension EnterConfirmationCodeTextField: UITextFieldDelegate {
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        guard let textFieldCount = textField.text?.count else { return false }
        // Сlosure
        let setValueAndMoveForward = {
            textField.text = string
            let nextTag = textField.tag + 1
            if let nextResponder = textField.superview?.viewWithTag(nextTag) {
                nextResponder.becomeFirstResponder()
            }
        }
        // Сlosure
        let clearValueAndMoveBack = {
            textField.text = ""
            let previousTag = textField.tag - 1
            if let previousResponder = textField.superview?.viewWithTag(previousTag) {
                previousResponder.becomeFirstResponder()
            }
        }
        if textFieldCount < 1 && string.count > 0 {
            setValueAndMoveForward()
            if textField.tag == 4 {
                print("Do something")
            }
            return false
        } else if textFieldCount >= 1 && string.count == 0 {
            clearValueAndMoveBack()
            return false
        } else if textFieldCount >= 1 && string.count > 0 {
            let nextTag = self.tag + 1
            if let previousResponder = self.superview?.viewWithTag(nextTag) {
                previousResponder.becomeFirstResponder()
                if let activeTextField = previousResponder as? UITextField {
                    activeTextField.text = string
                }
            }
            return false
        }
        return true
    }
}
Variant B (a little bit another behavior):
extension EnterConfirmationCodeTextField: UITextFieldDelegate {
        func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
            guard let textFieldCount = textField.text?.count else { return false }
            // Сlosure
            let setValueAndMoveForward = {
                textField.text = string
                let nextTag = textField.tag + 1
                if let nextResponder = textField.superview?.viewWithTag(nextTag) {
                    nextResponder.becomeFirstResponder()
                }
            }
            // Сlosure
            let clearValueAndMoveBack = {
                textField.text = ""
                let previousTag = textField.tag - 1
                if let previousResponder = textField.superview?.viewWithTag(previousTag) {
                    previousResponder.becomeFirstResponder()
                }
            }
            if textFieldCount < 1 && string.count > 0 {
                setValueAndMoveForward()
                if textField.tag == 4 {
                    print("Do something")
                }
                return false
            } else if textFieldCount >= 1 && string.count == 0 {
                clearValueAndMoveBack()
                return false
            } else if textFieldCount >= 1 {
                setValueAndMoveForward()
                return false
            }
            return true
        }
 }
Also, I implemented this feature:

In the case where the last textFiled is empty, I just want to switch to the previous textFiled. I tried all this methods. But as for me the method below more elegant and works like a charm:
Variant A
class EnterConfirmationCodeTextField: UITextField {
    // MARK: Life cycle
    override func awakeFromNib() {
        super.awakeFromNib()
        delegate = self
    }
    // MARK: Methods
    override func deleteBackward() {
        super.deleteBackward()
        let previousTag = self.tag - 1
        if let previousResponder = self.superview?.viewWithTag(previousTag) {
            previousResponder.becomeFirstResponder()
            if let activeTextField = previousResponder as? UITextField {
                if let isEmpty = activeTextField.text?.isEmpty, !isEmpty {
                    activeTextField.text = String()
                }
            }
        }
    }
}
Variant B (a little bit another behavior):
class EnterConfirmationCodeTextField: UITextField {
    // MARK: Life cycle
    override func awakeFromNib() {
        super.awakeFromNib()
        delegate = self
    }
    // MARK: Methods
    override func deleteBackward() {
        super.deleteBackward()
        let previousTag = self.tag - 1
        if let previousResponder = self.superview?.viewWithTag(previousTag) {
            previousResponder.becomeFirstResponder()
        }
    }
}
Assign EnterConfirmationCodeTextField for each of your textFields and set they appropriate tag value.