Skip to content

Commit

Permalink
Make AccountConfiguration non-generic, introduce XCTSpeziAccount (#76)
Browse files Browse the repository at this point in the history
# Make AccountConfiguration non-generic, introduce XCTSpeziAccount

## ♻️ Current situation & Problem
This PR introduces some final adjustments for the SpeziAccount 2.0
release. Most importantly, it removes the generic constraint for the
`AccountConfiguration` allowing to more easily write code that is
agnostic to the account service implementation.
Secondly, we introduce the `XCTSpeziAccount` target, to make it easier
to implement UI test with SpeziAccount UI components.


## ⚙️ Release Notes 
* Make `AccountConfiguration` non-generic
* Introduce `XCTSpeziAccount` target.

## 📚 Documentation
Additional documentation target was provided.

## ✅ Testing
Existing unit and UI tests are used.

## 📝 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 30, 2024
1 parent 7ef29a7 commit 272ae7c
Show file tree
Hide file tree
Showing 38 changed files with 287 additions and 275 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ jobs:
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
with:
runsonlabels: '["macOS", "self-hosted"]'
scheme: SpeziAccount
scheme: SpeziAccount-Package
artifactname: SpeziAccount.xcresult
resultBundle: SpeziAccount.xcresult
buildandtest_macos:
name: Build and Test Swift Package macOS
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
with:
runsonlabels: '["macOS", "self-hosted"]'
scheme: SpeziAccount
scheme: SpeziAccount-Package
destination: 'platform=macOS,arch=arm64'
artifactname: SpeziAccount-macOS.xcresult
resultBundle: SpeziAccount-macOS.xcresult
Expand All @@ -38,7 +38,7 @@ jobs:
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
with:
runsonlabels: '["macOS", "self-hosted"]'
scheme: SpeziAccount
scheme: SpeziAccount-Package
destination: 'platform=visionOS Simulator,name=Apple Vision Pro'
resultBundle: SpeziAccount-visionOS.xcresult
artifactname: SpeziAccount-visionOS.xcresult
Expand Down
1 change: 1 addition & 0 deletions .spi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ builder:
- platform: ios
documentation_targets:
- SpeziAccount
- XCTSpeziAccount
16 changes: 13 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,16 @@ let package = Package(
.visionOS(.v1)
],
products: [
.library(name: "SpeziAccount", targets: ["SpeziAccount"])
.library(name: "SpeziAccount", targets: ["SpeziAccount"]),
.library(name: "XCTSpeziAccount", targets: ["XCTSpeziAccount"])
],
dependencies: [
.package(url: "https://github.com/StanfordSpezi/SpeziFoundation.git", from: "2.0.0-beta.2"),
.package(url: "https://github.com/StanfordSpezi/SpeziFoundation.git", from: "2.0.0"),
.package(url: "https://github.com/StanfordSpezi/Spezi.git", from: "1.7.3"),
.package(url: "https://github.com/StanfordSpezi/SpeziViews.git", from: "1.6.0"),
.package(url: "https://github.com/StanfordSpezi/SpeziViews.git", from: "1.7.0"),
.package(url: "https://github.com/StanfordSpezi/SpeziStorage.git", from: "1.2.0"),
.package(url: "https://github.com/StanfordBDHG/XCTRuntimeAssertions.git", from: "1.1.1"),
.package(url: "https://github.com/StanfordBDHG/XCTestExtensions.git", from: "1.1.0"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.1.2"),
.package(url: "https://github.com/apple/swift-atomics.git", from: "1.2.0"),
.package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0-prerelease-2024-08-14"),
Expand Down Expand Up @@ -65,6 +67,14 @@ let package = Package(
],
plugins: [] + swiftLintPlugin()
),
.target(
name: "XCTSpeziAccount",
dependencies: [
.target(name: "SpeziAccount"),
.product(name: "XCTestExtensions", package: "XCTestExtensions")
],
plugins: [] + swiftLintPlugin()
),
.testTarget(
name: "SpeziAccountTests",
dependencies: [
Expand Down
33 changes: 20 additions & 13 deletions Sources/SpeziAccount/AccountConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@ import XCTRuntimeAssertions
/// using the `@Dependency` property wrapper from other Spezi `Module`s.
///
/// - Note: For more information on how to provide an ``AccountService`` refer to the <doc:Creating-your-own-Account-Service> article.
public final class AccountConfiguration<Service: AccountService> {
public final class AccountConfiguration {
@Application(\.logger)
private var logger

@Dependency(Account.self)
var account
@Dependency(ExternalAccountStorage.self)
private var externalStorage
@Dependency(Service.self)
private var accountService

@Dependency private var accountService: [any Module]
@Dependency private var storageProvider: [any Module]

@StandardActor private var standard: any Standard


Expand All @@ -43,7 +43,7 @@ public final class AccountConfiguration<Service: AccountService> {
/// - Parameters:
/// - service: The `AccountService` to use with the framework.
/// - configuration: The user-defined configuration of account values that all user accounts need to support.
public convenience init(
public convenience init<Service: AccountService>(
service: Service,
configuration: AccountValueConfiguration
) {
Expand All @@ -59,7 +59,7 @@ public final class AccountConfiguration<Service: AccountService> {
/// - service: The `AccountService` to use with the framework.
/// - storageProvider: The storage provider that will be used to store additional account details.
/// - configuration: The user-defined configuration of account values that all user accounts need to support.
public convenience init<Storage: AccountStorageProvider>(
public convenience init<Service: AccountService, Storage: AccountStorageProvider>(
service: Service,
storageProvider: Storage,
configuration: AccountValueConfiguration
Expand All @@ -77,7 +77,7 @@ public final class AccountConfiguration<Service: AccountService> {
/// - service: The `AccountService` to use with the framework.
/// - configuration: The user-defined configuration of account values that all user accounts need to support.
@_spi(TestingSupport)
public convenience init(
public convenience init<Service: AccountService>(
service: Service
) {
self.init(accountService: service, configuration: .default)
Expand All @@ -93,7 +93,7 @@ public final class AccountConfiguration<Service: AccountService> {
/// - Parameters:
/// - service: The `AccountService` to use with the framework.
/// - storageProvider: The storage provider that will be used to store additional account details.
public convenience init<Storage: AccountStorageProvider>(
public convenience init<Service: AccountService, Storage: AccountStorageProvider>(
service: Service,
storageProvider: Storage
) {
Expand All @@ -107,7 +107,7 @@ public final class AccountConfiguration<Service: AccountService> {
/// - configuration: The user-defined configuration of account values that all user accounts need to support.
/// - activeDetails: The account details you want to simulate.
@_spi(TestingSupport)
public convenience init( // swiftlint:disable:this function_default_parameter_at_end
public convenience init<Service: AccountService>( // swiftlint:disable:this function_default_parameter_at_end
service: Service,
configuration: AccountValueConfiguration = .default,
activeDetails: AccountDetails
Expand All @@ -123,7 +123,7 @@ public final class AccountConfiguration<Service: AccountService> {
/// - configuration: The user-defined configuration of account values that all user accounts need to support.
/// - activeDetails: The account details you want to simulate.
@_spi(TestingSupport)
public convenience init<Storage: AccountStorageProvider>( // swiftlint:disable:this function_default_parameter_at_end
public convenience init<Service: AccountService, Storage: AccountStorageProvider>( // swiftlint:disable:this function_default_parameter_at_end
service: Service,
storageProvider: Storage,
configuration: AccountValueConfiguration = .default,
Expand All @@ -132,13 +132,15 @@ public final class AccountConfiguration<Service: AccountService> {
self.init(accountService: service, storageProvider: storageProvider, configuration: configuration, defaultActiveDetails: activeDetails)
}

init( // swiftlint:disable:this function_default_parameter_at_end
init<Service: AccountService>( // swiftlint:disable:this function_default_parameter_at_end
accountService: Service,
storageProvider: (any AccountStorageProvider)? = nil,
configuration: AccountValueConfiguration,
defaultActiveDetails: AccountDetails? = nil
) {
self._accountService = Dependency(load: accountService)
self._accountService = Dependency {
accountService
}
self._storageProvider = Dependency {
if let storageProvider {
storageProvider
Expand All @@ -156,15 +158,20 @@ public final class AccountConfiguration<Service: AccountService> {
/// Configure the module.
@MainActor
public func configure() {
guard let service = accountService.first as? AccountService,
accountService.count == 1 else {
preconditionFailure("Unexpected error when trying to configure account service.")
}

// Verify account service can store all configured account keys.
// If applicable, wraps the service into an StandardBackedAccountService
verify(configurationRequirements: account.configuration, against: accountService)
verify(configurationRequirements: account.configuration, against: service)
}

@MainActor
private func verify(
configurationRequirements configuration: AccountValueConfiguration,
against service: Service
against service: any AccountService
) {
logger.debug("Checking \(service.description) against the configured account keys.")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// This source file is part of the Spezi open-source project
//
// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import Foundation


@_spi(TestingSupport)
extension AccountDetails {
static func createMock(
id: String = UUID().uuidString,
userId: String = "lelandstanford@stanford.edu",
name: PersonNameComponents? = PersonNameComponents(givenName: "Leland", familyName: "Stanford"),
genderIdentity: GenderIdentity? = nil,
dateOfBirth: Date? = nil
) -> AccountDetails {
var details = AccountDetails()
details.accountId = id
details.userId = userId
details.name = name
details.genderIdentity = genderIdentity
details.dateOfBirth = dateOfBirth
return details
}
}
2 changes: 1 addition & 1 deletion Sources/SpeziAccount/Environment/AccountRequiredKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ struct AccountRequiredKey: EnvironmentKey {
extension EnvironmentValues {
/// An environment variable that indicates if an account was configured to be required for the app.
///
/// Fore more information have a look at ``SwiftUI/View/accountRequired(_:considerAnonymousAccounts:setupSheet:)``.
/// Fore more information have a look at ``SwiftUICore/View/accountRequired(_:considerAnonymousAccounts:setupSheet:)``.
public var accountRequired: Bool {
get {
self[AccountRequiredKey.self]
Expand Down
2 changes: 1 addition & 1 deletion Sources/SpeziAccount/Environment/AccountViewType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import SwiftUI

/// Defines the type of `SpeziAccount` view a ``DataEntryView`` or ``DataDisplayView`` is placed in.
///
/// Access this property inside supporting views using the ``SwiftUI/EnvironmentValues/accountViewType`` environment key.
/// Access this property inside supporting views using the ``SwiftUICore/EnvironmentValues/accountViewType`` environment key.
///
/// ```swift
/// struct MyView: View {
Expand Down
2 changes: 1 addition & 1 deletion Sources/SpeziAccount/Environment/FollowUpBehavior.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import SwiftUI

/// Determine how the Follow-Up information sheet is presented after account setup.
///
/// Use the ``SwiftUI/View/followUpBehaviorAfterSetup(_:)`` modifier to set how the follow-up information sheet is
/// Use the ``SwiftUICore/View/followUpBehaviorAfterSetup(_:)`` modifier to set how the follow-up information sheet is
/// shown inside the ``AccountSetup`` after a successful setup (login or signup).
public enum FollowUpBehavior {
/// Follow up information will never be asked for after account setup.
Expand Down
2 changes: 1 addition & 1 deletion Sources/SpeziAccount/Environment/PreferredSetupStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import SwiftUI
/// In some situations, views might be able to present themselves such that login or signup operations are favored.
/// For example, an `AccountSetup` view that is displayed in the onboarding flow, might favor a presentation that highlights signup functionality.
///
/// - Note: Use the ``SwiftUI/View/preferredAccountSetupStyle(_:)`` to set the preferred account setup style.
/// - Note: Use the ``SwiftUICore/View/preferredAccountSetupStyle(_:)`` to set the preferred account setup style.
public enum PreferredSetupStyle {
/// Let the view automatically decide on how to present itself.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ private struct SignupProviderComplianceKey: PreferenceKey {
///
/// The `SignupProviderCompliance` is used by the ``AccountSetup`` view to reason about the compliance of the signup provider.
///
/// Use the ``SwiftUI/View/reportSignupProviderCompliance(_:)`` to set the compliance for your custom signup provider, if necessary.
/// Use the ``SwiftUICore/View/reportSignupProviderCompliance(_:)`` to set the compliance for your custom signup provider, if necessary.
///
/// - Note: The compliance preference is automatically set when using the ``SignupForm`` or the ``SignInWithAppleButton``.
public struct SignupProviderCompliance {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ A ``AccountKey/DataEntry`` view is automatically provide if:
A simple number entry will appear. You have to implement your own view if you have special formatting requirements.
* The `Value` is a [`BinaryFloatingPoint`](https://developer.apple.com/documentation/swift/binaryfloatingpoint) (e.g., `Double` or `Float`).
A simple decimal entry will appear. You have to implement your own view if you have special formatting requirements.
* The `Value` conforms to the ``PickerValue`` protocols. This is provides a Picker UI for enum types.
* The `Value` conforms to the `PickerValue` protocols. This is provides a Picker UI for enum types.
`PickerValue` is shorthand to conform to the [`CaseIterable`](https://developer.apple.com/documentation/swift/caseiterable),
[`CustomLocalizedStringResourceConvertible`](https://developer.apple.com/documentation/foundation/customlocalizedstringresourceconvertible)
and [`Hashable`](https://developer.apple.com/documentation/swift/hashable) protocols.
Expand Down Expand Up @@ -209,6 +209,6 @@ Still, you are required to evaluate to which extent validation has to be handled

### Available Environment Keys

- ``SwiftUI/EnvironmentValues/accountViewType``
- ``SwiftUI/EnvironmentValues/passwordFieldType``
- ``SwiftUI/EnvironmentValues/accountServiceConfiguration``
- ``SwiftUICore/EnvironmentValues/accountViewType``
- ``SwiftUICore/EnvironmentValues/passwordFieldType``
- ``SwiftUICore/EnvironmentValues/accountServiceConfiguration``
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ actor MyAccountService: AccountService {

### UI Customization

- ``SwiftUI/View/reportSignupProviderCompliance(_:)``
- ``SwiftUICore/View/reportSignupProviderCompliance(_:)``
- ``SignupProviderCompliance``

### Credentials
Expand Down
8 changes: 4 additions & 4 deletions Sources/SpeziAccount/SpeziAccount.docc/SpeziAccount.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,13 @@ Refer to the <doc:Creating-your-own-Account-Service> article if you plan on impl
- ``AccountOverview``
- ``AccountHeader``
- ``FollowUpInfoSheet``
- ``SwiftUI/View/accountRequired(_:considerAnonymousAccounts:setupSheet:)``
- ``SwiftUI/EnvironmentValues/accountRequired``
- ``SwiftUICore/View/accountRequired(_:considerAnonymousAccounts:setupSheet:)``
- ``SwiftUICore/EnvironmentValues/accountRequired``

### Environment & Preferences

- ``SwiftUI/View/preferredAccountSetupStyle(_:)``
- ``SwiftUI/View/followUpBehaviorAfterSetup(_:)``
- ``SwiftUICore/View/preferredAccountSetupStyle(_:)``
- ``SwiftUICore/View/followUpBehaviorAfterSetup(_:)``

### Reacting to Events

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ extension View {
/// If account requirement is set, this modifier will automatically pop open an account setup sheet if
/// it is detected that the associated user account was removed.
///
/// - Note: This modifier injects the ``SwiftUI/EnvironmentValues/accountRequired`` property depending on the `required` argument.
/// - Note: This modifier injects the ``SwiftUICore/EnvironmentValues/accountRequired`` property depending on the `required` argument.
///
/// - Parameters:
/// - required: The flag indicating if an account is required at all times.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,14 @@ struct AccountKeyOverviewRow: View {
#if DEBUG && !os(macOS)
private let key = AccountKeys.genderIdentity
#Preview {
var details = AccountDetails()
details.userId = "lelandstanford@stanford.edu"
details.name = PersonNameComponents(givenName: "Leland", familyName: "Stanford")
details.genderIdentity = .male

return AccountDetailsReader { account, details in
AccountDetailsReader { account, details in
let model = AccountOverviewFormViewModel(account: account, details: details)

AccountKeyOverviewRow(details: details, for: key, model: model)
.injectEnvironmentObjects(configuration: details.accountServiceConfiguration, model: model)
}
.previewWith {
AccountConfiguration(service: InMemoryAccountService(), activeDetails: details)
AccountConfiguration(service: InMemoryAccountService(), activeDetails: .createMock(genderIdentity: .male))
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,6 @@ struct AccountOverviewHeader: View {

#if DEBUG
#Preview {
var details = AccountDetails()
details.userId = "lelandstanford@stanford.edu"
details.name = PersonNameComponents(givenName: "Leland", familyName: "Stanford")

return AccountOverviewHeader(details: details)
AccountOverviewHeader(details: .createMock())
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -321,12 +321,7 @@ struct AccountOverviewSections<AdditionalSections: View>: View { // swiftlint:di

#if DEBUG && !os(macOS)
#Preview {
var details = AccountDetails()
details.userId = "lelandstanford@stanford.edu"
details.name = PersonNameComponents(givenName: "Leland", familyName: "Stanford")
details.genderIdentity = .male

return NavigationStack {
NavigationStack {
AccountOverview {
Section(header: Text(verbatim: "App")) {
NavigationLink {
Expand All @@ -343,17 +338,12 @@ struct AccountOverviewSections<AdditionalSections: View>: View { // swiftlint:di
}
}
.previewWith {
AccountConfiguration(service: InMemoryAccountService(), activeDetails: details)
AccountConfiguration(service: InMemoryAccountService(), activeDetails: .createMock(genderIdentity: .male))
}
}

#Preview {
var details = AccountDetails()
details.userId = "lelandstanford@stanford.edu"
details.name = PersonNameComponents(givenName: "Leland", familyName: "Stanford")
details.genderIdentity = .male

return NavigationStack {
NavigationStack {
AccountOverview(deletion: .belowLogout) {
Section(header: Text(verbatim: "App")) {
NavigationLink {
Expand All @@ -370,7 +360,7 @@ struct AccountOverviewSections<AdditionalSections: View>: View { // swiftlint:di
}
}
.previewWith {
AccountConfiguration(service: InMemoryAccountService(), activeDetails: details)
AccountConfiguration(service: InMemoryAccountService(), activeDetails: .createMock(genderIdentity: .male))
}
}
#endif
Loading

0 comments on commit 272ae7c

Please sign in to comment.