Skip to content

Instantly share code, notes, and snippets.

@azamsharp
Created March 24, 2025 16:03
Show Gist options
  • Select an option

  • Save azamsharp/cf7eb9b66fe6d04a2ef19c73394035b0 to your computer and use it in GitHub Desktop.

Select an option

Save azamsharp/cf7eb9b66fe6d04a2ef19c73394035b0 to your computer and use it in GitHub Desktop.

Revisions

  1. azamsharp created this gist Mar 24, 2025.
    142 changes: 142 additions & 0 deletions ValidatePropertyWrapper.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,142 @@
    //
    // ContentView.swift
    // ValidationDemo
    //
    // Created by Mohammad Azam on 3/24/25.
    //

    import SwiftUI

    // Validation View Modifier
    struct ValidationModifier: ViewModifier {

    let errorMessage: String?

    func body(content: Content) -> some View {
    VStack(alignment: .leading, spacing: 4) {
    content

    Text(errorMessage ?? "")
    .font(.caption) // Smaller text for error message
    .foregroundColor(.red)
    .frame(height: 20) // Keep consistent height
    .opacity(errorMessage == nil ? 0 : 1) // Hide when no error
    .animation(.easeInOut(duration: 0.2), value: errorMessage) // Smooth transition
    }
    }
    }

    extension View {
    func withValidation(_ errorMessage: String?) -> some View {
    self.modifier(ValidationModifier(errorMessage: errorMessage))
    }
    }




    enum RegexExpression: String {
    case email = #"^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$"#
    }

    enum ValidationRule {
    case required(String)
    case regularExpression(RegexExpression, String)
    }

    @propertyWrapper
    struct Validate<T: Equatable> {

    private var value: T
    private var initialValue: T
    private var rules: [ValidationRule]
    private var isDirty = false

    var wrappedValue: T {
    get { value }
    set {

    if newValue != initialValue {
    isDirty = true
    }

    value = newValue
    }
    }

    var projectedValue: String? {

    guard isDirty else { return nil }

    for rule in rules {
    if let message = validate(rule: rule) {
    return message
    }
    }

    return nil
    }

    private func validate(rule: ValidationRule) -> String? {

    switch rule {
    case .required(let message):
    return validateRequired(message: message)
    case .regularExpression(let regexExpression, let message):
    return validateRegex(pattern: regexExpression.rawValue, message: message)
    }
    }

    private func validateRequired(message: String) -> String? {

    if let stringValue = value as? String, stringValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
    return message
    }

    return nil
    }

    private func validateRegex(pattern: String, message: String) -> String? {
    guard let stringValue = value as? String else { return nil }

    let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive)
    let range = NSRange(location: 0, length: stringValue.utf16.count)

    return regex?.firstMatch(in: stringValue, options: [], range: range) == nil ? message : nil
    }

    init(wrappedValue: T, _ rules: ValidationRule...) {
    self.value = wrappedValue
    self.initialValue = wrappedValue
    self.rules = rules
    }

    }

    struct Customer {

    @Validate(
    .required("Email is required."),
    .regularExpression(.email, "Email should be in correct format.")
    )
    var email: String = ""

    }

    struct ContentView: View {

    @State private var customer = Customer()

    var body: some View {
    VStack {
    TextField("Email", text: $customer.email)
    .textFieldStyle(.roundedBorder)
    .withValidation(customer.$email)
    }
    .padding()
    }
    }

    #Preview {
    ContentView()
    }