Last active
May 4, 2024 09:04
-
-
Save Codelaby/51596073c7ab4ead2f7a272007b45535 to your computer and use it in GitHub Desktop.
validate_field.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//https://betterprogramming.pub/a-data-validation-solution-utilizing-swift-property-wrappers-and-swiftui-view-extensions-ae2db2209a32 | |
import SwiftUI | |
public protocol ValidationRule { | |
associatedtype Value: Equatable | |
associatedtype Failure: Error | |
typealias ValidationResult = Result<Value, Failure> | |
init() | |
var fallbackValue: Value { get } | |
func validate(_ value: Value) -> Result<Value, Failure> | |
} | |
extension ValidationRule where Value == String { | |
var fallbackValue: Value { .init() } // returns empty String | |
} | |
extension ValidationRule where Value: ExpressibleByNilLiteral { | |
var fallbackValue: Value { .init(nilLiteral: ()) } // returns nil | |
} | |
typealias ErrorMessage = String | |
extension ErrorMessage: Error {} | |
struct WordRule: ValidationRule { | |
let maxLength: Int | |
init() { | |
self.maxLength = 12 // default value for maxLength | |
} | |
init(maxLength: Int) { | |
self.maxLength = maxLength | |
} | |
func validate(_ value: String) -> Result<String, ErrorMessage> { | |
// value must be less than or equal to max length | |
guard value.count <= maxLength else { | |
return .failure("Word may not exceed " + maxLength.description + " characters") | |
} | |
guard value.allSatisfy({char in char.isLetter}) else { | |
return .failure("Word may contain only letters") | |
} | |
// successful validation | |
return .success(value) | |
} | |
} | |
@propertyWrapper | |
struct Validated<Rule: ValidationRule> { | |
var wrappedValue: Rule.Value | |
private var rule: Rule | |
// usage: @Validated(Rule()) var value: String = "initial value" | |
init(wrappedValue: Rule.Value, _ rule: Rule) { | |
self.rule = rule | |
self.wrappedValue = wrappedValue | |
} | |
} | |
extension Validated { | |
// usage: @Validated<Rule> var value: String = "initial value" | |
init(wrappedValue: Rule.Value) { | |
self.init(wrappedValue: wrappedValue, Rule.init()) | |
} | |
// usage: @Validated<Rule> var value { | |
init() { | |
let rule = Rule.init() | |
self.init(wrappedValue: rule.fallbackValue, rule) | |
} | |
} | |
struct DictionaryEntry { | |
// property wrapper using the default init for WordRule | |
@Validated(WordRule()) var headWord = "" | |
} | |
extension Validated { | |
// provides access to the validation result using $ notation | |
public var projectedValue: Rule.ValidationResult { rule.validate(wrappedValue) } | |
} | |
extension Validated: Encodable where Rule.Value: Encodable { | |
func encode(to encoder: Encoder) throws { | |
var container = encoder.singleValueContainer() | |
switch projectedValue { | |
case .success(let validated): | |
try container.encode(validated) | |
case .failure(_): | |
try container.encode(Rule().fallbackValue) | |
} | |
} | |
} | |
extension Validated: Decodable where Rule.Value: Decodable { | |
init(from decoder: Decoder) throws { | |
let container = try decoder.singleValueContainer() | |
if let value = try? container.decode(Rule.Value.self) { | |
self.init(wrappedValue: value, Rule()) | |
} | |
else { // decoding FAILED. Recover by returning fallback value | |
self.init(wrappedValue: Rule().fallbackValue, Rule()) | |
} | |
} | |
} | |
extension View { | |
public func validate<Rule>(_ value: Binding<Rule.Value>, rule: Rule, validation: @escaping (Rule.ValidationResult) -> Void) -> some View where Rule: ValidationRule { | |
self | |
// when value changes, the escaping function will fire | |
.onChange(of: value.wrappedValue) { oldValue, newValue in | |
let result = rule.validate(newValue) | |
validation(result) // fire escaping function | |
} | |
// when field is submitted, the value will be replaced with a valid value | |
// this is important if any transformation was made to the value | |
// within the validation rule | |
.onSubmit { | |
let result = rule.validate(value.wrappedValue) | |
if case .success(let validated) = result { | |
if value.wrappedValue != validated { | |
value.wrappedValue = validated // update value | |
} | |
} | |
} | |
} | |
} | |
struct EmailRule: ValidationRule { | |
func validate(_ value: String) -> Result<String, ErrorMessage> { | |
// Verificar si el valor está vacío | |
guard !value.isEmpty else { | |
return .failure("Campo requerido") | |
} | |
// Utilizar expresión regular para validar el formato del correo electrónico | |
let emailRegex = #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"# | |
let predicate = NSPredicate(format:"SELF MATCHES %@", emailRegex) | |
// Verificar si el valor coincide con el patrón de correo electrónico | |
guard predicate.evaluate(with: value) else { | |
return .failure("Correo electrónico inválido") | |
} | |
// Validación exitosa | |
return .success(value) | |
} | |
} | |
//....... | |
struct RangeIntegerStyle: ParseableFormatStyle { | |
var parseStrategy: RangeIntegerStrategy = .init() | |
let range: ClosedRange<Int> | |
func format(_ value: Int) -> String { | |
let constrainedValue = min(max(value, range.lowerBound), range.upperBound) | |
return "\(constrainedValue)" | |
} | |
} | |
struct RangeIntegerStrategy: ParseStrategy { | |
func parse(_ value: String) throws -> Int { | |
return Int(value) ?? 1 | |
} | |
} | |
/// Allow writing `.ranged(0...5)` instead of `RangeIntegerStyle(range: 0...5)`. | |
extension FormatStyle where Self == RangeIntegerStyle { | |
static func ranged(_ range: ClosedRange<Int>) -> RangeIntegerStyle { | |
return RangeIntegerStyle(range: range) | |
} | |
} | |
struct ContentView: View { | |
@State var field1: String = "" | |
@State var field2: String = "" | |
@State private var errorMessages: [String: String] = [:] | |
let urlStyle = URL.FormatStyle(path: .omitWhen(.path, matches: ["/"]), query: .omitWhen(.query, matches: [""])) // <- Customise your URL Style Format | |
@State private var url: URL? | |
@State private var name = "" | |
@State private var gridBlockSize: Int = 0 | |
let numberFormatter: NumberFormatter = { | |
let formatter = NumberFormatter() | |
formatter.minimum = .init(integerLiteral: 1) | |
formatter.maximum = .init(integerLiteral: 10) | |
formatter.generatesDecimalNumbers = false | |
formatter.maximumFractionDigits = 0 | |
return formatter | |
}() | |
var body: some View { | |
VStack { | |
TextField("URL", value: $url, format: urlStyle , prompt: Text("URL")) | |
.keyboardType(.URL) | |
TextField("Name", text: $name, prompt: Text("Name")) | |
Button("save") { | |
if let url { | |
print("url = \(url)") | |
} else { | |
print("url is nil") | |
} | |
} | |
} | |
Form { | |
// TextField("range int", value: $gridBlockSize, format: .ranged(1...10)) | |
// .keyboardType(.numberPad) | |
TextField("range int", value: $gridBlockSize, format: .number) | |
.keyboardType(.numberPad) | |
.onChange(of: gridBlockSize) { oldValue, newValue in | |
if newValue > 10 { | |
gridBlockSize = oldValue | |
} | |
} | |
Section() { | |
VStack { | |
TextField("new word", text: $field1) | |
Text( errorMessages["username_field"] ?? "") | |
.foregroundColor(.red) | |
} | |
VStack { | |
TextField("email", text: $field2) | |
.keyboardType(.emailAddress) | |
Text(errorMessages["email_field"] ?? "") | |
.foregroundColor(.red) | |
} | |
} | |
} | |
.validate($field1, rule: WordRule(maxLength: 10)) { result in | |
switch result { | |
case .success(_): // clear error message | |
errorMessages["username_field"] = "" | |
case .failure(let errorMessage): // display error message | |
//errorMessageField1 = message.description | |
errorMessages["username_field"] = errorMessage.description | |
} | |
} | |
.validate($field2, rule: EmailRule()) { result in | |
switch result { | |
case .success(_): // clear error message | |
errorMessages["email_field"] = "" | |
case .failure(let errorMessage): // display error message | |
//errorMessageField1 = message.description | |
errorMessages["email_field"] = errorMessage.description | |
} | |
} | |
.onSubmit { | |
let emailRule = EmailRule() | |
let validationResult = emailRule.validate(field2) | |
switch validationResult { | |
case .success(let value): | |
errorMessages["email_field"] = "" | |
case .failure(let errorMessage): | |
errorMessages["email_field"] = errorMessage.description | |
} | |
} | |
} | |
func testValidation() { | |
let wordRule = WordRule() // initialize validator object | |
// create some sample strings to validate | |
let tooLong = wordRule.validate("toomanycharactersforoneword") | |
let hasNumbers = wordRule.validate("h3ll0") | |
let noErrors = wordRule.validate("hello") | |
let unTrimmed = wordRule.validate(" world") | |
// print validation results | |
print(tooLong) // failure(Word may not exceed 12 characters) | |
print(hasNumbers) // failure(Word may contain only letters) | |
print(noErrors) // success("hello") | |
print(unTrimmed) // success("world") | |
} | |
} | |
#Preview { | |
ContentView() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment