Reputation: 707
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
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)
}
}
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
Reputation: 5249
This is as close as I could get to what I've seen in Android. Tested in SwiftUI 5
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
Reputation: 1356
After a day of work, I made an analogue of TextView.setError()
on Swift. Here's what I got:
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
Reputation: 4487
No, you need to
subclass the UITextField
Create a function that setError, let's call it func setError()
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
UITextField.rightViewMode
to always showEDIT:
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
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
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
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
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
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