Skip to content

Commit

Permalink
Upgrade to Observable, String Catalogs and bump Spezi dependencies (#31)
Browse files Browse the repository at this point in the history
# Upgrade to Observable, String Catalogs and bump Spezi dependencies

## ♻️ Current situation & Problem
This PR migrates SpeziOnboarding from using ObservableObject to the new
Observable framework. Additionally it bumps all Spezi dependencies.
Lastly, we migrate to using String Catalogs. This induced some changes
in the views, such that we make sure to not translate standard String
instances. Minor changes in the public API.


## ⚙️ Release Notes 
* Upgraded to new Spezi 0.8.0
* Migrate to Observable framework
* Migrate to String Catalogs


## 📚 Documentation
Some broken documentation was fixed.


## ✅ Testing
Tests didn't require any modifications.


## 📝 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 12, 2023
1 parent 52250fa commit 70d1a74
Show file tree
Hide file tree
Showing 28 changed files with 505 additions and 274 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ let package = Package(
.library(name: "SpeziOnboarding", targets: ["SpeziOnboarding"])
],
dependencies: [
.package(url: "https://github.com/StanfordSpezi/Spezi", .upToNextMinor(from: "0.7.2")),
.package(url: "https://github.com/StanfordSpezi/Spezi", .upToNextMinor(from: "0.8.0")),
.package(url: "https://github.com/StanfordSpezi/SpeziViews", .upToNextMinor(from: "0.6.1"))
],
targets: [
Expand Down
23 changes: 13 additions & 10 deletions Sources/SpeziOnboarding/ConsentView/ConsentDocument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import SpeziViews
import SwiftUI


/// The ``ConsentDocument`` allows the display of markdown-based consent documents that can be signed using a family and given name and a hand drawn signature. In addition, it enables the export of the signed form as a PDF document.
/// Allows the display of markdown-based consent documents that can be signed using a family and given name and a hand drawn signature. In addition, it enables the export of the signed form as a PDF document.
///
/// To observe and control the current state of the ``ConsentDocument``, the view requires passing down a ``ConsentDocument/ConsentViewState`` as a SwiftUI `Binding` in the ``ConsentView/init(header:asyncMarkdown:footer:givenNameField:familyNameField:exportConfiguration:action:)`` initializer.
/// This `Binding` can then be used to trigger the export of the consent form via setting the state to ``ConsentDocument/ConsentViewState/export``.
/// After the rendering completes, the finished `PDFDocument` from Apple's PDFKit is accessible via the associated value of the view state in ``ConsentDocument/ConsentViewState/exported(document:)``.
/// Other possible states of the ``ConsentDocument`` are the SpeziViews `ViewState`'s accessible via the associated value in ``ConsentDocument/ConsentViewState/base(_:)``. In addition, the view provides information about the signing progress via the ``ConsentDocument/ConsentViewState/signing`` and ``ConsentDocument/ConsentViewState/signed`` states.
/// To observe and control the current state of the ``ConsentDocument``, the view requires passing down a ``ConsentViewState`` as a SwiftUI `Binding` in the
/// ``init(markdown:viewState:givenNameTitle:givenNamePlaceholder:familyNameTitle:familyNamePlaceholder:exportConfiguration:)`` initializer.
/// This `Binding` can then be used to trigger the export of the consent form via setting the state to ``ConsentViewState/export``.
/// After the rendering completes, the finished `PDFDocument` from Apple's PDFKit is accessible via the associated value of the view state in ``ConsentViewState/exported(document:)``.
/// Other possible states of the ``ConsentDocument`` are the SpeziViews `ViewState`'s accessible via the associated value in ``ConsentViewState/base(_:)``.
/// In addition, the view provides information about the signing progress via the ``ConsentViewState/signing`` and ``ConsentViewState/signed`` states.
///
/// ```swift
/// // Enables observing the view state of the consent document
Expand Down Expand Up @@ -137,10 +139,10 @@ public struct ConsentDocument: View {
}


/// Creates a ``ConsentDocument`` which renders a consent document with a markdown view. The passed ``ConsentDocument/ConsentViewState`` indicates in which state the view currently is. This is especially useful for exporting the consent form as well as error management.
/// Creates a ``ConsentDocument`` which renders a consent document with a markdown view. The passed ``ConsentViewState`` indicates in which state the view currently is. This is especially useful for exporting the consent form as well as error management.
/// - Parameters:
/// - markdown: The markdown content provided as an UTF8 encoded `Data` instance that can be provided asynchronously.
/// - viewState: A `Binding` to observe the ``ConsentDocument/ConsentViewState`` of the ``ConsentDocument``.
/// - viewState: A `Binding` to observe the ``ConsentViewState`` of the ``ConsentDocument``.
/// - givenNameField: The localization to use for the given (first) name field.
/// - familyNameField: The localization to use for the family (last) name field.
/// - exportConfiguration: Defines the properties of the exported consent form via ``ConsentDocument/ExportConfiguration``.
Expand Down Expand Up @@ -226,8 +228,9 @@ extension ConsentDocument {
if exportConfiguration.includingTimestamp {
HStack {
Spacer()

Text("\(LocalizedStringResource("EXPORTED_TAG", bundle: .atURL(from: .module))): \(DateFormatter.localizedString(from: Date(), dateStyle: .medium, timeStyle: .short))")

Text("EXPORTED_TAG", bundle: .module)
+ Text(verbatim: ": \(DateFormatter.localizedString(from: Date(), dateStyle: .medium, timeStyle: .short))")
}
.font(.caption)
.padding()
Expand Down Expand Up @@ -267,7 +270,7 @@ struct ConsentDocument_Previews: PreviewProvider {
},
viewState: $viewState
)
.navigationTitle("Consent")
.navigationTitle(Text(verbatim: "Consent"))
.padding()
}
}
Expand Down
16 changes: 8 additions & 8 deletions Sources/SpeziOnboarding/ConsentView/ConsentViewState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,21 @@ import SwiftUI
/// It can be used to observe and control the behaviour of the ``ConsentDocument``, especially in regards
/// to the export functionality.
public enum ConsentViewState: Equatable {
/// The ``ConsentDocument/ConsentViewState/base(_:)`` state utilizes the
/// The ``ConsentViewState/base(_:)`` state utilizes the
/// SpeziViews `ViewState`'s to indicate the state of the ``ConsentDocument``, either `.idle`, `.processing`, or `.error(_:)`.
case base(SpeziViews.ViewState)
/// The ``ConsentDocument/ConsentViewState/namesEntered`` state signifies that all required name fields in the ``ConsentDocument`` view have been completed.
/// The ``ConsentViewState/namesEntered`` state signifies that all required name fields in the ``ConsentDocument`` view have been completed.
case namesEntered
/// The ``ConsentDocument/ConsentViewState/signing`` state indicates that the ``ConsentDocument`` is currently being signed by the user.
/// The ``ConsentViewState/signing`` state indicates that the ``ConsentDocument`` is currently being signed by the user.
case signing
/// The ``ConsentDocument/ConsentViewState/signed`` state indicates that the ``ConsentDocument`` is signed by the user.
/// The ``ConsentViewState/signed`` state indicates that the ``ConsentDocument`` is signed by the user.
case signed
/// The ``ConsentDocument/ConsentViewState/export`` state can be set by an outside view
/// The ``ConsentViewState/export`` state can be set by an outside view
/// encapsulating the ``ConsentDocument`` to trigger the export of the consent document as a PDF.
/// The previous state must be ``ConsentDocument/ConsentViewState/signed``, indicating that the consent document is signed.
/// The previous state must be ``ConsentViewState/signed``, indicating that the consent document is signed.
case export
/// The ``ConsentDocument/ConsentViewState/exported(document:)`` state indicates that the
/// The ``ConsentViewState/exported(document:)`` state indicates that the
/// ``ConsentDocument`` has been successfully exported. The rendered `PDFDocument` can be found as the associated value of the state.
/// The export procedure (resulting in the ``ConsentDocument/ConsentViewState/exported(document:)`` state) can be triggered via setting the ``ConsentDocument/ConsentViewState/export`` state of the ``ConsentDocument`` .
/// The export procedure (resulting in the ``ConsentViewState/exported(document:)`` state) can be triggered via setting the ``ConsentViewState/export`` state of the ``ConsentDocument`` .
case exported(document: PDFDocument)
}
58 changes: 33 additions & 25 deletions Sources/SpeziOnboarding/OnboardingActionsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ import SwiftUI
/// )
/// ```
public struct OnboardingActionsView: View {
private let primaryText: String
private let primaryText: Text
private let primaryAction: () async throws -> Void
private let secondaryText: String?
private let secondaryText: Text?
private let secondaryAction: (() async throws -> Void)?

@State private var primaryActionState: ViewState = .idle
Expand All @@ -38,34 +38,45 @@ public struct OnboardingActionsView: View {
public var body: some View {
VStack {
AsyncButton(state: $primaryActionState, action: primaryAction) {
Text(primaryText)
primaryText
.frame(maxWidth: .infinity, minHeight: 38)
}
.buttonStyle(.borderedProminent)
if let secondaryText, let secondaryAction {
AsyncButton(secondaryText, state: $secondaryActionState, action: secondaryAction)
AsyncButton(state: $secondaryActionState, action: secondaryAction) {
secondaryText
}
.padding(.top, 10)
}
}
.disabled(primaryActionState != .idle || secondaryActionState != .idle)
.viewStateAlert(state: $primaryActionState)
.viewStateAlert(state: $secondaryActionState)
}




init(
primaryText: Text,
primaryAction: @escaping () async throws -> Void,
secondaryText: Text? = nil,
secondaryAction: (() async throws -> Void)? = nil
) {
self.primaryText = primaryText
self.primaryAction = primaryAction
self.secondaryText = secondaryText
self.secondaryAction = secondaryAction
}

/// Creates an ``OnboardingActionsView`` instance that only contains a primary button.
/// - Parameters:
/// - text: The title of the primary button without localization.
/// - action: The action that should be performed when pressing the primary button
@_disfavoredOverload
public init<Text: StringProtocol>(
_ text: Text,
verbatim text: Text,
action: @escaping () async throws -> Void
) {
self.primaryText = String(text)
self.primaryAction = action
self.secondaryText = nil
self.secondaryAction = nil
self.init(primaryText: SwiftUI.Text(verbatim: String(text)), primaryAction: action)
}

/// Creates an ``OnboardingActionsView`` instance that only contains a primary button.
Expand All @@ -76,7 +87,7 @@ public struct OnboardingActionsView: View {
_ text: LocalizedStringResource,
action: @escaping () async throws -> Void
) {
self.init(text.localizedString(), action: action)
self.init(primaryText: Text(text), primaryAction: action)
}

/// Creates an ``OnboardingActionsView`` instance that contains a primary button and a secondary button.
Expand All @@ -91,12 +102,7 @@ public struct OnboardingActionsView: View {
secondaryText: LocalizedStringResource,
secondaryAction: @escaping () async throws -> Void
) {
self.init(
primaryText: primaryText.localizedString(),
primaryAction: primaryAction,
secondaryText: secondaryText.localizedString(),
secondaryAction: secondaryAction
)
self.init(primaryText: Text(primaryText), primaryAction: primaryAction, secondaryText: Text(secondaryText), secondaryAction: secondaryAction)
}

/// Creates an ``OnboardingActionsView`` instance that contains a primary button and a secondary button.
Expand All @@ -112,10 +118,12 @@ public struct OnboardingActionsView: View {
secondaryText: SecondaryText,
secondaryAction: @escaping () async throws -> Void
) {
self.primaryText = String(primaryText)
self.primaryAction = primaryAction
self.secondaryText = String(secondaryText)
self.secondaryAction = secondaryAction
self.init(
primaryText: Text(verbatim: String(primaryText)),
primaryAction: primaryAction,
secondaryText: Text(verbatim: String(secondaryText)),
secondaryAction: secondaryAction
)
}
}

Expand All @@ -124,15 +132,15 @@ public struct OnboardingActionsView: View {
struct OnboardingActionsView_Previews: PreviewProvider {
static var previews: some View {
VStack {
OnboardingActionsView("PRIMARY") {
OnboardingActionsView(verbatim: "PRIMARY") {
print("Primary!")
}
OnboardingActionsView(
primaryText: "PRIMARY",
primaryText: String("PRIMARY"),
primaryAction: {
print("Primary")
},
secondaryText: "SECONDARY",
secondaryText: String("SECONDARY"),
secondaryAction: {
print("Secondary")
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/SpeziOnboarding/OnboardingConsentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public struct OnboardingConsentView: View {
private let title: LocalizedStringResource?
private let exportConfiguration: ConsentDocument.ExportConfiguration

@EnvironmentObject private var onboardingDataSource: OnboardingDataSource
@Environment(OnboardingDataSource.self) private var onboardingDataSource
@State private var viewState: ConsentViewState = .base(.idle)
@State private var willShowShareSheet = false
@State private var showShareSheet = false
Expand Down Expand Up @@ -105,7 +105,7 @@ public struct OnboardingConsentView: View {
willShowShareSheet = true
}) {
Image(systemName: "square.and.arrow.up")
.accessibilityLabel(LocalizedStringResource("CONSENT_SHARE", bundle: .atURL(from: .module)).localizedString())
.accessibilityLabel(Text("CONSENT_SHARE", bundle: .module))
.opacity(actionButtonsEnabled ? 1.0 : 0.0)
.scaleEffect(actionButtonsEnabled ? 1.0 : 0.8)
.animation(.easeInOut, value: actionButtonsEnabled)
Expand Down
2 changes: 1 addition & 1 deletion Sources/SpeziOnboarding/OnboardingDataSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import SwiftUI
/// }
/// }
/// ```
public class OnboardingDataSource: Component, ObservableObject, ObservableObjectProvider {
public class OnboardingDataSource: Module, EnvironmentAccessible {
@StandardActor var standard: any OnboardingConstraint


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import SwiftUI
/// This behavior shouldn't occur at all as there are lots of checks performed within the ``OnboardingNavigationPath`` that prevent such illegal steps.
struct IllegalOnboardingStepView: View {
var body: some View {
Text("ILLEGAL_ONBOARDING_STEP")
Text("ILLEGAL_ONBOARDING_STEP", bundle: .module)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import Foundation
import SwiftUI


/// A ``_OnboardingFlowViewCollection`` defines a collection of SwiftUI `View`s that are defined with an ``OnboardingStack``.
/// Defines a collection of SwiftUI `View`s that are defined with an ``OnboardingStack``.
///
/// You can not create a ``_OnboardingFlowViewCollection`` yourself. Please use the ``OnboardingStack`` that internally creates a ``_OnboardingFlowViewCollection`` with the passed views.
public class _OnboardingFlowViewCollection: ObservableObject { // swiftlint:disable:this type_name
@Published var views: [any View]
public class _OnboardingFlowViewCollection { // swiftlint:disable:this type_name
let views: [any View]


init(views: [any View]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import SwiftUI
///
/// ```swift
/// struct Welcome: View {
/// @EnvironmentObject private var onboardingNavigationPath: OnboardingNavigationPath
/// @Environment(OnboardingNavigationPath.self) private var onboardingNavigationPath
///
/// var body: some View {
/// OnboardingView(
Expand All @@ -38,14 +38,15 @@ import SwiftUI
/// }
/// ```
@MainActor
public class OnboardingNavigationPath: ObservableObject {
@Observable
public class OnboardingNavigationPath {
/// Internal SwiftUI `NavigationPath` that serves as the source of truth for the navigation state.
/// Holds elements of type `OnboardingStepIdentifier` which identify the individual onboarding steps.
@MainActor @Published var path = NavigationPath()
var path = NavigationPath()
/// Boolean binding that is injected via the ``OnboardingStack``.
/// Indicates if the onboarding flow is completed, meaning the last view declared within the ``OnboardingStack`` is completed.
@MainActor private var complete: Binding<Bool>?
private let complete: Binding<Bool>?

/// Stores all onboarding views as declared within the ``OnboardingStack``.
private var onboardingSteps: [OnboardingStepIdentifier: any View] = [:]
/// Stores all custom onboarding views that are appended to the ``OnboardingNavigationPath`` via the ``append(customView:)`` or ``append(customViewInit:)`` instance methods
Expand Down Expand Up @@ -86,10 +87,9 @@ public class OnboardingNavigationPath: ObservableObject {
/// - complete: An optional SwiftUI `Binding` that is injected by the ``OnboardingStack``. Is managed by the ``OnboardingNavigationPath`` to indicate whether the onboarding flow is complete.
/// - startAtStep: An optional SwiftUI (Onboarding) `View` type indicating the first to-be-shown step of the onboarding flow.
init(views: [any View], complete: Binding<Bool>?, startAtStep: (any View.Type)?) {
updateViews(with: views)

self.complete = complete

updateViews(with: views)

// If specified, navigate to the first to-be-shown onboarding step
if let startAtStep {
append(startAtStep)
Expand Down
Loading

0 comments on commit 70d1a74

Please sign in to comment.