Skip to content

Instantly share code, notes, and snippets.

@Codelaby
Last active May 4, 2024 09:04
Show Gist options
  • Save Codelaby/51596073c7ab4ead2f7a272007b45535 to your computer and use it in GitHub Desktop.
Save Codelaby/51596073c7ab4ead2f7a272007b45535 to your computer and use it in GitHub Desktop.
validate_field.swift
//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