Last active
April 8, 2024 14:21
-
-
Save jeneiv/70f5ffcaa90c04b4dab1182ceaccf08e to your computer and use it in GitHub Desktop.
SwiftUI Form with `PreferenceKey` based validation
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
// MARK: - Preference Key Declaration | |
struct FormValidationPreferenceKey: PreferenceKey { | |
static var defaultValue: [Bool] = [] | |
static func reduce(value: inout [Bool], nextValue: () -> [Bool]) { | |
value += nextValue() | |
} | |
} | |
// MARK: - `validate` viewmodifier | |
struct ValidationModifier : ViewModifier { | |
let validation: () -> Bool | |
func body(content: Content) -> some View { | |
content | |
.preference( | |
key: FormValidationPreferenceKey.self, | |
value: [validation()] | |
) | |
} | |
} | |
// MARK: - These two extensions are just shorthands for adding modifiers | |
extension TextField { | |
func validate(_ flag : @escaping () -> Bool) -> some View { | |
self | |
.modifier(ValidationModifier(validation: flag)) | |
} | |
} | |
extension SecureField { | |
func validate(_ flag : @escaping () -> Bool) -> some View { | |
self | |
.modifier(ValidationModifier(validation: flag)) | |
} | |
} | |
// MARK: - Form Container Implementation | |
struct ValidatedFormContainerView<Content : View> : View { | |
@State private var validationResults : [Bool] = [] | |
@ViewBuilder var content : (( @escaping () -> Bool)) -> Content | |
var body: some View { | |
content(validate) | |
.onPreferenceChange(FormValidationPreferenceKey.self) { value in | |
validationResults = value | |
} | |
} | |
private func validate() -> Bool { | |
validationResults.allSatisfy{ $0 } | |
} | |
} | |
// MARK: ViewModel Implementation | |
class FormViewModel: ObservableObject { | |
@Published var email = "" | |
@Published var password = "" | |
@Published var name = "" | |
@Published var surname = "" | |
func isEmailValid() -> Bool { | |
matchesRegex( | |
regex: "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}", | |
on: email | |
) | |
} | |
private func matchesRegex(regex: String, on string: String) -> Bool { | |
let predicate = NSPredicate(format: "SELF MATCHES %@", regex) | |
return predicate.evaluate(with: string) | |
} | |
} | |
// MARK: Example Form View Implementation | |
struct FormView: View { | |
@StateObject var formViewModel: FormViewModel | |
var body: some View { | |
ValidatedFormContainerView { isValid in | |
VStack { | |
Text("Form") | |
.font(.headline) | |
TextField("Email", text: $formViewModel.email) | |
.validate { | |
formViewModel.isEmailValid() | |
} | |
.disableAutocorrection(true) | |
.autocapitalization(.none) | |
TextField("Name", text: $formViewModel.name) | |
.validate { | |
!formViewModel.name.isEmpty | |
} | |
TextField("Surname", text: $formViewModel.surname) | |
.validate { | |
!formViewModel.surname.isEmpty | |
} | |
SecureField("Password", text: $formViewModel.password) | |
.validate { | |
formViewModel.password.count > 10 | |
} | |
Spacer() | |
Button { | |
if !isValid() { | |
return | |
} | |
print("Form Valid") | |
} label: { | |
Text("Ok") | |
.cornerRadius(16) | |
} | |
.disabled(!isValid()) | |
} | |
} | |
.padding() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment