ryosuke1025/validationlibrary
A lightweight SwiftUI validation library that provides real-time input validation with customizable rules and behaviors.
Demo
| Quick Start| Advanced Usage| |---|---| | <img src="docs/assets/phone_number.gif" width="300"> | <img src="docs/assets/sign_up_form.gif" width="300"> |
Table of Contents
- Validation Modifier - ValidationState - ValidationContainer - ValidationRule - InvalidReason - ValidationTiming - ValidationInitialBehavior
Installation
Option 1: Xcode
- File → Add Package Dependencies...
- Enter:
https://github.com/Ryosuke1025/ValidationLibrary - Select a version rule
- Add to your target
Option 2: Package.swift
dependencies: [
.package(url: "https://github.com/Ryosuke1025/ValidationLibrary.git", from: "1.0.0")
]Quick Start
Minimal working example:
import SwiftUI
import ValidationLibrary
struct QuickStartView: View {
@State private var phoneNumber = ""
@State private var validationState = ValidationState()
var body: some View {
VStack {
TextField("Phone Number", text: $phoneNumber)
.keyboardType(.phonePad)
.validation(
text: $phoneNumber,
state: validationState,
rule: .numeric(minLength: 10, maxLength: 11)
)
if validationState.hasBeenValidated {
Text(validationState.isValid ? "Valid" : "Invalid")
.foregroundStyle(validationState.isValid ? .green : .red)
}
}
}
}<details> <summary><b>Full example (matches the demo capture)</b></summary>
import SwiftUI
import ValidationLibrary
struct PhoneNumberView: View {
@FocusState private var isFocused: Bool
@State private var phoneNumber = ""
@State private var validationState = ValidationState()
var body: some View {
NavigationStack {
VStack(alignment: .leading, spacing: 8) {
TextField("Phone Number", text: $phoneNumber)
.keyboardType(.phonePad)
.focused($isFocused)
.validation(
text: $phoneNumber,
state: validationState,
rule: .numeric(minLength: 10, maxLength: 11)
)
.padding(16)
.background(in: .rect(cornerRadius: 16))
Text("10-11 digits, numbers only")
.foregroundStyle(.secondary)
.font(.caption)
if validationState.hasBeenValidated {
if validationState.isValid {
Text("Valid phone number")
.foregroundStyle(.green)
.font(.caption)
} else {
Text("Invalid phone number")
.foregroundStyle(.red)
.font(.caption)
}
}
}
.padding(16)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.background(Color(.systemGroupedBackground))
.onTapGesture { isFocused = false }
.navigationTitle("Phone Number")
}
}
}</details>
Advanced Usage
Use `ValidationContainer` to validate multiple fields together.
<details>
<summary><b>Full example (matches the demo capture)</b></summary>
```swift
import SwiftUI
import ValidationLibrary
struct SignUpForm: View {
enum FocusField {
case email
case password
case confirmPassword
}
@FocusState private var focusedField: FocusField?
@State private var email = ""
@State private var password = ""
@State private var confirmPassword = ""
@State private var container = ValidationContainer()
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 32) {
emailSection
passwordSection
confirmPasswordSection
statusSection
registerButton
}
.padding(16)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color(.systemGroupedBackground))
.onTapGesture { focusedField = nil }
.navigationTitle("Sign Up")
}
}
private var emailSection: some View {
VStack(alignment: .leading, spacing: 8) {
TextField("Email", text: $email)
.textInputAutocapitalization(.never)
.keyboardType(.emailAddress)
.focused($focusedField, equals: .email)
.validation(
text: $email,
container: container,
id: "email",
rule: .email
)
.padding(16)
.background(in: .rect(cornerRadius: 16))
.overlay(alignment: .trailing) {
if let state = container.existingState(for: "email"),
state.hasBeenValidated {
if state.isValid {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green)
.padding(.trailing, 16)
} else {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.red)
.padding(.trailing, 16)
}
}
}
Text("Enter your email address")
.font(.caption)
.foregroundStyle(.secondary)
}
}
// Note: Using TextField instead of SecureField for demo recording purposes
private var passwordSection: some View {
VStack(alignment: .leading, spacing: 8) {
TextField("Password", text: $password)
.textInputAutocapitalization(.never)
.focused($focusedField, equals: .password)
.validation(
text: $password,
container: container,
id: "password",
rules: [.required, .password(minLength: 8)]
)
.padding(16)
.background(in: .rect(cornerRadius: 16))
.overlay(alignment: .trailing) {
if let state = container.existingState(for: "password"), state.hasBeenValidated {
if state.isValid {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green)
.padding(.trailing, 16)
} else {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.red)
.padding(.trailing, 16)
}
}
}
Text("8+ characters, letters and numbers")
.font(.caption)
.foregroundStyle(.secondary)
}
}
// Note: Using TextField instead of SecureField for demo recording purposes
private var confirmPasswordSection: some View {
VStack(alignment: .leading, spacing: 8) {
TextField("Confirm Password", text: $confirmPassword)
.textInputAutocapitalization(.never)
.focused($focusedField, equals: .confirmPassword)
.validation(
text: $confirmPassword,
container: container,
id: "confirmPassword",
rule: .custom { text in
text == password && !text.isEmpty
}
)
.onChange(of: password) {
if let state = container.existingState(for: "confirmPassword"),
state.hasBeenValidated {
container.validateAll()
}
}
.padding(16)
.background(in: .rect(cornerRadius: 16))
.overlay(alignment: .trailing) {
if let state = container.existingState(for: "confirmPassword"), state.hasBeenValidated {
if state.isValid {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green)
.padding(.trailing, 16)
} else {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.red)
.padding(.trailing, 16)
}
}
}
Text("Must match password")
.font(.caption)
.foregroundStyle(.secondary)
}
}
@ViewBuilder
private var statusSection: some View {
if container.hasBeenFullyValidated {
if container.isFullyValid {
Text("Valid Input")
.foregroundStyle(.green)
} else {
Text("Invalid Input")
.foregroundStyle(.red)
}
}
}
private var registerButton: some View {
Button("Register") {
focusedField = nil
container.validateAll()
}
.buttonStyle(.borderedProminent)
}
}
```
</details>Debug Warnings (Misconfiguration)
In DEBUG builds, the library reports developer-facing issues to help you catch misconfigurations early:
| Type | Examples | |---|---| | <img src="docs/assets/runtime-warning.svg" width="18" height="18" alt="Runtime warning" /> Runtime warning | <ul><li>Duplicate <code>.required</code></li><li>Multiple format rules (<code>.email</code> / <code>.password</code> / <code>.numeric</code>)</li></ul> | | <img src="docs/assets/assertion-failure.svg" width="18" height="18" alt="Assertion failure" /> Assertion failure | <ul><li><code>minLength</code> > <code>maxLength</code></li></ul> |
API Reference
Validation Modifier
Single State
func validation(
text: Binding<String>,
state: ValidationState,
rules: [ValidationRule], // or rule: ValidationRule
timing: ValidationTiming = .realtime(),
behavior: ValidationInitialBehavior = .afterFirstEdit
) -> some ViewWith Container
func validation(
text: Binding<String>,
container: ValidationContainer,
id: String,
rules: [ValidationRule], // or rule: ValidationRule
timing: ValidationTiming = .realtime(),
behavior: ValidationInitialBehavior = .afterFirstEdit
) -> some ViewValidationState
| Member | Type | Notes | |---|---|---| | isValid | Bool | Current validity. | | hasBeenValidated | Bool | Whether validation has run at least once. | | hasBeenEdited | Bool | Whether the user has edited the field. | | hasBeenBlurred | Bool | Whether the field has lost focus at least once. | | invalidReason | InvalidReason? | Failure reason (nil when valid / not validated). | | reset() | () | Reset state flags and validity. |
ValidationContainer
| Member | Type | Notes | |---|---|---| | isFullyValid | Bool | False when empty; otherwise true only when all fields are valid. | | hasBeenFullyValidated | Bool | False when empty; otherwise true only when all states have been validated. | | state(for:) | ValidationState | Get or create a state for an id. | | existingState(for:) | ValidationState? | Get an existing state (nil if not created yet). | | resetAll() | () | Reset all states. | | validateAll() | () | Validate all registered states. |
ValidationRule
| Case | Notes | |---|---| | .required(minLength:maxLength:) | Optional length constraints. | | .email(minLength:maxLength:) | Optional length constraints. | | .password(minLength:maxLength:) | Must contain at least one letter and one digit. Optional length constraints. | | .numeric(minLength:maxLength:) | Optional length constraints. | | .regex(String) | Pattern string is used for validation. | | .custom(messageKey:validator:) | messageKey is optional and can help map UI messages when multiple customs exist. |
InvalidReason
| Case | Meaning | |---|---| | .empty | The input is empty (or whitespace-only). | | .emailFormat | The input is not a valid email format. | | .passwordFormat | The input is not a valid password format. | | .numericFormat | The input contains non-numeric characters. | | .regexFormat(id:regex:) | The input does not match a regex. (id is currently nil from the library.) | | .tooShort(min:actual:) | The input is shorter than the minimum length. | | .tooLong(max:actual:) | The input is longer than the maximum length. | | .custom(id:) | Custom validation failed (id can be used for UI mapping). |
ValidationTiming
| Case | Meaning | |---|---| | .realtime(debounce:) | Validate while typing (optional debounce). | | .onFocusLoss | Validate when focus leaves the field. |
ValidationInitialBehavior
| Case | Meaning | |---|---| | .afterFirstEdit | Start validating after the first edit. | | .onStart | Validate immediately on appearance. | | .afterFirstBlur | Start validating after the first blur. |
Requirements
- iOS 17.0+ / macOS 14.0+ / watchOS 10.0+ / tvOS 17.0+
- Swift 6.0+
- Xcode 16.0+
License
ValidationLibrary is available under the MIT license. See the LICENSE file for more info.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Package Metadata
Repository: ryosuke1025/validationlibrary
Default branch: main
README: README.md