Skip to content

Commit

Permalink
Add ImageReference, ManagedViewUpdate, CaseIterablePicker and Tiles (#45
Browse files Browse the repository at this point in the history
)

# Add ImageReference, ManagedViewUpdate, CaseIterablePicker and Tiles

## ♻️ Current situation & Problem
This PR moves some infrastructure from other Spezi packages to
SpeziViews (this includes SpeziScheduler, NAMS and SpeziDevices).


## ⚙️ Release Notes 
* Added `ImageReference` as a way to pass around image resources
including system image names.
* Added `ManagedViewUpdate` as a mechanism to manually trigger view
updates include based on dates
* Added `SimpleTile` and `TileHeader` as new layout components.
* Adde new `CaseIterablePicker`.
* `ListRow` is now using
[`LabeledContent`](https://developer.apple.com/documentation/swiftui/labeledcontent/)
under the hood. Additionally, `LabeledContent` received some convenience
initializers.


## 📚 Documentation
Reorganized some of the sections of the documentation catalog.


## ✅ Testing
Added snapshot testing for the new layout components.
We added an UI test to verify the behavior of `@ManagedViewUpdate`.

## 📝 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 Oct 29, 2024
1 parent 0cdbcc6 commit f875144
Show file tree
Hide file tree
Showing 60 changed files with 1,603 additions and 166 deletions.
23 changes: 1 addition & 22 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,6 @@ jobs:
scheme: SpeziViews-Package
resultBundle: SpeziViews-iOS.xcresult
artifactname: SpeziViews-iOS.xcresult
buildandtest_ios_latest:
name: Build and Test Swift Package iOS Latest
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
with:
runsonlabels: '["macOS", "self-hosted"]'
scheme: SpeziViews-Package
xcodeversion: latest
swiftVersion: 6
resultBundle: SpeziViews-iOS-Latest.xcresult
artifactname: SpeziViews-iOS-Latest.xcresult
buildandtest_watchos:
name: Build and Test Swift Package watchOS
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
Expand Down Expand Up @@ -79,25 +69,14 @@ jobs:
scheme: TestApp
resultBundle: TestApp-iOS.xcresult
artifactname: TestApp-iOS.xcresult
buildandtestuitests_ios_latest:
name: Build and Test UI Tests iOS Latest
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
with:
runsonlabels: '["macOS", "self-hosted"]'
path: Tests/UITests
scheme: TestApp
xcodeversion: latest
swiftVersion: 6
resultBundle: TestApp-iOS-Latest.xcresult
artifactname: TestApp-iOS-Latest.xcresult
buildandtestuitests_ipad:
name: Build and Test UI Tests iPadOS
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
with:
runsonlabels: '["macOS", "self-hosted"]'
path: Tests/UITests
scheme: TestApp
destination: 'platform=iOS Simulator,name=iPad Pro 11-inch (M4)'
destination: 'platform=iOS Simulator,name=iPad Pro 13-inch (M4)'
resultBundle: TestApp-iPad.xcresult
artifactname: TestApp-iPad.xcresult
buildandtestuitests_visionos:
Expand Down
21 changes: 1 addition & 20 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.9
// swift-tools-version:6.0

//
// This source file is part of the Stanford Spezi open-source project
Expand All @@ -12,13 +12,6 @@ import class Foundation.ProcessInfo
import PackageDescription


#if swift(<6)
let swiftConcurrency: SwiftSetting = .enableExperimentalFeature("StrictConcurrency")
#else
let swiftConcurrency: SwiftSetting = .enableUpcomingFeature("StrictConcurrency")
#endif


let package = Package(
name: "SpeziViews",
defaultLocalization: "en",
Expand All @@ -45,19 +38,13 @@ let package = Package(
dependencies: [
.product(name: "Spezi", package: "Spezi")
],
swiftSettings: [
swiftConcurrency
],
plugins: [] + swiftLintPlugin()
),
.target(
name: "SpeziPersonalInfo",
dependencies: [
.target(name: "SpeziViews")
],
swiftSettings: [
swiftConcurrency
],
plugins: [] + swiftLintPlugin()
),
.target(
Expand All @@ -66,9 +53,6 @@ let package = Package(
.target(name: "SpeziViews"),
.product(name: "OrderedCollections", package: "swift-collections")
],
swiftSettings: [
swiftConcurrency
],
plugins: [] + swiftLintPlugin()
),
.testTarget(
Expand All @@ -78,9 +62,6 @@ let package = Package(
.target(name: "SpeziValidation"),
.product(name: "SnapshotTesting", package: "swift-snapshot-testing")
],
swiftSettings: [
swiftConcurrency
],
plugins: [] + swiftLintPlugin()
)
]
Expand Down
5 changes: 2 additions & 3 deletions Sources/SpeziPersonalInfo/Fields/NameFieldRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ public struct NameFieldRow<Description: View, Label: View>: View {
/// - Parameters:
/// - name: The name to display and edit.
/// - component: The `KeyPath` to the property of the provided `PersonNameComponents` to display and edit.
/// - prompt: An optional `Text` prompt. Refer to the documentation of `TextField` for more information.
/// - description: The description label displayed before the text field.
/// - label: A view that describes the purpose of the text field.
public init(
Expand All @@ -108,7 +107,7 @@ public struct NameFieldRow<Description: View, Label: View>: View {

#if DEBUG
#Preview {
@State var name = PersonNameComponents()
@Previewable @State var name = PersonNameComponents()
return Grid(horizontalSpacing: 15) {
NameFieldRow(name: $name, for: \.familyName) {
Text(verbatim: "First")
Expand All @@ -127,7 +126,7 @@ public struct NameFieldRow<Description: View, Label: View>: View {
}
}
#Preview {
@State var name = PersonNameComponents()
@Previewable @State var name = PersonNameComponents()
return Form {
Grid(horizontalSpacing: 15) {
NameFieldRow(name: $name, for: \.givenName) {
Expand Down
2 changes: 1 addition & 1 deletion Sources/SpeziPersonalInfo/Fields/NameTextField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public struct NameTextField<Label: View>: View {

#if DEBUG
#Preview {
@State var name = PersonNameComponents()
@Previewable @State var name = PersonNameComponents()
return List {
NameTextField(name: $name, for: \.givenName) {
Text(verbatim: "enter first name")
Expand Down
2 changes: 1 addition & 1 deletion Sources/SpeziValidation/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Diese Feld kann nicht leer sein."
"value" : "Dieses Feld kann nicht leer sein."
}
},
"en" : {
Expand Down
16 changes: 8 additions & 8 deletions Sources/SpeziValidation/SpeziValidation.docc/SpeziValidation.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ 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
The only thing you have to do, is to set up the ``SwiftUICore/View/validate(input:rules:)-5dac4`` modifier for your
text input.
Supply your input and validation rules.

Expand All @@ -50,7 +50,7 @@ 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.
and the ``SwiftUICore/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.
Expand Down Expand Up @@ -79,19 +79,19 @@ var body: some View {
### Performing Validation

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

### Managing Validation

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

### Configuration

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

### Visualizing Validation

Expand Down
4 changes: 2 additions & 2 deletions Sources/SpeziValidation/ValidationEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,13 @@ public class ValidationEngine: Identifiable {

/// Access the configuration of the validation engine.
///
/// You may use the ``SwiftUI/EnvironmentValues/validationConfiguration`` environment key to configure this value from
/// You may use the ``SwiftUICore/EnvironmentValues/validationConfiguration`` environment key to configure this value from
/// the environment.
public var configuration: Configuration
/// The configurable debounce duration for input submission.
///
/// This duration is used to debounce repeated calls to ``submit(input:debounce:)`` where `debounce` is set to `true`.
/// You may use the ``SwiftUI/EnvironmentValues/validationDebounce`` environment key to configure this value from
/// You may use the ``SwiftUICore/EnvironmentValues/validationDebounce`` environment key to configure this value from
/// the environment.
public var debounceDuration: Duration

Expand Down
2 changes: 1 addition & 1 deletion Sources/SpeziValidation/ValidationRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ enum CascadingValidationEffect {
/// )
/// ```
///
/// Use the ``SwiftUI/View/validate(input:rules:)-5dac4`` modifier to apply a validation rule to a given `String` input.
/// Use the ``SwiftUICore/View/validate(input:rules:)-5dac4`` modifier to apply a validation rule to a given `String` input.
///
/// ### Discussion on security-related client-side Validation
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import SwiftUI
/// To do so, you would typically call ``ValidationContext/validateSubviews(switchFocus:)`` within the `Button`
/// action. This call can be used to automatically switch focus to the first field that failed validation.
///
/// The `ValidationState` property wrapper works in conjunction with the ``SwiftUI/View/receiveValidation(in:)`` modifier
/// The `ValidationState` property wrapper works in conjunction with the ``SwiftUICore/View/receiveValidation(in:)`` modifier
/// to receive validation state from the child views.
///
/// Below is a short code example of a typical setup:
Expand Down
2 changes: 1 addition & 1 deletion Sources/SpeziValidation/Views/VerifiableTextField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public struct VerifiableTextField<FieldLabel: View, FieldFooter: View>: View {

#if DEBUG
#Preview {
@State var text = ""
@Previewable @State var text = ""
return Form {
VerifiableTextField(text: $text) {
Text(verbatim: "Password Text")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import SwiftUI
/// This might be helpful for views that rely on ``AnyLocalizedError``. Outer views can define a
/// sensible default for a localized default error description in the case that a sub-view has to display
/// an ``AnyLocalizedError`` for a generic error.
struct DefaultErrorDescription: EnvironmentKey {
private struct DefaultErrorDescription: EnvironmentKey {
static let defaultValue: LocalizedStringResource? = nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import SwiftUI
///
/// This might be helpful to provide extensive customization points without introducing clutter in the initializer of views.
/// The ``AsyncButton`` is one example where this `EnvironmentKey` is used.
struct ProcessingDebounceDuration: EnvironmentKey {
private struct ProcessingDebounceDuration: EnvironmentKey {
static let defaultValue: Duration = .milliseconds(150)
}

Expand Down
91 changes: 91 additions & 0 deletions Sources/SpeziViews/Model/ImageReference.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//
// This source file is part of the Stanford Spezi open-project
//
// SPDX-FileCopyrightText: 2024 Stanford University
//
// SPDX-License-Identifier: MIT
//

import SwiftUI


/// Reference an Image Resource.
public enum ImageReference {
/// Provides the system name for an image.
case system(String)
/// Reference an image from the asset catalog of a bundle.
case asset(String, bundle: Bundle? = nil)


/// A system image is referenced.
public var isSystemImage: Bool {
if case .system = self {
true
} else {
false
}
}
}


extension ImageReference {
/// Retrieve Image.
///
/// Returns `nil` if the image resource could not be located.
public var image: Image? {
switch self {
case let .system(name):
return Image(systemName: name)
case let .asset(name, bundle: bundle):
#if canImport(UIKit)
// also available on watchOS
guard UIImage(named: name, in: bundle, with: nil) != nil else {
return nil
}
#elseif canImport(AppKit)
guard NSImage(named: name) != nil else {
return nil
}
#endif
return Image(name, bundle: bundle)
}
}

#if canImport(UIKit) // also available on watchOS
/// Retrieve an UIImage.
///
/// Returns `nil` if the image resource could not be located.
public var uiImage: UIImage? {
switch self {
case let .system(name):
UIImage(systemName: name)
case let .asset(name, bundle):
UIImage(named: name, in: bundle, with: nil)
}
}

#if canImport(WatchKit)
/// Retrieve a WKImage.
///
/// Returns `nil` if the image resource could not be located.
public var wkImage: WKImage? {
uiImage.map { WKImage(image: $0) }
}
#endif
#elseif canImport(AppKit)
/// Retrieve a NSImage.
///
/// Returns `nil` if the image resource could not be located.
public var nsImage: NSImage? {
switch self {
case let .system(name):
NSImage(systemSymbolName: name, accessibilityDescription: nil)
case let .asset(name, _):
NSImage(named: name)
}
}
#endif
}


extension ImageReference: Hashable, Sendable {}
4 changes: 2 additions & 2 deletions Sources/SpeziViews/Model/OperationState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
///
/// The ``OperationState`` encapsulates the core state of an application's behavior, which directly impacts the user interface and interaction.
/// To effectively manage the UI's state in the Spezi framework, the ``OperationState`` can be represented as a ``ViewState``.
/// This bridging mechanism allows Spezi to monitor and respond to changes in the view's state, for example via the ``SwiftUI/View/viewStateAlert(state:)-27a86`` view modifier.
/// This bridging mechanism allows Spezi to monitor and respond to changes in the view's state, for example via the ``SwiftUICore/View/viewStateAlert(state:)-27a86`` view modifier.
///
/// - Note: It's important to note that this conversion is a lossy process, where a potentially intricate ``OperationState`` is
/// distilled into a simpler ``ViewState``.
Expand Down Expand Up @@ -66,7 +66,7 @@
///
/// > Tip:
/// > In the case that no SwiftUI `Binding` to the ``ViewState`` of the ``OperationState`` (so ``OperationState/representation``)
/// > is required (e.g., no use of the ``SwiftUI/View/viewStateAlert(state:)-4wzs4`` view modifier), one is able to omit the separately defined ``ViewState``
/// > is required (e.g., no use of the ``SwiftUICore/View/viewStateAlert(state:)-4wzs4`` view modifier), one is able to omit the separately defined ``ViewState``
/// > within a SwiftUI `View` and directly access the ``OperationState/representation`` property.
public protocol OperationState {
/// Defines the lossy abstraction logic from the possibly complex ``OperationState`` to the simple ``ViewState``.
Expand Down
2 changes: 1 addition & 1 deletion Sources/SpeziViews/Model/ViewState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import Foundation
/// A `ViewState` provides a built-in mechanism for tracking the state of a Spezi UI component.
/// A view can be in one of three states: `idle`, `processing`, or `error`.
///
/// The ``SwiftUI/View/viewStateAlert(state:)-4wzs4`` view modifier can be used to automatically notify users with an
/// The ``SwiftUICore/View/viewStateAlert(state:)-4wzs4`` view modifier can be used to automatically notify users with an
/// [`Alert`](https://developer.apple.com/documentation/swiftui/view/alert(_:ispresented:actions:)-3npin) when the
/// `ViewState` transitions into an error state.
///
Expand Down
Loading

0 comments on commit f875144

Please sign in to comment.