Skip to content

Instantly share code, notes, and snippets.

@jeneiv
Last active April 8, 2024 14:21
Show Gist options
  • Save jeneiv/70f5ffcaa90c04b4dab1182ceaccf08e to your computer and use it in GitHub Desktop.
Save jeneiv/70f5ffcaa90c04b4dab1182ceaccf08e to your computer and use it in GitHub Desktop.
SwiftUI Form with `PreferenceKey` based validation
// 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