From 714f01ae1e67bf9c1c0e7c07624380f9bea772b7 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Wed, 10 Jan 2024 17:11:58 -0800 Subject: [PATCH] Allow to supply active account details with the AccountConfiguration for previews (#46) # Allow to supply active account details with the AccountConfiguration for previews ## :recycle: Current situation & Problem As of right now the desired way to provide access to the `Account` object for previewing purposes with active account details, is by creating the `Account` instance yourself and putting it into the environment. With the introduction of the new `previewWith` modifier in Spezi `1.0`, we now have a framework-defined way of configuring modules for Previews. Therefore, this PR adds support to configure the `AccountConfiguration` for previewing purposes. ## :gear: Release Notes * Add `AccountConfiguration(building:active:configuration:)` initializer to easily preview Spezi Account using the new spezi `previewWith` modifier. ## :books: Documentation Documentation was added. ## :white_check_mark: Testing _TBA_ ## :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). --- Sources/SpeziAccount/Account.swift | 3 +- .../SpeziAccount/AccountConfiguration.swift | 25 ++++++++++++-- Sources/SpeziAccount/AccountHeader.swift | 33 ++++++++++++++----- Sources/SpeziAccount/AccountOverview.swift | 5 ++- Sources/SpeziAccount/AccountSetup.swift | 12 +++++-- .../AccountOverviewFormViewModel.swift | 4 ++- .../AccountKeyOverviewRow.swift | 13 ++++---- .../AccountOverviewSections.swift | 5 ++- .../Views/AccountOverview/NameOverview.swift | 22 ++++++------- .../AccountOverview/PasswordChangeSheet.swift | 14 ++++---- .../AccountOverview/SecurityOverview.swift | 14 ++++---- .../AccountOverview/SingleEditView.swift | 14 ++++---- .../AccountSetup/FollowUpInfoSheet.swift | 9 ++--- .../Views/Preview/AccountDetailsReader.swift | 26 +++++++++++++++ 14 files changed, 136 insertions(+), 63 deletions(-) create mode 100644 Sources/SpeziAccount/Views/Preview/AccountDetailsReader.swift diff --git a/Sources/SpeziAccount/Account.swift b/Sources/SpeziAccount/Account.swift index 7039ee2c..8f05e8fa 100644 --- a/Sources/SpeziAccount/Account.swift +++ b/Sources/SpeziAccount/Account.swift @@ -97,7 +97,7 @@ public final class Account: Sendable { /// - services: A collection of ``AccountService`` that are used to handle account-related functionality. /// - supportedConfiguration: The ``AccountValueConfiguration`` to user intends to support. /// - details: A initial ``AccountDetails`` object. The ``signedIn`` is set automatically based on the presence of this argument. - private nonisolated init( + nonisolated init( services: [any AccountService], supportedConfiguration: AccountValueConfiguration = .default, details: AccountDetails? = nil @@ -159,6 +159,7 @@ public final class Account: Sendable { /// - builder: A ``AccountValuesBuilder`` for ``AccountDetails`` with all account details for the logged in user. /// - accountService: The ``AccountService`` that is managing the provided ``AccountDetails``. /// - configuration: The ``AccountValueConfiguration`` to user intends to support. + @available(*, deprecated, message: "Use the AccountConfiguration(building:active:configuration) and previewWith(_:) modifier for previews.") public nonisolated convenience init( building builder: AccountDetails.Builder, active accountService: Service, diff --git a/Sources/SpeziAccount/AccountConfiguration.swift b/Sources/SpeziAccount/AccountConfiguration.swift index 3f5d4a23..7ea6acb8 100644 --- a/Sources/SpeziAccount/AccountConfiguration.swift +++ b/Sources/SpeziAccount/AccountConfiguration.swift @@ -30,12 +30,14 @@ public final class AccountConfiguration: Module { /// An array of ``AccountService``s provided directly in the initializer of the configuration object. private let providedAccountServices: [any AccountService] - @Model private var account: Account + @Model private(set) var account: Account @StandardActor private var standard: any Standard /// The array of ``AccountService``s provided through other Spezi `Components`. @Collect private var accountServices: [any AccountService] + /// Default active Account Details provided for previewing opportunities. + private let defaultActiveDetails: AccountDetails? /// Initializes a `AccountConfiguration` without directly providing any ``AccountService`` instances. @@ -46,6 +48,7 @@ public final class AccountConfiguration: Module { public init(configuration: AccountValueConfiguration = .default) { self.configuredAccountKeys = configuration self.providedAccountServices = [] + self.defaultActiveDetails = nil } /// Initializes a `AccountConfiguration` by directly providing a set of ``AccountService`` instances. @@ -62,6 +65,23 @@ public final class AccountConfiguration: Module { ) { self.configuredAccountKeys = configuration self.providedAccountServices = accountServices() + self.defaultActiveDetails = nil + } + + /// Configure the Account Module for previewing purposes with default `AccountDetails`. + /// + /// - Parameters: + /// - builder: The ``AccountDetails`` Builder for the account details that you want to supply. + /// - accountService: The ``AccountService`` that is responsible for the supplied account details. + /// - configuration: The user-defined configuration of account values that all user accounts need to support. + public init( + building builder: AccountDetails.Builder, + active accountService: Service, + configuration: AccountValueConfiguration = .default + ) { + self.configuredAccountKeys = configuration + self.providedAccountServices = [accountService] + self.defaultActiveDetails = builder.build(owner: accountService) } @@ -81,7 +101,8 @@ public final class AccountConfiguration: Module { self.account = Account( services: accountServices, - configuration: configuredAccountKeys + supportedConfiguration: configuredAccountKeys, + details: defaultActiveDetails ) self.account.injectWeakAccount(into: standard) diff --git a/Sources/SpeziAccount/AccountHeader.swift b/Sources/SpeziAccount/AccountHeader.swift index 41a298fc..065ad293 100644 --- a/Sources/SpeziAccount/AccountHeader.swift +++ b/Sources/SpeziAccount/AccountHeader.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import Spezi import SpeziPersonalInfo import SwiftUI @@ -42,8 +43,8 @@ public struct AccountHeader: View { } @Environment(Account.self) private var account - private var caption: LocalizedStringResource - + private let caption: Text + public var body: some View { let accountDetails = account.details @@ -68,7 +69,7 @@ public struct AccountHeader: View { .font(.title2) .fontWeight(.semibold) .redacted(reason: account.details == nil ? .placeholder : []) - Text(caption) + caption .font(.caption) } } @@ -77,6 +78,12 @@ public struct AccountHeader: View { /// Display a new Account Header. /// - Parameter caption: A descriptive text displayed under the account name giving the user a brief explanation of what to expect when they interact with the header. public init(caption: LocalizedStringResource = Defaults.caption) { + self.init(caption: Text(caption)) + } + + /// Display a new Account Header. + /// - Parameter caption: A descriptive text displayed under the account name giving the user a brief explanation of what to expect when they interact with the header. + public init(caption: Text) { self.caption = caption } } @@ -89,11 +96,13 @@ public struct AccountHeader: View { .set(\.name, value: PersonNameComponents(givenName: "Andreas", familyName: "Bauer")) return AccountHeader() - .environment(Account(building: details, active: MockUserIdPasswordAccountService())) + .previewWith { + AccountConfiguration(building: details, active: MockUserIdPasswordAccountService()) + } } #Preview { - AccountHeader(caption: "Email, Password, Preferences") + AccountHeader(caption: Text(verbatim: "Email, Password, Preferences")) .environment(Account(MockUserIdPasswordAccountService())) } @@ -113,7 +122,9 @@ public struct AccountHeader: View { } } } - .environment(Account(building: details, active: MockUserIdPasswordAccountService())) + .previewWith { + AccountConfiguration(building: details, active: MockUserIdPasswordAccountService()) + } } #Preview { @@ -131,7 +142,9 @@ public struct AccountHeader: View { } } } - .environment(Account(building: details, active: MockUserIdPasswordAccountService())) + .previewWith { + AccountConfiguration(building: details, active: MockUserIdPasswordAccountService()) + } } #Preview { @@ -146,6 +159,10 @@ public struct AccountHeader: View { } } } - .environment(Account(MockUserIdPasswordAccountService())) + .previewWith { + AccountConfiguration { + MockUserIdPasswordAccountService() + } + } } #endif diff --git a/Sources/SpeziAccount/AccountOverview.swift b/Sources/SpeziAccount/AccountOverview.swift index c829cf79..f4803372 100644 --- a/Sources/SpeziAccount/AccountOverview.swift +++ b/Sources/SpeziAccount/AccountOverview.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import Spezi import SpeziViews import SwiftUI @@ -126,7 +127,9 @@ struct AccountOverView_Previews: PreviewProvider { } } } - .environment(Account(building: details, active: MockUserIdPasswordAccountService())) + .previewWith { + AccountConfiguration(building: details, active: MockUserIdPasswordAccountService()) + } NavigationStack { AccountOverview() diff --git a/Sources/SpeziAccount/AccountSetup.swift b/Sources/SpeziAccount/AccountSetup.swift index 366ec737..d518a9d1 100644 --- a/Sources/SpeziAccount/AccountSetup.swift +++ b/Sources/SpeziAccount/AccountSetup.swift @@ -241,10 +241,14 @@ struct AccountView_Previews: PreviewProvider { } AccountSetup() - .environment(Account(building: detailsBuilder, active: MockUserIdPasswordAccountService())) + .previewWith { + AccountConfiguration(building: detailsBuilder, active: MockUserIdPasswordAccountService()) + } AccountSetup(state: .setupShown) - .environment(Account(building: detailsBuilder, active: MockUserIdPasswordAccountService())) + .previewWith { + AccountConfiguration(building: detailsBuilder, active: MockUserIdPasswordAccountService()) + } AccountSetup(continue: { Button(action: { @@ -255,7 +259,9 @@ struct AccountView_Previews: PreviewProvider { }) .buttonStyle(.borderedProminent) }) - .environment(Account(building: detailsBuilder, active: MockUserIdPasswordAccountService())) + .previewWith { + AccountConfiguration(building: detailsBuilder, active: MockUserIdPasswordAccountService()) + } } } #endif diff --git a/Sources/SpeziAccount/ViewModel/AccountOverviewFormViewModel.swift b/Sources/SpeziAccount/ViewModel/AccountOverviewFormViewModel.swift index 98367ca1..d7688c06 100644 --- a/Sources/SpeziAccount/ViewModel/AccountOverviewFormViewModel.swift +++ b/Sources/SpeziAccount/ViewModel/AccountOverviewFormViewModel.swift @@ -57,7 +57,9 @@ class AccountOverviewFormViewModel { for describedKey in details.accountService.configuration.requiredAccountKeys where describedKey.key.category == category { - result.append(describedKey.key) + if !result.contains(where: { $0 == describedKey.key }) { + result.append(describedKey.key) + } } return result diff --git a/Sources/SpeziAccount/Views/AccountOverview/AccountKeyOverviewRow.swift b/Sources/SpeziAccount/Views/AccountOverview/AccountKeyOverviewRow.swift index 35750cab..edc618d1 100644 --- a/Sources/SpeziAccount/Views/AccountOverview/AccountKeyOverviewRow.swift +++ b/Sources/SpeziAccount/Views/AccountOverview/AccountKeyOverviewRow.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import Spezi import SwiftUI @@ -84,16 +85,16 @@ struct AccountKeyEditRow_Previews: PreviewProvider { .set(\.name, value: PersonNameComponents(givenName: "Andreas", familyName: "Bauer")) .set(\.genderIdentity, value: .male) - static let account = Account(building: details, active: MockUserIdPasswordAccountService()) - - @State private static var model = AccountOverviewFormViewModel(account: account) - static var previews: some View { - if let details = account.details { + AccountDetailsReader { account, details in + let model = AccountOverviewFormViewModel(account: account) + AccountKeyOverviewRow(details: details, for: GenderIdentityKey.self, model: model) .injectEnvironmentObjects(service: details.accountService, model: model) - .environment(account) } + .previewWith { + AccountConfiguration(building: details, active: MockUserIdPasswordAccountService()) + } } } #endif diff --git a/Sources/SpeziAccount/Views/AccountOverview/AccountOverviewSections.swift b/Sources/SpeziAccount/Views/AccountOverview/AccountOverviewSections.swift index 0b201184..836c5f98 100644 --- a/Sources/SpeziAccount/Views/AccountOverview/AccountOverviewSections.swift +++ b/Sources/SpeziAccount/Views/AccountOverview/AccountOverviewSections.swift @@ -7,6 +7,7 @@ // import OrderedCollections +import Spezi import SpeziValidation import SpeziViews import SwiftUI @@ -285,7 +286,9 @@ struct AccountOverviewSections_Previews: PreviewProvider { } } } - .environment(Account(building: details, active: MockUserIdPasswordAccountService())) + .previewWith { + AccountConfiguration(building: details, active: MockUserIdPasswordAccountService()) + } } } #endif diff --git a/Sources/SpeziAccount/Views/AccountOverview/NameOverview.swift b/Sources/SpeziAccount/Views/AccountOverview/NameOverview.swift index 80cf90e8..7433271a 100644 --- a/Sources/SpeziAccount/Views/AccountOverview/NameOverview.swift +++ b/Sources/SpeziAccount/Views/AccountOverview/NameOverview.swift @@ -76,26 +76,24 @@ struct NameOverview_Previews: PreviewProvider { static let detailsWithoutName = AccountDetails.Builder() .set(\.userId, value: "andi.bauer@tum.de") - static let account = Account(building: details, active: MockUserIdPasswordAccountService()) - static let accountWithoutName = Account(building: detailsWithoutName, active: MockUserIdPasswordAccountService()) - - // be aware, modifications won't be displayed due to declaration in PreviewProvider that do not trigger an UI update - @State static var model = AccountOverviewFormViewModel(account: account) - static var previews: some View { NavigationStack { - if let details = account.details { - NameOverview(model: model, details: details) + AccountDetailsReader { account, details in + NameOverview(model: AccountOverviewFormViewModel(account: account), details: details) } } - .environment(account) + .previewWith { + AccountConfiguration(building: details, active: MockUserIdPasswordAccountService()) + } NavigationStack { - if let details = accountWithoutName.details { - NameOverview(model: model, details: details) + AccountDetailsReader { account, details in + NameOverview(model: AccountOverviewFormViewModel(account: account), details: details) } } - .environment(accountWithoutName) + .previewWith { + AccountConfiguration(building: detailsWithoutName, active: MockUserIdPasswordAccountService()) + } } } #endif diff --git a/Sources/SpeziAccount/Views/AccountOverview/PasswordChangeSheet.swift b/Sources/SpeziAccount/Views/AccountOverview/PasswordChangeSheet.swift index 9c27c5d9..bbd1190a 100644 --- a/Sources/SpeziAccount/Views/AccountOverview/PasswordChangeSheet.swift +++ b/Sources/SpeziAccount/Views/AccountOverview/PasswordChangeSheet.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import Spezi import SpeziValidation import SpeziViews import SwiftUI @@ -136,18 +137,15 @@ struct PasswordChangeSheet_Previews: PreviewProvider { .set(\.name, value: PersonNameComponents(givenName: "Andreas", familyName: "Bauer")) .set(\.genderIdentity, value: .male) - static let account = Account(building: details, active: MockUserIdPasswordAccountService()) - - // be aware, modifications won't be displayed due to declaration in PreviewProvider that do not trigger an UI update - @State static var model = AccountOverviewFormViewModel(account: account) - static var previews: some View { NavigationStack { - if let details = account.details { - PasswordChangeSheet(model: model, details: details) + AccountDetailsReader { account, details in + PasswordChangeSheet(model: AccountOverviewFormViewModel(account: account), details: details) } } - .environment(account) + .previewWith { + AccountConfiguration(building: details, active: MockUserIdPasswordAccountService()) + } } } #endif diff --git a/Sources/SpeziAccount/Views/AccountOverview/SecurityOverview.swift b/Sources/SpeziAccount/Views/AccountOverview/SecurityOverview.swift index 87e76009..c1f5526e 100644 --- a/Sources/SpeziAccount/Views/AccountOverview/SecurityOverview.swift +++ b/Sources/SpeziAccount/Views/AccountOverview/SecurityOverview.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import Spezi import SpeziViews import SwiftUI @@ -79,18 +80,15 @@ struct SecurityOverview_Previews: PreviewProvider { .set(\.name, value: PersonNameComponents(givenName: "Andreas", familyName: "Bauer")) .set(\.genderIdentity, value: .male) - static let account = Account(building: details, active: MockUserIdPasswordAccountService()) - - // be aware, modifications won't be displayed due to declaration in PreviewProvider that do not trigger an UI update - @State static var model = AccountOverviewFormViewModel(account: account) - static var previews: some View { NavigationStack { - if let details = account.details { - SecurityOverview(model: model, details: details) + AccountDetailsReader { account, details in + SecurityOverview(model: AccountOverviewFormViewModel(account: account), details: details) } } - .environment(account) + .previewWith { + AccountConfiguration(building: details, active: MockUserIdPasswordAccountService()) + } } } #endif diff --git a/Sources/SpeziAccount/Views/AccountOverview/SingleEditView.swift b/Sources/SpeziAccount/Views/AccountOverview/SingleEditView.swift index aa843675..d4ae63c5 100644 --- a/Sources/SpeziAccount/Views/AccountOverview/SingleEditView.swift +++ b/Sources/SpeziAccount/Views/AccountOverview/SingleEditView.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // +import Spezi import SpeziValidation import SpeziViews import SwiftUI @@ -86,18 +87,15 @@ struct SingleEditView_Previews: PreviewProvider { .set(\.userId, value: "andi.bauer@tum.de") .set(\.name, value: PersonNameComponents(givenName: "Andreas", familyName: "Bauer")) - static let account = Account(building: details, active: MockUserIdPasswordAccountService()) - - // be aware, modifications won't be displayed due to declaration in PreviewProvider that do not trigger an UI update - @State static var model = AccountOverviewFormViewModel(account: account) - static var previews: some View { NavigationStack { - if let details = account.details { - SingleEditView(model: model, details: details) + AccountDetailsReader { account, details in + SingleEditView(model: AccountOverviewFormViewModel(account: account), details: details) } } - .environment(account) + .previewWith { + AccountConfiguration(building: details, active: MockUserIdPasswordAccountService()) + } } } #endif diff --git a/Sources/SpeziAccount/Views/AccountSetup/FollowUpInfoSheet.swift b/Sources/SpeziAccount/Views/AccountSetup/FollowUpInfoSheet.swift index 07757fad..b59813b1 100644 --- a/Sources/SpeziAccount/Views/AccountSetup/FollowUpInfoSheet.swift +++ b/Sources/SpeziAccount/Views/AccountSetup/FollowUpInfoSheet.swift @@ -7,6 +7,7 @@ // import OrderedCollections +import Spezi import SpeziValidation import SpeziViews import SwiftUI @@ -143,15 +144,15 @@ struct FollowUpInfoSheet_Previews: PreviewProvider { static let details = AccountDetails.Builder() .set(\.userId, value: "lelandstanford@stanford.edu") - static let account = Account(building: details, active: MockUserIdPasswordAccountService()) - static var previews: some View { NavigationStack { - if let details = account.details { + AccountDetailsReader { _, details in FollowUpInfoSheet(details: details, requiredKeys: [PersonNameKey.self]) } } - .environment(account) + .previewWith { + AccountConfiguration(building: details, active: MockUserIdPasswordAccountService()) + } } } #endif diff --git a/Sources/SpeziAccount/Views/Preview/AccountDetailsReader.swift b/Sources/SpeziAccount/Views/Preview/AccountDetailsReader.swift new file mode 100644 index 00000000..2cec868b --- /dev/null +++ b/Sources/SpeziAccount/Views/Preview/AccountDetailsReader.swift @@ -0,0 +1,26 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + + +import SwiftUI + + +struct AccountDetailsReader: View { + @Environment(Account.self) var account + private let bodyClosure: (Account, AccountDetails) -> Content + + var body: some View { + if let details = account.details { + bodyClosure(account, details) + } + } + + init(@ViewBuilder _ bodyClosure: @escaping (Account, AccountDetails) -> Content) { + self.bodyClosure = bodyClosure + } +}