skip to Main Content

Im trying to build a custom UITextField that shows a validation error inline below the textfield akin to material design. Im adding a UILabel lazily and constraining to the textfield when i present the error

    private lazy var errorLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 14, weight: .bold)
        label.textColor = errorColor
        label.numberOfLines = 0
        label.isHidden = true
        return label
    }()

    open override func didMoveToSuperview() {
        super.didMoveToSuperview()
        
        // Make sure that errorLabel is added once to the superview, not to the text field itself
        if let superview = superview, errorLabel.superview == nil {
            superview.addSubview(errorLabel)
            configureErrorLabelConstraints()
        }
    }

    private func configureErrorLabelConstraints() {
        errorLabel.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            errorLabel.topAnchor.constraint(equalTo: self.bottomAnchor, constant: 5),
            errorLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor),
            errorLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor)
        ])

    public func showInlineValidationError(message: String) {
        errorLabel.textColor = errorColor
        layer.borderColor = errorColor.cgColor
        errorLabel.text = message
        errorLabel.isHidden = false
    }
    
    public func hideInlineValidationError() {
        layer.borderColor = borderColor.cgColor
        errorLabel.isHidden = true
    }
    }

This all works great. My issue is, say i have a UIImage constrained 12 points below the custom textfield, when the label is presented it does not move the image down by the label height, and rather reduces the space between the two. What i want is if the label is displayed, the constraint between the textfield and image to be increased by how much additional height the label takes up. How can i go about this? AI has not been helpful at all in what i might be able to do here.

Im open to a 3rd party library if its basic enough. I just need a basic UITextfield with rounded corners and the error label below that can support my height change requirements

2

Answers


  1. Chosen as BEST ANSWER

    This is similar to what sonle suggested. I ended up moving this to a UIView containing a stackview and hide/show the label. if i have a height constraint it must be set to greater than or equal to so the view is allow to grow with the stackview.

    open class ErrorTextField: UIView {
        public var errorColor: UIColor = .red
        public var errorLabelTopSpacing: CGFloat = 5
        public var textfieldHeight = 48
        public var textfieldBorderWidth: CGFloat = 1
        public var textfieldCornerRadius: CGFloat = 4
        public var errorLabelFont: UIFont = .systemFont(ofSize: 14, weight: .bold)
        
        public let textField = UITextField()
        
        public var errorLabel: UILabel = {
            let label = UILabel()
            label.numberOfLines = 0
            label.isHidden = true
            return label
        }()
        
        private lazy var stackView: UIStackView = {
            let stack = UIStackView(arrangedSubviews: [textField, errorLabel])
            return stack
        }()
        
        override public init(frame: CGRect) {
            super.init(frame: frame)
            setupViews()
        }
        
        required public init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            setupViews()
        }
        
        private func setupViews() {
            textField.layer.cornerRadius = textfieldCornerRadius
            textField.layer.borderWidth = textfieldBorderWidth
            errorLabel.font = errorLabelFont
            errorLabel.textColor = errorColor
            stackView.axis = .vertical
            stackView.spacing = errorLabelTopSpacing
            
            addSubview(stackView)
            textField.translatesAutoresizingMaskIntoConstraints = false
            stackView.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                textField.heightAnchor.constraint(equalToConstant: CGFloat(textfieldHeight)),
                stackView.topAnchor.constraint(equalTo: topAnchor),
                stackView.bottomAnchor.constraint(equalTo: bottomAnchor),
                stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
                stackView.trailingAnchor.constraint(equalTo: trailingAnchor)
            ])
        }
        
        public func showError(message: String) {
            errorLabel.text = message
            textField.borderColor = errorColor
            UIView.animate(withDuration: 0.25, delay: .zero, options: .curveEaseOut) {
            self.errorLabel.isHidden = false
            self.stackView.layoutIfNeeded()
            }
    
        }
        
        public func hideError() {
            UIView.animate(withDuration: 0.25) {
                self.errorLabel.isHidden = true
                self.stackView.layoutIfNeeded()
            }
        }
    }
    
    

  2. Update: According to your comment I assume this is what you’re looking for.

    1. Declare a base class, let’s call ValidationTextField, that contains three subviews like below.
    2. Next, subclass it for a specific purpose, I will do a example about EmailTextField.
    class ValidationTextField: UIView {
        struct Config {
            let keyboardType: UIKeyboardType
            let error: String
            //Other configures if needed
        }
        
        private var textField = UITextField()
        private var errorLabel = UILabel()
        private var errorImage = UIImageView()
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            //Add subViews within stackView here
            textField.addTarget(self, action: #selector(editingChanged(_:)), for: .editingChanged)
        }
    
        func bind(_ config: ValidationTextField.Config) {
            textField.keyboardType = config.keyboardType
            errorLabel.text = config.error
        }
    
        func validate(_ text: String?) -> Bool {
            //TODO: should be overridden
            return true
        }
    
        private func toggleError(isShow: Bool) {
            guard (!errorLabel.isHidden && !isShow) || (errorLabel.isHidden && isShow) else { return }
            UIView.animate(withDuration: 0.3) {
                self.errorLabel.isHidden = !isShow
            }
        }
    
        @objc private func editingChanged(_ textField: UITextField) {
            let isValid = validate(textField.text)
    
            toggleError(isShow: !isValid)
        }
    }
    

    And this is what EmailTextField could look like:

    final class EmailTextField: ValidationTextField {
        override init(frame: CGRect) {
            super.init(frame: frame)
    
            bind(.init(keyboardType: .emailAddress, 
                       error: "Email is not valid"))
        }
    
        override func validate(_ text: String?) -> Bool {
            if let text {
                return validateEmail(text)
            } else {
                return true
            }
        }
    
        private func validateEmail(_ text: String) -> Bool {
            let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}"
            let emailTest = NSPredicate(format:"SELF MATCHES %@", emailRegEx)
            return emailTest.evaluate(with: text)
        }
    

    I think the better approach for this scenario is to add a container StackView that contains all textField, errorLabel and imageView vertically.

    enter image description here

    Then you can simply toggle the errorLabel regardless of Label’s height and the constraints between TextField and ImageView.

    UIView.animate(withDuration: 0.3) {
        self.errorLabel.isHidden.toggle()
    }
    

    Output:

    enter image description here

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search