Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce SpeziValidation #21

Merged
merged 13 commits into from
Nov 5, 2023
16 changes: 13 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ let package = Package(
],
products: [
.library(name: "SpeziViews", targets: ["SpeziViews"]),
.library(name: "SpeziPersonalInfo", targets: ["SpeziPersonalInfo"])
.library(name: "SpeziPersonalInfo", targets: ["SpeziPersonalInfo"]),
.library(name: "SpeziValidation", targets: ["SpeziValidation"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.4"))
],
targets: [
.target(
Expand All @@ -31,11 +35,17 @@ let package = Package(
.target(name: "SpeziViews")
]
),
.target(
name: "SpeziValidation",
dependencies: [
.target(name: "SpeziViews"),
.product(name: "OrderedCollections", package: "swift-collections")
]
),
.testTarget(
name: "SpeziViewsTests",
dependencies: [
.target(name: "SpeziViews"),
.target(name: "SpeziPersonalInfo")
.target(name: "SpeziViews")
]
)
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// This source file is part of the Stanford Spezi open-source project
//
// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import SwiftUI


/// The debounce duration for Validation Engine
struct ValidationDebounceDurationKey: EnvironmentKey {
static let defaultValue: Duration = .seconds(0.5)
}


extension EnvironmentValues {
/// The configurable debounce duration for input submission.
///
/// Having a debounce like this, ensures that validation error messages don't get into the way when a user
/// is actively typing into a text field.
/// This duration is used to debounce repeated calls to ``ValidationEngine/submit(input:debounce:)`` where `debounce` is set to `true`.
public var validationDebounce: Duration {
get {
self[ValidationDebounceDurationKey.self]
}
set {
self[ValidationDebounceDurationKey.self] = newValue
}

Check warning on line 30 in Sources/SpeziValidation/Configuration/ValidationDebounceDuration.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziValidation/Configuration/ValidationDebounceDuration.swift#L28-L30

Added lines #L28 - L30 were not covered by tests
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// This source file is part of the Stanford Spezi open-source project
//
// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import SwiftUI


extension ValidationEngine {
/// The configuration of a ``ValidationEngine``.
public struct Configuration: OptionSet, EnvironmentKey, Equatable {
/// This configuration controls the behavior of the ``ValidationEngine/displayedValidationResults`` property.
///
/// If ``ValidationEngine/submit(input:debounce:)`` is called with empty input and this option is set, then the
/// ``ValidationEngine/displayedValidationResults`` will display no failed validations. However,
/// ``ValidationEngine/displayedValidationResults`` will still display all validations if validation is done through a manual call to ``ValidationEngine/runValidation(input:)``.
public static let hideFailedValidationOnEmptySubmit = Configuration(rawValue: 1 << 0)

/// This configuration controls the behavior of the ``ValidationEngine/inputValid`` property.
///
/// If this configuration is set, the Validation Engine will treat no input (a validation engine
/// that was never run) as being valid. Otherwise, invalid.
public static let considerNoInputAsValid = Configuration(rawValue: 1 << 1)

/// Default value without any configuration options.
public static let defaultValue: Configuration = []


public let rawValue: UInt


public init(rawValue: UInt) {
self.rawValue = rawValue
}
}
}


extension EnvironmentValues {
/// Access the ``ValidationEngine/Configuration-swift.struct`` of a ValidationEngine through the environment.
///
/// - Note: Supplying a value into the environment is always an additive change!
public var validationConfiguration: ValidationEngine.Configuration {
get {
self[ValidationEngine.Configuration.self]
}
set {
self[ValidationEngine.Configuration.self].formUnion(newValue)
}

Check warning on line 52 in Sources/SpeziValidation/Configuration/ValidationEngine+Configuration.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziValidation/Configuration/ValidationEngine+Configuration.swift#L50-L52

Added lines #L50 - L52 were not covered by tests
}
}
116 changes: 116 additions & 0 deletions Sources/SpeziValidation/Resources/Localizable.xcstrings
Supereg marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
{
"sourceLanguage" : "en",
"strings" : {
"VALIDATION_RULE_MINIMAL_EMAIL" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Diese E-Mail ist ungültig."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "The provided email is invalid."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "El correo electrónico proporcionado no es válido."
}
}
}
},
"VALIDATION_RULE_NON_EMPTY" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Diese Feld kann nicht leer sein."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "This field cannot be empty."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Este espacio no puede estar vacío."
}
}
}
},
"VALIDATION_RULE_PASSWORD_LENGTH %lld" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Dein Passwort muss mindestens %lld Zeichen lang sein."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Your password must be at least %lld characters long."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Tu contraseña debe tener al menos %lld caracteres."
}
}
}
},
"VALIDATION_RULE_UNICODE_LETTERS" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Du kannst nur Zeichen verwenden."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "You must only use letters."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Debes usar solo letras."
}
}
}
},
"VALIDATION_RULE_UNICODE_LETTERS_ASCII" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Du kannst nur Zeichen aus dem englischen Alphabet verwenden."
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "You must only use standard English letters."
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Debes usar solo letras estándar en inglés."
}
}
}
}
},
"version" : "1.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

This source file is part of the Stanford Spezi open-source project

SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)

SPDX-License-Identifier: MIT
93 changes: 93 additions & 0 deletions Sources/SpeziValidation/SpeziValidation.docc/SpeziValidation.md
Supereg marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# ``SpeziValidation``

Perform input validation and visualize it to the user.

<!--

This source file is part of the Spezi open-source project

SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)

SPDX-License-Identifier: MIT

-->

## Overview

`SpeziValidation` can be used to perform input validation on `String`-based inputs and provides easy-to-use
mechanism to communicate validation feedback back to the user.
The library is based on a rule-based approach using ``ValidationRule``s.

### Performing Validation

The only thing you have to do, is to set up the ``SwiftUI/View/validate(input:rules:)-5dac4`` modifier for your
text input.
Supply your input and validation rules.

The below code example shows a basic validation setup.
Note that we are using the ``VerifiableTextField`` to automatically visualize validation errors to the user.

```swift
@State var phrase: String = ""

var body: some View {
Form {
VerifiableTextField("your favorite phrase", text: $phrase)
.validate(input: phrase, rules: .nonEmpty)
}
}
```

> Note: The inner views can access the ``ValidationEngine`` using the [Environment](https://developer.apple.com/documentation/swiftui/environment/init(_:)-8slkf)
property wrapper.

### Managing Validation

Parent views can access the validation state of their child views using the ``ValidationState`` property wrapper
and the ``SwiftUI/View/receiveValidation(in:)`` modifier.

The code example below shows
how you can use the validation state of your subview to perform final validation on a button press.

```swift
@ValidationState var validation

var body: some View {
Form {
// all subviews that collect data ...

Button("Submit") {
guard validation.validateSubviews() else {
return
}

// save data ...
}
}
.receiveValidation(in: $validation)
}
```

## Topics

### Performing Validation

- ``ValidationRule``
- ``SwiftUI/View/validate(input:rules:)-5dac4``
- ``SwiftUI/View/validate(input:rules:)-9vks0``

### Managing Validation

- ``ValidationState``
- ``SwiftUI/View/receiveValidation(in:)``

### Configuration

- ``SwiftUI/EnvironmentValues/validationConfiguration``
- ``SwiftUI/EnvironmentValues/validationDebounce``

### Visualizing Validation

- ``VerifiableTextField``
- ``ValidationResultsView``
- ``FailedValidationResult``
Loading