diff --git a/Package.swift b/Package.swift index db87835a..4348a729 100644 --- a/Package.swift +++ b/Package.swift @@ -21,7 +21,8 @@ let package = Package( .library(name: "SpeziAccount", targets: ["SpeziAccount"]) ], dependencies: [ - .package(url: "https://github.com/StanfordSpezi/Spezi", .upToNextMinor(from: "0.7.2")), + .package(url: "https://github.com/StanfordSpezi/SpeziFoundation.git", .upToNextMinor(from: "0.1.0")), + .package(url: "https://github.com/StanfordSpezi/Spezi", .upToNextMinor(from: "0.8.0")), .package(url: "https://github.com/StanfordSpezi/SpeziViews", .upToNextMinor(from: "0.6.1")), .package(url: "https://github.com/StanfordBDHG/XCTRuntimeAssertions", .upToNextMinor(from: "0.2.5")), .package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.4")) @@ -30,6 +31,7 @@ let package = Package( .target( name: "SpeziAccount", dependencies: [ + .product(name: "SpeziFoundation", package: "SpeziFoundation"), .product(name: "Spezi", package: "Spezi"), .product(name: "SpeziViews", package: "SpeziViews"), .product(name: "SpeziPersonalInfo", package: "SpeziViews"), diff --git a/Sources/SpeziAccount/Account.swift b/Sources/SpeziAccount/Account.swift index 97141102..685908f4 100644 --- a/Sources/SpeziAccount/Account.swift +++ b/Sources/SpeziAccount/Account.swift @@ -59,8 +59,9 @@ import SwiftUI /// - ``init(services:configuration:)`` /// - ``init(_:configuration:)`` /// - ``init(building:active:configuration:)`` +@Observable @MainActor -public class Account: ObservableObject, Sendable { +public final class Account: Sendable { private let logger: Logger /// The `signedIn` property determines if the the current Account context is signed in or not yet signed in. @@ -71,7 +72,7 @@ public class Account: ObservableObject, Sendable { /// This has the following implications. When `signedIn` is `false`, there might still be a `details` instance present. /// Similarly, when `details` is set to `nil, `signedIn` is guaranteed to be `false`. Otherwise, /// if `details` is set to some value, the `signedIn` property might still be set to `false`. - @Published public private(set) var signedIn: Bool + public private(set) var signedIn: Bool /// Provides access to associated data of the currently associated user account. /// @@ -80,7 +81,7 @@ public class Account: ObservableObject, Sendable { /// /// - Note: The associated ``AccountService`` that is responsible for managing the associated user can be retrieved /// using the ``AccountDetails/accountService`` property. - @Published public private(set) var details: AccountDetails? + public private(set) var details: AccountDetails? /// The user-defined configuration of account values that all user accounts need to support. public let configuration: AccountValueConfiguration @@ -103,8 +104,10 @@ public class Account: ObservableObject, Sendable { ) { self.logger = LoggerKey.defaultValue - self._signedIn = Published(wrappedValue: details != nil) - self._details = Published(wrappedValue: details) + // we have to initialize the macro generated properties directly to stay non-isolated. + self._signedIn = details != nil + self._details = details + self.configuration = supportedConfiguration self.registeredAccountServices = services diff --git a/Sources/SpeziAccount/AccountConfiguration.swift b/Sources/SpeziAccount/AccountConfiguration.swift index c7229bbc..2fe4c052 100644 --- a/Sources/SpeziAccount/AccountConfiguration.swift +++ b/Sources/SpeziAccount/AccountConfiguration.swift @@ -22,7 +22,7 @@ import XCTRuntimeAssertions /// /// - Note: For more information on how to provide an ``AccountService`` if you are implementing your own Spezi `Component` /// refer to the article. -public final class AccountConfiguration: Component, ObservableObjectProvider { +public final class AccountConfiguration: Module { private let logger = LoggerKey.defaultValue /// The user-defined configuration of account values that all user accounts need to support. @@ -30,7 +30,7 @@ public final class AccountConfiguration: Component, ObservableObjectProvider { /// An array of ``AccountService``s provided directly in the initializer of the configuration object. private let providedAccountServices: [any AccountService] - private var account: Account? + @Model private var account: Account @StandardActor private var standard: any Standard @@ -38,15 +38,6 @@ public final class AccountConfiguration: Component, ObservableObjectProvider { @Collect private var accountServices: [any AccountService] - public var observableObjects: [any ObservableObject] { - guard let account else { - preconditionFailure("Tried to access ObservableObjectProvider before \(Self.self).configure() was called") - } - - return [account] - } - - /// Initializes a `AccountConfiguration` without directly providing any ``AccountService`` instances. /// /// ``AccountService`` instances might be automatically collected from other Spezi `Component`s that provide some. @@ -93,7 +84,7 @@ public final class AccountConfiguration: Component, ObservableObjectProvider { configuration: configuredAccountKeys ) - self.account?.injectWeakAccount(into: standard) + self.account.injectWeakAccount(into: standard) } private func verifyConfigurationRequirements(against service: any AccountService) -> any AccountService { diff --git a/Sources/SpeziAccount/AccountHeader.swift b/Sources/SpeziAccount/AccountHeader.swift index 6c2acc93..41a298fc 100644 --- a/Sources/SpeziAccount/AccountHeader.swift +++ b/Sources/SpeziAccount/AccountHeader.swift @@ -41,7 +41,7 @@ public struct AccountHeader: View { // swiftlint:disable:previous attributes } - @EnvironmentObject private var account: Account + @Environment(Account.self) private var account private var caption: LocalizedStringResource public var body: some View { @@ -89,12 +89,12 @@ public struct AccountHeader: View { .set(\.name, value: PersonNameComponents(givenName: "Andreas", familyName: "Bauer")) return AccountHeader() - .environmentObject(Account(building: details, active: MockUserIdPasswordAccountService())) + .environment(Account(building: details, active: MockUserIdPasswordAccountService())) } #Preview { AccountHeader(caption: "Email, Password, Preferences") - .environmentObject(Account(MockUserIdPasswordAccountService())) + .environment(Account(MockUserIdPasswordAccountService())) } #Preview { @@ -113,7 +113,7 @@ public struct AccountHeader: View { } } } - .environmentObject(Account(building: details, active: MockUserIdPasswordAccountService())) + .environment(Account(building: details, active: MockUserIdPasswordAccountService())) } #Preview { @@ -131,7 +131,7 @@ public struct AccountHeader: View { } } } - .environmentObject(Account(building: details, active: MockUserIdPasswordAccountService())) + .environment(Account(building: details, active: MockUserIdPasswordAccountService())) } #Preview { @@ -146,6 +146,6 @@ public struct AccountHeader: View { } } } - .environmentObject(Account(MockUserIdPasswordAccountService())) + .environment(Account(MockUserIdPasswordAccountService())) } #endif diff --git a/Sources/SpeziAccount/AccountOverview.swift b/Sources/SpeziAccount/AccountOverview.swift index 0de22bed..c829cf79 100644 --- a/Sources/SpeziAccount/AccountOverview.swift +++ b/Sources/SpeziAccount/AccountOverview.swift @@ -56,7 +56,7 @@ import SwiftUI /// current edit mode of the view. This can be helpful to, e.g., render a custom `Close` Button if the /// view is not editing when presenting the AccountOverview in a sheet. public struct AccountOverview: View { - @EnvironmentObject private var account: Account + @Environment(Account.self) private var account @Binding private var isEditing: Bool @@ -126,12 +126,12 @@ struct AccountOverView_Previews: PreviewProvider { } } } - .environmentObject(Account(building: details, active: MockUserIdPasswordAccountService())) - + .environment(Account(building: details, active: MockUserIdPasswordAccountService())) + NavigationStack { AccountOverview() } - .environmentObject(Account()) + .environment(Account()) } } #endif diff --git a/Sources/SpeziAccount/AccountService/AccountService.swift b/Sources/SpeziAccount/AccountService/AccountService.swift index cb10197f..26cfdef4 100644 --- a/Sources/SpeziAccount/AccountService/AccountService.swift +++ b/Sources/SpeziAccount/AccountService/AccountService.swift @@ -6,7 +6,6 @@ // SPDX-License-Identifier: MIT // -import Spezi import SwiftUI diff --git a/Sources/SpeziAccount/AccountService/Configuration/AccountServiceConfiguration.swift b/Sources/SpeziAccount/AccountService/Configuration/AccountServiceConfiguration.swift index e1097bd4..8febe767 100644 --- a/Sources/SpeziAccount/AccountService/Configuration/AccountServiceConfiguration.swift +++ b/Sources/SpeziAccount/AccountService/Configuration/AccountServiceConfiguration.swift @@ -7,7 +7,7 @@ // import Foundation -import Spezi +import SpeziFoundation import SwiftUI diff --git a/Sources/SpeziAccount/AccountService/Configuration/AccountServiceConfigurationKey.swift b/Sources/SpeziAccount/AccountService/Configuration/AccountServiceConfigurationKey.swift index f4117a87..00a11fb7 100644 --- a/Sources/SpeziAccount/AccountService/Configuration/AccountServiceConfigurationKey.swift +++ b/Sources/SpeziAccount/AccountService/Configuration/AccountServiceConfigurationKey.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import Spezi +import SpeziFoundation /// A `KnowledgeSource` that implements a configuration option for ``AccountServiceConfiguration``. diff --git a/Sources/SpeziAccount/AccountService/Configuration/AccountServiceImage.swift b/Sources/SpeziAccount/AccountService/Configuration/AccountServiceImage.swift index 32f89fba..c2a14878 100644 --- a/Sources/SpeziAccount/AccountService/Configuration/AccountServiceImage.swift +++ b/Sources/SpeziAccount/AccountService/Configuration/AccountServiceImage.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import Spezi +import SpeziFoundation import SwiftUI diff --git a/Sources/SpeziAccount/AccountService/Configuration/AccountServiceName.swift b/Sources/SpeziAccount/AccountService/Configuration/AccountServiceName.swift index f2229593..1c3807d6 100644 --- a/Sources/SpeziAccount/AccountService/Configuration/AccountServiceName.swift +++ b/Sources/SpeziAccount/AccountService/Configuration/AccountServiceName.swift @@ -7,7 +7,7 @@ // import Foundation -import Spezi +import SpeziFoundation /// The localized name of an ``AccountService``. diff --git a/Sources/SpeziAccount/AccountService/Configuration/FieldValidationRules.swift b/Sources/SpeziAccount/AccountService/Configuration/FieldValidationRules.swift index 3a813fc0..2f682a14 100644 --- a/Sources/SpeziAccount/AccountService/Configuration/FieldValidationRules.swift +++ b/Sources/SpeziAccount/AccountService/Configuration/FieldValidationRules.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import Spezi +import SpeziFoundation import SpeziValidation diff --git a/Sources/SpeziAccount/AccountService/Configuration/RequiredAccountKeys.swift b/Sources/SpeziAccount/AccountService/Configuration/RequiredAccountKeys.swift index 729d8435..175840cc 100644 --- a/Sources/SpeziAccount/AccountService/Configuration/RequiredAccountKeys.swift +++ b/Sources/SpeziAccount/AccountService/Configuration/RequiredAccountKeys.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import Spezi +import SpeziFoundation /// The collection of ``AccountKey``s that are required when using the associated ``AccountService``. diff --git a/Sources/SpeziAccount/AccountService/Configuration/UserIdConfiguration.swift b/Sources/SpeziAccount/AccountService/Configuration/UserIdConfiguration.swift index 34f852ac..da1671e3 100644 --- a/Sources/SpeziAccount/AccountService/Configuration/UserIdConfiguration.swift +++ b/Sources/SpeziAccount/AccountService/Configuration/UserIdConfiguration.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import Spezi +import SpeziFoundation import SwiftUI diff --git a/Sources/SpeziAccount/AccountSetup.swift b/Sources/SpeziAccount/AccountSetup.swift index 73a2018f..fffbfa77 100644 --- a/Sources/SpeziAccount/AccountSetup.swift +++ b/Sources/SpeziAccount/AccountSetup.swift @@ -53,12 +53,13 @@ public enum _AccountSetupState: EnvironmentKey { // swiftlint:disable:this type_ /// } /// } /// ``` +@MainActor public struct AccountSetup: View { private let setupCompleteClosure: (AccountDetails) -> Void private let header: Header private let continueButton: Continue - @EnvironmentObject var account: Account + @Environment(Account.self) var account @State private var setupState: _AccountSetupState = .generic @State private var followUpSheet = false @@ -112,8 +113,8 @@ public struct AccountSetup: View { .frame(maxWidth: .infinity) } } - .onReceive(account.$details) { details in - if let details, case .setupShown = setupState { + .onChange(of: account.signedIn) { + if let details = account.details, case .setupShown = setupState { let missingKeys = account.configuration.missingRequiredKeys(for: details, includeCollected: details.isNewUser) if missingKeys.isEmpty { @@ -236,14 +237,14 @@ struct AccountView_Previews: PreviewProvider { @MainActor static var previews: some View { ForEach(accountServicePermutations.indices, id: \.self) { index in AccountSetup() - .environmentObject(Account(services: accountServicePermutations[index] + [MockSignInWithAppleProvider()])) + .environment(Account(services: accountServicePermutations[index] + [MockSignInWithAppleProvider()])) } AccountSetup() - .environmentObject(Account(building: detailsBuilder, active: MockUserIdPasswordAccountService())) + .environment(Account(building: detailsBuilder, active: MockUserIdPasswordAccountService())) AccountSetup(state: .setupShown) - .environmentObject(Account(building: detailsBuilder, active: MockUserIdPasswordAccountService())) + .environment(Account(building: detailsBuilder, active: MockUserIdPasswordAccountService())) AccountSetup(continue: { Button(action: { @@ -254,7 +255,7 @@ struct AccountView_Previews: PreviewProvider { }) .buttonStyle(.borderedProminent) }) - .environmentObject(Account(building: detailsBuilder, active: MockUserIdPasswordAccountService())) + .environment(Account(building: detailsBuilder, active: MockUserIdPasswordAccountService())) } } #endif diff --git a/Sources/SpeziAccount/AccountSetupViewStyle/AccountSetupViewStyle.swift b/Sources/SpeziAccount/AccountSetupViewStyle/AccountSetupViewStyle.swift index a9dbaa83..4bf3a910 100644 --- a/Sources/SpeziAccount/AccountSetupViewStyle/AccountSetupViewStyle.swift +++ b/Sources/SpeziAccount/AccountSetupViewStyle/AccountSetupViewStyle.swift @@ -26,6 +26,16 @@ public protocol AccountSetupViewStyle { /// The associated ``AccountService`` instance. var service: Service { get } + /// A `ViewModifier` that is injected into views with security related operations. + /// + /// This modifier is injected into views that expose security related operations like changing the password + /// or change the user identifier. It is guaranteed that this modifier is not injected twice into the same + /// view hierarchy. + /// + /// - Note: It is advised to implement this as a computed property. + var securityRelatedViewModifier: any ViewModifier { get } + + /// The button label in the list of account services for the ``AccountSetup`` view. @ViewBuilder func makeServiceButtonLabel() -> ButtonLabel @@ -41,6 +51,12 @@ public protocol AccountSetupViewStyle { extension AccountSetupViewStyle { + /// Default implementation that doesn't modify anything. + public var securityRelatedViewModifier: any ViewModifier { + NoopModifier() + } + + /// Default service button label using the ``AccountServiceName`` and ``AccountServiceImage`` configurations. public func makeServiceButtonLabel() -> some View { Group { diff --git a/Sources/SpeziAccount/AccountValue/AccountAnchor.swift b/Sources/SpeziAccount/AccountValue/AccountAnchor.swift index 811dbbf9..3ced160b 100644 --- a/Sources/SpeziAccount/AccountValue/AccountAnchor.swift +++ b/Sources/SpeziAccount/AccountValue/AccountAnchor.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import Spezi +import SpeziFoundation /// A `RepositoryAnchor` for ``AccountStorage``. diff --git a/Sources/SpeziAccount/AccountValue/AccountKey+Views.swift b/Sources/SpeziAccount/AccountValue/AccountKey+Views.swift index a475060e..b186be59 100644 --- a/Sources/SpeziAccount/AccountValue/AccountKey+Views.swift +++ b/Sources/SpeziAccount/AccountValue/AccountKey+Views.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import Spezi +import SpeziFoundation import SwiftUI @@ -21,6 +21,7 @@ extension AccountKey where Value: CustomLocalizedStringResourceConvertible { } +@MainActor extension AccountKey { static func emptyDataEntryView(for values: Values.Type) -> AnyView { AnyView(GeneralizedDataEntryView(initialValue: initialValue.value)) diff --git a/Sources/SpeziAccount/AccountValue/AccountKey.swift b/Sources/SpeziAccount/AccountValue/AccountKey.swift index 3a761e1c..450ccd5e 100644 --- a/Sources/SpeziAccount/AccountValue/AccountKey.swift +++ b/Sources/SpeziAccount/AccountValue/AccountKey.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import Spezi +import SpeziFoundation import SwiftUI import XCTRuntimeAssertions diff --git a/Sources/SpeziAccount/AccountValue/AccountStorage.swift b/Sources/SpeziAccount/AccountValue/AccountStorage.swift index 7f6eb318..8d55931e 100644 --- a/Sources/SpeziAccount/AccountValue/AccountStorage.swift +++ b/Sources/SpeziAccount/AccountValue/AccountStorage.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import Spezi +import SpeziFoundation /// A `ValueRepository` that stores `KnowledgeSource`s anchored to the ``AccountAnchor``. diff --git a/Sources/SpeziAccount/AccountValue/AccountValues.swift b/Sources/SpeziAccount/AccountValue/AccountValues.swift index 4582efb0..c7244b99 100644 --- a/Sources/SpeziAccount/AccountValue/AccountValues.swift +++ b/Sources/SpeziAccount/AccountValue/AccountValues.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import Spezi +import SpeziFoundation /// An arbitrary collection of account values. diff --git a/Sources/SpeziAccount/AccountValue/AccountValuesBuilder.swift b/Sources/SpeziAccount/AccountValue/AccountValuesBuilder.swift index d7df5a28..a8ea8137 100644 --- a/Sources/SpeziAccount/AccountValue/AccountValuesBuilder.swift +++ b/Sources/SpeziAccount/AccountValue/AccountValuesBuilder.swift @@ -7,7 +7,7 @@ // import Foundation -import Spezi +import SpeziFoundation private class RemoveVisitor: AccountKeyVisitor { @@ -86,9 +86,10 @@ private class CopyKeyVisitor: /// - ``AccountValuesBuilder/build()-pqt5`` /// - ``AccountValuesBuilder/build(owner:)`` /// - ``AccountValuesBuilder/build(checking:)`` -public class AccountValuesBuilder: ObservableObject, AccountValuesCollection { - @Published var storage: AccountStorage - @Published var defaultValues: AccountStorage +@Observable +public class AccountValuesBuilder: AccountValuesCollection { + var storage: AccountStorage + var defaultValues: AccountStorage init(from storage: AccountStorage) { diff --git a/Sources/SpeziAccount/AccountValue/Collections/AccountDetails.swift b/Sources/SpeziAccount/AccountValue/Collections/AccountDetails.swift index fc55de04..53f512a3 100644 --- a/Sources/SpeziAccount/AccountValue/Collections/AccountDetails.swift +++ b/Sources/SpeziAccount/AccountValue/Collections/AccountDetails.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import Spezi +import SpeziFoundation /// A typed storage container to easily access any information for the currently signed in user. diff --git a/Sources/SpeziAccount/AccountValue/Collections/ModifiedAccountDetails.swift b/Sources/SpeziAccount/AccountValue/Collections/ModifiedAccountDetails.swift index b1c7104c..e8eb7aa0 100644 --- a/Sources/SpeziAccount/AccountValue/Collections/ModifiedAccountDetails.swift +++ b/Sources/SpeziAccount/AccountValue/Collections/ModifiedAccountDetails.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import Spezi +import SpeziFoundation /// Set of ``AccountValues`` that were modified or added. diff --git a/Sources/SpeziAccount/AccountValue/Collections/PartialAccountDetails.swift b/Sources/SpeziAccount/AccountValue/Collections/PartialAccountDetails.swift index ca63d6bf..f3f2a17d 100644 --- a/Sources/SpeziAccount/AccountValue/Collections/PartialAccountDetails.swift +++ b/Sources/SpeziAccount/AccountValue/Collections/PartialAccountDetails.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import Spezi +import SpeziFoundation /// Set of ``AccountValues`` that resembled ``AccountDetails`` but may be incomplete in respect to the diff --git a/Sources/SpeziAccount/AccountValue/Collections/RemovedAccountDetails.swift b/Sources/SpeziAccount/AccountValue/Collections/RemovedAccountDetails.swift index 45eac818..1e56cc8e 100644 --- a/Sources/SpeziAccount/AccountValue/Collections/RemovedAccountDetails.swift +++ b/Sources/SpeziAccount/AccountValue/Collections/RemovedAccountDetails.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import Spezi +import SpeziFoundation /// Set of ``AccountValues`` that were removed providing access to the old values. diff --git a/Sources/SpeziAccount/AccountValue/Collections/SignupDetails.swift b/Sources/SpeziAccount/AccountValue/Collections/SignupDetails.swift index 4d5f0b27..a580e85e 100644 --- a/Sources/SpeziAccount/AccountValue/Collections/SignupDetails.swift +++ b/Sources/SpeziAccount/AccountValue/Collections/SignupDetails.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import Spezi +import SpeziFoundation /// Set of ``AccountValues`` that were collected at signup to create a new user account. diff --git a/Sources/SpeziAccount/AccountValue/Keys/ActiveAccountServiceKey.swift b/Sources/SpeziAccount/AccountValue/Keys/ActiveAccountServiceKey.swift index 6ab4ea6c..b6d5d12e 100644 --- a/Sources/SpeziAccount/AccountValue/Keys/ActiveAccountServiceKey.swift +++ b/Sources/SpeziAccount/AccountValue/Keys/ActiveAccountServiceKey.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import Spezi +import SpeziFoundation /// A `KnowledgeSource` to access the ``AccountService`` associated with the ``AccountDetails``. diff --git a/Sources/SpeziAccount/AccountValue/Keys/EmailAddressKey.swift b/Sources/SpeziAccount/AccountValue/Keys/EmailAddressKey.swift index 29e90af5..38df51d3 100644 --- a/Sources/SpeziAccount/AccountValue/Keys/EmailAddressKey.swift +++ b/Sources/SpeziAccount/AccountValue/Keys/EmailAddressKey.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import Spezi +import SpeziFoundation import SpeziValidation import SwiftUI diff --git a/Sources/SpeziAccount/AccountValue/Keys/IsNewUserKey.swift b/Sources/SpeziAccount/AccountValue/Keys/IsNewUserKey.swift index f3f87e8a..4708afef 100644 --- a/Sources/SpeziAccount/AccountValue/Keys/IsNewUserKey.swift +++ b/Sources/SpeziAccount/AccountValue/Keys/IsNewUserKey.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import Spezi +import SpeziFoundation struct IsNewUserKey: KnowledgeSource { diff --git a/Sources/SpeziAccount/AccountValue/Keys/PersonNameKey.swift b/Sources/SpeziAccount/AccountValue/Keys/PersonNameKey.swift index c6f4c1ac..c7bd24fd 100644 --- a/Sources/SpeziAccount/AccountValue/Keys/PersonNameKey.swift +++ b/Sources/SpeziAccount/AccountValue/Keys/PersonNameKey.swift @@ -66,7 +66,7 @@ extension PersonNameKey { public typealias Key = PersonNameKey - @EnvironmentObject private var account: Account + @Environment(Account.self) private var account @ValidationState private var givenNameValidation @ValidationState private var familyNameValidation diff --git a/Sources/SpeziAccount/AccountValue/Keys/UserIdKey.swift b/Sources/SpeziAccount/AccountValue/Keys/UserIdKey.swift index f63c2380..6c1a8c27 100644 --- a/Sources/SpeziAccount/AccountValue/Keys/UserIdKey.swift +++ b/Sources/SpeziAccount/AccountValue/Keys/UserIdKey.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import Spezi +import SpeziFoundation import SpeziValidation import SwiftUI diff --git a/Sources/SpeziAccount/AccountValue/RequiredAccountKey.swift b/Sources/SpeziAccount/AccountValue/RequiredAccountKey.swift index 4e77c3ce..30371b95 100644 --- a/Sources/SpeziAccount/AccountValue/RequiredAccountKey.swift +++ b/Sources/SpeziAccount/AccountValue/RequiredAccountKey.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import Spezi +import SpeziFoundation /// A typed storage key extending ``AccountKey`` for values that are required for every user account if used. diff --git a/Sources/SpeziAccount/AccountValue/Visitor/AccountKey+Visitor.swift b/Sources/SpeziAccount/AccountValue/Visitor/AccountKey+Visitor.swift index 9dfa44ad..dd1a30de 100644 --- a/Sources/SpeziAccount/AccountValue/Visitor/AccountKey+Visitor.swift +++ b/Sources/SpeziAccount/AccountValue/Visitor/AccountKey+Visitor.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import Spezi +import SpeziFoundation extension AccountKey { diff --git a/Sources/SpeziAccount/AccountValue/Visitor/AccountValueVisitor.swift b/Sources/SpeziAccount/AccountValue/Visitor/AccountValueVisitor.swift index de87e0c5..15f6e8d8 100644 --- a/Sources/SpeziAccount/AccountValue/Visitor/AccountValueVisitor.swift +++ b/Sources/SpeziAccount/AccountValue/Visitor/AccountValueVisitor.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import Spezi +import SpeziFoundation /// A collection type that is capable of accepting an ``AccountValueVisitor``. diff --git a/Sources/SpeziAccount/SpeziAccount.docc/Account Services/Creating your own Account Service.md b/Sources/SpeziAccount/SpeziAccount.docc/Account Services/Creating your own Account Service.md index 8fd9e09f..72730a5b 100644 --- a/Sources/SpeziAccount/SpeziAccount.docc/Account Services/Creating your own Account Service.md +++ b/Sources/SpeziAccount/SpeziAccount.docc/Account Services/Creating your own Account Service.md @@ -81,13 +81,13 @@ The first approach is to provide your ``AccountService`` as is and let the user pass it to the result builder closure of the ``AccountServiceConfiguration/init(name:supportedKeys:configuration:)`` initializer. This is the preferred approach for account services that don't require additional setup or rely on any special infrastructure. -If your account service requires additional setup or any infrastructure that relies on other `Component`s you can implement -your own `Spezi` [Component](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/component) that _provides_ your -``AccountService`` directly to the ``AccountServiceConfiguration`` component. To do so, declare the `@Provide` property wrapper with a type of +If your account service requires additional setup or any infrastructure that relies on other `Module`s you can implement +your own `Spezi` [Module](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/module) that _provides_ your +``AccountService`` directly to the ``AccountServiceConfiguration`` module. To do so, declare the `@Provide` property wrapper with a type of `any AccountService` and populate it within your initializer. Below is a code example. ```swift -class MyComponent: Component { +class MyModule: Module { @Provide var accountService: any AccountService // you can also use a type of [any AccountService] to provide multiple with a single @Provide init() { @@ -100,7 +100,7 @@ class MyComponent: Component { } ``` -> Note: Refer to the [Component Communication](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/component#Communication) documentation +> Note: Refer to the [Module Communication](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/module-communication) documentation of the `Spezi` framework for more detailed information. diff --git a/Sources/SpeziAccount/SpeziAccount.docc/Account Values/Adding new Account Values.md b/Sources/SpeziAccount/SpeziAccount.docc/Account Values/Adding new Account Values.md index 5e30f0f6..c31a8677 100644 --- a/Sources/SpeziAccount/SpeziAccount.docc/Account Values/Adding new Account Values.md +++ b/Sources/SpeziAccount/SpeziAccount.docc/Account Values/Adding new Account Values.md @@ -26,15 +26,15 @@ The first step is to create a new type that adopts the ``AccountKey`` protocol. > Note: Refer to the ``RequiredAccountKey`` protocol if you require an account value that is always required to be supplied if configured. -When adopting the protocol, you have to provide the associated [Value](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/knowledgesource/value) +When adopting the protocol, you have to provide the associated [Value](https://swiftpackageindex.com/stanfordspezi/spezifoundation/documentation/spezifoundation/knowledgesource/value) type, a ``AccountKey/name``, a ``AccountKey/category`` and an ``AccountKey/initialValue-6h1oo``. The `Value` defines the type of the account value, the `name` is used to textually refer to the account value and the `category` is used to group the account values in UI components (see ``AccountKeyCategory`` for more information). The `initialValue` defines the initial value on signup and how it is used. For some types like String a default ``InitialValue/empty(_:)`` implementation is provided. > Note: The associated type for the value is coming from the underlying - [KnowledgeSource](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/knowledgesource) protocol from the Spezi framework. - Refer to the [Shared Repository](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/shared-repository) + [KnowledgeSource](https://swiftpackageindex.com/stanfordspezi/spezifoundation/documentation/spezifoundation/knowledgesource) protocol from the Spezi framework. + Refer to the [Shared Repository](https://swiftpackageindex.com/stanfordspezi/spezifoundation/documentation/spezifoundation/shared-repository) documentation for more information. Below is a code example implementing a simple string-based biography that a user might show on their profile. diff --git a/Sources/SpeziAccount/SpeziAccount.docc/Account Values/Handling Account Value Storage.md b/Sources/SpeziAccount/SpeziAccount.docc/Account Values/Handling Account Value Storage.md index 0bab3cfe..010bc683 100644 --- a/Sources/SpeziAccount/SpeziAccount.docc/Account Values/Handling Account Value Storage.md +++ b/Sources/SpeziAccount/SpeziAccount.docc/Account Values/Handling Account Value Storage.md @@ -23,7 +23,7 @@ few differences in construction operations, they convey entirely different seman ``AccountKey``s define an extension to the ``AccountValues`` so account values can be conveniently accessed. For example, the ``PersonNameKey`` defines the ``AccountValues/name`` property as an extension to access the name of a person if it exists. -Otherwise, you can always access the underlying [Shared Repository](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/shared-repository) +Otherwise, you can always access the underlying [Shared Repository](https://swiftpackageindex.com/stanfordspezi/spezifoundation/documentation/spezifoundation/shared-repository) using the ``AccountValues/storage`` property. ### Iterating through Account Values @@ -56,7 +56,7 @@ let encoded = details.acceptAll(&visitor) ``` > Important: ``AccountValues`` implement the `Collection` protocol and therefore support iteration. However, `Element`s of the collection are of type - [AnyRepositoryValue](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/anyrepositoryvalue) as ``AccountValues`` might store + [AnyRepositoryValue](https://swiftpackageindex.com/stanfordspezi/spezifoundation/documentation/spezifoundation/anyrepositoryvalue) as ``AccountValues`` might store non-``AccountKey`` conforming knowledge sources like the ``ActiveAccountServiceKey``. ### Iterating through Account Keys diff --git a/Sources/SpeziAccount/ViewModel/AccountOverviewFormViewModel.swift b/Sources/SpeziAccount/ViewModel/AccountOverviewFormViewModel.swift index 785fe8d8..98367ca1 100644 --- a/Sources/SpeziAccount/ViewModel/AccountOverviewFormViewModel.swift +++ b/Sources/SpeziAccount/ViewModel/AccountOverviewFormViewModel.swift @@ -14,7 +14,8 @@ import SwiftUI @MainActor -class AccountOverviewFormViewModel: ObservableObject { +@Observable +class AccountOverviewFormViewModel { private static var logger: Logger { LoggerKey.defaultValue } @@ -27,14 +28,14 @@ class AccountOverviewFormViewModel: ObservableObject { private let categorizedAccountKeys: OrderedDictionary - let modifiedDetailsBuilder = ModifiedAccountDetails.Builder() // nested ObservableObject, see init + let modifiedDetailsBuilder = ModifiedAccountDetails.Builder() - @Published var presentingCancellationDialog = false - @Published var presentingLogoutAlert = false - @Published var presentingRemovalAlert = false + var presentingCancellationDialog = false + var presentingLogoutAlert = false + var presentingRemovalAlert = false - @Published var addedAccountKeys = CategorizedAccountKeys() - @Published var removedAccountKeys = CategorizedAccountKeys() + var addedAccountKeys = CategorizedAccountKeys() + var removedAccountKeys = CategorizedAccountKeys() var hasUnsavedChanges: Bool { !modifiedDetailsBuilder.isEmpty @@ -44,17 +45,9 @@ class AccountOverviewFormViewModel: ObservableObject { .init("ACCOUNT_OVERVIEW_EDIT_DEFAULT_ERROR", bundle: .atURL(from: .module)) } - private var anyCancellable: [AnyCancellable] = [] - init(account: Account) { self.categorizedAccountKeys = account.configuration.allCategorized() - - // We forward the objectWillChange publisher. Our `hasUnsavedChanges` is affected by changes to the builder. - // Otherwise, changes to the object wouldn't be important. - anyCancellable.append(modifiedDetailsBuilder.objectWillChange.sink { [weak self] _ in - self?.objectWillChange.send() - }) } @@ -211,9 +204,4 @@ class AccountOverviewFormViewModel: ObservableObject { categorizedAccountKeys[.credentials]?.contains(where: { $0 == UserIdKey.self }) == true || categorizedAccountKeys[.name]?.isEmpty != true } - - - deinit { - anyCancellable.forEach { $0.cancel() } - } } diff --git a/Sources/SpeziAccount/ViewModifier/AccountRequiredModifier.swift b/Sources/SpeziAccount/ViewModifier/AccountRequiredModifier.swift index 89ae2a6f..c9abc0d8 100644 --- a/Sources/SpeziAccount/ViewModifier/AccountRequiredModifier.swift +++ b/Sources/SpeziAccount/ViewModifier/AccountRequiredModifier.swift @@ -13,7 +13,7 @@ struct AccountRequiredModifier: ViewModifier { private let required: Bool private let setupSheet: SetupSheet - @EnvironmentObject private var account: Account + @Environment(Account.self) private var account @State private var presentingSheet = false diff --git a/Sources/SpeziAccount/ViewModifier/AnyViewModifier.swift b/Sources/SpeziAccount/ViewModifier/AnyViewModifier.swift new file mode 100644 index 00000000..cc6d30c5 --- /dev/null +++ b/Sources/SpeziAccount/ViewModifier/AnyViewModifier.swift @@ -0,0 +1,26 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +extension ViewModifier { + func inject(into view: V) -> AnyView { + AnyView(view.modifier(self)) + } +} + + +extension View { + /// Modify the view with an type-erased `ViewModifier`. + /// - Parameter modifier: The view modifier. + /// - Returns: The modified view. + func anyViewModifier(_ modifier: any ViewModifier) -> some View { + modifier.inject(into: self) + } +} diff --git a/Sources/SpeziAccount/ViewModifier/NoopModifier.swift b/Sources/SpeziAccount/ViewModifier/NoopModifier.swift new file mode 100644 index 00000000..18a9b3b3 --- /dev/null +++ b/Sources/SpeziAccount/ViewModifier/NoopModifier.swift @@ -0,0 +1,16 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +struct NoopModifier: ViewModifier { + func body(content: Content) -> some View { + content + } +} diff --git a/Sources/SpeziAccount/ViewModifier/RequiredValidationModifier.swift b/Sources/SpeziAccount/ViewModifier/RequiredValidationModifier.swift index 122f30cb..85f8acf5 100644 --- a/Sources/SpeziAccount/ViewModifier/RequiredValidationModifier.swift +++ b/Sources/SpeziAccount/ViewModifier/RequiredValidationModifier.swift @@ -11,7 +11,7 @@ import SwiftUI private struct RequiredValidationModifier: ViewModifier { - @EnvironmentObject private var detailsBuilder: AccountValuesBuilder + @Environment(AccountValuesBuilder.self) private var detailsBuilder @ValidationState private var validation @ValidationState private var innerValidation diff --git a/Sources/SpeziAccount/ViewModifier/VerifyRequiredAccountDetailsModifier.swift b/Sources/SpeziAccount/ViewModifier/VerifyRequiredAccountDetailsModifier.swift index dad206b9..d058556f 100644 --- a/Sources/SpeziAccount/ViewModifier/VerifyRequiredAccountDetailsModifier.swift +++ b/Sources/SpeziAccount/ViewModifier/VerifyRequiredAccountDetailsModifier.swift @@ -22,7 +22,7 @@ private struct FollowUpSession: Identifiable { struct VerifyRequiredAccountDetailsModifier: ViewModifier { private let verify: Bool - @EnvironmentObject private var account: Account + @Environment(Account.self) private var account @SceneStorage("edu.stanford.spezi-account.startup-account-check") private var verifiedAccount = false @State private var followUpSession: FollowUpSession? diff --git a/Sources/SpeziAccount/Views/AccountOverview/AccountKeyOverviewRow.swift b/Sources/SpeziAccount/Views/AccountOverview/AccountKeyOverviewRow.swift index 9f84b818..35750cab 100644 --- a/Sources/SpeziAccount/Views/AccountOverview/AccountKeyOverviewRow.swift +++ b/Sources/SpeziAccount/Views/AccountOverview/AccountKeyOverviewRow.swift @@ -12,12 +12,11 @@ import SwiftUI struct AccountKeyOverviewRow: View { private let accountDetails: AccountDetails private let accountKey: any AccountKey.Type + private let model: AccountOverviewFormViewModel - @EnvironmentObject private var account: Account + @Environment(Account.self) private var account @Environment(\.editMode) private var editMode - @ObservedObject private var model: AccountOverviewFormViewModel - var body: some View { if editMode?.wrappedValue.isEditing == true { // we place everything in the same HStack, such that animations are smooth @@ -67,6 +66,7 @@ struct AccountKeyOverviewRow: View { } + @MainActor func isDeleteDisabled(for key: any AccountKey.Type) -> Bool { if accountDetails.contains(key) && !model.removedAccountKeys.contains(key) { return account.configuration[key]?.requirement == .required @@ -86,12 +86,13 @@ struct AccountKeyEditRow_Previews: PreviewProvider { static let account = Account(building: details, active: MockUserIdPasswordAccountService()) - @StateObject private static var model = AccountOverviewFormViewModel(account: account) + @State private static var model = AccountOverviewFormViewModel(account: account) static var previews: some View { if let details = account.details { AccountKeyOverviewRow(details: details, for: GenderIdentityKey.self, model: model) .injectEnvironmentObjects(service: details.accountService, model: model) + .environment(account) } } } diff --git a/Sources/SpeziAccount/Views/AccountOverview/AccountOverviewSections.swift b/Sources/SpeziAccount/Views/AccountOverview/AccountOverviewSections.swift index def723ab..1bc45908 100644 --- a/Sources/SpeziAccount/Views/AccountOverview/AccountOverviewSections.swift +++ b/Sources/SpeziAccount/Views/AccountOverview/AccountOverviewSections.swift @@ -13,6 +13,7 @@ import SwiftUI /// A internal subview of ``AccountOverview`` that expects to be embedded into a `Form`. +@MainActor struct AccountOverviewSections: View { let additionalSections: AdditionalSections private let accountDetails: AccountDetails @@ -21,13 +22,13 @@ struct AccountOverviewSections: View { accountDetails.accountService } - @EnvironmentObject private var account: Account - + @Environment(Account.self) private var account + @Environment(\.logger) private var logger @Environment(\.editMode) private var editMode @Environment(\.dismiss) private var dismiss - @StateObject private var model: AccountOverviewFormViewModel + @State private var model: AccountOverviewFormViewModel @ValidationState private var validation @State private var viewState: ViewState = .idle @@ -213,7 +214,7 @@ struct AccountOverviewSections: View { @ViewBuilder additionalSections: (() -> AdditionalSections) = { EmptyView() } ) { self.accountDetails = accountDetails - self._model = StateObject(wrappedValue: AccountOverviewFormViewModel(account: account)) + self._model = State(wrappedValue: AccountOverviewFormViewModel(account: account)) self._isEditing = isEditing self.additionalSections = additionalSections() } @@ -283,7 +284,7 @@ struct AccountOverviewSections_Previews: PreviewProvider { } } } - .environmentObject(Account(building: details, active: MockUserIdPasswordAccountService())) + .environment(Account(building: details, active: MockUserIdPasswordAccountService())) } } #endif diff --git a/Sources/SpeziAccount/Views/AccountOverview/NameOverview.swift b/Sources/SpeziAccount/Views/AccountOverview/NameOverview.swift index 2ef57ec5..80cf90e8 100644 --- a/Sources/SpeziAccount/Views/AccountOverview/NameOverview.swift +++ b/Sources/SpeziAccount/Views/AccountOverview/NameOverview.swift @@ -11,11 +11,15 @@ import SwiftUI struct NameOverview: View { + private let model: AccountOverviewFormViewModel private let accountDetails: AccountDetails - @EnvironmentObject private var account: Account + private var service: any AccountService { + accountDetails.accountService + } + + @Environment(Account.self) private var account - @ObservedObject private var model: AccountOverviewFormViewModel var body: some View { Form { @@ -37,7 +41,7 @@ struct NameOverview: View { Text("VALUE_ADD \(wrapper.accountKey.name)", bundle: .module) .foregroundColor(.secondary) } - .accessibilityElement(children: .combine) + .accessibilityElement(children: .combine) } } } header: { @@ -48,6 +52,7 @@ struct NameOverview: View { } } } + .anyViewModifier(service.viewStyle.securityRelatedViewModifier) .navigationTitle(model.accountIdentifierLabel(configuration: account.configuration, userIdType: accountDetails.userIdType)) .navigationBarTitleDisplayMode(.inline) .injectEnvironmentObjects(service: accountDetails.accountService, model: model) @@ -75,7 +80,7 @@ struct NameOverview_Previews: PreviewProvider { 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 - @StateObject static var model = AccountOverviewFormViewModel(account: account) + @State static var model = AccountOverviewFormViewModel(account: account) static var previews: some View { NavigationStack { @@ -83,14 +88,14 @@ struct NameOverview_Previews: PreviewProvider { NameOverview(model: model, details: details) } } - .environmentObject(account) + .environment(account) NavigationStack { if let details = accountWithoutName.details { NameOverview(model: model, details: details) } } - .environmentObject(accountWithoutName) + .environment(accountWithoutName) } } #endif diff --git a/Sources/SpeziAccount/Views/AccountOverview/PasswordChangeSheet.swift b/Sources/SpeziAccount/Views/AccountOverview/PasswordChangeSheet.swift index feba317e..9c27c5d9 100644 --- a/Sources/SpeziAccount/Views/AccountOverview/PasswordChangeSheet.swift +++ b/Sources/SpeziAccount/Views/AccountOverview/PasswordChangeSheet.swift @@ -11,8 +11,10 @@ import SpeziViews import SwiftUI +@MainActor struct PasswordChangeSheet: View { private let accountDetails: AccountDetails + private let model: AccountOverviewFormViewModel private var service: any AccountService { accountDetails.accountService @@ -22,7 +24,6 @@ struct PasswordChangeSheet: View { @Environment(\.logger) private var logger @Environment(\.dismiss) private var dismiss - @ObservedObject private var model: AccountOverviewFormViewModel @ValidationState private var validation @State private var viewState: ViewState = .idle @@ -45,6 +46,7 @@ struct PasswordChangeSheet: View { .environment(\.defaultErrorDescription, model.defaultErrorDescription) } .viewStateAlert(state: $viewState) + .anyViewModifier(service.viewStyle.securityRelatedViewModifier) .navigationTitle(Text("CHANGE_PASSWORD", bundle: .module)) .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -137,7 +139,7 @@ struct PasswordChangeSheet_Previews: PreviewProvider { 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 - @StateObject static var model = AccountOverviewFormViewModel(account: account) + @State static var model = AccountOverviewFormViewModel(account: account) static var previews: some View { NavigationStack { @@ -145,7 +147,7 @@ struct PasswordChangeSheet_Previews: PreviewProvider { PasswordChangeSheet(model: model, details: details) } } - .environmentObject(account) + .environment(account) } } #endif diff --git a/Sources/SpeziAccount/Views/AccountOverview/SecurityOverview.swift b/Sources/SpeziAccount/Views/AccountOverview/SecurityOverview.swift index 11f88fc2..87e76009 100644 --- a/Sources/SpeziAccount/Views/AccountOverview/SecurityOverview.swift +++ b/Sources/SpeziAccount/Views/AccountOverview/SecurityOverview.swift @@ -12,14 +12,14 @@ import SwiftUI struct SecurityOverview: View { private let accountDetails: AccountDetails + private let model: AccountOverviewFormViewModel private var service: any AccountService { accountDetails.accountService } - @EnvironmentObject private var account: Account - @ObservedObject private var model: AccountOverviewFormViewModel + @Environment(Account.self) private var account @State private var viewState: ViewState = .idle @State private var presentingPasswordChangeSheet = false @@ -82,7 +82,7 @@ struct SecurityOverview_Previews: PreviewProvider { 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 - @StateObject static var model = AccountOverviewFormViewModel(account: account) + @State static var model = AccountOverviewFormViewModel(account: account) static var previews: some View { NavigationStack { @@ -90,7 +90,7 @@ struct SecurityOverview_Previews: PreviewProvider { SecurityOverview(model: model, details: details) } } - .environmentObject(account) + .environment(account) } } #endif diff --git a/Sources/SpeziAccount/Views/AccountOverview/SingleEditView.swift b/Sources/SpeziAccount/Views/AccountOverview/SingleEditView.swift index 1c2c3889..aa843675 100644 --- a/Sources/SpeziAccount/Views/AccountOverview/SingleEditView.swift +++ b/Sources/SpeziAccount/Views/AccountOverview/SingleEditView.swift @@ -11,7 +11,9 @@ import SpeziViews import SwiftUI +@MainActor struct SingleEditView: View { + private let model: AccountOverviewFormViewModel private let accountDetails: AccountDetails private var service: any AccountService { @@ -22,7 +24,6 @@ struct SingleEditView: View { @Environment(\.logger) private var logger @Environment(\.dismiss) private var dismiss - @ObservedObject private var model: AccountOverviewFormViewModel @ValidationState private var validation @State private var viewState: ViewState = .idle @@ -88,7 +89,7 @@ struct SingleEditView_Previews: PreviewProvider { 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 - @StateObject static var model = AccountOverviewFormViewModel(account: account) + @State static var model = AccountOverviewFormViewModel(account: account) static var previews: some View { NavigationStack { @@ -96,7 +97,7 @@ struct SingleEditView_Previews: PreviewProvider { SingleEditView(model: model, details: details) } } - .environmentObject(account) + .environment(account) } } #endif diff --git a/Sources/SpeziAccount/Views/AccountOverview/View+AccountOverviewObjects.swift b/Sources/SpeziAccount/Views/AccountOverview/View+AccountOverviewObjects.swift index 059ecc58..4a4d146e 100644 --- a/Sources/SpeziAccount/Views/AccountOverview/View+AccountOverviewObjects.swift +++ b/Sources/SpeziAccount/Views/AccountOverview/View+AccountOverviewObjects.swift @@ -13,6 +13,6 @@ extension View { func injectEnvironmentObjects(service: any AccountService, model: AccountOverviewFormViewModel) -> some View { self .environment(\.accountServiceConfiguration, service.configuration) - .environmentObject(model.modifiedDetailsBuilder) + .environment(model.modifiedDetailsBuilder) } } diff --git a/Sources/SpeziAccount/Views/AccountSetup/DefaultAccountSetupHeader.swift b/Sources/SpeziAccount/Views/AccountSetup/DefaultAccountSetupHeader.swift index 3fa89054..1bcef030 100644 --- a/Sources/SpeziAccount/Views/AccountSetup/DefaultAccountSetupHeader.swift +++ b/Sources/SpeziAccount/Views/AccountSetup/DefaultAccountSetupHeader.swift @@ -14,7 +14,7 @@ import SwiftUI /// This view expects a ``Account`` object to be in the environment to dynamically /// present the appropriate subtitle. public struct DefaultAccountSetupHeader: View { - @EnvironmentObject private var account: Account + @Environment(Account.self) private var account @Environment(\._accountSetupState) private var setupState public var body: some View { @@ -46,10 +46,10 @@ public struct DefaultAccountSetupHeader: View { struct DefaultAccountSetupHeader_Previews: PreviewProvider { static var previews: some View { DefaultAccountSetupHeader() - .environmentObject(Account()) + .environment(Account()) DefaultAccountSetupHeader() - .environmentObject(Account(building: .init().set(\.userId, value: "myUser"), active: MockUserIdPasswordAccountService())) + .environment(Account(building: .init().set(\.userId, value: "myUser"), active: MockUserIdPasswordAccountService())) } } #endif diff --git a/Sources/SpeziAccount/Views/AccountSetup/FollowUpInfoSheet.swift b/Sources/SpeziAccount/Views/AccountSetup/FollowUpInfoSheet.swift index 39ec7478..07757fad 100644 --- a/Sources/SpeziAccount/Views/AccountSetup/FollowUpInfoSheet.swift +++ b/Sources/SpeziAccount/Views/AccountSetup/FollowUpInfoSheet.swift @@ -12,6 +12,7 @@ import SpeziViews import SwiftUI +@MainActor struct FollowUpInfoSheet: View { private let accountDetails: AccountDetails private let accountKeyByCategory: OrderedDictionary @@ -23,9 +24,9 @@ struct FollowUpInfoSheet: View { @Environment(\.logger) private var logger @Environment(\.dismiss) private var dismiss - @EnvironmentObject private var account: Account + @Environment(Account.self) private var account - @StateObject private var detailsBuilder = ModifiedAccountDetails.Builder() + @State private var detailsBuilder = ModifiedAccountDetails.Builder() @ValidationState private var validation @State private var viewState: ViewState = .idle @@ -89,7 +90,7 @@ struct FollowUpInfoSheet: View { SignupSectionsView(for: ModifiedAccountDetails.self, service: service, sections: accountKeyByCategory) .environment(\.accountServiceConfiguration, service.configuration) .environment(\.accountViewType, .signup) - .environmentObject(detailsBuilder) + .environment(detailsBuilder) .focused($isFocused) AsyncButton(state: $viewState, action: completeButtonAction) { @@ -150,7 +151,7 @@ struct FollowUpInfoSheet_Previews: PreviewProvider { FollowUpInfoSheet(details: details, requiredKeys: [PersonNameKey.self]) } } - .environmentObject(account) + .environment(account) } } #endif diff --git a/Sources/SpeziAccount/Views/AccountSetup/SignupSectionsView.swift b/Sources/SpeziAccount/Views/AccountSetup/SignupSectionsView.swift index 9d16e9f7..a6e4928e 100644 --- a/Sources/SpeziAccount/Views/AccountSetup/SignupSectionsView.swift +++ b/Sources/SpeziAccount/Views/AccountSetup/SignupSectionsView.swift @@ -24,7 +24,7 @@ struct SignupSectionsView: View { private let sections: OrderedDictionary private let storageType: Storage.Type - @EnvironmentObject private var account: Account + @Environment(Account.self) private var account var body: some View { // OrderedDictionary `elements` conforms to RandomAccessCollection so we can directly use it @@ -67,7 +67,7 @@ struct SignupSectionsView_Previews: PreviewProvider { .name: [PersonNameKey.self] ]) } - .environmentObject(Account(service)) + .environment(Account(service)) } } #endif diff --git a/Sources/SpeziAccount/Views/DataDisplay/DataDisplayView.swift b/Sources/SpeziAccount/Views/DataDisplay/DataDisplayView.swift index ad12b78e..cb8036e6 100644 --- a/Sources/SpeziAccount/Views/DataDisplay/DataDisplayView.swift +++ b/Sources/SpeziAccount/Views/DataDisplay/DataDisplayView.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import Spezi +import SpeziFoundation import SwiftUI diff --git a/Sources/SpeziAccount/Views/DataDisplay/LocalizableStringBasedDisplayView.swift b/Sources/SpeziAccount/Views/DataDisplay/LocalizableStringBasedDisplayView.swift index 7902a417..b55f36f1 100644 --- a/Sources/SpeziAccount/Views/DataDisplay/LocalizableStringBasedDisplayView.swift +++ b/Sources/SpeziAccount/Views/DataDisplay/LocalizableStringBasedDisplayView.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import Spezi +import SpeziFoundation import SwiftUI diff --git a/Sources/SpeziAccount/Views/DataDisplay/StringBasedDisplayView.swift b/Sources/SpeziAccount/Views/DataDisplay/StringBasedDisplayView.swift index d8d8c317..d6250f85 100644 --- a/Sources/SpeziAccount/Views/DataDisplay/StringBasedDisplayView.swift +++ b/Sources/SpeziAccount/Views/DataDisplay/StringBasedDisplayView.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import Spezi +import SpeziFoundation import SwiftUI diff --git a/Sources/SpeziAccount/Views/DataEntry/DataEntryView.swift b/Sources/SpeziAccount/Views/DataEntry/DataEntryView.swift index 9ae26da3..62f58c40 100644 --- a/Sources/SpeziAccount/Views/DataEntry/DataEntryView.swift +++ b/Sources/SpeziAccount/Views/DataEntry/DataEntryView.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -import Spezi +import SpeziFoundation import SwiftUI diff --git a/Sources/SpeziAccount/Views/DataEntry/GeneralizedDataEntryView.swift b/Sources/SpeziAccount/Views/DataEntry/GeneralizedDataEntryView.swift index 08ffdc09..b2482730 100644 --- a/Sources/SpeziAccount/Views/DataEntry/GeneralizedDataEntryView.swift +++ b/Sources/SpeziAccount/Views/DataEntry/GeneralizedDataEntryView.swift @@ -6,7 +6,6 @@ // SPDX-License-Identifier: MIT // -import Spezi import SpeziValidation import SwiftUI @@ -29,9 +28,9 @@ public struct GeneralizedDataEntryView + @Environment(AccountValuesBuilder.self) private var detailsBuilder @Environment(\.accountServiceConfiguration) private var configuration @Environment(\.accountViewType) private var viewType diff --git a/Sources/SpeziAccount/Views/Fields/DateOfBirthPicker.swift b/Sources/SpeziAccount/Views/Fields/DateOfBirthPicker.swift index b2db7123..63689b3c 100644 --- a/Sources/SpeziAccount/Views/Fields/DateOfBirthPicker.swift +++ b/Sources/SpeziAccount/Views/Fields/DateOfBirthPicker.swift @@ -15,7 +15,7 @@ public struct DateOfBirthPicker: DataEntryView { private let titleLocalization: LocalizedStringResource - @EnvironmentObject private var account: Account + @Environment(Account.self) private var account @Environment(\.accountViewType) private var viewType @Binding private var date: Date @@ -133,18 +133,18 @@ struct DateOfBirthPicker_Previews: PreviewProvider { static var previews: some View { // preview entering new data. Preview() - .environmentObject(Account(MockUserIdPasswordAccountService())) + .environment(Account(MockUserIdPasswordAccountService())) .environment(\.accountViewType, .signup) // preview entering new data but displaying existing data. Preview() - .environmentObject(Account(MockUserIdPasswordAccountService())) + .environment(Account(MockUserIdPasswordAccountService())) .environment(\.accountViewType, .overview(mode: .existing)) // preview entering new data but required. Preview() + .environment(Account(MockUserIdPasswordAccountService(), configuration: [.requires(\.dateOfBirth)])) .environment(\.accountViewType, .signup) - .environmentObject(Account(MockUserIdPasswordAccountService(), configuration: [.requires(\.dateOfBirth)])) } } #endif diff --git a/Sources/SpeziAccount/Views/SignupForm.swift b/Sources/SpeziAccount/Views/SignupForm.swift index 820f1b46..206cad04 100644 --- a/Sources/SpeziAccount/Views/SignupForm.swift +++ b/Sources/SpeziAccount/Views/SignupForm.swift @@ -47,10 +47,10 @@ public struct SignupForm: View { private let service: Service private let header: Header - @EnvironmentObject private var account: Account + @Environment(Account.self) private var account @Environment(\.dismiss) private var dismiss - @StateObject private var signupDetailsBuilder = SignupDetails.Builder() + @State private var signupDetailsBuilder = SignupDetails.Builder() @ValidationState private var validation @State private var viewState: ViewState = .idle @@ -107,14 +107,14 @@ public struct SignupForm: View { } } - @ViewBuilder var form: some View { + @MainActor @ViewBuilder var form: some View { Form { header SignupSectionsView(for: SignupDetails.self, service: service, sections: accountKeyByCategory) .environment(\.accountServiceConfiguration, service.configuration) .environment(\.accountViewType, .signup) - .environmentObject(signupDetailsBuilder) + .environment(signupDetailsBuilder) AsyncButton(state: $viewState, action: signupButtonAction) { Text("UP_SIGNUP", bundle: .module) @@ -143,6 +143,7 @@ public struct SignupForm: View { } + @MainActor private func signupButtonAction() async throws { guard validation.validateSubviews() else { return @@ -168,7 +169,7 @@ struct DefaultUserIdPasswordSignUpView_Previews: PreviewProvider { NavigationStack { SignupForm(using: accountService) } - .environmentObject(Account(accountService)) + .environment(Account(accountService)) } } #endif diff --git a/Sources/SpeziAccount/Views/UserIdPasswordEmbeddedView.swift b/Sources/SpeziAccount/Views/UserIdPasswordEmbeddedView.swift index 10fe2818..1c3c1991 100644 --- a/Sources/SpeziAccount/Views/UserIdPasswordEmbeddedView.swift +++ b/Sources/SpeziAccount/Views/UserIdPasswordEmbeddedView.swift @@ -28,7 +28,7 @@ public struct UserIdPasswordEmbeddedView: service.configuration.userIdConfiguration } - @EnvironmentObject private var account: Account + @Environment(Account.self) private var account // for login we do all checks server-side. Except that we don't pass empty values. @ValidationState private var validation @@ -128,6 +128,7 @@ public struct UserIdPasswordEmbeddedView: } + @MainActor private func loginButtonAction() async throws { guard validation.validateSubviews() else { return @@ -146,6 +147,6 @@ public struct UserIdPasswordEmbeddedView: return NavigationStack { UserIdPasswordEmbeddedView(using: accountService) } - .environmentObject(Account(accountService)) + .environment(Account(accountService)) } #endif diff --git a/Sources/SpeziAccount/Views/UserIdPasswordPrimaryView.swift b/Sources/SpeziAccount/Views/UserIdPasswordPrimaryView.swift index 96a77e38..c1e3c315 100644 --- a/Sources/SpeziAccount/Views/UserIdPasswordPrimaryView.swift +++ b/Sources/SpeziAccount/Views/UserIdPasswordPrimaryView.swift @@ -54,7 +54,7 @@ struct DefaultUserIdPasswordPrimaryView_Previews: PreviewProvider { static var previews: some View { NavigationStack { UserIdPasswordPrimaryView(using: accountService) - .environmentObject(Account(accountService)) + .environment(Account(accountService)) } } } diff --git a/Sources/SpeziAccount/Views/UserIdPasswordResetView.swift b/Sources/SpeziAccount/Views/UserIdPasswordResetView.swift index cdb7d1b1..e7c54897 100644 --- a/Sources/SpeziAccount/Views/UserIdPasswordResetView.swift +++ b/Sources/SpeziAccount/Views/UserIdPasswordResetView.swift @@ -137,14 +137,14 @@ struct DefaultUserIdPasswordResetView_Previews: PreviewProvider { UserIdPasswordResetView(using: accountService) { SuccessfulPasswordResetView() } - .environmentObject(Account(accountService)) + .environment(Account(accountService)) } NavigationStack { UserIdPasswordResetView(using: accountService, requestSubmitted: true) { SuccessfulPasswordResetView() } - .environmentObject(Account(accountService)) + .environment(Account(accountService)) } } } diff --git a/Tests/UITests/TestApp/AccountTests/AccountTestsView.swift b/Tests/UITests/TestApp/AccountTests/AccountTestsView.swift index 23a51f4a..b0a8833e 100644 --- a/Tests/UITests/TestApp/AccountTests/AccountTestsView.swift +++ b/Tests/UITests/TestApp/AccountTests/AccountTestsView.swift @@ -12,12 +12,13 @@ import SpeziViews import SwiftUI +@MainActor struct AccountTestsView: View { @Environment(\.features) var features - @EnvironmentObject var account: Account - @EnvironmentObject var standard: TestStandard - + @Environment(Account.self) var account: Account + @Environment(TestStandard.self) var standard + @State var showSetup = false @State var showOverview = false @State var isEditing = false @@ -133,13 +134,13 @@ struct AccountTestsView_Previews: PreviewProvider { static var previews: some View { AccountTestsView() - .environmentObject(Account(TestAccountService(.emailAddress))) - + .environment(Account(TestAccountService(TestAlertModel(), .emailAddress))) + AccountTestsView() - .environmentObject(Account(building: details, active: TestAccountService(.emailAddress))) - + .environment(Account(building: details, active: TestAccountService(TestAlertModel(), .emailAddress))) + AccountTestsView() - .environmentObject(Account()) + .environment(Account()) } } #endif diff --git a/Tests/UITests/TestApp/AccountTests/TestAccountConfiguration.swift b/Tests/UITests/TestApp/AccountTests/TestAccountConfiguration.swift index e6bf3d6d..59211fa7 100644 --- a/Tests/UITests/TestApp/AccountTests/TestAccountConfiguration.swift +++ b/Tests/UITests/TestApp/AccountTests/TestAccountConfiguration.swift @@ -12,26 +12,32 @@ import Spezi import SpeziAccount -final class TestAccountConfiguration: Component { +final class TestAccountConfiguration: Module { @Provide var accountServices: [any AccountService] + @Model var testModel: TestAlertModel + init(features: Features) { + let model = TestAlertModel() + switch features.serviceType { case .mail: - accountServices = [TestAccountService(.emailAddress, defaultAccount: features.defaultCredentials, noName: features.noName)] + accountServices = [TestAccountService(model, .emailAddress, defaultAccount: features.defaultCredentials, noName: features.noName)] case .both: accountServices = [ - TestAccountService(.emailAddress, defaultAccount: features.defaultCredentials, noName: features.noName), - TestAccountService(.username) + TestAccountService(model, .emailAddress, defaultAccount: features.defaultCredentials, noName: features.noName), + TestAccountService(model, .username) ] case .withIdentityProvider: accountServices = [ - TestAccountService(.emailAddress, defaultAccount: features.defaultCredentials, noName: features.noName), + TestAccountService(model, .emailAddress, defaultAccount: features.defaultCredentials, noName: features.noName), MockSignInWithAppleProvider() ] case .empty: accountServices = [] } + + testModel = model } func configure() { diff --git a/Tests/UITests/TestApp/AccountTests/TestAccountService.swift b/Tests/UITests/TestApp/AccountTests/TestAccountService.swift index 609c5ebb..fb33ef7b 100644 --- a/Tests/UITests/TestApp/AccountTests/TestAccountService.swift +++ b/Tests/UITests/TestApp/AccountTests/TestAccountService.swift @@ -7,10 +7,23 @@ // import SpeziAccount +import SwiftUI + + +struct TestViewStyle: UserIdPasswordAccountSetupViewStyle { + let service: TestAccountService + + + var securityRelatedViewModifier: any ViewModifier { + TestAlertModifier() + } +} actor TestAccountService: UserIdPasswordAccountService { nonisolated let configuration: AccountServiceConfiguration + + private let model: TestAlertModel private let defaultUserId: String private let defaultAccountOnConfigure: Bool private var excludeName: Bool @@ -18,8 +31,12 @@ actor TestAccountService: UserIdPasswordAccountService { @AccountReference var account: Account var registeredUser: UserStorage // simulates the backend + nonisolated var viewStyle: TestViewStyle { + TestViewStyle(service: self) + } - init(_ type: UserIdType, defaultAccount: Bool = false, noName: Bool = false) { + + init(_ model: TestAlertModel, _ type: UserIdType, defaultAccount: Bool = false, noName: Bool = false) { configuration = AccountServiceConfiguration( name: "\(type.localizedStringResource) and Password", supportedKeys: .exactly(UserStorage.supportedKeys) @@ -31,10 +48,11 @@ actor TestAccountService: UserIdPasswordAccountService { UserIdConfiguration(type: type, keyboardType: type == .emailAddress ? .emailAddress : .default) } - defaultUserId = type == .emailAddress ? UserStorage.defaultEmail : UserStorage.defaultUsername + self.model = model + self.defaultUserId = type == .emailAddress ? UserStorage.defaultEmail : UserStorage.defaultUsername self.defaultAccountOnConfigure = defaultAccount self.excludeName = noName - registeredUser = UserStorage(userId: defaultUserId) + self.registeredUser = UserStorage(userId: defaultUserId) } nonisolated func configure() { @@ -76,7 +94,16 @@ actor TestAccountService: UserIdPasswordAccountService { } func updateAccountDetails(_ modifications: AccountModifications) async throws { - try await Task.sleep(for: .seconds(1)) + if modifications.modifiedDetails.storage.get(UserIdKey.self) != nil + || modifications.modifiedDetails.storage.get(PasswordKey.self) != nil { + await withCheckedContinuation { continuation in + model.presentingAlert = true + model.continuation = continuation + } + } else { + try await Task.sleep(for: .seconds(1)) + } + registeredUser.update(modifications) try await updateUser() diff --git a/Tests/UITests/TestApp/AccountTests/TestAlertModifier.swift b/Tests/UITests/TestApp/AccountTests/TestAlertModifier.swift new file mode 100644 index 00000000..30da11db --- /dev/null +++ b/Tests/UITests/TestApp/AccountTests/TestAlertModifier.swift @@ -0,0 +1,39 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +@Observable +final class TestAlertModel: Sendable { + var presentingAlert = false + var continuation: CheckedContinuation? +} + + +struct TestAlertModifier: ViewModifier { + @Environment(TestAlertModel.self) var model + + var isPresented: Binding { + Binding { + model.presentingAlert + } set: { newValue in + model.presentingAlert = newValue + } + } + + + func body(content: Content) -> some View { + content + .alert("Security Alert", isPresented: isPresented, presenting: model.continuation) { continuation in + Button("Dismiss") { + continuation.resume() + } + } + } +} diff --git a/Tests/UITests/TestApp/AccountTests/TestStandard.swift b/Tests/UITests/TestApp/AccountTests/TestStandard.swift index 9ed97092..14006f01 100644 --- a/Tests/UITests/TestApp/AccountTests/TestStandard.swift +++ b/Tests/UITests/TestApp/AccountTests/TestStandard.swift @@ -12,7 +12,7 @@ import SwiftUI // mock implementation of the AccountStorageStandard -actor TestStandard: AccountStorageStandard, AccountNotifyStandard, ObservableObject, ObservableObjectProvider { +actor TestStandard: AccountStorageStandard, AccountNotifyStandard, EnvironmentAccessible { @MainActor @Published var deleteNotified = false var records: [AdditionalRecordId: PartialAccountDetails.Builder] = [:] diff --git a/Tests/UITests/TestAppUITests/AccountOverviewTests.swift b/Tests/UITests/TestAppUITests/AccountOverviewTests.swift index 0dd430c8..a0d52cb8 100644 --- a/Tests/UITests/TestAppUITests/AccountOverviewTests.swift +++ b/Tests/UITests/TestAppUITests/AccountOverviewTests.swift @@ -194,6 +194,9 @@ final class AccountOverviewTests: XCTestCase { overview.app.typeText("tum.de") // we still have keyboard focus overview.app.dismissKeyboard() overview.tap(button: "Done") + + XCTAssertTrue(app.alerts["Security Alert"].buttons["Dismiss"].waitForExistence(timeout: 2.0)) + app.alerts["Security Alert"].buttons["Dismiss"].tap() sleep(3) overview.verifyExistence(text: "lelandstanford@tum.de") @@ -268,6 +271,9 @@ final class AccountOverviewTests: XCTestCase { overview.app.typeText("6789") overview.tap(button: "Done") + + XCTAssertTrue(app.alerts["Security Alert"].buttons["Dismiss"].waitForExistence(timeout: 2.0)) + app.alerts["Security Alert"].buttons["Dismiss"].tap() sleep(2) XCTAssertFalse(overview.secureTextFields["enter password"].waitForExistence(timeout: 2.0)) diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index f09d88b9..39c0204f 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 2FAD38C02A455FC200E79ED1 /* SpeziAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 2FAD38BF2A455FC200E79ED1 /* SpeziAccount */; }; A969240F2A9A198800E2128B /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = A969240E2A9A198800E2128B /* ArgumentParser */; }; A96924112A9A2E7800E2128B /* TestAccountService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A96924102A9A2E7800E2128B /* TestAccountService.swift */; }; + A99D25052AFCB7AD002CC42A /* TestAlertModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99D25042AFCB7AD002CC42A /* TestAlertModifier.swift */; }; A9B6E3F72A9B6F5B0008B232 /* AccountSetupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B6E3F62A9B6F5B0008B232 /* AccountSetupTests.swift */; }; A9B6E3F92A9B6F660008B232 /* AccountOverviewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B6E3F82A9B6F660008B232 /* AccountOverviewTests.swift */; }; A9B6E3FB2A9B70360008B232 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9B6E3FA2A9B70360008B232 /* Defaults.swift */; }; @@ -60,6 +61,7 @@ 2FE750C92A8720CE00723EAE /* TestApp.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestApp.xctestplan; sourceTree = ""; }; 636D985F2AF188E00020B8BC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; A96924102A9A2E7800E2128B /* TestAccountService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAccountService.swift; sourceTree = ""; }; + A99D25042AFCB7AD002CC42A /* TestAlertModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestAlertModifier.swift; sourceTree = ""; }; A9B6E3F62A9B6F5B0008B232 /* AccountSetupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSetupTests.swift; sourceTree = ""; }; A9B6E3F82A9B6F660008B232 /* AccountOverviewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountOverviewTests.swift; sourceTree = ""; }; A9B6E3FA2A9B70360008B232 /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = ""; }; @@ -99,6 +101,7 @@ 2F027C9629D6C63300234098 /* AccountTests */ = { isa = PBXGroup; children = ( + A99D25042AFCB7AD002CC42A /* TestAlertModifier.swift */, 2F027C7B29D6C29B00234098 /* AccountTestsView.swift */, 2F027C7A29D6C29B00234098 /* MockAccountServiceError.swift */, 2F027C7E29D6C29B00234098 /* TestAccountConfiguration.swift */, @@ -313,6 +316,7 @@ files = ( 2F027C8629D6C2AD00234098 /* AccountTestsView.swift in Sources */, 2FA7382C290ADFAA007ACEB9 /* TestApp.swift in Sources */, + A99D25052AFCB7AD002CC42A /* TestAlertModifier.swift in Sources */, 2F027C8829D6C2AD00234098 /* MockAccountServiceError.swift in Sources */, A9EE7D2A2A3359E800C2B9A9 /* Features.swift in Sources */, 2F027C9529D6C63100234098 /* TestAppDelegate.swift in Sources */, @@ -574,6 +578,120 @@ }; name = Release; }; + A94FDCE12AFC4A86008026CE /* Test */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = TEST; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Test; + }; + A94FDCE22AFC4A86008026CE /* Test */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = TestApp/TestApp.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + DEVELOPMENT_TEAM = 637867499T; + ENABLE_PREVIEWS = YES; + ENABLE_TESTING_SEARCH_PATHS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = TestApp/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.account.testapp; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Test; + }; + A94FDCE32AFC4A86008026CE /* Test */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 637867499T; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.spezi.account.testappuitests; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE = ""; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = TestApp; + }; + name = Test; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -581,6 +699,7 @@ isa = XCConfigurationList; buildConfigurations = ( 2F6D13B428F5F386007C25D6 /* Debug */, + A94FDCE12AFC4A86008026CE /* Test */, 2F6D13B528F5F386007C25D6 /* Release */, ); defaultConfigurationIsVisible = 0; @@ -590,6 +709,7 @@ isa = XCConfigurationList; buildConfigurations = ( 2F6D13B728F5F386007C25D6 /* Debug */, + A94FDCE22AFC4A86008026CE /* Test */, 2F6D13B828F5F386007C25D6 /* Release */, ); defaultConfigurationIsVisible = 0; @@ -599,6 +719,7 @@ isa = XCConfigurationList; buildConfigurations = ( 2F6D13BD28F5F386007C25D6 /* Debug */, + A94FDCE32AFC4A86008026CE /* Test */, 2F6D13BE28F5F386007C25D6 /* Release */, ); defaultConfigurationIsVisible = 0; diff --git a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme index 995374b0..38ec0896 100644 --- a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme +++ b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme @@ -22,7 +22,7 @@