在 iOS UITextField 上显示类似于 Android 的 TextView.setError() 的验证错误

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

有没有办法在 UITextField 上显示验证错误,类似于 Android 在 swift 中的 TextView.setError()

您可以通过将 UITextField 委托设置到您的视图控制器来验证文本,然后执行如下操作:

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

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

   return true
}

如果需要,您甚至可以更改其边框颜色:

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

希望对你有所帮助。

不,没有可用于执行相同操作的内置方法。为此,您需要自定义 UITextField.

有一些开源库可用于执行此操作。你可以在这里找到一个:US2FormValidator

UITextField 没有开箱即用的验证功能。您可以找到一些开源 API 来帮助您完成此任务。一种可能的选择是查看 SSValidationTextField api。

代码为

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)

不,你需要

  1. 子类 UITextField

  2. 创建一个设置错误的函数,我们称之为func setError()

  3. 在该函数中,您可以创建一个包含 UIImage(错误图像)的 UIImageView。使用UITextField.rightView

  4. 设置到UITextField的rightView
  5. 不要忘记将 UITextField.rightViewMode 设置为始终显示

编辑:

或者,如果您不喜欢子类化。您可以直接将 UITextField 的 rightVIew 设置为包含错误图像

UIImageView

只是分享一些我经常使用的东西。这是为 "bottom border only" TextFields 设计的。 - 因为我喜欢它们 ;) - 但可以轻松定制以适应任何风格

Bottom border only example

将文本字段设置为仅显示一条底线的扩展:

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
    }
}

再用一个插件让它闪红摇晃表示验证错误:

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")
    }
}

使用方法:

设置 UITextField 以在 viewDidLoad 中仅显示底部边框:

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

然后当某些按钮被点击并且你没有验证字段时:

@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)
    }
}

经过一天的工作,我在Swift上做了一个TextView.setError()的类比。这是我得到的:

代码 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()
    }
}

使用方法:

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
    }
}

这是我在 Android 中看到的最接近的内容。在 SwiftUI 5

中测试

第一步,让我们定义一些变量:

@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))

第二步,让我们修改我们的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)))
                        )
                )

最后,我们需要定义onCommit(你也可以在onEdit中定义)

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

其中 validateText() 是您的自定义文本字段验证函数,如果未发现错误或错误消息,则返回 nil

或者,您可能想创建自己的结构体 TextField,然后按如下方式使用它:

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

其中EditBox定义如下:

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

}

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

用法:

rightView 是根据您的要求传递的图像名称。如果要删除右视图,请设置为空。

tintColor 是可选的,可以根据您的要求使用任何一个。

设置消息(errorMessage) 值,如果你想显示像 android 这样的错误消息。

移除 RightView

textField.addRightView(rightView: "")

添加 RightView

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

将 RightView 和错误消息添加为 android

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