Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions ExampleApp/ExampleApp/UI/ContentScreen/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,35 +11,42 @@ import FormView
struct ContentView: View {
@ObservedObject var viewModel: ContentViewModel

@State private var isAllFieldValid = false

var body: some View {
FormView(
validate: [.manual, .onFieldValueChanged, .onFieldFocus],
hideError: .onValueChanged
hideError: .onValueChanged,
isAllFieldValid: $isAllFieldValid
) { proxy in
FormField(
value: $viewModel.name,
rules: viewModel.nameValidationRules
rules: viewModel.nameValidationRules,
isRequired: true
) { failedRules in
TextInputField(title: "Name", text: $viewModel.name, failedRules: failedRules)
}
.disabled(viewModel.isLoading)
FormField(
value: $viewModel.age,
rules: viewModel.ageValidationRules
rules: viewModel.ageValidationRules,
isRequired: false
) { failedRules in
TextInputField(title: "Age", text: $viewModel.age, failedRules: failedRules)
}
.disabled(viewModel.isLoading)
FormField(
value: $viewModel.pass,
rules: viewModel.passValidationRules
rules: viewModel.passValidationRules,
isRequired: true
) { failedRules in
SecureInputField(title: "Password", text: $viewModel.pass, failedRules: failedRules)
}
.disabled(viewModel.isLoading)
FormField(
value: $viewModel.confirmPass,
rules: viewModel.confirmPassValidationRules
rules: viewModel.confirmPassValidationRules,
isRequired: true
) { failedRules in
SecureInputField(title: "Confirm Password", text: $viewModel.confirmPass, failedRules: failedRules)
}
Expand All @@ -52,7 +59,7 @@ struct ContentView: View {
print("Form is valid: \(await proxy.validate())")
}
}
.disabled(viewModel.isLoading)
.disabled(isAllFieldValid == false || viewModel.isLoading)
}
.padding(.horizontal, 16)
.padding(.top, 40)
Expand Down
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,18 @@ struct MyField: View {
struct ContentView: View {
@State var name: String = ""

@State private var isAllFieldValid = false

var body: some View {
FormView( First failed field
validate: [.manual], // Form will be validated on user action.
hideError: .onValueChanged // Error for field wil be hidden on field value change.
hideError: .onValueChanged, // Error for field wil be hidden on field value change.
isAllFieldValid: $isAllFieldValid // Property indicating the result of validation of all fields without focus
) { proxy in
FormField(
value: $name,
rules: [ValidationRule.notEmpty(conditions: [.manual], message: "Name field should no be empty")]
rules: [TextValidationRule.notEmpty(message: "Name field should no be empty")],
isRequired: true, // field parameter, necessary for correct determination of validity of all fields
) { failedRules in
MyField(title: "Name", text: $name, failedRules: failedRules)
}
Expand All @@ -73,6 +77,7 @@ struct ContentView: View {
// Validate form on user action.
print("Form is valid: \(proxy.validate())")
}
.disabled(isAllFieldValid == false) // Use isAllFieldValid to automatically disable the action button
}
}
}
Expand All @@ -92,6 +97,9 @@ Error for each field gets hidden at one of three specific times:
* `onFocus` - field with error is focused..
* `onFucusLost` - field with error lost focus.

### Is All Field Valid
Property indicating the result of validation of all fields without focus. Using this property you can additionally build ui update logic, for example block the next button.

### Custom Validation Rules

Extend `ValidationRule`:
Expand Down Expand Up @@ -146,7 +154,7 @@ FormView doesn't use any external dependencies.
dependencies: [
.package(
url: "https://github.com/MobileUpLLC/FormView",
.upToNextMajor(from: "1.1.2")
.upToNextMajor(from: "1.3.0")
)
]
```
Expand Down
14 changes: 12 additions & 2 deletions Sources/FormView/FormField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ public struct FormField<Content: View>: View {

@State private var failedValidationRules: [ValidationRule] = []

private let isRequired: Bool
private var isValid: Bool { getValidationStatus() }

// Fields Focus
@FocusState private var isFocused: Bool
@State private var id: String = UUID().uuidString
Expand All @@ -26,9 +29,11 @@ public struct FormField<Content: View>: View {
public init(
value: Binding<String>,
rules: [ValidationRule] = [],
isRequired: Bool,
@ViewBuilder content: @escaping ([ValidationRule]) -> Content
) {
self._value = value
self.isRequired = isRequired
self.content = content
self.validator = FieldValidator(rules: rules)
}
Expand Down Expand Up @@ -57,6 +62,7 @@ public struct FormField<Content: View>: View {
}
]
)
.preference(key: FieldsValidationKey.self, value: [isValid])
.focused($isFocused)

// Fields Validation
Expand Down Expand Up @@ -93,8 +99,8 @@ public struct FormField<Content: View>: View {

if
validationBehaviour.contains(.onFieldFocus)
&& failedValidationRules.isEmpty
&& newValue == true
&& failedValidationRules.isEmpty
&& newValue == true
{
failedValidationRules = await validator.validate(
value: value,
Expand All @@ -105,4 +111,8 @@ public struct FormField<Content: View>: View {
}
}
}

private func getValidationStatus() -> Bool {
isRequired ? failedValidationRules.isEmpty && value.isEmpty == false : failedValidationRules.isEmpty
}
}
6 changes: 6 additions & 0 deletions Sources/FormView/FormView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ private class FormStateHandler: ObservableObject {

public struct FormView<Content: View>: View {
@StateObject private var formStateHandler = FormStateHandler()
@Binding private var isAllFieldValid: Bool
@ViewBuilder private let content: (FormValidator) -> Content

private let errorHideBehaviour: ErrorHideBehaviour
Expand All @@ -71,11 +72,13 @@ public struct FormView<Content: View>: View {
public init(
validate: [ValidationBehaviour] = [.manual],
hideError: ErrorHideBehaviour = .onValueChanged,
isAllFieldValid: Binding<Bool> = .constant(true),
@ViewBuilder content: @escaping (FormValidator) -> Content
) {
self.content = content
self.validationBehaviour = validate
self.errorHideBehaviour = hideError
self._isAllFieldValid = isAllFieldValid
}

public var body: some View {
Expand All @@ -85,6 +88,9 @@ public struct FormView<Content: View>: View {
.onPreferenceChange(FieldStatesKey.self) { [weak formStateHandler] newStates in
formStateHandler?.updateFieldStates(newStates: newStates)
}
.onPreferenceChange(FieldsValidationKey.self) { validationResults in
isAllFieldValid = validationResults.contains(false) == false
}
.onSubmit(of: .text) { [weak formStateHandler] in
formStateHandler?.submit()
}
Expand Down
16 changes: 16 additions & 0 deletions Sources/FormView/Preference/FieldsValidationKey.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// FieldsValidationKey.swift
// FormView
//
// Created by Victor Kostin on 31.03.2025.
//

import SwiftUI

struct FieldsValidationKey: PreferenceKey {
static var defaultValue: [Bool] = []

static func reduce(value: inout [Bool], nextValue: () -> [Bool]) {
value += nextValue()
}
}