Skip to content

Ryosuke1025/ValidationLibrary

Repository files navigation

ValidationLibrary

A lightweight SwiftUI validation library that provides real-time input validation with customizable rules and behaviors.

Release License

Demo

Quick Start Advanced Usage

Table of Contents

Installation

Option 1: Xcode

  1. File → Add Package Dependencies...
  2. Enter: https://github.com/Ryosuke1025/ValidationLibrary
  3. Select a version rule
  4. 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)
            }
        }
    }
}
Full example (matches the demo capture)
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")
        }
    }
}

Advanced Usage

Use ValidationContainer to validate multiple fields together.

Full example (matches the demo capture)
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)
    }
}

Debug Warnings (Misconfiguration)

In DEBUG builds, the library reports developer-facing issues to help you catch misconfigurations early:

Type Examples
Runtime warning Runtime warning
  • Duplicate .required
  • Multiple format rules (.email / .password / .numeric)
Assertion failure Assertion failure
  • minLength > maxLength

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 View

With Container

func validation(
    text: Binding<String>,
    container: ValidationContainer,
    id: String,
    rules: [ValidationRule], // or rule: ValidationRule
    timing: ValidationTiming = .realtime(),
    behavior: ValidationInitialBehavior = .afterFirstEdit
) -> some View

ValidationState

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.

Author

Ryosuke Suzaki (@Ryosuke1025)

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages