From 70d1a740a0da8a5bc83d95bcff177ebd3e7402c0 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Sun, 12 Nov 2023 11:45:36 -0800 Subject: [PATCH] Upgrade to Observable, String Catalogs and bump Spezi dependencies (#31) # Upgrade to Observable, String Catalogs and bump Spezi dependencies ## :recycle: 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. ## :gear: Release Notes * Upgraded to new Spezi 0.8.0 * Migrate to Observable framework * Migrate to String Catalogs ## :books: Documentation Some broken documentation was fixed. ## :white_check_mark: Testing Tests didn't require any modifications. ## :pencil: 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). --- Package.swift | 2 +- .../ConsentView/ConsentDocument.swift | 23 +- .../ConsentView/ConsentViewState.swift | 16 +- .../OnboardingActionsView.swift | 58 ++-- .../OnboardingConsentView.swift | 4 +- .../OnboardingDataSource.swift | 2 +- .../IllegalOnboardingStepView.swift | 2 +- .../OnboardingFlowViewCollection.swift | 6 +- .../OnboardingNavigationPath.swift | 16 +- .../OnboardingFlow/OnboardingStack.swift | 25 +- .../OnboardingInformationView.swift | 45 +-- .../SpeziOnboarding/OnboardingTitleView.swift | 26 +- Sources/SpeziOnboarding/OnboardingView.swift | 10 +- .../Resources/Localizable.xcstrings | 310 ++++++++++++++++++ .../Resources/Localizable.xcstrings.license | 5 + .../Resources/de.lproj/Localizable.strings | 42 --- .../Resources/en.lproj/Localizable.strings | 42 --- .../SequentialOnboardingView.swift | 106 +++--- Tests/UITests/TestApp/ExampleStandard.swift | 2 +- .../Views/OnboardingConditionalTestView.swift | 2 +- ...boardingConsentMarkdownRenderingView.swift | 6 +- .../OnboardingConsentMarkdownTestView.swift | 2 +- .../Views/OnboardingCustomTestView1.swift | 2 +- .../Views/OnboardingCustomTestView2.swift | 2 +- .../Views/OnboardingSequentialTestView.swift | 2 +- .../Views/OnboardingStartTestView.swift | 2 +- .../Views/OnboardingWelcomeTestView.swift | 2 +- .../UITests/UITests.xcodeproj/project.pbxproj | 17 - 28 files changed, 505 insertions(+), 274 deletions(-) create mode 100644 Sources/SpeziOnboarding/Resources/Localizable.xcstrings create mode 100644 Sources/SpeziOnboarding/Resources/Localizable.xcstrings.license delete mode 100644 Sources/SpeziOnboarding/Resources/de.lproj/Localizable.strings delete mode 100644 Sources/SpeziOnboarding/Resources/en.lproj/Localizable.strings diff --git a/Package.swift b/Package.swift index 3cd0dd2..4667b0e 100644 --- a/Package.swift +++ b/Package.swift @@ -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: [ diff --git a/Sources/SpeziOnboarding/ConsentView/ConsentDocument.swift b/Sources/SpeziOnboarding/ConsentView/ConsentDocument.swift index 496f234..db3f48e 100644 --- a/Sources/SpeziOnboarding/ConsentView/ConsentDocument.swift +++ b/Sources/SpeziOnboarding/ConsentView/ConsentDocument.swift @@ -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 @@ -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``. @@ -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() @@ -267,7 +270,7 @@ struct ConsentDocument_Previews: PreviewProvider { }, viewState: $viewState ) - .navigationTitle("Consent") + .navigationTitle(Text(verbatim: "Consent")) .padding() } } diff --git a/Sources/SpeziOnboarding/ConsentView/ConsentViewState.swift b/Sources/SpeziOnboarding/ConsentView/ConsentViewState.swift index 878f586..7b9ddbd 100644 --- a/Sources/SpeziOnboarding/ConsentView/ConsentViewState.swift +++ b/Sources/SpeziOnboarding/ConsentView/ConsentViewState.swift @@ -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) } diff --git a/Sources/SpeziOnboarding/OnboardingActionsView.swift b/Sources/SpeziOnboarding/OnboardingActionsView.swift index efd88f8..96dc274 100644 --- a/Sources/SpeziOnboarding/OnboardingActionsView.swift +++ b/Sources/SpeziOnboarding/OnboardingActionsView.swift @@ -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 @@ -38,12 +38,14 @@ 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) } } @@ -51,21 +53,30 @@ public struct OnboardingActionsView: View { .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: 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. @@ -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. @@ -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. @@ -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 + ) } } @@ -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") } diff --git a/Sources/SpeziOnboarding/OnboardingConsentView.swift b/Sources/SpeziOnboarding/OnboardingConsentView.swift index 387a611..c4e249c 100644 --- a/Sources/SpeziOnboarding/OnboardingConsentView.swift +++ b/Sources/SpeziOnboarding/OnboardingConsentView.swift @@ -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 @@ -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) diff --git a/Sources/SpeziOnboarding/OnboardingDataSource.swift b/Sources/SpeziOnboarding/OnboardingDataSource.swift index a65440a..3bb18c4 100644 --- a/Sources/SpeziOnboarding/OnboardingDataSource.swift +++ b/Sources/SpeziOnboarding/OnboardingDataSource.swift @@ -33,7 +33,7 @@ import SwiftUI /// } /// } /// ``` -public class OnboardingDataSource: Component, ObservableObject, ObservableObjectProvider { +public class OnboardingDataSource: Module, EnvironmentAccessible { @StandardActor var standard: any OnboardingConstraint diff --git a/Sources/SpeziOnboarding/OnboardingFlow/IllegalOnboardingStepView.swift b/Sources/SpeziOnboarding/OnboardingFlow/IllegalOnboardingStepView.swift index 1482bf6..65b61d9 100644 --- a/Sources/SpeziOnboarding/OnboardingFlow/IllegalOnboardingStepView.swift +++ b/Sources/SpeziOnboarding/OnboardingFlow/IllegalOnboardingStepView.swift @@ -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) } } diff --git a/Sources/SpeziOnboarding/OnboardingFlow/OnboardingFlowViewCollection.swift b/Sources/SpeziOnboarding/OnboardingFlow/OnboardingFlowViewCollection.swift index 30f21d6..c10484d 100644 --- a/Sources/SpeziOnboarding/OnboardingFlow/OnboardingFlowViewCollection.swift +++ b/Sources/SpeziOnboarding/OnboardingFlow/OnboardingFlowViewCollection.swift @@ -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]) { diff --git a/Sources/SpeziOnboarding/OnboardingFlow/OnboardingNavigationPath.swift b/Sources/SpeziOnboarding/OnboardingFlow/OnboardingNavigationPath.swift index 9812318..1b923d7 100644 --- a/Sources/SpeziOnboarding/OnboardingFlow/OnboardingNavigationPath.swift +++ b/Sources/SpeziOnboarding/OnboardingFlow/OnboardingNavigationPath.swift @@ -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( @@ -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? - + private let complete: Binding? + /// 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 @@ -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?, 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) diff --git a/Sources/SpeziOnboarding/OnboardingFlow/OnboardingStack.swift b/Sources/SpeziOnboarding/OnboardingFlow/OnboardingStack.swift index 3073dc7..20c9603 100644 --- a/Sources/SpeziOnboarding/OnboardingFlow/OnboardingStack.swift +++ b/Sources/SpeziOnboarding/OnboardingFlow/OnboardingStack.swift @@ -41,9 +41,9 @@ import SwiftUI /// } /// ``` public struct OnboardingStack: View { - @StateObject var onboardingNavigationPath: OnboardingNavigationPath - @ObservedObject var onboardingFlowViewCollection: _OnboardingFlowViewCollection - + @State var onboardingNavigationPath: OnboardingNavigationPath + private let collection: _OnboardingFlowViewCollection + /// The ``OnboardingStack/body`` contains a SwiftUI `NavigationStack` that is responsible for the navigation between the different onboarding views via an ``OnboardingNavigationPath`` public var body: some View { @@ -53,11 +53,11 @@ public struct OnboardingStack: View { onboardingNavigationPath.navigate(to: onboardingStep) } } - .environmentObject(onboardingNavigationPath) - /// Inject onboarding views resulting from a re-triggered evaluation of the onboarding result builder into the `OnboardingNavigationPath` - .onReceive(onboardingFlowViewCollection.$views, perform: { updatedOnboardingViews in - self.onboardingNavigationPath.updateViews(with: updatedOnboardingViews) - }) + .environment(onboardingNavigationPath) + .onChange(of: ObjectIdentifier(collection)) { + // ensure the model uses the latest views from the initializer + self.onboardingNavigationPath.updateViews(with: collection.views) + } } @@ -66,15 +66,16 @@ public struct OnboardingStack: View { /// - onboardingFlowComplete: An optional SwiftUI `Binding` that is automatically set to true by the ``OnboardingNavigationPath`` once the onboarding flow is completed. Can be used to conditionally show/hide the ``OnboardingStack``. /// - startAtStep: An optional SwiftUI (Onboarding) `View` type indicating the first to-be-shown step of the onboarding flow. /// - content: The SwiftUI (Onboarding) `View`s that are part of the onboarding flow. You can define the `View`s using the onboarding view builder. + @MainActor public init( onboardingFlowComplete: Binding? = nil, startAtStep: (any View.Type)? = nil, @OnboardingViewBuilder _ content: @escaping () -> _OnboardingFlowViewCollection ) { let onboardingFlowViewCollection = content() - self.onboardingFlowViewCollection = onboardingFlowViewCollection - - self._onboardingNavigationPath = StateObject( + self.collection = onboardingFlowViewCollection + + self._onboardingNavigationPath = State( wrappedValue: OnboardingNavigationPath( views: onboardingFlowViewCollection.views, complete: onboardingFlowComplete, @@ -89,7 +90,7 @@ public struct OnboardingStack: View { struct OnboardingStack_Previews: PreviewProvider { static var previews: some View { OnboardingStack { - Text("Hello Spezi!") + Text(verbatim: "Hello Spezi!") } } } diff --git a/Sources/SpeziOnboarding/OnboardingInformationView.swift b/Sources/SpeziOnboarding/OnboardingInformationView.swift index 1853fe1..25f546f 100644 --- a/Sources/SpeziOnboarding/OnboardingInformationView.swift +++ b/Sources/SpeziOnboarding/OnboardingInformationView.swift @@ -36,11 +36,16 @@ public struct OnboardingInformationView: View { /// The icon of the area in the ``OnboardingInformationView``. public let icon: AnyView /// The title of the area in the ``OnboardingInformationView``. - public let title: String + public let title: Text /// The description of the area in the ``OnboardingInformationView``. - public let description: String - - + public let description: Text + + private init(icon: AnyView, title: Text, description: Text) { + self.icon = icon + self.title = title + self.description = description + } + /// Creates a new content for an area in the ``OnboardingInformationView``. /// - Parameters: /// - icon: The icon of the area in the ``OnboardingInformationView``. @@ -52,9 +57,7 @@ public struct OnboardingInformationView: View { title: Title, description: Description ) { - self.icon = AnyView(icon()) - self.title = String(title) - self.description = String(description) + self.init(icon: AnyView(icon()), title: Text(verbatim: String(title)), description: Text(verbatim: String(description))) } /// Creates a new content for an area in the ``OnboardingInformationView``. @@ -67,7 +70,7 @@ public struct OnboardingInformationView: View { title: LocalizedStringResource, description: LocalizedStringResource ) { - self.init(icon: icon, title: title.localizedString(), description: description.localizedString()) + self.init(icon: AnyView(icon()), title: Text(title), description: Text(description)) } /// Creates a new content for an area in the ``OnboardingInformationView``. @@ -94,7 +97,7 @@ public struct OnboardingInformationView: View { title: LocalizedStringResource, description: LocalizedStringResource ) { - self.init(icon: { icon }, title: title.localizedString(), description: description.localizedString()) + self.init(icon: { icon }, title: title, description: description) } } @@ -128,10 +131,10 @@ public struct OnboardingInformationView: View { .accessibilityHidden(true) VStack(alignment: .leading) { - Text(area.title) + area.title .bold() .accessibilityAddTraits(.isHeader) - Text(area.description) + area.description .foregroundColor(.secondary) .fixedSize(horizontal: false, vertical: true) } @@ -147,18 +150,18 @@ struct AreasView_Previews: PreviewProvider { [ OnboardingInformationView.Content( icon: Image(systemName: "pc"), - title: "PC", - description: "This is a PC. And we can write a lot about PCs in a section like this. A very long text!" + title: String("PC"), + description: String("This is a PC. And we can write a lot about PCs in a section like this. A very long text!") ), OnboardingInformationView.Content( icon: Image(systemName: "desktopcomputer"), - title: "Mac", - description: "This is an iMac" + title: String("Mac"), + description: String("This is an iMac") ), OnboardingInformationView.Content( icon: Image(systemName: "laptopcomputer"), - title: "MacBook", - description: "This is a MacBook" + title: String("MacBook"), + description: String("This is a MacBook") ) ] } @@ -169,13 +172,13 @@ struct AreasView_Previews: PreviewProvider { areas: [ OnboardingInformationView.Content( icon: Image(systemName: "pc"), - title: "PC", - description: "This is a PC." + title: String("PC"), + description: String("This is a PC.") ), OnboardingInformationView.Content( icon: Image(systemName: "desktopcomputer"), - title: "Mac", - description: "This is an iMac." + title: String("Mac"), + description: String("This is an iMac.") ) ] ) diff --git a/Sources/SpeziOnboarding/OnboardingTitleView.swift b/Sources/SpeziOnboarding/OnboardingTitleView.swift index 2628f50..f230982 100644 --- a/Sources/SpeziOnboarding/OnboardingTitleView.swift +++ b/Sources/SpeziOnboarding/OnboardingTitleView.swift @@ -16,21 +16,21 @@ import SwiftUI /// OnboardingTitleView(title: "Title", subtitle: "Subtitle") /// ``` public struct OnboardingTitleView: View { - private let title: String - private let subtitle: String? - + private let title: Text + private let subtitle: Text? + public var body: some View { VStack { - Text(title) + title .bold() .font(.largeTitle) .multilineTextAlignment(.center) .padding(.bottom) .accessibilityAddTraits(.isHeader) - if let subtitle = subtitle { - Text(subtitle) + if let subtitle { + subtitle .multilineTextAlignment(.center) .padding(.bottom) } @@ -42,15 +42,14 @@ public struct OnboardingTitleView: View { /// Creates an ``OnboardingTitleView`` instance that only contains a title. /// - Parameter title: The localized title of the ``OnboardingTitleView``. public init(title: LocalizedStringResource) { - self.title = title.localizedString() - self.subtitle = nil + self.init(title: title, subtitle: nil) } /// Creates an ``OnboardingTitleView`` instance that only contains a title. /// - Parameter title: The title of the ``OnboardingTitleView`` without localization. @_disfavoredOverload public init(title: Title) { - self.title = String(title) + self.title = Text(verbatim: String(title)) self.subtitle = nil } @@ -59,7 +58,8 @@ public struct OnboardingTitleView: View { /// - title: The localized title of the ``OnboardingTitleView``. /// - subtitle: The localized subtitle of the ``OnboardingTitleView``. public init(title: LocalizedStringResource, subtitle: LocalizedStringResource?) { - self.init(title: title.localizedString(), subtitle: subtitle?.localizedString()) + self.title = Text(title) + self.subtitle = subtitle.map { Text($0) } } /// Creates an ``OnboardingTitleView`` instance that contains a title and a subtitle. @@ -68,8 +68,8 @@ public struct OnboardingTitleView: View { /// - subtitle: The subtitle of the ``OnboardingTitleView`` without localization. @_disfavoredOverload public init(title: Title, subtitle: Subtitle?) { - self.title = String(title) - self.subtitle = subtitle.flatMap { String($0) } + self.title = Text(verbatim: String(title)) + self.subtitle = subtitle.map { Text(verbatim: String($0)) } } } @@ -77,7 +77,7 @@ public struct OnboardingTitleView: View { #if DEBUG struct OnboardingTitleView_Previews: PreviewProvider { static var previews: some View { - OnboardingTitleView(title: "Title", subtitle: "Subtitle") + OnboardingTitleView(title: String("Title"), subtitle: String("Subtitle")) } } #endif diff --git a/Sources/SpeziOnboarding/OnboardingView.swift b/Sources/SpeziOnboarding/OnboardingView.swift index 4b10df2..fe7ee9c 100644 --- a/Sources/SpeziOnboarding/OnboardingView.swift +++ b/Sources/SpeziOnboarding/OnboardingView.swift @@ -137,7 +137,7 @@ public struct OnboardingView: View { /// A ``Content`` defines the way that information is displayed in an ``SequentialOnboardingView``. public struct Content { /// The title of the area in the ``SequentialOnboardingView``. - public let title: String? + public let title: Text? /// The description of the area in the ``SequentialOnboardingView``. - public let description: String - + public let description: Text + /// Creates a new content for an area in the ``SequentialOnboardingView``. /// - Parameters: @@ -54,11 +54,8 @@ public struct SequentialOnboardingView: View { title: LocalizedStringResource? = nil, description: LocalizedStringResource ) { - if let title { - self.init(title: title.localizedString(), description: description.localizedString()) - } else { - self.init(description: description.localizedString()) - } + self.title = title.map { Text($0) } + self.description = Text(description) } /// Creates a new content for an area in the ``SequentialOnboardingView``. @@ -70,8 +67,8 @@ public struct SequentialOnboardingView: View { title: Title, description: Description ) { - self.title = String(title) - self.description = String(description) + self.title = Text(verbatim: String(title)) + self.description = Text(verbatim: String(description)) } /// Creates a new content for an area in the ``SequentialOnboardingView``. @@ -82,17 +79,16 @@ public struct SequentialOnboardingView: View { description: Description ) { self.title = nil - self.description = String(description) + self.description = Text(verbatim: String(description)) } } - private let titleView: AnyView + private let titleView: TitleView private let content: [Content] - private let actionText: String + private let actionText: Text private let action: () async throws -> Void - @Environment(\.locale) private var locale @State private var currentContentIndex: Int = 0 @@ -112,7 +108,7 @@ public struct SequentialOnboardingView: View { }, actionView: { OnboardingActionsView( - actionButtonTitle + primaryText: actionButtonTitle ) { if currentContentIndex < content.count - 1 { currentContentIndex += 1 @@ -128,15 +124,21 @@ public struct SequentialOnboardingView: View { } } - private var actionButtonTitle: String { + private var actionButtonTitle: Text { if currentContentIndex < content.count - 1 { - return String(localized: "SEQUENTIAL_ONBOARDING_NEXT", bundle: .module, locale: locale) + return Text("SEQUENTIAL_ONBOARDING_NEXT", bundle: .module) } else { return actionText } } - - + + private init(titleView: TitleView, content: [Content], actionText: Text, action: @escaping () async throws -> Void) { + self.titleView = titleView + self.content = content + self.actionText = actionText + self.action = action + } + /// Creates the default style of the ``SequentialOnboardingView`` that uses a combination of an ``OnboardingTitleView`` /// and ``OnboardingActionsView``. /// @@ -152,11 +154,11 @@ public struct SequentialOnboardingView: View { content: [Content], actionText: LocalizedStringResource, action: @escaping () async throws -> Void - ) { + ) where TitleView == OnboardingTitleView { self.init( titleView: OnboardingTitleView(title: title, subtitle: subtitle), content: content, - actionText: actionText.localizedString(), + actionText: Text(actionText), action: action ) } @@ -175,11 +177,11 @@ public struct SequentialOnboardingView: View { content: [Content], actionText: ActionText, action: @escaping () async throws -> Void - ) { + ) where TitleView == OnboardingTitleView { self.init( titleView: OnboardingTitleView(title: title), content: content, - actionText: String(actionText), + actionText: Text(verbatim: String(actionText)), action: action ) } @@ -200,11 +202,11 @@ public struct SequentialOnboardingView: View { content: [Content], actionText: ActionText, action: @escaping () async throws -> Void - ) { + ) where TitleView == OnboardingTitleView { self.init( titleView: OnboardingTitleView(title: title, subtitle: subtitle), content: content, - actionText: String(actionText), + actionText: Text(verbatim: String(actionText)), action: action ) } @@ -218,16 +220,18 @@ public struct SequentialOnboardingView: View { /// - actionText: The text that should appear on the ``SequentialOnboardingView``'s primary button without localization. /// - action: The close that is called then the primary button is pressed. @_disfavoredOverload - public init( + public init( titleView: TitleView, content: [Content], actionText: ActionText, action: @escaping () async throws -> Void ) { - self.titleView = AnyView(titleView) - self.content = content - self.actionText = String(actionText) - self.action = action + self.init( + titleView: titleView, + content: content, + actionText: Text(verbatim: String(actionText)), + action: action + ) } /// Creates a customized ``SequentialOnboardingView`` allowing a complete customization of the ``SequentialOnboardingView``'s title view. @@ -237,16 +241,18 @@ public struct SequentialOnboardingView: View { /// - content: The areas of the ``SequentialOnboardingView`` defined using ``SequentialOnboardingView/Content`` instances.. /// - actionText: The localized text that should appear on the ``SequentialOnboardingView``'s primary button. /// - action: The close that is called then the primary button is pressed. - public init( + public init( titleView: TitleView, content: [Content], actionText: LocalizedStringResource, action: @escaping () async throws -> Void ) { - self.titleView = AnyView(titleView) - self.content = content - self.actionText = actionText.localizedString() - self.action = action + self.init( + titleView: titleView, + content: content, + actionText: Text(actionText), + action: action + ) } @@ -262,16 +268,16 @@ public struct SequentialOnboardingView: View { Circle() .fill(Color.accentColor) } - .accessibilityLabel("\(index + 1).") + .accessibilityLabel(String("\(index + 1).")) .accessibilityHidden(content.title != nil) VStack(alignment: .leading, spacing: 8) { if let title = content.title { - Text(title) + title .bold() - .accessibilityLabel(Text(verbatim: "\(index + 1). ") + Text(title)) + .accessibilityLabel(Text(verbatim: "\(index + 1). ") + title) .accessibilityAddTraits(.isHeader) } - Text(content.description) + content.description } Spacer() } @@ -289,20 +295,16 @@ public struct SequentialOnboardingView: View { #if DEBUG struct SequentialOnboardingView_Previews: PreviewProvider { - static var mock: [SequentialOnboardingView.Content] { - [ - .init(title: "A thing to know", description: "This is a first thing that you should know, read carefully!"), - .init(title: "Second thing to know", description: "This is a second thing that you should know, read carefully!"), - .init(title: "Third thing to know", description: "This is a third thing that you should know, read carefully!") - ] - } - static var previews: some View { SequentialOnboardingView( - title: "Title", - subtitle: "Subtitle", - content: mock, - actionText: "Continue" + title: String("Title"), + subtitle: String("Subtitle"), + content: [ + .init(title: String("A thing to know"), description: String("This is a first thing that you should know, read carefully!")), + .init(title: String("Second thing to know"), description: String("This is a second thing that you should know, read carefully!")), + .init(title: String("Third thing to know"), description: String("This is a third thing that you should know, read carefully!")) + ], + actionText: String("Continue") ) { print("Done!") } diff --git a/Tests/UITests/TestApp/ExampleStandard.swift b/Tests/UITests/TestApp/ExampleStandard.swift index fb46b38..f1791ed 100644 --- a/Tests/UITests/TestApp/ExampleStandard.swift +++ b/Tests/UITests/TestApp/ExampleStandard.swift @@ -13,7 +13,7 @@ import SwiftUI /// An example Standard used for the configuration. -actor ExampleStandard: Standard, ObservableObject, ObservableObjectProvider { +actor ExampleStandard: Standard, EnvironmentAccessible { @Published @MainActor var consentData: PDFDocument = .init() } diff --git a/Tests/UITests/TestApp/Views/OnboardingConditionalTestView.swift b/Tests/UITests/TestApp/Views/OnboardingConditionalTestView.swift index 438c64d..9684758 100644 --- a/Tests/UITests/TestApp/Views/OnboardingConditionalTestView.swift +++ b/Tests/UITests/TestApp/Views/OnboardingConditionalTestView.swift @@ -12,7 +12,7 @@ import SwiftUI struct OnboardingConditionalTestView: View { - @EnvironmentObject private var path: OnboardingNavigationPath + @Environment(OnboardingNavigationPath.self) private var path var body: some View { diff --git a/Tests/UITests/TestApp/Views/OnboardingConsentMarkdownRenderingView.swift b/Tests/UITests/TestApp/Views/OnboardingConsentMarkdownRenderingView.swift index 47faea6..8e31019 100644 --- a/Tests/UITests/TestApp/Views/OnboardingConsentMarkdownRenderingView.swift +++ b/Tests/UITests/TestApp/Views/OnboardingConsentMarkdownRenderingView.swift @@ -13,8 +13,8 @@ import SwiftUI struct OnboardingConsentMarkdownRenderingView: View { - @EnvironmentObject private var path: OnboardingNavigationPath - @EnvironmentObject private var standard: ExampleStandard + @Environment(OnboardingNavigationPath.self) private var path + @Environment(ExampleStandard.self) private var standard @State var exportedConsent: PDFDocument? @@ -69,7 +69,7 @@ struct OnboardingConsentMarkdownRenderingView_Previews: PreviewProvider { OnboardingStack(startAtStep: OnboardingConsentMarkdownRenderingView.self) { for onboardingView in OnboardingFlow.previewSimulatorViews { onboardingView - .environmentObject(standard) + .environment(standard) } } } diff --git a/Tests/UITests/TestApp/Views/OnboardingConsentMarkdownTestView.swift b/Tests/UITests/TestApp/Views/OnboardingConsentMarkdownTestView.swift index 605fcb3..69da0aa 100644 --- a/Tests/UITests/TestApp/Views/OnboardingConsentMarkdownTestView.swift +++ b/Tests/UITests/TestApp/Views/OnboardingConsentMarkdownTestView.swift @@ -12,7 +12,7 @@ import SwiftUI struct OnboardingConsentMarkdownTestView: View { - @EnvironmentObject private var path: OnboardingNavigationPath + @Environment(OnboardingNavigationPath.self) private var path var body: some View { diff --git a/Tests/UITests/TestApp/Views/OnboardingCustomTestView1.swift b/Tests/UITests/TestApp/Views/OnboardingCustomTestView1.swift index 28fd224..a9932ec 100644 --- a/Tests/UITests/TestApp/Views/OnboardingCustomTestView1.swift +++ b/Tests/UITests/TestApp/Views/OnboardingCustomTestView1.swift @@ -11,7 +11,7 @@ import SwiftUI struct OnboardingCustomTestView1: View { - @EnvironmentObject private var path: OnboardingNavigationPath + @Environment(OnboardingNavigationPath.self) private var path var exampleArgument: String diff --git a/Tests/UITests/TestApp/Views/OnboardingCustomTestView2.swift b/Tests/UITests/TestApp/Views/OnboardingCustomTestView2.swift index a9df9d9..332c219 100644 --- a/Tests/UITests/TestApp/Views/OnboardingCustomTestView2.swift +++ b/Tests/UITests/TestApp/Views/OnboardingCustomTestView2.swift @@ -10,7 +10,7 @@ import SpeziOnboarding import SwiftUI struct OnboardingCustomTestView2: View { - @EnvironmentObject private var path: OnboardingNavigationPath + @Environment(OnboardingNavigationPath.self) private var path var body: some View { diff --git a/Tests/UITests/TestApp/Views/OnboardingSequentialTestView.swift b/Tests/UITests/TestApp/Views/OnboardingSequentialTestView.swift index 6b4af61..26fc22b 100644 --- a/Tests/UITests/TestApp/Views/OnboardingSequentialTestView.swift +++ b/Tests/UITests/TestApp/Views/OnboardingSequentialTestView.swift @@ -12,7 +12,7 @@ import SwiftUI struct OnboardingSequentialTestView: View { - @EnvironmentObject private var path: OnboardingNavigationPath + @Environment(OnboardingNavigationPath.self) private var path var body: some View { diff --git a/Tests/UITests/TestApp/Views/OnboardingStartTestView.swift b/Tests/UITests/TestApp/Views/OnboardingStartTestView.swift index 5a85f5e..3fe0920 100644 --- a/Tests/UITests/TestApp/Views/OnboardingStartTestView.swift +++ b/Tests/UITests/TestApp/Views/OnboardingStartTestView.swift @@ -12,7 +12,7 @@ import SwiftUI struct OnboardingStartTestView: View { - @EnvironmentObject private var path: OnboardingNavigationPath + @Environment(OnboardingNavigationPath.self) private var path @Binding var showConditionalView: Bool diff --git a/Tests/UITests/TestApp/Views/OnboardingWelcomeTestView.swift b/Tests/UITests/TestApp/Views/OnboardingWelcomeTestView.swift index 1fe5b87..5723b50 100644 --- a/Tests/UITests/TestApp/Views/OnboardingWelcomeTestView.swift +++ b/Tests/UITests/TestApp/Views/OnboardingWelcomeTestView.swift @@ -13,7 +13,7 @@ import SwiftUI // swiftlint:disable accessibility_label_for_image struct OnboardingWelcomeTestView: View { - @EnvironmentObject private var path: OnboardingNavigationPath + @Environment(OnboardingNavigationPath.self) private var path var body: some View { diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index 100e29c..49991d9 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -10,7 +10,6 @@ 2F61BDC329DD02D600D71D33 /* SpeziOnboarding in Frameworks */ = {isa = PBXBuildFile; productRef = 2F61BDC229DD02D600D71D33 /* SpeziOnboarding */; }; 2F61BDC929DD3CC000D71D33 /* OnboardingTestsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F61BDC829DD3CC000D71D33 /* OnboardingTestsView.swift */; }; 2F61BDCB29DDE76D00D71D33 /* SpeziOnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F61BDCA29DDE76D00D71D33 /* SpeziOnboardingTests.swift */; }; - 2F61BDCE29DDE7B300D71D33 /* Spezi in Frameworks */ = {isa = PBXBuildFile; productRef = 2F61BDCD29DDE7B300D71D33 /* Spezi */; }; 2F6D139A28F5F386007C25D6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2F6D139928F5F386007C25D6 /* Assets.xcassets */; }; 2FA7382C290ADFAA007ACEB9 /* TestApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */; }; 970D444B2A6F031200756FE2 /* OnboardingConsentMarkdownTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 970D444A2A6F031200756FE2 /* OnboardingConsentMarkdownTestView.swift */; }; @@ -67,7 +66,6 @@ buildActionMask = 2147483647; files = ( 2F61BDC329DD02D600D71D33 /* SpeziOnboarding in Frameworks */, - 2F61BDCE29DDE7B300D71D33 /* Spezi in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -175,7 +173,6 @@ name = TestApp; packageProductDependencies = ( 2F61BDC229DD02D600D71D33 /* SpeziOnboarding */, - 2F61BDCD29DDE7B300D71D33 /* Spezi */, ); productName = Example; productReference = 2F6D139228F5F384007C25D6 /* TestApp.app */; @@ -232,7 +229,6 @@ ); mainGroup = 2F6D138928F5F384007C25D6; packageReferences = ( - 2F61BDCC29DDE7B300D71D33 /* XCRemoteSwiftPackageReference "Spezi" */, 97C6AF752ACC74080060155B /* XCRemoteSwiftPackageReference "XCTestExtensions" */, ); productRefGroup = 2F6D139328F5F384007C25D6 /* Products */; @@ -690,14 +686,6 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 2F61BDCC29DDE7B300D71D33 /* XCRemoteSwiftPackageReference "Spezi" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/StanfordSpezi/Spezi.git"; - requirement = { - kind = upToNextMinorVersion; - minimumVersion = 0.7.0; - }; - }; 97C6AF752ACC74080060155B /* XCRemoteSwiftPackageReference "XCTestExtensions" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/StanfordBDHG/XCTestExtensions"; @@ -713,11 +701,6 @@ isa = XCSwiftPackageProductDependency; productName = SpeziOnboarding; }; - 2F61BDCD29DDE7B300D71D33 /* Spezi */ = { - isa = XCSwiftPackageProductDependency; - package = 2F61BDCC29DDE7B300D71D33 /* XCRemoteSwiftPackageReference "Spezi" */; - productName = Spezi; - }; 97C6AF7A2ACC89000060155B /* XCTestExtensions */ = { isa = XCSwiftPackageProductDependency; package = 97C6AF752ACC74080060155B /* XCRemoteSwiftPackageReference "XCTestExtensions" */;