Search code examples
iosswiftuitextfield

How to create a UITextField with inline validation error?


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


Solution

  • 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()
            }
        }
    }