diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 457d3887..48499587 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -24,16 +24,6 @@ jobs: scheme: Spezi-Package resultBundle: Spezi-Package-iOS.xcresult artifactname: Spezi-Package-iOS.xcresult - buildandtest_ios_latest: - name: Build and Test Swift Package iOS Latest - uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 - with: - runsonlabels: '["macOS", "self-hosted"]' - scheme: Spezi-Package - xcodeversion: latest - swiftVersion: 6 - resultBundle: Spezi-Package-iOS-Latest.xcresult - artifactname: Spezi-Package-iOS-Latest.xcresult buildandtest_watchos: name: Build and Test Swift Package watchOS uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 @@ -79,17 +69,6 @@ jobs: scheme: TestApp resultBundle: TestApp-iOS.xcresult artifactname: TestApp-iOS.xcresult - buildandtestuitests_ios_latest: - name: Build and Test UI Tests iOS Latest - uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 - with: - runsonlabels: '["macOS", "self-hosted"]' - path: Tests/UITests - scheme: TestApp - xcodeversion: latest - swiftVersion: 6 - resultBundle: TestApp-iOS-Latest.xcresult - artifactname: TestApp-iOS-Latest.xcresult buildandtestuitests_visionos: name: Build and Test UI Tests visionOS uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 diff --git a/.spi.yml b/.spi.yml index 27b5b560..5c74bcc4 100644 --- a/.spi.yml +++ b/.spi.yml @@ -11,6 +11,7 @@ builder: configs: - platform: ios scheme: Spezi + swift_version: 6 documentation_targets: - Spezi - XCTSpezi diff --git a/Package.swift b/Package.swift index 98c5072c..81a2d326 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.0 // // This source file is part of the Stanford Spezi open-source project @@ -11,12 +11,6 @@ import class Foundation.ProcessInfo import PackageDescription -#if swift(<6) -let swiftConcurrency: SwiftSetting = .enableExperimentalFeature("StrictConcurrency") -#else -let swiftConcurrency: SwiftSetting = .enableUpcomingFeature("StrictConcurrency") -#endif - let package = Package( name: "Spezi", @@ -33,8 +27,8 @@ let package = Package( .library(name: "XCTSpezi", targets: ["XCTSpezi"]) ], dependencies: [ - .package(url: "https://github.com/StanfordSpezi/SpeziFoundation", from: "2.0.0-beta.1"), - .package(url: "https://github.com/StanfordBDHG/XCTRuntimeAssertions", from: "1.1.1"), + .package(url: "https://github.com/StanfordSpezi/SpeziFoundation.git", from: "2.0.0"), + .package(url: "https://github.com/StanfordBDHG/XCTRuntimeAssertions.git", from: "1.1.1"), .package(url: "https://github.com/apple/swift-collections.git", from: "1.1.1") ] + swiftLintPackage(), targets: [ @@ -45,9 +39,6 @@ let package = Package( .product(name: "XCTRuntimeAssertions", package: "XCTRuntimeAssertions"), .product(name: "OrderedCollections", package: "swift-collections") ], - swiftSettings: [ - swiftConcurrency - ], plugins: [] + swiftLintPlugin() ), .target( @@ -55,9 +46,6 @@ let package = Package( dependencies: [ .target(name: "Spezi") ], - swiftSettings: [ - swiftConcurrency - ], plugins: [] + swiftLintPlugin() ), .testTarget( @@ -67,9 +55,6 @@ let package = Package( .target(name: "XCTSpezi"), .product(name: "XCTRuntimeAssertions", package: "XCTRuntimeAssertions") ], - swiftSettings: [ - swiftConcurrency - ], plugins: [] + swiftLintPlugin() ) ] diff --git a/Sources/Spezi/Capabilities/ApplicationPropertyWrapper.swift b/Sources/Spezi/Capabilities/ApplicationPropertyWrapper.swift index f0aaad58..d6d33ad8 100644 --- a/Sources/Spezi/Capabilities/ApplicationPropertyWrapper.swift +++ b/Sources/Spezi/Capabilities/ApplicationPropertyWrapper.swift @@ -6,24 +6,29 @@ // SPDX-License-Identifier: MIT // +import SwiftUI -/// Refer to the documentation of ``Module/Application``. + +/// Access a property or action of the Spezi application. @propertyWrapper -public class _ApplicationPropertyWrapper { // swiftlint:disable:this type_name - private let keyPath: KeyPath +public struct _ApplicationPropertyWrapper { // swiftlint:disable:this type_name + private final class State { + weak var spezi: Spezi? + /// Some KeyPaths are declared to copy the value upon injection and not query them every time. + var shadowCopy: Value? + } - private weak var spezi: Spezi? - /// Some KeyPaths are declared to copy the value upon injection and not query them every time. - private var shadowCopy: Value? + private let keyPath: KeyPath + private let state = State() /// Access the application property. public var wrappedValue: Value { - if let shadowCopy { + if let shadowCopy = state.shadowCopy { return shadowCopy } - guard let spezi else { + guard let spezi = state.spezi else { preconditionFailure("Underlying Spezi instance was not yet injected. @Application cannot be accessed within the initializer!") } return spezi[keyPath: keyPath] @@ -39,15 +44,15 @@ public class _ApplicationPropertyWrapper { // swiftlint:disable:this type extension _ApplicationPropertyWrapper: SpeziPropertyWrapper { func inject(spezi: Spezi) { - self.spezi = spezi + state.spezi = spezi if spezi.createsCopy(keyPath) { - self.shadowCopy = spezi[keyPath: keyPath] + state.shadowCopy = spezi[keyPath: keyPath] } } func clear() { - spezi = nil - shadowCopy = nil + state.spezi = nil + state.shadowCopy = nil } } diff --git a/Sources/Spezi/Capabilities/Notifications/RegisterRemoteNotificationsAction.swift b/Sources/Spezi/Capabilities/Notifications/RegisterRemoteNotificationsAction.swift deleted file mode 100644 index 6375a5b4..00000000 --- a/Sources/Spezi/Capabilities/Notifications/RegisterRemoteNotificationsAction.swift +++ /dev/null @@ -1,242 +0,0 @@ -// -// This source file is part of the Stanford Spezi open-source project -// -// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import SpeziFoundation -import SwiftUI - - -@MainActor -private final class RemoteNotificationContinuation: KnowledgeSource, Sendable { - typealias Anchor = SpeziAnchor - - fileprivate(set) var continuation: CheckedContinuation? - fileprivate(set) var access = AsyncSemaphore() - - - init() {} - - - @MainActor - func resume(with result: Result) { - if let continuation { - self.continuation = nil - access.signal() - continuation.resume(with: result) - } - } -} - - -/// Registers to receive remote notifications through Apple Push Notification service. -/// -/// For more information refer to the [`registerForRemoteNotifications()`](https://developer.apple.com/documentation/uikit/uiapplication/1623078-registerforremotenotifications) -/// documentation for `UIApplication` or for the respective equivalent for your current platform. -/// -/// - Note: For more information on the general topic on how to register your app with APNs, -/// refer to the [Registering your app with APNs](https://developer.apple.com/documentation/usernotifications/registering-your-app-with-apns) -/// article. -/// > Tip: Make sure to request authorization by calling [`requestAuthorization(options:completionHandler:)`](https://developer.apple.com/documentation/usernotifications/unusernotificationcenter/requestauthorization(options:completionhandler:)) -/// to have your remote notifications be able to display alerts, badges or use sound. Otherwise, all remote notifications will be delivered silently. -/// -/// Below is a short code example on how to use this action within your ``Module``. -/// ```swift -/// class ExampleModule: Module { -/// @Application(\.registerRemoteNotifications) -/// var registerRemoteNotifications -/// -/// func handleNotificationsPermissions() async throws { -/// // Make sure to request notifications permissions before registering for remote notifications -/// try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) -/// let deviceToken = try await registerRemoteNotifications() -/// -/// // ... send the device token to your remote server that generates push notifications -/// } -/// } -/// ``` -/// -/// > Warning: The method might throw a [`TimeoutError`](https://swiftpackageindex.com/stanfordspezi/spezifoundation/documentation/spezifoundation/timeouterror) -/// if registering for remote notifications is not possible. -/// -/// Registering for Remote Notifications on Simulator devices might not be possible due to multiple reasons. -/// -/// #### Your application delegate, which is a subclass of SpeziAppDelegate, overrides some notification-related application delegate functions. -/// -/// **Solution:** Ensure that you correctly call the overridden method using super to pass all relevant information to Spezi. -/// -/// #### Your application does not have the correct entitlements and configuration in place to allow registering for remote notifications. -/// -/// **Solution:** Follow the [Apple Documentation](https://developer.apple.com/documentation/usernotifications/registering-your-app-with-apns). -/// -/// #### Your code or a dependency uses method swizzling, preventing the relevant methods in the application delegate from being called. -/// -/// **Solution:** Remove your method swizzling code or configure your dependency to disable this behavior. -/// For example, to [disable method swizzling in the iOS Firebase SDK](https://firebase.google.com/docs/cloud-messaging/ios/client#method_swizzling_in). -/// -/// #### The application is running in the iOS simulator on a Mac that is not signed into an Apple account (e.g., on a CI environment). -/// **Solution:** Sign in with an Apple account on your Mac and Xcode. For CI environments, use a special flag or compilation directive to catch the `TimeoutError`. -public struct RegisterRemoteNotificationsAction: Sendable { - private weak var spezi: Spezi? - - init(_ spezi: Spezi) { - self.spezi = spezi - } - - /// Registers to receive remote notifications through Apple Push Notification service. - /// - /// - Returns: A globally unique token that identifies this device to APNs. - /// Send this token to the server that you use to generate remote notifications. - /// Your server must pass this token unmodified back to APNs when sending those remote notifications. - /// For more information refer to the documentation of - /// [`application(_:didRegisterForRemoteNotificationsWithDeviceToken:)`](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622958-application). - /// - Throws: Registration might fail if the user's device isn't connected to the network or - /// if your app is not properly configured for remote notifications. It might also throw a `TimeoutError` when running on a simulator device running on a host - /// that is not connected to an Apple ID. - @discardableResult - @MainActor - public func callAsFunction() async throws -> Data { - guard let spezi else { - preconditionFailure("RegisterRemoteNotificationsAction was used in a scope where Spezi was not available anymore!") - } - - -#if os(watchOS) - let application = _Application.shared() -#else - let application = _Application.shared -#endif // os(watchOS) - - let registration: RemoteNotificationContinuation - if let existing = spezi.storage[RemoteNotificationContinuation.self] { - registration = existing - } else { - registration = RemoteNotificationContinuation() - spezi.storage[RemoteNotificationContinuation.self] = registration - } - - try await registration.access.waitCheckingCancellation() - - async let _ = withTimeout(of: .seconds(5)) { @MainActor in - spezi.logger.warning( - """ - Registering for Remote Notifications Timed Out - - This issue can occur for several reasons: - - - Your application delegate (subclass of `SpeziAppDelegate`) overrides some notification-related application delegate functions. - Solution: Ensure that you correctly call the overridden method using `super` to pass all relevant information to Spezi. - - - Your application does not have the correct entitlements and configuration in place to allow registering for remote notifications. - Solution: Follow the Apple Documentation at https://developer.apple.com/documentation/usernotifications/registering-your-app-with-apns. - - - Your code or a dependency uses method swizzling, preventing the relevant methods in the application delegate from being called. - Solution: Remove your method swizzling code or configure your dependency to disable this behavior. - For example, to disable method swizzling in the iOS Firebase SDK, follow their guidelines at - https://firebase.google.com/docs/cloud-messaging/ios/client#method_swizzling_in. - - - The application is running in the iOS simulator on a Mac that is not signed into an Apple account (e.g., on a CI environment). - Solution: Sign in with an Apple account on your Mac and Xcode. - For CI environments, use a special flag or compilation directive to catch the `TimeoutError`. - """ - ) - spezi.storage[RemoteNotificationContinuation.self]?.resume(with: .failure(TimeoutError())) - } - - return try await withCheckedThrowingContinuation { continuation in - assert(registration.continuation == nil, "continuation wasn't nil") - registration.continuation = continuation - application.registerForRemoteNotifications() - } - } -} - - -extension Spezi { - /// Registers to receive remote notifications through Apple Push Notification service. - /// - /// For more information refer to the [`registerForRemoteNotifications()`](https://developer.apple.com/documentation/uikit/uiapplication/1623078-registerforremotenotifications) - /// documentation for `UIApplication` or for the respective equivalent for your current platform. - /// - /// - Note: For more information on the general topic on how to register your app with APNs, - /// refer to the [Registering your app with APNs](https://developer.apple.com/documentation/usernotifications/registering-your-app-with-apns) - /// article. - /// > Tip: Make sure to request authorization by calling [`requestAuthorization(options:completionHandler:)`](https://developer.apple.com/documentation/usernotifications/unusernotificationcenter/requestauthorization(options:completionhandler:)) - /// to have your remote notifications be able to display alerts, badges or use sound. Otherwise, all remote notifications will be delivered silently. - /// - /// Below is a short code example on how to use this action within your ``Module``. - /// ```swift - /// class ExampleModule: Module { - /// @Application(\.registerRemoteNotifications) - /// var registerRemoteNotifications - /// - /// func handleNotificationsPermissions() async throws { - /// // Make sure to request notifications permissions before registering for remote notifications - /// try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) - /// let deviceToken = try await registerRemoteNotifications() - /// - /// // ... send the device token to your remote server that generates push notifications - /// } - /// } - /// ``` - /// - /// > Warning: The method might throw a [`TimeoutError`](https://swiftpackageindex.com/stanfordspezi/spezifoundation/documentation/spezifoundation/timeouterror) - /// if registering for remote notifications is not possible. - /// - /// Registering for Remote Notifications on Simulator devices might not be possible due to multiple reasons. - /// - /// #### Your application delegate, which is a subclass of SpeziAppDelegate, overrides some notification-related application delegate functions. - /// - /// **Solution:** Ensure that you correctly call the overridden method using super to pass all relevant information to Spezi. - /// - /// #### Your application does not have the correct entitlements and configuration in place to allow registering for remote notifications. - /// - /// **Solution:** Follow the [Apple Documentation](https://developer.apple.com/documentation/usernotifications/registering-your-app-with-apns). - /// - /// #### Your code or a dependency uses method swizzling, preventing the relevant methods in the application delegate from being called. - /// - /// **Solution:** Remove your method swizzling code or configure your dependency to disable this behavior. - /// For example, to [disable method swizzling in the iOS Firebase SDK](https://firebase.google.com/docs/cloud-messaging/ios/client#method_swizzling_in). - /// - /// #### The application is running in the iOS simulator on a Mac that is not signed into an Apple account (e.g., on a CI environment). - /// **Solution:** Sign in with an Apple account on your Mac and Xcode. For CI environments, use a special flag or compilation directive to catch the `TimeoutError`. - /// - /// - /// ## Topics - /// ### Action - /// - ``RegisterRemoteNotificationsAction`` - public var registerRemoteNotifications: RegisterRemoteNotificationsAction { - RegisterRemoteNotificationsAction(self) - } -} - - -extension RegisterRemoteNotificationsAction { - @MainActor - static func handleDeviceTokenUpdate(_ spezi: Spezi, _ deviceToken: Data) { - guard let registration = spezi.storage[RemoteNotificationContinuation.self] else { - return - } - - // might also be called if, e.g., app is restored from backup and is automatically registered for remote notifications. - // This can be handled through the `NotificationHandler` protocol. - - registration.resume(with: .success(deviceToken)) - } - - @MainActor - static func handleFailedRegistration(_ spezi: Spezi, _ error: Error) { - guard let registration = spezi.storage[RemoteNotificationContinuation.self] else { - return - } - - if registration.continuation == nil { - spezi.logger.warning("Received a call to \(#function) while we were not waiting for a notifications registration request.") - } - - registration.resume(with: .failure(error)) - } -} diff --git a/Sources/Spezi/Dependencies/DependencyManager.swift b/Sources/Spezi/Dependencies/DependencyManager.swift index b821b63b..64589d90 100644 --- a/Sources/Spezi/Dependencies/DependencyManager.swift +++ b/Sources/Spezi/Dependencies/DependencyManager.swift @@ -59,12 +59,12 @@ public class DependencyManager: Sendable { /// Resolves the dependency order. /// /// After calling `resolve()` you can safely access `initializedModules`. - func resolve() { + func resolve() throws(DependencyManagerError) { while let nextModule = modulesWithDependencies.first { - push(nextModule) + try push(nextModule) } - injectDependencies() + try injectDependencies() assert(searchStacks.isEmpty, "`searchStacks` are not getting cleaned up!") assert(currentPushedModule == nil, "`currentPushedModule` is never reset!") assert(modulesWithDependencies.isEmpty, "modulesWithDependencies has remaining entries \(modulesWithDependencies)") @@ -109,18 +109,18 @@ public class DependencyManager: Sendable { return order } - private func injectDependencies() { + private func injectDependencies() throws(DependencyManagerError) { // We inject dependencies into existingModules as well as a new dependency might be an optional dependency from a existing module // that wasn't previously injected. for module in initializedModules + existingModules { for dependency in module.dependencyDeclarations { - dependency.inject(from: self, for: module) + try dependency.inject(from: self, for: module) } } } /// Push a module on the search stack and resolve dependency information. - private func push(_ module: any Module) { + private func push(_ module: any Module) throws(DependencyManagerError) { assert(currentPushedModule == nil, "Module already pushed. Did the algorithm turn into an recursive one by accident?") currentPushedModule = ModuleReference(module) @@ -128,7 +128,7 @@ public class DependencyManager: Sendable { .append(type(of: module)) for dependency in module.dependencyDeclarations { - dependency.collect(into: self) // leads to calls to `require(_:defaultValue:)` + try dependency.collect(into: self) // leads to calls to `require(_:defaultValue:)` } finishSearch(for: module) @@ -138,8 +138,8 @@ public class DependencyManager: Sendable { /// - Parameters: /// - dependency: The type of the dependency that should be resolved. /// - defaultValue: A default instance of the dependency that is used when the `dependencyType` is not present in the `initializedModules` or `modulesWithDependencies`. - func require(_ dependency: M.Type, type dependencyType: DependencyType, defaultValue: (() -> M)?) { - testForSearchStackCycles(M.self) + func require(_ dependency: M.Type, type dependencyType: DependencyType, defaultValue: (() -> M)?) throws(DependencyManagerError) { + try testForSearchStackCycles(M.self) // 1. Check if it is actively requested to load this module. if case .load = dependencyType { @@ -177,18 +177,13 @@ public class DependencyManager: Sendable { /// - module: The ``Module`` type to return. /// - optional: Flag indicating if it is a optional return. /// - Returns: Returns the Module instance. Only optional, if `optional` is set to `true` and no Module was found. - func retrieve(module: M.Type, type dependencyType: DependencyType, for owner: any Module) -> M? { + func retrieve(module: M.Type, type dependencyType: DependencyType, for owner: any Module) throws(DependencyManagerError) -> M? { guard let candidate = existingModules.first(where: { type(of: $0) == M.self }) ?? initializedModules.first(where: { type(of: $0) == M.self }), let module = candidate as? M else { - precondition( - dependencyType.isOptional, - """ - '\(type(of: owner)) requires dependency of type '\(M.self)' which wasn't configured. - Please make sure this module is configured by including it in the configuration of your `SpeziAppDelegate` or following \ - Module-specific instructions. - """ - ) + if !dependencyType.isOptional { + throw DependencyManagerError.missingRequiredModule(module: "\(type(of: owner))", requiredModule: "\(M.self)") + } return nil } @@ -231,20 +226,16 @@ public class DependencyManager: Sendable { searchStacks[ModuleReference(module)] = searchStack } - private func testForSearchStackCycles(_ module: M.Type) { + private func testForSearchStackCycles(_ module: M.Type) throws(DependencyManagerError) { if let currentPushedModule { let searchStack = searchStacks[currentPushedModule, default: []] - precondition( - !searchStack.contains(where: { $0 == M.self }), - """ - The `DependencyManager` has detected a dependency cycle of your Spezi modules. - The current dependency chain is: \(searchStack.map { String(describing: $0) }.joined(separator: ", ")). \ - The module '\(searchStack.last.unsafelyUnwrapped)' required '\(M.self)' which is contained in its own dependency chain. - - Please ensure that the modules you use or develop can not trigger a dependency cycle. - """ - ) + if searchStack.contains(where: { $0 == M.self }) { + let module = "\(searchStack.last.unsafelyUnwrapped)" + let dependencyChain = searchStack + .map { String(describing: $0) } + throw DependencyManagerError.searchStackCycle(module: module, requestedModule: "\(M.self)", dependencyChain: dependencyChain) + } } } } diff --git a/Sources/Spezi/Dependencies/DependencyManagerError.swift b/Sources/Spezi/Dependencies/DependencyManagerError.swift new file mode 100644 index 00000000..640d4155 --- /dev/null +++ b/Sources/Spezi/Dependencies/DependencyManagerError.swift @@ -0,0 +1,35 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + + +enum DependencyManagerError: Error { + case searchStackCycle(module: String, requestedModule: String, dependencyChain: [String]) + case missingRequiredModule(module: String, requiredModule: String) +} + + +extension DependencyManagerError: CustomStringConvertible { + var description: String { + switch self { + case let .searchStackCycle(module, requestedModule, dependencyChain): + """ + The `DependencyManager` has detected a dependency cycle of your Spezi modules. + The current dependency chain is: \(dependencyChain.joined(separator: ", ")). \ + The module '\(module)' required '\(requestedModule)' which is contained in its own dependency chain. + + Please ensure that the modules you use or develop can not trigger a dependency cycle. + """ + case let .missingRequiredModule(module, requiredModule): + """ + '\(module) requires dependency of type '\(requiredModule)' which wasn't configured. + Please make sure this module is configured by including it in the configuration of your `SpeziAppDelegate` or following \ + Module-specific instructions. + """ + } + } +} diff --git a/Sources/Spezi/Dependencies/Property/DependencyCollection.swift b/Sources/Spezi/Dependencies/Property/DependencyCollection.swift index 16cca771..d911880e 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyCollection.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyCollection.swift @@ -127,15 +127,15 @@ extension DependencyCollection: DependencyDeclaration { } - func collect(into dependencyManager: DependencyManager) { + func collect(into dependencyManager: DependencyManager) throws(DependencyManagerError) { for entry in entries { - entry.collect(into: dependencyManager) + try entry.collect(into: dependencyManager) } } - func inject(from dependencyManager: DependencyManager, for module: any Module) { + func inject(from dependencyManager: DependencyManager, for module: any Module) throws(DependencyManagerError) { for entry in entries { - entry.inject(from: dependencyManager, for: module) + try entry.inject(from: dependencyManager, for: module) } } diff --git a/Sources/Spezi/Dependencies/Property/DependencyContext.swift b/Sources/Spezi/Dependencies/Property/DependencyContext.swift index 041ff82d..8c0e0bb1 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyContext.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyContext.swift @@ -92,12 +92,12 @@ class DependencyContext: AnyDependencyContext { } } - func collect(into dependencyManager: DependencyManager) { - dependencyManager.require(Dependency.self, type: type, defaultValue: defaultValue) + func collect(into dependencyManager: DependencyManager) throws(DependencyManagerError) { + try dependencyManager.require(Dependency.self, type: type, defaultValue: defaultValue) } - func inject(from dependencyManager: DependencyManager, for module: any Module) { - guard let dependency = dependencyManager.retrieve(module: Dependency.self, type: type, for: module) else { + func inject(from dependencyManager: DependencyManager, for module: any Module) throws(DependencyManagerError) { + guard let dependency = try dependencyManager.retrieve(module: Dependency.self, type: type, for: module) else { injectedDependency = nil return } diff --git a/Sources/Spezi/Dependencies/Property/DependencyDeclaration.swift b/Sources/Spezi/Dependencies/Property/DependencyDeclaration.swift index 13a942f8..87ea0779 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyDeclaration.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyDeclaration.swift @@ -28,13 +28,13 @@ protocol DependencyDeclaration { /// Request from the ``DependencyManager`` to collect all dependencies. Mark required by calling `DependencyManager/require(_:defaultValue:)`. @MainActor - func collect(into dependencyManager: DependencyManager) + func collect(into dependencyManager: DependencyManager) throws(DependencyManagerError) /// Inject the dependency instance from the ``DependencyManager``. Use `DependencyManager/retrieve(module:)`. /// - Parameters: /// - dependencyManager: The dependency manager to inject the dependencies from. /// - module: The module where the dependency declaration is located at. @MainActor - func inject(from dependencyManager: DependencyManager, for module: any Module) + func inject(from dependencyManager: DependencyManager, for module: any Module) throws(DependencyManagerError) @MainActor func inject(spezi: Spezi) diff --git a/Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift b/Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift index 417eb1da..4f0be72c 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift @@ -118,12 +118,12 @@ extension _DependencyPropertyWrapper: DependencyDeclaration { dependencies.dependencyRelation(to: module) } - func collect(into dependencyManager: DependencyManager) { - dependencies.collect(into: dependencyManager) + func collect(into dependencyManager: DependencyManager) throws(DependencyManagerError) { + try dependencies.collect(into: dependencyManager) } - func inject(from dependencyManager: DependencyManager, for module: any Module) { - dependencies.inject(from: dependencyManager, for: module) + func inject(from dependencyManager: DependencyManager, for module: any Module) throws(DependencyManagerError) { + try dependencies.inject(from: dependencyManager, for: module) } func uninjectDependencies(notifying spezi: Spezi) { diff --git a/Sources/Spezi/Utilities/BackgroundFetchResult.swift b/Sources/Spezi/Notifications/BackgroundFetchResult.swift similarity index 100% rename from Sources/Spezi/Utilities/BackgroundFetchResult.swift rename to Sources/Spezi/Notifications/BackgroundFetchResult.swift diff --git a/Sources/Spezi/Capabilities/Notifications/NotificationHandler.swift b/Sources/Spezi/Notifications/NotificationHandler.swift similarity index 100% rename from Sources/Spezi/Capabilities/Notifications/NotificationHandler.swift rename to Sources/Spezi/Notifications/NotificationHandler.swift diff --git a/Sources/Spezi/Capabilities/Notifications/NotificationTokenHandler.swift b/Sources/Spezi/Notifications/NotificationTokenHandler.swift similarity index 100% rename from Sources/Spezi/Capabilities/Notifications/NotificationTokenHandler.swift rename to Sources/Spezi/Notifications/NotificationTokenHandler.swift diff --git a/Sources/Spezi/Notifications/RemoteNotificationRegistrationSupport.swift b/Sources/Spezi/Notifications/RemoteNotificationRegistrationSupport.swift new file mode 100644 index 00000000..81b0c6a6 --- /dev/null +++ b/Sources/Spezi/Notifications/RemoteNotificationRegistrationSupport.swift @@ -0,0 +1,90 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import OSLog +import SpeziFoundation +import SwiftUI + + +@MainActor +@_spi(APISupport) +public final class RemoteNotificationRegistrationSupport: KnowledgeSource, Sendable { + public typealias Anchor = SpeziAnchor + + private let logger = Logger(subsystem: "edu.stanford.spezi", category: "RemoteNotificationRegistrationSupport") + + fileprivate(set) var continuation: CheckedContinuation? + fileprivate(set) var access = AsyncSemaphore() + + + nonisolated init() {} + + + func handleDeviceTokenUpdate(_ deviceToken: Data) { + // might also be called if, e.g., app is restored from backup and is automatically registered for remote notifications. + // This can be handled through the `NotificationHandler` protocol. + + resume(with: .success(deviceToken)) + } + + func handleFailedRegistration(_ error: Error) { + let resumed = resume(with: .failure(error)) + + if !resumed { + logger.warning("Received a call to \(#function) while we were not waiting for a notifications registration request.") + } + } + + + @discardableResult + private func resume(with result: Result) -> Bool { + if let continuation { + self.continuation = nil + access.signal() + continuation.resume(with: result) + return true + } + return false + } + + public func callAsFunction() async throws -> Data { + try await access.waitCheckingCancellation() + +#if targetEnvironment(simulator) + async let _ = withTimeout(of: .seconds(5)) { @MainActor in + logger.warning("Registering for remote notifications seems to be not possible on this simulator device. Timing out ...") + self.continuation?.resume(with: .failure(TimeoutError())) + } +#endif + + return try await withCheckedThrowingContinuation { continuation in + assert(self.continuation == nil, "continuation wasn't nil") + self.continuation = continuation + _Application.shared.registerForRemoteNotifications() + } + } +} + + +extension Spezi { + /// Provides support to call the `registerForRemoteNotifications()` method on the application. + /// + /// This helper type makes sure to bridge access to the delegate methods that will be called when executing `registerForRemoteNotifications()`. + @MainActor + @_spi(APISupport) + public var remoteNotificationRegistrationSupport: RemoteNotificationRegistrationSupport { + let support: RemoteNotificationRegistrationSupport + if let existing = spezi.storage[RemoteNotificationRegistrationSupport.self] { + support = existing + } else { + support = RemoteNotificationRegistrationSupport() + spezi.storage[RemoteNotificationRegistrationSupport.self] = support + } + return support + } +} diff --git a/Sources/Spezi/Notifications/Spezi+RegisterRemoteNotificationsAction.swift b/Sources/Spezi/Notifications/Spezi+RegisterRemoteNotificationsAction.swift new file mode 100644 index 00000000..62793253 --- /dev/null +++ b/Sources/Spezi/Notifications/Spezi+RegisterRemoteNotificationsAction.swift @@ -0,0 +1,106 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SpeziFoundation +import SwiftUI + + +/// Registers to receive remote notifications through Apple Push Notification service. +/// +/// Refer to the documentation of ``Spezi/registerRemoteNotifications``. +@_documentation(visibility: internal) +@available(*, deprecated, renamed: "Spezi.RegisterRemoteNotificationsAction", message: "Please use Spezi.RegisterRemoteNotificationsAction instead.") +public typealias RegisterRemoteNotificationsAction = Spezi.RegisterRemoteNotificationsAction + + +extension Spezi { + /// Registers to receive remote notifications through Apple Push Notification service. + /// + /// Refer to the documentation of ``Spezi/registerRemoteNotifications``. + public struct RegisterRemoteNotificationsAction: Sendable { + private weak var spezi: Spezi? + + fileprivate init(_ spezi: Spezi) { + self.spezi = spezi + } + + /// Registers to receive remote notifications through Apple Push Notification service. + /// + /// - Returns: A globally unique token that identifies this device to APNs. + /// Send this token to the server that you use to generate remote notifications. + /// Your server must pass this token unmodified back to APNs when sending those remote notifications. + /// For more information refer to the documentation of + /// [`application(_:didRegisterForRemoteNotificationsWithDeviceToken:)`](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622958-application). + /// - Throws: Registration might fail if the user's device isn't connected to the network or + /// if your app is not properly configured for remote notifications. It might also throw a `TimeoutError` when running on a simulator device running on a host + /// that is not connected to an Apple ID. + @discardableResult + @MainActor + public func callAsFunction() async throws -> Data { + guard let spezi else { + preconditionFailure("\(Self.self) was used in a scope where Spezi was not available anymore!") + } + + return try await spezi.remoteNotificationRegistrationSupport() + } + } + + /// Registers to receive remote notifications through Apple Push Notification service. + /// + /// For more information refer to the [`registerForRemoteNotifications()`](https://developer.apple.com/documentation/uikit/uiapplication/1623078-registerforremotenotifications) + /// documentation for `UIApplication` or for the respective equivalent for your current platform. + /// + /// - Note: For more information on the general topic on how to register your app with APNs, + /// refer to the [Registering your app with APNs](https://developer.apple.com/documentation/usernotifications/registering-your-app-with-apns) + /// article. + /// + /// Below is a short code example on how to use this action within your ``Module``. + /// + /// - Warning: Registering for Remote Notifications on Simulator devices might not be possible if your are not signed into an Apple ID on the host machine. + /// The method might throw a [`TimeoutError`](https://swiftpackageindex.com/stanfordspezi/spezifoundation/documentation/spezifoundation/timeouterror) + /// in such a case. + /// + /// ```swift + /// import SpeziFoundation + /// + /// class ExampleModule: Module { + /// @Application(\.registerRemoteNotifications) + /// var registerRemoteNotifications + /// + /// func handleNotificationsPermissions() async throws { + /// // Make sure to request notifications permissions before registering for remote notifications + /// try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) + /// + /// + /// do { + /// let deviceToken = try await registerRemoteNotifications() + /// } catch let error as TimeoutError { + /// #if targetEnvironment(simulator) + /// return // override logic when running within a simulator + /// #else + /// throw error + /// #endif + /// } + /// + /// // .. send the device token to your remote server that generates push notifications + /// } + /// } + /// ``` + /// + /// > Tip: Make sure to request authorization by calling [`requestAuthorization(options:completionHandler:)`](https://developer.apple.com/documentation/usernotifications/unusernotificationcenter/requestauthorization(options:completionhandler:)) + /// to have your remote notifications be able to display alerts, badges or use sound. Otherwise, all remote notifications will be delivered silently. + /// + /// ## Topics + /// ### Action + /// - ``RegisterRemoteNotificationsAction`` + @_disfavoredOverload + @available(*, deprecated, message: "Please migrate to the new SpeziNotifications package: https://github.com/StanfordSpezi/SpeziNotifications") + public var registerRemoteNotifications: RegisterRemoteNotificationsAction { + RegisterRemoteNotificationsAction(self) + } +} diff --git a/Sources/Spezi/Capabilities/Notifications/UnregisterRemoteNotificationsAction.swift b/Sources/Spezi/Notifications/Spezi+UnregisterRemoteNotifications.swift similarity index 58% rename from Sources/Spezi/Capabilities/Notifications/UnregisterRemoteNotificationsAction.swift rename to Sources/Spezi/Notifications/Spezi+UnregisterRemoteNotifications.swift index 7b1bebea..023fd5b0 100644 --- a/Sources/Spezi/Capabilities/Notifications/UnregisterRemoteNotificationsAction.swift +++ b/Sources/Spezi/Notifications/Spezi+UnregisterRemoteNotifications.swift @@ -11,41 +11,28 @@ import SwiftUI /// Unregisters for all remote notifications received through Apple Push Notification service. /// -/// For more information refer to the [`unregisterForRemoteNotifications()`](https://developer.apple.com/documentation/uikit/uiapplication/1623093-unregisterforremotenotifications) -/// documentation for `UIApplication` or for the respective equivalent for your current platform. -/// -/// Below is a short code example on how to use this action within your ``Module``. -/// -/// ```swift -/// class ExampleModule: Module { -/// @Application(\.unregisterRemoteNotifications) -/// var unregisterRemoteNotifications -/// -/// func onAccountLogout() { -/// // handling your cleanup ... -/// unregisterRemoteNotifications() -/// } -/// } -/// ``` -public struct UnregisterRemoteNotificationsAction: Sendable { - init() {} +/// Refer to the documentation of ``Spezi/unregisterRemoteNotifications``. +@_documentation(visibility: internal) +@available(*, deprecated, renamed: "Spezi.UnregisterRemoteNotificationsAction", message: "Please use Spezi.UnregisterRemoteNotificationsAction") +public typealias UnregisterRemoteNotificationsAction = Spezi.UnregisterRemoteNotificationsAction +extension Spezi { /// Unregisters for all remote notifications received through Apple Push Notification service. - @MainActor - public func callAsFunction() { -#if os(watchOS) - let application = _Application.shared() -#else - let application = _Application.shared -#endif - - application.unregisterForRemoteNotifications() - } -} + /// + /// Refer to the documentation of ``Spezi/unregisterRemoteNotifications``. + @available(*, deprecated, message: "Please migrate to the new SpeziNotifications package: https://github.com/StanfordSpezi/SpeziNotifications") + public struct UnregisterRemoteNotificationsAction: Sendable { + fileprivate init() {} -extension Spezi { + /// Unregisters for all remote notifications received through Apple Push Notification service. + @MainActor + public func callAsFunction() { + _Application.shared.unregisterForRemoteNotifications() + } + } + /// Unregisters for all remote notifications received through Apple Push Notification service. /// /// For more information refer to the [`unregisterForRemoteNotifications()`](https://developer.apple.com/documentation/uikit/uiapplication/1623093-unregisterforremotenotifications) @@ -68,6 +55,8 @@ extension Spezi { /// ## Topics /// ### Action /// - ``UnregisterRemoteNotificationsAction`` + @_disfavoredOverload + @available(*, deprecated, message: "Please migrate to the new SpeziNotifications package: https://github.com/StanfordSpezi/SpeziNotifications") public var unregisterRemoteNotifications: UnregisterRemoteNotificationsAction { UnregisterRemoteNotificationsAction() } diff --git a/Sources/Spezi/Spezi/SpeziNotificationCenterDelegate.swift b/Sources/Spezi/Notifications/SpeziNotificationCenterDelegate.swift similarity index 85% rename from Sources/Spezi/Spezi/SpeziNotificationCenterDelegate.swift rename to Sources/Spezi/Notifications/SpeziNotificationCenterDelegate.swift index d23464e3..bc62e5d7 100644 --- a/Sources/Spezi/Spezi/SpeziNotificationCenterDelegate.swift +++ b/Sources/Spezi/Notifications/SpeziNotificationCenterDelegate.swift @@ -20,10 +20,7 @@ class SpeziNotificationCenterDelegate: NSObject { // The completion handler would also be called on a background thread which results in a crash. // Declaring the method as @MainActor requires a @preconcurrency inheritance from the delegate to silence Sendable warnings. - await withTaskGroup(of: Void.self) { @MainActor group in - // Moving this inside here (@MainActor isolated task group body) helps us avoid making the whole delegate method @MainActor. - // Apparently having the non-Sendable `UNNotificationResponse` as a parameter to a @MainActor annotated method doesn't suppress - // the warning with @preconcurrency, but capturing `response` in a @MainActor isolated closure does. + await withTaskGroup(of: Void.self) { group in guard let delegate = SpeziAppDelegate.appDelegate else { return } @@ -44,8 +41,7 @@ class SpeziNotificationCenterDelegate: NSObject { _ center: UNUserNotificationCenter, willPresent notification: UNNotification ) async -> UNNotificationPresentationOptions { - await withTaskGroup(of: UNNotificationPresentationOptions?.self) { @MainActor group in - // See comment in method above. + await withTaskGroup(of: UNNotificationPresentationOptions?.self) { group in guard let delegate = SpeziAppDelegate.appDelegate else { return [] } diff --git a/Sources/Spezi/Spezi.docc/Interactions with Application.md b/Sources/Spezi/Spezi.docc/Interactions with Application.md new file mode 100644 index 00000000..ebaa68b9 --- /dev/null +++ b/Sources/Spezi/Spezi.docc/Interactions with Application.md @@ -0,0 +1,42 @@ +# Interactions with Application + +Interact with the Application. + + + +## Overview + +Spezi provides platform-agnostic mechanisms to interact with your application instance. +To access application properties or actions you can use the ``Module/Application`` property wrapper within your +``Module`` or ``Standard``. + +> Tip: The articles illustrates how you can easily manage user notifications within your Spezi application. + +## Topics + +### Application Interaction + +- ``Module/Application`` + +### Properties + +- ``Spezi/logger`` +- ``Spezi/launchOptions`` + +### Notifications + +- ``Spezi/registerRemoteNotifications`` +- ``Spezi/unregisterRemoteNotifications`` + +### Platform-agnostic type-aliases + +- ``ApplicationDelegateAdaptor`` +- ``BackgroundFetchResult`` diff --git a/Sources/Spezi/Spezi.docc/Module/Interactions with Application.md b/Sources/Spezi/Spezi.docc/Module/Interactions with Application.md index be9b9de4..ebaa68b9 100644 --- a/Sources/Spezi/Spezi.docc/Module/Interactions with Application.md +++ b/Sources/Spezi/Spezi.docc/Module/Interactions with Application.md @@ -31,7 +31,7 @@ To access application properties or actions you can use the ``Module/Application - ``Spezi/logger`` - ``Spezi/launchOptions`` -### Actions +### Notifications - ``Spezi/registerRemoteNotifications`` - ``Spezi/unregisterRemoteNotifications`` diff --git a/Sources/Spezi/Spezi.docc/Module/Notifications.md b/Sources/Spezi/Spezi.docc/Module/Notifications.md index 0dd3d0e0..ecabc70e 100644 --- a/Sources/Spezi/Spezi.docc/Module/Notifications.md +++ b/Sources/Spezi/Spezi.docc/Module/Notifications.md @@ -60,9 +60,10 @@ implement the ``NotificationHandler/receiveRemoteNotification(_:)`` method. ### Notifications - ``NotificationHandler`` -- ``NotificationTokenHandler`` -### Remote Notification Registration +### Apple Push Notification Service +- ``NotificationTokenHandler`` - ``Spezi/registerRemoteNotifications`` - ``Spezi/unregisterRemoteNotifications`` + diff --git a/Sources/Spezi/Spezi.docc/Spezi.md b/Sources/Spezi/Spezi.docc/Spezi.md index 66116d71..1b4b90cd 100644 --- a/Sources/Spezi/Spezi.docc/Spezi.md +++ b/Sources/Spezi/Spezi.docc/Spezi.md @@ -92,7 +92,7 @@ You can learn more about modules in the documentation. - - ``SpeziAppDelegate`` - ``Configuration`` -- ``SwiftUI/View/spezi(_:)-3bn89`` +- ``SwiftUICore/View/spezi(_:)-3bn89`` ### Essential Concepts @@ -102,8 +102,8 @@ You can learn more about modules in the documentation. ### Previews -- ``SwiftUI/View/previewWith(standard:simulateLifecycle:_:)`` -- ``SwiftUI/View/previewWith(simulateLifecycle:_:)`` +- ``SwiftUICore/View/previewWith(standard:simulateLifecycle:_:)`` +- ``SwiftUICore/View/previewWith(simulateLifecycle:_:)`` - ``Foundation/ProcessInfo/isPreviewSimulator`` - ``LifecycleSimulationOptions`` diff --git a/Sources/Spezi/Spezi/Spezi+Preview.swift b/Sources/Spezi/Spezi/Spezi+Preview.swift index 69a81698..c28d6456 100644 --- a/Sources/Spezi/Spezi/Spezi+Preview.swift +++ b/Sources/Spezi/Spezi/Spezi+Preview.swift @@ -11,6 +11,17 @@ import SwiftUI import XCTRuntimeAssertions +#if os(iOS) || os(visionOS) || os(tvOS) +/// Protocol used to silence deprecation warnings. +@_spi(Internal) +public protocol DeprecatedLaunchOptionsCall { + /// Forward to legacy lifecycle handlers. + @MainActor + func callWillFinishLaunching(_ application: UIApplication, launchOptions: [UIApplication.LaunchOptionsKey: Any]) +} +#endif + + /// Options to simulate behavior for a ``LifecycleHandler`` in cases where there is no app delegate like in Preview setups. @MainActor public enum LifecycleSimulationOptions { @@ -31,6 +42,17 @@ public enum LifecycleSimulationOptions { } +#if os(iOS) || os(visionOS) || os(tvOS) +@_spi(Internal) +extension Spezi: DeprecatedLaunchOptionsCall { + @available(*, deprecated, message: "Propagate deprecation warning.") + public func callWillFinishLaunching(_ application: UIApplication, launchOptions: [UIApplication.LaunchOptionsKey: Any]) { + lifecycleHandler.willFinishLaunchingWithOptions(application, launchOptions: launchOptions) + } +} +#endif + + extension View { /// Configure Spezi for your previews using a Standard and a collection of Modules. /// @@ -62,13 +84,13 @@ extension View { } let spezi = Spezi(standard: standard, modules: modules().elements, storage: storage) - let lifecycleHandlers = spezi.lifecycleHandler return modifier(SpeziViewModifier(spezi)) #if os(iOS) || os(visionOS) || os(tvOS) .task { @MainActor in if case let .launchWithOptions(options) = simulateLifecycle { - lifecycleHandlers.willFinishLaunchingWithOptions(UIApplication.shared, launchOptions: options) + (spezi as DeprecatedLaunchOptionsCall) + .callWillFinishLaunching(UIApplication.shared, launchOptions: options) } } #endif diff --git a/Sources/Spezi/Spezi/Spezi.swift b/Sources/Spezi/Spezi/Spezi.swift index 22f01052..2e90aa50 100644 --- a/Sources/Spezi/Spezi/Spezi.swift +++ b/Sources/Spezi/Spezi/Spezi.swift @@ -90,8 +90,6 @@ import XCTRuntimeAssertions public final class Spezi: Sendable { static let logger = Logger(subsystem: "edu.stanford.spezi", category: "Spezi") - @TaskLocal static var moduleInitContext: ModuleDescription? - let standard: any Standard /// A shared repository to store any `KnowledgeSource`s restricted to the ``SpeziAnchor``. @@ -188,9 +186,13 @@ public final class Spezi: Sendable { self.standard = standard self.storage = consume storage - self.loadModules(modules, ownership: .spezi) - // load standard separately, such that all module loading takes precedence - self.loadModule(standard, ownership: .spezi) + do { + try self.loadModules(modules, ownership: .spezi) + // load standard separately, such that all module loading takes precedence + try self.loadModules([standard], ownership: .spezi) + } catch { + preconditionFailure(error.description) + } } /// Load a new Module. @@ -211,11 +213,15 @@ public final class Spezi: Sendable { /// - ``ModuleOwnership`` @MainActor public func loadModule(_ module: any Module, ownership: ModuleOwnership = .spezi) { - loadModules([module], ownership: ownership) + do { + try loadModules([module], ownership: ownership) + } catch { + preconditionFailure(error.description) + } } @MainActor - private func loadModules(_ modules: [any Module], ownership: ModuleOwnership) { + func loadModules(_ modules: [any Module], ownership: ModuleOwnership) throws(SpeziModuleError) { precondition(Self.moduleInitContext == nil, "Modules cannot be loaded within the `configure()` method.") purgeWeaklyReferenced() @@ -227,13 +233,17 @@ public final class Spezi: Sendable { let existingModules = self.modules let dependencyManager = DependencyManager(modules, existing: existingModules) - dependencyManager.resolve() + do { + try dependencyManager.resolve() + } catch { + throw .dependency(error) + } implicitlyCreatedModules.formUnion(dependencyManager.implicitlyCreatedModules) // we pass through the whole list of modules once to collect all @Provide values for module in dependencyManager.initializedModules { - Self.$moduleInitContext.withValue(module.moduleDescription) { + withModuleInitContext(module.moduleDescription) { module.collectModuleValues(into: &storage) } } @@ -241,9 +251,9 @@ public final class Spezi: Sendable { for module in dependencyManager.initializedModules { if requestedModules.contains(ModuleReference(module)) { // the policy only applies to the requested modules, all other are always managed and owned by Spezi - self.initModule(module, ownership: ownership) + try self.initModule(module, ownership: ownership) } else { - self.initModule(module, ownership: .spezi) + try self.initModule(module, ownership: .spezi) } } @@ -265,6 +275,15 @@ public final class Spezi: Sendable { /// - Parameter module: The Module to unload. @MainActor public func unloadModule(_ module: any Module) { + do { + try _unloadModule(module) + } catch { + preconditionFailure(error.description) + } + } + + @MainActor + func _unloadModule(_ module: any Module) throws(SpeziModuleError) { // swiftlint:disable:this identifier_name precondition(Self.moduleInitContext == nil, "Modules cannot be unloaded within the `configure()` method.") purgeWeaklyReferenced() @@ -276,7 +295,9 @@ public final class Spezi: Sendable { logger.debug("Unloading module \(type(of: module)) ...") let dependents = retrieveDependingModules(module.dependencyReference, considerOptionals: false) - precondition(dependents.isEmpty, "Tried to unload Module \(type(of: module)) that is still required by peer Modules: \(dependents)") + if !dependents.isEmpty { + throw SpeziModuleError.moduleStillRequired(module: "\(type(of: module))", dependents: dependents.map { "\(type(of: $0))" }) + } module.clearModule(from: self) @@ -296,7 +317,11 @@ public final class Spezi: Sendable { // re-injecting all dependencies ensures that the unloaded module is cleared from optional Dependencies from // pre-existing Modules. let dependencyManager = DependencyManager([], existing: modules) - dependencyManager.resolve() + do { + try dependencyManager.resolve() + } catch { + preconditionFailure("Internal inconsistency. Repeated dependency resolve resulted in error: \(error)") + } module.clear() // automatically removes @Provide values and recursively unloads implicitly created modules } @@ -309,38 +334,42 @@ public final class Spezi: Sendable { /// - module: The module to initialize. /// - ownership: Define the type of ownership when loading the module. @MainActor - private func initModule(_ module: any Module, ownership: ModuleOwnership) { + private func initModule(_ module: any Module, ownership: ModuleOwnership) throws(SpeziModuleError) { precondition(!module.isLoaded(in: self), "Tried to initialize Module \(type(of: module)) that was already loaded!") - Self.$moduleInitContext.withValue(module.moduleDescription) { - module.inject(spezi: self) + do { + try withModuleInitContext(module.moduleDescription) { () throws(SpeziPropertyError) in + try module.inject(spezi: self) - // supply modules values to all @Collect - module.injectModuleValues(from: storage) + // supply modules values to all @Collect + module.injectModuleValues(from: storage) - module.configure() + module.configure() - switch ownership { - case .spezi: - module.storeModule(into: self) - case .external: - module.storeWeakly(into: self) - } + switch ownership { + case .spezi: + module.storeModule(into: self) + case .external: + module.storeWeakly(into: self) + } - // If a module is @Observable, we automatically inject it view the `ModelModifier` into the environment. - if let observable = module as? EnvironmentAccessible { - // we can't guarantee weak references for EnvironmentAccessible modules - precondition(ownership != .external, "Modules loaded with self-managed policy cannot conform to `EnvironmentAccessible`.") - _viewModifiers[ModuleReference(module)] = observable.viewModifier - } + // If a module is @Observable, we automatically inject it view the `ModelModifier` into the environment. + if let observable = module as? EnvironmentAccessible { + // we can't guarantee weak references for EnvironmentAccessible modules + precondition(ownership != .external, "Modules loaded with self-managed policy cannot conform to `EnvironmentAccessible`.") + _viewModifiers[ModuleReference(module)] = observable.viewModifier + } - let modifierEntires: [(id: UUID, modifier: any ViewModifier)] = module.viewModifierEntires - // this check is important. Change to viewModifiers re-renders the whole SwiftUI view hierarchy. So avoid to do it unnecessarily - if !modifierEntires.isEmpty { - for entry in modifierEntires.reversed() { // reversed, as we re-reverse things in the `viewModifier` getter - _viewModifiers.updateValue(entry.modifier, forKey: entry.id) + let modifierEntires: [(id: UUID, modifier: any ViewModifier)] = module.viewModifierEntires + // this check is important. Change to viewModifiers re-renders the whole SwiftUI view hierarchy. So avoid to do it unnecessarily + if !modifierEntires.isEmpty { + for entry in modifierEntires.reversed() { // reversed, as we re-reverse things in the `viewModifier` getter + _viewModifiers.updateValue(entry.modifier, forKey: entry.id) + } } } + } catch { + throw .property(error) } } @@ -475,3 +504,34 @@ extension Module { } } } + + +extension Spezi { + private static let initContextLock = NSLock() + private static nonisolated(unsafe) var _moduleInitContext: ModuleDescription? + + private(set) static var moduleInitContext: ModuleDescription? { + get { + initContextLock.withLock { + _moduleInitContext + } + } + set { + initContextLock.withLock { + _moduleInitContext = newValue + } + } + } + + @MainActor + func withModuleInitContext(_ context: ModuleDescription, perform action: () throws(E) -> Void) throws(E) { + Self.moduleInitContext = context + defer { + Self.moduleInitContext = nil + } + + try action() + } +} + +// swiftlint:disable:this file_length diff --git a/Sources/Spezi/Spezi/SpeziAppDelegate.swift b/Sources/Spezi/Spezi/SpeziAppDelegate.swift index a4c5a4f1..f000e1b0 100644 --- a/Sources/Spezi/Spezi/SpeziAppDelegate.swift +++ b/Sources/Spezi/Spezi/SpeziAppDelegate.swift @@ -53,7 +53,17 @@ open class SpeziAppDelegate: NSObject, ApplicationDelegate, Sendable { private(set) static weak var appDelegate: SpeziAppDelegate? static var notificationDelegate: SpeziNotificationCenterDelegate? // swiftlint:disable:this weak_delegate - private var _spezi: Spezi? + /// Access the Spezi instance. + /// + /// Use this property as a basis for creating your own APIs (e.g., providing SwiftUI Environment values that use information from Spezi). + /// To not make it directly available to the user. + @_spi(APISupport) + public static var spezi: Spezi? { + SpeziAppDelegate.appDelegate?._spezi + } + + private(set) var _spezi: Spezi? // swiftlint:disable:this identifier_name + var spezi: Spezi { guard let spezi = _spezi else { @@ -63,8 +73,8 @@ open class SpeziAppDelegate: NSObject, ApplicationDelegate, Sendable { } return spezi } - - + + /// Register your different ``Module``s (or more sophisticated ``Module``s) using the ``SpeziAppDelegate/configuration`` property,. /// /// The ``Standard`` acts as a central message broker in the application. @@ -142,7 +152,7 @@ open class SpeziAppDelegate: NSObject, ApplicationDelegate, Sendable { open func application(_ application: _Application, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { MainActor.assumeIsolated { // on macOS there is a missing MainActor annotation - RegisterRemoteNotificationsAction.handleDeviceTokenUpdate(spezi, deviceToken) + spezi.remoteNotificationRegistrationSupport.handleDeviceTokenUpdate(deviceToken) // notify all notification handlers of an updated token for handler in spezi.notificationTokenHandler { @@ -153,7 +163,7 @@ open class SpeziAppDelegate: NSObject, ApplicationDelegate, Sendable { open func application(_ application: _Application, didFailToRegisterForRemoteNotificationsWithError error: Error) { MainActor.assumeIsolated { // on macOS there is a missing MainActor annotation - RegisterRemoteNotificationsAction.handleFailedRegistration(spezi, error) + spezi.remoteNotificationRegistrationSupport.handleFailedRegistration(error) } } diff --git a/Sources/Spezi/Spezi/SpeziModuleError.swift b/Sources/Spezi/Spezi/SpeziModuleError.swift new file mode 100644 index 00000000..936b5a1b --- /dev/null +++ b/Sources/Spezi/Spezi/SpeziModuleError.swift @@ -0,0 +1,26 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + + +enum SpeziModuleError: Error, CustomStringConvertible { + case dependency(DependencyManagerError) + case property(SpeziPropertyError) + + case moduleStillRequired(module: String, dependents: [String]) + + var description: String { + switch self { + case let .dependency(error): + error.description + case let .property(error): + error.description + case let .moduleStillRequired(module, dependents): + "Tried to unload Module \(type(of: module)) that is still required by peer Module(s): \(dependents.joined(separator: ", "))" + } + } +} diff --git a/Sources/Spezi/Spezi/SpeziPropertyWrapper.swift b/Sources/Spezi/Spezi/SpeziPropertyWrapper.swift index 5060448e..da944b48 100644 --- a/Sources/Spezi/Spezi/SpeziPropertyWrapper.swift +++ b/Sources/Spezi/Spezi/SpeziPropertyWrapper.swift @@ -7,6 +7,11 @@ // +enum SpeziPropertyError: Error { + case unsatisfiedStandardConstraint(constraint: String, standard: String) +} + + protocol SpeziPropertyWrapper { /// Inject the global Spezi instance. /// @@ -14,7 +19,7 @@ protocol SpeziPropertyWrapper { /// An empty default implementation is provided. /// - Parameter spezi: The global ``Spezi/Spezi`` instance. @MainActor - func inject(spezi: Spezi) + func inject(spezi: Spezi) throws(SpeziPropertyError) /// Clear the property wrapper state before the Module is unloaded. @MainActor @@ -29,9 +34,9 @@ extension SpeziPropertyWrapper { extension Module { @MainActor - func inject(spezi: Spezi) { + func inject(spezi: Spezi) throws(SpeziPropertyError) { for wrapper in retrieveProperties(ofType: SpeziPropertyWrapper.self) { - wrapper.inject(spezi: spezi) + try wrapper.inject(spezi: spezi) } } @@ -42,3 +47,32 @@ extension Module { } } } + + +extension SpeziPropertyError: CustomStringConvertible { + var description: String { + switch self { + case let .unsatisfiedStandardConstraint(constraint, standard): + """ + The `Standard` defined in the `Configuration` does not conform to \(constraint). + + Ensure that you define an appropriate standard in your configuration in your `SpeziAppDelegate` subclass ... + ``` + var configuration: Configuration { + Configuration(standard: \(standard)()) { + // ... + } + } + ``` + + ... and that your standard conforms to \(constraint): + + ```swift + actor \(standard): Standard, \(constraint) { + // ... + } + ``` + """ + } + } +} diff --git a/Sources/Spezi/Standard/StandardPropertyWrapper.swift b/Sources/Spezi/Standard/StandardPropertyWrapper.swift index 7b3669c0..eae08123 100644 --- a/Sources/Spezi/Standard/StandardPropertyWrapper.swift +++ b/Sources/Spezi/Standard/StandardPropertyWrapper.swift @@ -38,30 +38,12 @@ public class _StandardPropertyWrapper { extension _StandardPropertyWrapper: SpeziPropertyWrapper { - func inject(spezi: Spezi) { + func inject(spezi: Spezi) throws(SpeziPropertyError) { guard let standard = spezi.standard as? Constraint else { let standardType = type(of: spezi.standard) - preconditionFailure( - """ - The `Standard` defined in the `Configuration` does not conform to \(String(describing: Constraint.self)). - - Ensure that you define an appropriate standard in your configuration in your `SpeziAppDelegate` subclass ... - ``` - var configuration: Configuration { - Configuration(standard: \(String(describing: standardType))()) { - // ... - } - } - ``` - - ... and that your standard conforms to \(String(describing: Constraint.self)): - - ```swift - actor \(String(describing: standardType)): Standard, \(String(describing: Constraint.self)) { - // ... - } - ``` - """ + throw SpeziPropertyError.unsatisfiedStandardConstraint( + constraint: String(describing: Constraint.self), + standard: String(describing: standardType) ) } diff --git a/Sources/Spezi/Utilities/Application.swift b/Sources/Spezi/Utilities/Application+TypeAlias.swift similarity index 77% rename from Sources/Spezi/Utilities/Application.swift rename to Sources/Spezi/Utilities/Application+TypeAlias.swift index c2a6f0ea..158fafe6 100644 --- a/Sources/Spezi/Utilities/Application.swift +++ b/Sources/Spezi/Utilities/Application+TypeAlias.swift @@ -24,4 +24,12 @@ public typealias _Application = NSApplication // swiftlint:disable:this type_nam /// /// Type-alias for the `WKApplication`. public typealias _Application = WKApplication // swiftlint:disable:this type_name + +extension WKApplication { + /// Allow the same access pattern for WKApplication. Bridges to the `shared()` method. + @_documentation(visibility: internal) + public static var shared: WKApplication { + shared() + } +} #endif diff --git a/Sources/XCTSpezi/DependencyResolution.swift b/Sources/XCTSpezi/DependencyResolution.swift index 5e45f094..7f05206e 100644 --- a/Sources/XCTSpezi/DependencyResolution.swift +++ b/Sources/XCTSpezi/DependencyResolution.swift @@ -6,7 +6,7 @@ // SPDX-License-Identifier: MIT // -@_spi(Spezi) import Spezi +@_spi(Spezi) @_spi(Internal) import Spezi import SwiftUI @@ -34,7 +34,8 @@ public func withDependencyResolution( #if os(iOS) || os(visionOS) || os(tvOS) if case let .launchWithOptions(options) = simulateLifecycle { // maintain backwards compatibility - spezi.lifecycleHandler.willFinishLaunchingWithOptions(UIApplication.shared, launchOptions: options) + (spezi as DeprecatedLaunchOptionsCall) + .callWillFinishLaunching(UIApplication.shared, launchOptions: options) } #endif } diff --git a/Tests/SpeziTests/CapabilityTests/ModuleCommunicationTests.swift b/Tests/SpeziTests/CapabilityTests/ModuleCommunicationTests.swift index fc0968f9..98925d3a 100644 --- a/Tests/SpeziTests/CapabilityTests/ModuleCommunicationTests.swift +++ b/Tests/SpeziTests/CapabilityTests/ModuleCommunicationTests.swift @@ -55,7 +55,7 @@ final class ModuleCommunicationTests: XCTestCase { @MainActor - override func setUp() { + override func setUp() async throws { Self.provideModule = ProvideModule1() Self.collectModule = CollectModule() } @@ -69,19 +69,4 @@ final class ModuleCommunicationTests: XCTestCase { XCTAssertTrue(Self.collectModule.nothingProvided.isEmpty) XCTAssertEqual(Self.collectModule.strings, ["Hello World"]) } - - @MainActor - func testIllegalAccess() throws { - let delegate = TestApplicationDelegate() - - try XCTRuntimePrecondition { - _ = Self.collectModule.strings - } - - _ = delegate.spezi // ensure init - - try XCTRuntimePrecondition { - Self.provideModule.numMaybe2 = 12 - } - } } diff --git a/Tests/SpeziTests/CapabilityTests/NotificationsTests.swift b/Tests/SpeziTests/CapabilityTests/NotificationsTests.swift index ff8846b7..11e1de52 100644 --- a/Tests/SpeziTests/CapabilityTests/NotificationsTests.swift +++ b/Tests/SpeziTests/CapabilityTests/NotificationsTests.swift @@ -12,6 +12,7 @@ import UserNotifications import XCTest +@available(*, deprecated, message: "Forward decpreation warnings.") private final class TestNotificationHandler: Module, NotificationHandler, NotificationTokenHandler { @Application(\.registerRemoteNotifications) var registerRemoteNotifications @@ -75,6 +76,7 @@ private final class TestNotificationHandler: Module, NotificationHandler, Notifi private final class EmptyNotificationHandler: Module, NotificationHandler {} +@available(*, deprecated, message: "Forward depcreation warnings") private class TestNotificationApplicationDelegate: SpeziAppDelegate { private let injectedModule: TestNotificationHandler @@ -91,6 +93,7 @@ private class TestNotificationApplicationDelegate: SpeziAppDelegate { } +@available(*, deprecated, message: "Forward depcreation warnings") final class NotificationsTests: XCTestCase { @MainActor func testRegisterNotificationsSuccessful() async throws { diff --git a/Tests/SpeziTests/DependenciesTests/DependencyBuilderTests.swift b/Tests/SpeziTests/DependenciesTests/DependencyBuilderTests.swift index 734a5b00..9a5fa9de 100644 --- a/Tests/SpeziTests/DependenciesTests/DependencyBuilderTests.swift +++ b/Tests/SpeziTests/DependenciesTests/DependencyBuilderTests.swift @@ -61,7 +61,7 @@ final class DependencyBuilderTests: XCTestCase { let module = ExampleDependencyModule { ExampleDependentModule() } - let initializedModules = DependencyManager.resolve([module]) + let initializedModules = DependencyManager.resolveWithoutErrors([module]) XCTAssertEqual(initializedModules.count, 2) _ = try XCTUnwrap(initializedModules[0] as? ExampleDependentModule) _ = try XCTUnwrap(initializedModules[1] as? ExampleDependencyModule) diff --git a/Tests/SpeziTests/DependenciesTests/DependencyManager+OneShot.swift b/Tests/SpeziTests/DependenciesTests/DependencyManager+OneShot.swift index 127f3fd6..486757b5 100644 --- a/Tests/SpeziTests/DependenciesTests/DependencyManager+OneShot.swift +++ b/Tests/SpeziTests/DependenciesTests/DependencyManager+OneShot.swift @@ -7,12 +7,19 @@ // @testable import Spezi +import XCTest extension DependencyManager { - static func resolve(_ modules: [any Module]) -> [any Module] { + static func resolve(_ modules: [any Module]) throws -> [any Module] { let dependencyManager = DependencyManager(modules) - dependencyManager.resolve() + try dependencyManager.resolve() + return dependencyManager.initializedModules + } + + static func resolveWithoutErrors(_ modules: [any Module], file: StaticString = #filePath, line: UInt = #line) -> [any Module] { + let dependencyManager = DependencyManager(modules) + XCTAssertNoThrow(try dependencyManager.resolve(), file: file, line: line) return dependencyManager.initializedModules } } diff --git a/Tests/SpeziTests/DependenciesTests/DependencyTests.swift b/Tests/SpeziTests/DependenciesTests/DependencyTests.swift index 66974574..c2693b5d 100644 --- a/Tests/SpeziTests/DependenciesTests/DependencyTests.swift +++ b/Tests/SpeziTests/DependenciesTests/DependencyTests.swift @@ -245,9 +245,16 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le let module3 = TestModule3() let spezi = Spezi(standard: DefaultStandard(), modules: [TestModule1(), module3]) - try XCTRuntimePrecondition { - // cannot unload module that other modules still depend on - spezi.unloadModule(module3) + // cannot unload module that other modules still depend on + XCTAssertThrowsError(try spezi._unloadModule(module3)) { error in + guard let moduleError = error as? SpeziModuleError, + case let .moduleStillRequired(module, dependents) = moduleError else { + XCTFail("Received unexpected error: \(error)") + return + } + + XCTAssertEqual(module, "TestModule3") + XCTAssertEqual(Set(dependents), ["TestModule2", "TestModule1"]) } } @@ -367,7 +374,7 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le TestModule1(), TestModule7() ] - let initializedModules = DependencyManager.resolve(modules) + let initializedModules = DependencyManager.resolveWithoutErrors(modules) XCTAssertEqual(initializedModules.count, 7) @@ -395,7 +402,7 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le TestModule2(), TestModule5() ] - let initializedModules = DependencyManager.resolve(modules) + let initializedModules = DependencyManager.resolveWithoutErrors(modules) XCTAssertEqual(initializedModules.count, 4) @@ -417,7 +424,7 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le TestModule4(), TestModule4() ] - let initializedModules = DependencyManager.resolve(modules) + let initializedModules = DependencyManager.resolveWithoutErrors(modules) XCTAssertEqual(initializedModules.count, 3) @@ -437,7 +444,7 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le TestModule2(), TestModule2() ] - let initializedModules = DependencyManager.resolve(modules) + let initializedModules = DependencyManager.resolveWithoutErrors(modules) XCTAssertEqual(initializedModules.count, 5) @@ -466,7 +473,7 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le @MainActor func testModuleNoDependency() throws { let modules: [any Module] = [TestModule5()] - let initializedModules = DependencyManager.resolve(modules) + let initializedModules = DependencyManager.resolveWithoutErrors(modules) XCTAssertEqual(initializedModules.count, 1) @@ -480,7 +487,7 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le TestModule5(), TestModule5() ] - let initializedModules = DependencyManager.resolve(modules) + let initializedModules = DependencyManager.resolveWithoutErrors(modules) XCTAssertEqual(initializedModules.count, 3) @@ -495,7 +502,7 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le OptionalModuleDependency() ] - let modules = DependencyManager.resolve(nonPresent) + let modules = DependencyManager.resolveWithoutErrors(nonPresent) XCTAssertEqual(modules.count, 1) @@ -510,7 +517,7 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le TestModule3() ] - let modules = DependencyManager.resolve(nonPresent) + let modules = DependencyManager.resolveWithoutErrors(nonPresent) XCTAssertEqual(modules.count, 2) @@ -521,14 +528,14 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le @MainActor func testOptionalDependencyWithDynamicRuntimeDefaultValue() throws { - let nonPresent = DependencyManager.resolve([ + let nonPresent = DependencyManager.resolveWithoutErrors([ OptionalDependencyWithRuntimeDefault(defaultValue: nil) // stays optional ]) let dut1 = try XCTUnwrap(nonPresent[0] as? OptionalDependencyWithRuntimeDefault) XCTAssertNil(dut1.testModule3) - let configured = DependencyManager.resolve([ + let configured = DependencyManager.resolveWithoutErrors([ TestModule3(state: 1), OptionalDependencyWithRuntimeDefault(defaultValue: nil) ]) @@ -537,7 +544,7 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le let dut2Module = try XCTUnwrap(dut2.testModule3) XCTAssertEqual(dut2Module.state, 1) - let defaulted = DependencyManager.resolve([ + let defaulted = DependencyManager.resolveWithoutErrors([ OptionalDependencyWithRuntimeDefault(defaultValue: 2) ]) @@ -545,7 +552,7 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le let dut3Module = try XCTUnwrap(dut3.testModule3) XCTAssertEqual(dut3Module.state, 2) - let configuredAndDefaulted = DependencyManager.resolve([ + let configuredAndDefaulted = DependencyManager.resolveWithoutErrors([ TestModule3(state: 4), OptionalDependencyWithRuntimeDefault(defaultValue: 3) ]) @@ -645,8 +652,16 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le let module2 = TestModuleCircle2() let module1 = TestModuleCircle1(module: module2) - try XCTRuntimePrecondition { - _ = DependencyManager.resolve([module1]) + XCTAssertThrowsError(try DependencyManager.resolve([module1])) { error in + guard let dependencyError = error as? DependencyManagerError, + case let .searchStackCycle(module, requestedModule, dependencyChain) = dependencyError else { + XCTFail("Received unexpected error: \(error)") + return + } + + XCTAssertEqual(module, "TestModuleCircle2") + XCTAssertEqual(requestedModule, "TestModuleCircle1") + XCTAssertEqual(dependencyChain, ["TestModuleCircle1", "TestModuleCircle2"]) } } @@ -656,8 +671,16 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le @Dependency(TestModuleX.self) var module } - try XCTRuntimePrecondition { - _ = DependencyManager.resolve([Module1()]) + XCTAssertThrowsError(try DependencyManager.resolve([Module1()])) { error in + guard let dependencyError = error as? DependencyManagerError, + case let .missingRequiredModule(module, requiredModule) = dependencyError else { + XCTFail("Received unexpected error: \(error)") + return + } + + print(error) + XCTAssertEqual(module, "Module1") + XCTAssertEqual(requiredModule, "TestModuleX") } } diff --git a/Tests/SpeziTests/DependenciesTests/DynamicDependenciesTests.swift b/Tests/SpeziTests/DependenciesTests/DynamicDependenciesTests.swift index 3ee6bee4..e5d5f45a 100644 --- a/Tests/SpeziTests/DependenciesTests/DynamicDependenciesTests.swift +++ b/Tests/SpeziTests/DependenciesTests/DynamicDependenciesTests.swift @@ -96,7 +96,7 @@ final class DynamicDependenciesTests: XCTestCase { TestModule1(dynamicDependenciesTestCase) ] - let initializedModules = DependencyManager.resolve(modules) + let initializedModules = DependencyManager.resolveWithoutErrors(modules) XCTAssertEqual(initializedModules.count, dynamicDependenciesTestCase.expectedNumberOfModules) try initializedModules.moduleOfType(TestModule1.self).evaluateExpectations() diff --git a/Tests/SpeziTests/ModuleTests/ModuleBuilderTests.swift b/Tests/SpeziTests/ModuleTests/ModuleBuilderTests.swift index 890065d5..c76a6d67 100644 --- a/Tests/SpeziTests/ModuleTests/ModuleBuilderTests.swift +++ b/Tests/SpeziTests/ModuleTests/ModuleBuilderTests.swift @@ -90,9 +90,7 @@ final class ModuleBuilderTests: XCTestCase { expectations: expectations ) - let dependencyManager = DependencyManager(modules.elements) - dependencyManager.resolve() - for module in dependencyManager.initializedModules { + for module in DependencyManager.resolveWithoutErrors(modules.elements) { module.configure() } @@ -111,10 +109,8 @@ final class ModuleBuilderTests: XCTestCase { condition: false, expectations: expectations ) - - let dependencyManager = DependencyManager(modules.elements) - dependencyManager.resolve() - for module in dependencyManager.initializedModules { + + for module in DependencyManager.resolveWithoutErrors(modules.elements) { module.configure() } diff --git a/Tests/SpeziTests/ModuleTests/ModuleTests.swift b/Tests/SpeziTests/ModuleTests/ModuleTests.swift index 4772c039..16802d50 100644 --- a/Tests/SpeziTests/ModuleTests/ModuleTests.swift +++ b/Tests/SpeziTests/ModuleTests/ModuleTests.swift @@ -72,16 +72,6 @@ final class ModuleTests: XCTestCase { unsetenv(ProcessInfo.xcodeRunningForPreviewKey) } - @MainActor - func testPreviewModifierOnlyWithinPreview() throws { - try XCTRuntimePrecondition { - _ = Text("Spezi") - .previewWith { - TestModule() - } - } - } - @MainActor func testModuleCreation() { let expectation = XCTestExpectation(description: "DependingTestModule") diff --git a/Tests/SpeziTests/StandardTests/StandardUnfulfilledConstraintTests.swift b/Tests/SpeziTests/StandardTests/StandardUnfulfilledConstraintTests.swift index cd2d3613..f6ef4bbe 100644 --- a/Tests/SpeziTests/StandardTests/StandardUnfulfilledConstraintTests.swift +++ b/Tests/SpeziTests/StandardTests/StandardUnfulfilledConstraintTests.swift @@ -29,20 +29,21 @@ final class StandardUnfulfilledConstraintTests: XCTestCase { } } - class StandardUCTestApplicationDelegate: SpeziAppDelegate { - override var configuration: Configuration { - Configuration(standard: MockStandard()) { - StandardUCTestModule() - } - } - } - @MainActor func testStandardUnfulfilledConstraint() throws { - let standardCUTestApplicationDelegate = StandardUCTestApplicationDelegate() - try XCTRuntimePrecondition(timeout: 0.5) { - _ = standardCUTestApplicationDelegate.spezi + let configuration = Configuration(standard: MockStandard()) {} + let spezi = Spezi(from: configuration) + + XCTAssertThrowsError(try spezi.loadModules([StandardUCTestModule()], ownership: .spezi)) { error in + guard let moduleError = error as? SpeziModuleError, + case let .property(propertyError) = moduleError, + case let .unsatisfiedStandardConstraint(constraint, standard) = propertyError else { + XCTFail("Encountered unexpected error: \(error)") + return + } + XCTAssertEqual(constraint, "UnfulfilledExampleConstraint") + XCTAssertEqual(standard, "MockStandard") } } } diff --git a/Tests/UITests/TestApp/LifecycleHandler/LifecycleHandlerTestModule.swift b/Tests/UITests/TestApp/LifecycleHandler/LifecycleHandlerTestModule.swift index b3ad0b9e..83ad166c 100644 --- a/Tests/UITests/TestApp/LifecycleHandler/LifecycleHandlerTestModule.swift +++ b/Tests/UITests/TestApp/LifecycleHandler/LifecycleHandlerTestModule.swift @@ -43,7 +43,7 @@ struct LifecycleHandlerModifier: ViewModifier { } -final class LifecycleHandlerTestModule: Module, LifecycleHandler { +final class LifecycleHandlerTestModule: Module { private let model: LifecycleHandlerModel @Modifier var modifier: LifecycleHandlerModifier @@ -99,3 +99,7 @@ final class LifecycleHandlerTestModule: Module, LifecycleHandler { } #endif } + + +@available(*, deprecated, message: "Propagate deprecation warning.") +extension LifecycleHandlerTestModule: LifecycleHandler {} diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index bba55d60..7253e12c 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 77; objects = { /* Begin PBXBuildFile section */ @@ -245,7 +245,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1410; - LastUpgradeCheck = 1540; + LastUpgradeCheck = 1600; TargetAttributes = { 2F6D139128F5F384007C25D6 = { CreatedOnToolsVersion = 14.1; @@ -257,7 +257,6 @@ }; }; buildConfigurationList = 2F6D138D28F5F384007C25D6 /* Build configuration list for PBXProject "UITests" */; - compatibilityVersion = "Xcode 14.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -268,6 +267,7 @@ packageReferences = ( 2F746D9D29962B2A00BF54FE /* XCRemoteSwiftPackageReference "XCTestExtensions" */, ); + preferredProjectObjectVersion = 77; productRefGroup = 2F6D139328F5F384007C25D6 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -400,6 +400,7 @@ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; TVOS_DEPLOYMENT_TARGET = 17.0; XROS_DEPLOYMENT_TARGET = 1.0; }; @@ -460,6 +461,7 @@ SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_STRICT_CONCURRENCY = complete; + SWIFT_VERSION = 6.0; TVOS_DEPLOYMENT_TARGET = 17.0; VALIDATE_PRODUCT = YES; XROS_DEPLOYMENT_TARGET = 1.0; @@ -543,7 +545,6 @@ 2F6D13BD28F5F386007C25D6 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 637867499T; @@ -568,7 +569,6 @@ 2F6D13BE28F5F386007C25D6 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 637867499T; diff --git a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme index eb317413..de592e4c 100644 --- a/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme +++ b/Tests/UITests/UITests.xcodeproj/xcshareddata/xcschemes/TestApp.xcscheme @@ -1,6 +1,6 @@