Skip to content

Commit

Permalink
Introduce SpeziValidation (#21)
Browse files Browse the repository at this point in the history
# Introduce SpeziValidation

## ♻️ Current situation & Problem
[SpeziAccount](https://github.com/StanfordSpezi/SpeziAccount) currently
implements a lot of logic around input validation that can easily
separated into its own, self-contained target. This PR factors out those
validation-based components and moves them into a new target in
SpeziViews: SpeziValidation. This includes models to build
`ValidationRule`s and views like `VerifiableTextField` that
automatically perform validation on their input (given it's configured
in the environment) and display potential validation errors.

Would recommend to visit the nicely structured documentation for a great
overview of the different components :)

## ⚙️ Release Notes 
* Introduce SpeziValidation. A single-step solution to all client-side,
input validation.

### Breaking Changes
* The `onTapFocus` modifiers were replaced by a new, simple to use
`focusOnTap()` modifier that doesn't require passing in a FocusState
anymore.


## 📚 Documentation
Components coming from SpeziAccount were already documented in great
detail. This PR created the respective string catalog and structured all
new files within a new SPM target.


## ✅ Testing
Appropriate UI testing was added.


## 📝 Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).
  • Loading branch information
Supereg authored Nov 5, 2023
1 parent b27cb60 commit f6666dc
Show file tree
Hide file tree
Showing 29 changed files with 1,745 additions and 71 deletions.
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
}
}
}
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)
}
}
}
116 changes: 116 additions & 0 deletions Sources/SpeziValidation/Resources/Localizable.xcstrings
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
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

0 comments on commit f6666dc

Please sign in to comment.