A lightweight SwiftUI validation library that provides real-time input validation with customizable rules and behaviors.
File → Add Package Dependencies...
Enter: https://github.com/Ryosuke1025/ValidationLibrary
Select a version rule
Add to your target
dependencies: [
. package ( url: " https://github.com/Ryosuke1025/ValidationLibrary.git " , from: " 1.0.0 " )
]
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 " )
}
}
}
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
Duplicate .required Multiple format rules (.email / .password / .numeric)
Assertion failure
func validation(
text: Binding < String > ,
state: ValidationState ,
rules: [ ValidationRule ] , // or rule: ValidationRule
timing: ValidationTiming = . realtime( ) ,
behavior: ValidationInitialBehavior = . afterFirstEdit
) -> some View
func validation(
text: Binding < String > ,
container: ValidationContainer ,
id: String ,
rules: [ ValidationRule ] , // or rule: ValidationRule
timing: ValidationTiming = . realtime( ) ,
behavior: ValidationInitialBehavior = . afterFirstEdit
) -> some View
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.
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.
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.
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).
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.
iOS 17.0+ / macOS 14.0+ / watchOS 10.0+ / tvOS 17.0+
Swift 6.0+
Xcode 16.0+
ValidationLibrary is available under the MIT license. See the LICENSE file for more info.
Ryosuke Suzaki (@Ryosuke1025 )
Contributions are welcome! Please feel free to submit a Pull Request.