ihsan
ihsan

Reputation: 707

Displaying validation error on iOS UITextField similar to Android's TextView.setError()

Is there a way to show a validation error on a UITextField which is similar to Android's TextView.setError() in swift?

Upvotes: 27

Views: 42544

Answers (8)

Ankit Bansal
Ankit Bansal

Reputation: 21

extension UITextField {


/**
this function adds a right view on the text field
*/


func addRightView(rightView: String, tintColor: UIColor? = nil, errorMessage: String? = nil) {
    if rightView != "" {
        let rightview = UIButton(type: .custom)
        
        if tintColor != nil {
            let templateImage = UIImage(named: rightView)?.withRenderingMode(UIImage.RenderingMode.alwaysTemplate)
            rightview.setImage(templateImage, for: .normal)
            rightview.tintColor = tintColor
        }
        else{
            rightview.setImage(UIImage(named: rightView), for: .normal)
        }
        
        if let message = errorMessage {
            rightview.imageEdgeInsets = UIEdgeInsets(top: 0, left: -16, bottom: 5, right: 0)
            showErrorView(errorMessage: message)
        } else {
            rightview.imageEdgeInsets = UIEdgeInsets(top: 0, left: -16, bottom: 0, right: 0)
        }
        
        self.rightViewMode = .always
        self.rightView = rightview
    }
    else{
        self.rightView = .none
        for vw in self.subviews where vw.tag == 1000 {
            vw.removeFromSuperview()
        }
    }
}

/**
 this function add custom alert as a right view on the text field
 */

private func showErrorView(errorMessage: String) {
    
    let containerVw = UIView(frame: CGRect(x: self.frame.origin.x + 30, y: 30, width: self.frame.size.width - 60, height: 45))
    containerVw.backgroundColor = .clear
    containerVw.tag = 1000
    
    let triangleVw = UIButton(frame: CGRect(x: containerVw.frame.maxX - 25, y: 0, width: 15, height: 15))
    triangleVw.isUserInteractionEnabled = false
    triangleVw.setImage(UIImage(named: "arrowUp"), for: .normal)
    triangleVw.imageEdgeInsets = UIEdgeInsets(top: 3, left: 0, bottom: 0, right: 0)
    triangleVw.tintColor = AppColor.red1
    
    let messageVw = UIView(frame: CGRect(x: containerVw.frame.origin.x, y: triangleVw.frame.maxY - 2, width: containerVw.frame.width, height: 30))
    messageVw.backgroundColor = UIColor.red
    
    let errorLbl = UILabel(frame: CGRect(x: 0, y: 2, width: messageVw.frame.size.width, height: messageVw.frame.size.height - 2))
    errorLbl.backgroundColor = .black
    errorLbl.numberOfLines = 2
    messageVw.addSubview(errorLbl)
    errorLbl.text = errorMessage
    errorLbl.textColor = .white
    errorLbl.textAlignment = .left
    errorLbl.font = UIFont.systemFont(ofSize: 14)
    
    containerVw.addSubview(triangleVw)
    containerVw.sendSubviewToBack(triangleVw)
    containerVw.addSubview(messageVw)
    containerVw.layoutIfNeeded()
    
    self.addSubview(containerVw)
    self.bringSubviewToFront(containerVw)
}
}

Usage:

rightView is the image name pass any as per your requirement. Set empty if you want to remove right view.

tintColor is optional to use any as per your requirement.

Set message (errorMessage) value if you want to show an error message like an android.

Remove RightView

textField.addRightView(rightView: "")

Add RightView

textField.addRightView(rightView: "rightVwWarning", tintColor: UIColor.red)

Add RightView and error message as android

textField.addRightView(rightView: "rightVwWarning", tintColor: UIColor.red, errorMessage: "Error")

Upvotes: 2

Oleg Gryb
Oleg Gryb

Reputation: 5249

This is as close as I could get to what I've seen in Android. Tested in SwiftUI 5

The final result

First step, let us define some variables:

@State editText = ""
@State editTextError : String? = nil

let editFontSize = 18   
let editTextPadding = 2 // this depends on TextFieldStyle, 5 works well for default, for others you might need to check
let errorColor = Color(UIColor(red:1,green:0,blue:0, alpha:0.5))
let normalColor = Color(UIColor(red:0,green:0,blue:0, alpha:0.1))

Second step, let us modify our TextField

            TextField<Text>("text hint", text: self.$editText, onEditingChanged: self.onEdit, onCommit: self.onCommit)
                .disableAutocorrection(true)
                .autocapitalization(.none)
                .frame(maxWidth: .infinity, alignment:.topLeading)
                .font(Font(UIFont.systemFont(ofSize: CGFloat(editFontSize))))
                .border(editTextError == nil ? normalColor : errorColor)
                .overlay(editTextError == nil ? AnyView(EmptyView()) :
                    AnyView(
                    Text(editTextError!)
                        .offset(y:CGFloat(editFontSize + 2*editTextPadding))
                        .foregroundColor(Color.orange)
                        .frame(maxWidth: .infinity, alignment: .leading)
                        .multilineTextAlignment(TextAlignment.leading)
                        .font(Font(UIFont.preferredFont(forTextStyle:.caption1)))
                        )
                )

Finally, we need to define onCommit (you can do it in onEdit as well)

func onSubmit() {
   editTextError = validateText(editText)
}

where validateText() is your custom text field validation function returning nil if no errors found or an error message otherwise.

Alternatively, you might want to create your own struct TextField and then use it as below:

            EditBox("hint", text: self.$text, onEditingChanged: self.onEdit,
                            onCommit: onCommitAccount, error: self.$textError, textSize: editFontSize,
                            textPadding: editTextPadding)
                .disableAutocorrection(true)
                .autocapitalization(.none)
                .frame(maxWidth: .infinity, alignment:.topLeading)
                .font(Font(UIFont.boldSystemFont(ofSize: CGFloat(editFontSize))))
                .textFieldStyle(RoundedBorderTextFieldStyle())

where EditBox is defined as follows:

struct EditBox: View {

@Binding<String?> var error: String?
@Binding<String> var text: String
var title : String
var onEditingChanged : (Bool) -> Void
var onCommit : () -> Void
var errorColor : Color
var normalColor : Color
var textSize : Int
var textPadding : Int
var aboveText : Bool

public init(_ title: String, text: Binding<String>, onEditingChanged: @escaping (Bool) -> Void = { _ in },
    onCommit: @escaping () -> Void = {},
    error: Binding<String?>,
    errorColor: Color = Color(UIColor(red:1,green:0,blue:0, alpha:0.5)),
    normalColor: Color = Color(UIColor(red:0,green:0,blue:0, alpha:0.1)),
    textSize : Int = 18,
    textPadding : Int = 5, // This is tested for RoundedTextFieldStyle, for others you might need to change
    aboveText: Bool = false
    )  {

    self._text = text
    self._error = error

    self.title = title
    self.onEditingChanged = onEditingChanged
    self.onCommit = onCommit
    self.errorColor = errorColor
    self.normalColor = normalColor
    self.textSize = textSize
    self.textPadding = textPadding
    self.aboveText = aboveText
}

var body : some View {
    
    let offset = aboveText ? (0 - textSize - 2*textPadding) : (textSize + 2*textPadding)
    
    return TextField<Text> (title, text: $text, onEditingChanged: onEditingChanged, onCommit: onCommit)
        .border(self.error  != nil ? self.errorColor : self.normalColor)
        .overlay(self.error == nil ? AnyView(EmptyView()) :
            AnyView(
                Text(error!)
                    .foregroundColor(Color.orange)
                    .frame(maxWidth: .infinity, alignment: .leading)
                    .multilineTextAlignment(.leading)
                    .font(Font(UIFont.preferredFont(forTextStyle:.caption1)))
                    .offset(y:CGFloat(offset))
            )
        )
}

}

Upvotes: 0

Vergiliy
Vergiliy

Reputation: 1356

After a day of work, I made an analogue of TextView.setError() on Swift. Here's what I got:

enter image description here

Code on swift 5:

import UIKit

private var rightViews = NSMapTable<UITextField, UIView>(keyOptions: NSPointerFunctions.Options.weakMemory, valueOptions: NSPointerFunctions.Options.strongMemory)
private var errorViews = NSMapTable<UITextField, UIView>(keyOptions: NSPointerFunctions.Options.weakMemory, valueOptions: NSPointerFunctions.Options.strongMemory)    

extension UITextField {
    // Add/remove error message
    func setError(_ string: String? = nil, show: Bool = true) {
        if let rightView = rightView, rightView.tag != 999 {
            rightViews.setObject(rightView, forKey: self)
        }

        // Remove message
        guard string != nil else {
            if let rightView = rightViews.object(forKey: self) {
                self.rightView = rightView
                rightViews.removeObject(forKey: self)
            } else {
                self.rightView = nil
            }

            if let errorView = errorViews.object(forKey: self) {
                errorView.isHidden = true
                errorViews.removeObject(forKey: self)
            }

            return
        }

        // Create container
        let container = UIView()
        container.translatesAutoresizingMaskIntoConstraints = false

        // Create triangle
        let triagle = TriangleTop()
        triagle.backgroundColor = .clear
        triagle.translatesAutoresizingMaskIntoConstraints = false
        container.addSubview(triagle)

        // Create red line
        let line = UIView()
        line.backgroundColor = .red
        line.translatesAutoresizingMaskIntoConstraints = false
        container.addSubview(line)

        // Create message
        let label = UILabel()
        label.text = string
        label.textColor = .white
        label.numberOfLines = 0
        label.font = UIFont.systemFont(ofSize: 15)
        label.backgroundColor = .black
        label.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 250), for: .horizontal)
        label.translatesAutoresizingMaskIntoConstraints = false
        container.addSubview(label)

        // Set constraints for triangle
        triagle.heightAnchor.constraint(equalToConstant: 10).isActive = true
        triagle.widthAnchor.constraint(equalToConstant: 15).isActive = true
        triagle.topAnchor.constraint(equalTo: container.topAnchor, constant: -10).isActive = true
        triagle.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -15).isActive = true

        // Set constraints for line
        line.heightAnchor.constraint(equalToConstant: 3).isActive = true
        line.topAnchor.constraint(equalTo: triagle.bottomAnchor, constant: 0).isActive = true
        line.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 0).isActive = true
        line.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: 0).isActive = true

        // Set constraints for label
        label.topAnchor.constraint(equalTo: line.bottomAnchor, constant: 0).isActive = true
        label.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: 0).isActive = true
        label.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 0).isActive = true
        label.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: 0).isActive = true

        if !show {
            container.isHidden = true
        }
        // superview!.superview!.addSubview(container)
        UIApplication.shared.keyWindow!.addSubview(container)

        // Set constraints for container
        container.widthAnchor.constraint(lessThanOrEqualTo: superview!.widthAnchor, multiplier: 1).isActive = true
        container.trailingAnchor.constraint(equalTo: superview!.trailingAnchor, constant: 0).isActive = true
        container.topAnchor.constraint(equalTo: superview!.bottomAnchor, constant: 0).isActive = true

        // Hide other error messages
        let enumerator = errorViews.objectEnumerator()
        while let view = enumerator!.nextObject() as! UIView? {
            view.isHidden = true
        }

        // Add right button to textField
        let errorButton = UIButton(type: .custom)
        errorButton.tag = 999
        errorButton.setImage(UIImage(named: "ic_error"), for: .normal)
        errorButton.frame = CGRect(x: 0, y: 0, width: frame.size.height, height: frame.size.height)
        errorButton.addTarget(self, action: #selector(errorAction), for: .touchUpInside)
        rightView = errorButton
        rightViewMode = .always

        // Save view with error message
        errorViews.setObject(container, forKey: self)
    }

    // Show error message
    @IBAction
    func errorAction(_ sender: Any) {
        let errorButton = sender as! UIButton
        let textField = errorButton.superview as! UITextField

        let errorView = errorViews.object(forKey: textField)
        if let errorView = errorView {
            errorView.isHidden.toggle()
        }

        let enumerator = errorViews.objectEnumerator()
        while let view = enumerator!.nextObject() as! UIView? {
            if view != errorView {
                view.isHidden = true
            }
        }

        // Don't hide keyboard after click by icon
        UIViewController.isCatchTappedAround = false
    }
}

class TriangleTop: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override func draw(_ rect: CGRect) {
        guard let context = UIGraphicsGetCurrentContext() else {
            return
        }

        context.beginPath()
        context.move(to: CGPoint(x: (rect.maxX / 2.0), y: rect.minY))
        context.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
        context.addLine(to: CGPoint(x: (rect.minX / 2.0), y: rect.maxY))
        context.closePath()

        context.setFillColor(UIColor.red.cgColor)
        context.fillPath()
    }
}

How to use:

class MyViewController: UIViewController {
    @IBOutlet weak var emailField: UITextField!

    override func viewDidLoad() {
        super.viewDidLoad()

        emailField.delegate = self
    }
}

// Validation textFields
extension MyViewController: UITextFieldDelegate {
    // Remove error message after start editing
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        textField.setError()
        return true
    }

    // Check error
    func textFieldDidBeginEditing(_ textField: UITextField) {
        Validator.isValidEmail(field: textField)
    }

    // Check error
    func textFieldDidEndEditing(_ textField: UITextField) {
        Validator.isValidEmail(field: textField, show: false)
    }
}

class Validator {
    static let EMAIL_ADDRESS =
        "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" +
        "\\@" +
        "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" +
        "(" +
            "\\." +
            "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" +
        ")+"

    // Validator e-mail from string
    static func isValidEmail(_ value: String) -> Bool {
        let string = value.trimmingCharacters(in: .whitespacesAndNewlines)
        let predicate = NSPredicate(format: "SELF MATCHES %@", Validator.EMAIL_ADDRESS)
        return predicate.evaluate(with: string) || string.isEmpty
    }

    static func isValidEmail(field: UITextField, show: Bool = true) -> Bool {
        if Validator.isValidEmail(field.text!) {
            field.setError()
            return true
        } else {
            field.setError("Error message", show: show)
        }
        return false
    }
}

Upvotes: 14

JayVDiyk
JayVDiyk

Reputation: 4487

No, you need to

  1. subclass the UITextField

  2. Create a function that setError, let's call it func setError()

  3. In that function you can create an UIImageView which contains an UIImage (error image). Set it to the rightView of UITextField by using UITextField.rightView

  4. Don't forget to set the UITextField.rightViewMode to always show

EDIT:

Alternatively, if you does not prefer subclassing. You could directly set the rightVIew of the UITextField to an UIImageView that holds an error image

Upvotes: 4

SpanTag
SpanTag

Reputation: 203

Just sharing something I use often. This is designed to fit for "bottom border only" TextFields. - Because I like them ;) - but can with ease be customized to fit whatever style

Bottom border only example

Extension for setting up the text field to only show a single bottom line:

extension UITextField {
    func setBottomBorderOnlyWith(color: CGColor) {
        self.borderStyle = .none            
        self.layer.masksToBounds = false
        self.layer.shadowColor = color
        self.layer.shadowOffset = CGSize(width: 0.0, height: 1.0)
        self.layer.shadowOpacity = 1.0
        self.layer.shadowRadius = 0.0
    }
}

Then another extension to make it flash red and shake showing that there is a validation error:

extension UITextField {
    func isError(baseColor: CGColor, numberOfShakes shakes: Float, revert: Bool) {
        let animation: CABasicAnimation = CABasicAnimation(keyPath: "shadowColor")
        animation.fromValue = baseColor
        animation.toValue = UIColor.red.cgColor
        animation.duration = 0.4
        if revert { animation.autoreverses = true } else { animation.autoreverses = false }
        self.layer.add(animation, forKey: "")

        let shake: CABasicAnimation = CABasicAnimation(keyPath: "position")
        shake.duration = 0.07
        shake.repeatCount = shakes
        if revert { shake.autoreverses = true  } else { shake.autoreverses = false }
        shake.fromValue = NSValue(cgPoint: CGPoint(x: self.center.x - 10, y: self.center.y))
        shake.toValue = NSValue(cgPoint: CGPoint(x: self.center.x + 10, y: self.center.y))
        self.layer.add(shake, forKey: "position")
    }
}

How to use:

Setup the UITextField to show only bottom border in viewDidLoad:

override func viewDidLoad() {
    myTextField.setBottomBorderOnlyWith(color: UIColor.gray.cgColor)
}

Then when some button is clicked and you do not validate the field:

@IBAction func someButtonIsClicked(_ sender: Any) {
    if let someValue = myTextField, !name.isEmpty {
        // Good To Go!
    } else {
        myTextField.isError(baseColor: UIColor.gray.cgColor, numberOfShakes: 3, revert: true)
    }
}

Upvotes: 20

Shamas S
Shamas S

Reputation: 7549

UITextField doesn't come with a validation function out of the box. You can find some open source APIs to help you accomplish this. One possible option would be to look into the SSValidationTextField api.

Code would be

var phoneValidationTextField = SSValidationTextField(frame: CGRectMake(200, 200, 150, 50))
phoneValidationTextField.validityFunction = self.isValidPhone
phoneValidationTextField.delaytime = 0.5
phoneValidationTextField.errorText = "Incorrect Format"
phoneValidationTextField.successText = "Valid Format"
phoneValidationTextField.borderStyle = UITextBorderStyle.RoundedRect
self.addSubview(phoneValidationTextField)

Upvotes: 3

Midhun MP
Midhun MP

Reputation: 107141

No, there is no built in method available for doing the same. For that you need to customize the UITextField.

There are some open-source library available for doing that. You can find one here : US2FormValidator

Upvotes: 1

Victor Sigler
Victor Sigler

Reputation: 23449

You can validate the text by setting the UITextField delegate to your view controller then do something like this :

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

   // Your method to validate the input
   // self.validateInputText()

   return true
}

And you can even change its border color if you want:

textField.layer.borderColor = UIColor.redColor().CGColor

I hope this help you.

Upvotes: 12

Related Questions