diff --git a/Sources/Spezi/Capabilities/ApplicationPropertyWrapper.swift b/Sources/Spezi/Capabilities/ApplicationPropertyWrapper.swift new file mode 100644 index 00000000..d8408252 --- /dev/null +++ b/Sources/Spezi/Capabilities/ApplicationPropertyWrapper.swift @@ -0,0 +1,39 @@ +// +// 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 +// + + +@propertyWrapper +public class _ApplicationPropertyWrapper { // swiftlint:disable:this type_name + private let keyPath: KeyPath + + private weak var spezi: Spezi? + + + public var wrappedValue: Value { + guard let spezi else { + preconditionFailure("Underlying Spezi instance was not yet injected. @Application cannot be accessed within the initializer!") + } + return spezi[keyPath: keyPath] + } + + public init(_ keyPath: KeyPath) { + self.keyPath = keyPath + } +} + + +extension _ApplicationPropertyWrapper: SpeziPropertyWrapper { + func inject(spezi: Spezi) { + self.spezi = spezi + } +} + + +extension Module { + public typealias Application = _ApplicationPropertyWrapper +} diff --git a/Sources/Spezi/Capabilities/Lifecycle/LifecycleHandler.swift b/Sources/Spezi/Capabilities/Lifecycle/LifecycleHandler.swift index c854ccdd..49f9a4d2 100644 --- a/Sources/Spezi/Capabilities/Lifecycle/LifecycleHandler.swift +++ b/Sources/Spezi/Capabilities/Lifecycle/LifecycleHandler.swift @@ -10,15 +10,6 @@ import os import SwiftUI -// TODO: Docs -#if os(iOS) || os(visionOS) || os(tvOS) -public typealias LaunchOptionsKey = UIApplication.LaunchOptionsKey -#else -// Launch options are not part of WKApplicationDelegate or NSApplicationDelegate -public typealias LaunchOptionsKey = Never -#endif - - /// Delegate methods are related to the `UIApplication` and ``Spezi/Spezi`` lifecycle. /// /// Conform to the `LifecycleHandler` protocol to get updates about the application lifecycle similar to the `UIApplicationDelegate` on an app basis. @@ -28,7 +19,16 @@ public typealias LaunchOptionsKey = Never /// - ``LifecycleHandler/applicationWillTerminate(_:)-35fxv`` /// /// All methods supported by the module capability are listed blow. +@available( + *, + deprecated, + message: """ + Please use the new @Application property wrapper to access delegate functionality. \ + Otherwise use the SwiftUI onReceive(_:perform:) for UI related notifications. + """ +) public protocol LifecycleHandler { +#if os(iOS) || os(visionOS) || os(tvOS) /// Replicates the `application(_: UIApplication, willFinishLaunchingWithOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool` /// functionality of the `UIApplicationDelegate`. /// @@ -36,103 +36,232 @@ public protocol LifecycleHandler { /// - Parameters: /// - application: The singleton app object. /// - launchOptions: A dictionary indicating the reason the app was launched (if any). The contents of this dictionary may be empty in situations where the user launched the app directly. For information about the possible keys in this dictionary and how to handle them, see UIApplication.LaunchOptionsKey. - func willFinishLaunchingWithOptions(launchOptions: [LaunchOptionsKey: Any]) - + @available( + *, + deprecated, + message: """ + Please use the new @Application(\\.launchOptions) property wrapper within your Module \ + to access launchOptions in a platform independent way. + """ + ) + func willFinishLaunchingWithOptions(_ application: UIApplication, launchOptions: [UIApplication.LaunchOptionsKey: Any]) + /// Replicates the `sceneWillEnterForeground(_: UIScene)` functionality of the `UISceneDelegate`. /// /// Tells the delegate that the scene is about to begin running in the foreground and become visible to the user. + /// + /// - Important: This method is deprecated. This method is only called on iOS and not supported on other platforms. + /// + /// /// - Parameter scene: The scene that is about to enter the foreground. - func sceneWillEnterForeground() - + @available( + *, + deprecated, + message: """ + Using UISceneDelegate is deprecated. \ + Use the SwiftUI onReceive(_:perform:) modifier with the UIScene.willEnterForegroundNotification publisher on iOS \ + or other platform-specific mechanisms as a replacement. + """ + ) + func sceneWillEnterForeground(_ scene: UIScene) + /// Replicates the `sceneDidBecomeActive(_: UIScene)` functionality of the `UISceneDelegate`. /// /// Tells the delegate that the scene became active and is now responding to user events. /// - Parameter scene: The scene that became active and is now responding to user events. - func sceneDidBecomeActive() + @available( + *, + deprecated, + message: """ + Using UISceneDelegate is deprecated. \ + Use the SwiftUI onReceive(_:perform:) modifier with the UIScene.didActivateNotification publisher on iOS \ + or other platform-specific mechanisms as a replacement. + """ + ) + func sceneDidBecomeActive(_ scene: UIScene) /// Replicates the `sceneWillResignActive(_: UIScene)` functionality of the `UISceneDelegate`. /// /// Tells the delegate that the scene is about to resign the active state and stop responding to user events. /// - Parameter scene: The scene that is about to stop responding to user events. - func sceneWillResignActive() - + @available( + *, + deprecated, + message: """ + Using UISceneDelegate is deprecated. \ + Use the SwiftUI onReceive(_:perform:) modifier with the UIScene.willDeactivateNotification publisher on iOS \ + or other platform-specific mechanisms as a replacement. + """ + ) + func sceneWillResignActive(_ scene: UIScene) + /// Replicates the `sceneDidEnterBackground(_: UIScene)` functionality of the `UISceneDelegate`. /// /// Tells the delegate that the scene is running in the background and is no longer onscreen. /// - Parameter scene: The scene that entered the background. - func sceneDidEnterBackground() - + @available( + *, + deprecated, + message: """ + Using UISceneDelegate is deprecated. \ + Use the SwiftUI onReceive(_:perform:) modifier with the UIScene.didEnterBackgroundNotification publisher on iOS \ + or other platform-specific mechanisms as a replacement. + """ + ) + func sceneDidEnterBackground(_ scene: UIScene) + /// Replicates the `applicationWillTerminate(_: UIApplication)` functionality of the `UIApplicationDelegate`. /// /// Tells the delegate when the app is about to terminate. /// - Parameter application: Your singleton app object. - func applicationWillTerminate() - - // TODO: update ALL docs + @available( + *, + deprecated, + message: """ + Using UISceneDelegate is deprecated. \ + Use the SwiftUI onReceive(_:perform:) modifier with the UIApplication.willTerminateNotification publisher on iOS \ + or other platform-specific mechanisms as a replacement. + """ + ) + func applicationWillTerminate(_ application: UIApplication) +#endif } +@available( + *, + deprecated, + message: """ + Please use the new @Application property wrapper to access delegate functionality. \ + Otherwise use the SwiftUI onReceive(_:perform:) for UI related notifications. + """ +) extension LifecycleHandler { +#if os(iOS) || os(visionOS) || os(tvOS) // A documentation for this method exists in the `LifecycleHandler` type which SwiftLint doesn't recognize. // swiftlint:disable:next missing_docs - public func willFinishLaunchingWithOptions(launchOptions: [LaunchOptionsKey: Any]) {} + public func willFinishLaunchingWithOptions(_ application: UIApplication, launchOptions: [UIApplication.LaunchOptionsKey: Any]) {} // A documentation for this method exists in the `LifecycleHandler` type which SwiftLint doesn't recognize. // swiftlint:disable:next missing_docs - public func sceneWillEnterForeground() { } - + public func sceneWillEnterForeground(_ scene: UIScene) { } + // A documentation for this method exists in the `LifecycleHandler` type which SwiftLint doesn't recognize. // swiftlint:disable:next missing_docs - public func sceneDidBecomeActive() { } - + public func sceneDidBecomeActive(_ scene: UIScene) { } + // A documentation for this method exists in the `LifecycleHandler` type which SwiftLint doesn't recognize. // swiftlint:disable:next missing_docs - public func sceneWillResignActive() { } - + public func sceneWillResignActive(_ scene: UIScene) { } + // A documentation for this method exists in the `LifecycleHandler` type which SwiftLint doesn't recognize. // swiftlint:disable:next missing_docs - public func sceneDidEnterBackground() { } - + public func sceneDidEnterBackground(_ scene: UIScene) { } + // A documentation for this method exists in the `LifecycleHandler` type which SwiftLint doesn't recognize. // swiftlint:disable:next missing_docs - public func applicationWillTerminate() { } + public func applicationWillTerminate(_ application: UIApplication) { } +#endif } +@available( + *, + deprecated, + message: """ + Please use the new @Application property wrapper to access delegate functionality. \ + Otherwise use the SwiftUI onReceive(_:perform:) for UI related notifications. + """ +) extension Array: LifecycleHandler where Element == LifecycleHandler { - public func willFinishLaunchingWithOptions(launchOptions: [LaunchOptionsKey: Any]) { +#if os(iOS) || os(visionOS) || os(tvOS) + @available( + *, + deprecated, + message: """ + Please use the new @Application(\\.launchOptions) property wrapper within your Module \ + to access launchOptions in a platform independent way. + """ + ) + public func willFinishLaunchingWithOptions(_ application: UIApplication, launchOptions: [UIApplication.LaunchOptionsKey: Any]) { for lifecycleHandler in self { - lifecycleHandler.willFinishLaunchingWithOptions(launchOptions: launchOptions) + lifecycleHandler.willFinishLaunchingWithOptions(application, launchOptions: launchOptions) } } - - public func sceneWillEnterForeground() { + + @available( + *, + deprecated, + message: """ + Using UISceneDelegate is deprecated. \ + Use the SwiftUI onReceive(_:perform:) modifier with the UIScene.willEnterForegroundNotification publisher on iOS \ + or other platform-specific mechanisms as a replacement. + """ + ) + public func sceneWillEnterForeground(_ scene: UIScene) { for lifecycleHandler in self { - lifecycleHandler.sceneWillEnterForeground() + lifecycleHandler.sceneWillEnterForeground(scene) } } - public func sceneDidBecomeActive() { + @available( + *, + deprecated, + message: """ + Using UISceneDelegate is deprecated. \ + Use the SwiftUI onReceive(_:perform:) modifier with the UIScene.didActivateNotification publisher on iOS \ + or other platform-specific mechanisms as a replacement. + """ + ) + public func sceneDidBecomeActive(_ scene: UIScene) { for lifecycleHandler in self { - lifecycleHandler.sceneDidBecomeActive() + lifecycleHandler.sceneDidBecomeActive(scene) } } - public func sceneWillResignActive() { + @available( + *, + deprecated, + message: """ + Using UISceneDelegate is deprecated. \ + Use the SwiftUI onReceive(_:perform:) modifier with the UIScene.willDeactivateNotification publisher on iOS \ + or other platform-specific mechanisms as a replacement. + """ + ) + public func sceneWillResignActive(_ scene: UIScene) { for lifecycleHandler in self { - lifecycleHandler.sceneWillResignActive() + lifecycleHandler.sceneWillResignActive(scene) } } - public func sceneDidEnterBackground() { + @available( + *, + deprecated, + message: """ + Using UISceneDelegate is deprecated. \ + Use the SwiftUI onReceive(_:perform:) modifier with the UIScene.didEnterBackgroundNotification publisher on iOS \ + or other platform-specific mechanisms as a replacement. + """ + ) + public func sceneDidEnterBackground(_ scene: UIScene) { for lifecycleHandler in self { - lifecycleHandler.sceneDidEnterBackground() + lifecycleHandler.sceneDidEnterBackground(scene) } } - - public func applicationWillTerminate() { + + @available( + *, + deprecated, + message: """ + Using UISceneDelegate is deprecated. \ + Use the SwiftUI onReceive(_:perform:) modifier with the UIApplication.willTerminateNotification publisher on iOS \ + or other platform-specific mechanisms as a replacement. + """ + ) + public func applicationWillTerminate(_ application: UIApplication) { for lifecycleHandler in self { - lifecycleHandler.applicationWillTerminate() + lifecycleHandler.applicationWillTerminate(application) } } +#endif } diff --git a/Sources/Spezi/Capabilities/Lifecycle/Spezi+LifecycleHandlers.swift b/Sources/Spezi/Capabilities/Lifecycle/Spezi+LifecycleHandlers.swift deleted file mode 100644 index a99eeb09..00000000 --- a/Sources/Spezi/Capabilities/Lifecycle/Spezi+LifecycleHandlers.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// 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 -// - -import os -import SwiftUI - - -extension AnySpezi { - /// A collection of ``Spezi/Spezi`` `LifecycleHandler`s. - var lifecycleHandler: [LifecycleHandler] { - storage.collect(allOf: LifecycleHandler.self) - } - - - // MARK: LifecycleHandler Functions - func willFinishLaunchingWithOptions( - launchOptions: [LaunchOptionsKey: Any] - ) { - lifecycleHandler.willFinishLaunchingWithOptions(launchOptions: launchOptions) - } - - func sceneWillEnterForeground() { - lifecycleHandler.sceneWillEnterForeground() - } - - func sceneDidBecomeActive() { - lifecycleHandler.sceneDidBecomeActive() - } - - func sceneWillResignActive() { - lifecycleHandler.sceneWillResignActive() - } - - func sceneDidEnterBackground() { - lifecycleHandler.sceneDidEnterBackground() - } - - func applicationWillTerminate() { - lifecycleHandler.applicationWillTerminate() - } -} diff --git a/Sources/Spezi/Configuration/Configuration.swift b/Sources/Spezi/Configuration/Configuration.swift index 1601c57e..0562d7f6 100644 --- a/Sources/Spezi/Configuration/Configuration.swift +++ b/Sources/Spezi/Configuration/Configuration.swift @@ -48,7 +48,8 @@ /// - ``ModuleBuilder`` /// - ``ModuleCollection`` public struct Configuration { - let spezi: AnySpezi + let standard: any Standard + let modules: ModuleCollection /// A ``Configuration`` defines the ``Standard`` and ``Module``s that are used in a Spezi project. @@ -64,7 +65,8 @@ public struct Configuration { standard: S, @ModuleBuilder _ modules: () -> ModuleCollection ) { - self.spezi = Spezi(standard: standard, modules: modules().elements) + self.standard = standard + self.modules = modules() } @@ -78,6 +80,6 @@ public struct Configuration { public init( @ModuleBuilder _ modules: () -> ModuleCollection ) { - self.spezi = Spezi(standard: DefaultStandard(), modules: modules().elements) + self.init(standard: DefaultStandard(), modules) } } diff --git a/Sources/Spezi/Spezi/AnySpezi.swift b/Sources/Spezi/Spezi/AnySpezi.swift deleted file mode 100644 index 9a592719..00000000 --- a/Sources/Spezi/Spezi/AnySpezi.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// 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 -// - -import os -import SpeziFoundation -import SwiftUI - - -/// Type-erased version of a ``Spezi`` instance used internally in Spezi. -protocol AnySpezi { - /// A shared repository to store any ``KnowledgeSource``s restricted to the ``SpeziAnchor``. - var storage: SpeziStorage { get } - /// Logger used to log events in the ``Spezi/Spezi`` instance. - var logger: Logger { get } - /// Array of all SwiftUI `ViewModifiers` collected using ``_ModifierPropertyWrapper`` from the configured ``Module``s. - var viewModifiers: [any ViewModifier] { get } -} diff --git a/Sources/Spezi/Spezi/KnowledgeSources/LaunchOptionsKey.swift b/Sources/Spezi/Spezi/KnowledgeSources/LaunchOptionsKey.swift new file mode 100644 index 00000000..0d4eaafa --- /dev/null +++ b/Sources/Spezi/Spezi/KnowledgeSources/LaunchOptionsKey.swift @@ -0,0 +1,41 @@ +// +// 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 + +struct LaunchOptionsKey: DefaultProvidingKnowledgeSource { + typealias Anchor = SpeziAnchor + +#if os(iOS) || os(visionOS) || os(tvOS) + typealias Value = [UIApplication.LaunchOptionsKey: Any] +#elseif os(macOS) + typealias Value = [AnyHashable: Any] +#else // os(watchOS) + typealias Value = [Never: Any] +#endif + + static let defaultValue: Value = [:] +} + + +extension Spezi { +#if os(iOS) || os(visionOS) || os(tvOS) + public var launchOptions: [UIApplication.LaunchOptionsKey: Any] { + storage[LaunchOptionsKey.self] + } +#elseif os(macOS) + public var launchOptions: [AnyHashable: Any] { + storage[LaunchOptionsKey.self] + } +#else // os(watchOS) + public var launchOptions: [Never: Any] { + storage[LaunchOptionsKey.self] + } +#endif +} diff --git a/Sources/Spezi/Spezi/SpeziLogger.swift b/Sources/Spezi/Spezi/KnowledgeSources/SpeziLogger.swift similarity index 100% rename from Sources/Spezi/Spezi/SpeziLogger.swift rename to Sources/Spezi/Spezi/KnowledgeSources/SpeziLogger.swift diff --git a/Sources/Spezi/Spezi/Spezi+Preview.swift b/Sources/Spezi/Spezi/Spezi+Preview.swift index 4227ea6e..1e40c19f 100644 --- a/Sources/Spezi/Spezi/Spezi+Preview.swift +++ b/Sources/Spezi/Spezi/Spezi+Preview.swift @@ -15,9 +15,16 @@ import XCTRuntimeAssertions public enum LifecycleSimulationOptions { /// Simulation is disabled. case disabled - /// The ``LifecycleHandler/willFinishLaunchingWithOptions(_:launchOptions:)-8jatp`` method will be called for all - /// configured ``Module``s that conform to ``LifecycleHandler``. - case launchWithOptions(_ launchOptions: [LaunchOptionsKey: Any]) +#if os(iOS) || os(visionOS) || os(tvOS) + /// Injects the ``Spezi/launchOptions`` property to be accessed via the `@Application` property wrapper. + case launchWithOptions(_ launchOptions: [UIApplication.LaunchOptionsKey: Any]) +#elseif os(macOS) + /// Injects the ``Spezi/launchOptions`` property to be accessed via the `@Application` property wrapper. + case launchWithOptions(_ launchOptions: [AnyHashable: Any]) +#else // os(watchOS) + /// Injects the ``Spezi/launchOptions`` property to be accessed via the `@Application` property wrapper. + case launchWithOptions(_ launchOptions: [Never: Any]) +#endif static let launchWithOptions: LifecycleSimulationOptions = .launchWithOptions([:]) } @@ -47,15 +54,22 @@ extension View { "The Spezi previewWith(standard:_:) modifier can only used within Xcode preview processes." ) - let spezi = Spezi(standard: standard, modules: modules().elements) + var storage = SpeziStorage() + if case let .launchWithOptions(options) = simulateLifecycle { + storage[LaunchOptionsKey.self] = options + } + + let spezi = Spezi(standard: standard, modules: modules().elements, storage: storage) let lifecycleHandlers = spezi.lifecycleHandler return modifier(SpeziViewModifier(spezi)) - .task { +#if os(iOS) || os(visionOS) || os(tvOS) + .task { @MainActor in if case let .launchWithOptions(options) = simulateLifecycle { - lifecycleHandlers.willFinishLaunchingWithOptions(launchOptions: options) + lifecycleHandlers.willFinishLaunchingWithOptions(UIApplication.shared, launchOptions: options) } } +#endif } /// Configure Spezi for your previews using a collection of Modules. diff --git a/Sources/Spezi/Spezi/Spezi.swift b/Sources/Spezi/Spezi/Spezi.swift index 608a68c3..96cd75fd 100644 --- a/Sources/Spezi/Spezi/Spezi.swift +++ b/Sources/Spezi/Spezi/Spezi.swift @@ -57,28 +57,45 @@ public typealias SpeziStorage = HeapRepository /// /// The ``Module`` documentation provides more information about the structure of modules. /// Refer to the ``Configuration`` documentation to learn more about the Spezi configuration. -public actor Spezi: AnySpezi { +public class Spezi { /// A shared repository to store any ``KnowledgeSource``s restricted to the ``SpeziAnchor``. - let storage: SpeziStorage + /// + /// Every `Module` automatically conforms to `KnowledgeSource` and is stored within this storage object. + fileprivate(set) var storage: SpeziStorage /// Logger used to log events in the ``Spezi/Spezi`` instance. public let logger: Logger - /// The ``Standard`` used in the ``Spezi/Spezi`` instance. - public let standard: S /// Array of all SwiftUI `ViewModifiers` collected using ``_ModifierPropertyWrapper`` from the configured ``Module``s. - let viewModifiers: [any ViewModifier] + var viewModifiers: [any ViewModifier] + + /// A collection of ``Spezi/Spezi`` `LifecycleHandler`s. + @available( + *, + deprecated, + message: """ + Please use the new @Application property wrapper to access delegate functionality. \ + Otherwise use the SwiftUI onReceive(_:perform:) for UI related notifications. + """ + ) + nonisolated var lifecycleHandler: [LifecycleHandler] { + storage.collect(allOf: LifecycleHandler.self) + } + + convenience init(from configuration: Configuration, storage: consuming SpeziStorage = SpeziStorage()) { + self.init(standard: configuration.standard, modules: configuration.modules.elements, storage: storage) + } init( - standard: S, - modules: [any Module] + standard: any Standard, + modules: [any Module], + storage: consuming SpeziStorage = SpeziStorage() ) { // mutable property, as StorageValueProvider has inout protocol requirement. - var storage = SpeziStorage() + var storage = consume storage var collectedModifiers: [any ViewModifier] = [] self.logger = storage[SpeziLogger.self] - self.standard = standard let dependencyManager = DependencyManager(modules + [standard]) dependencyManager.resolve() @@ -87,14 +104,19 @@ public actor Spezi: AnySpezi { // we pass through the whole list of modules once to collect all @Provide values module.collectModuleValues(into: &storage) } - + + self.storage = storage + self.viewModifiers = [] // init all properties, we will store the final result later on + for module in dependencyManager.sortedModules { module.inject(standard: standard) + module.inject(spezi: self) + // supply modules values to all @Collect - module.injectModuleValues(from: storage) + module.injectModuleValues(from: self.storage) module.configure() - module.storeModule(into: &storage) + module.storeModule(into: self) collectedModifiers.append(contentsOf: module.viewModifiers) @@ -104,18 +126,17 @@ public actor Spezi: AnySpezi { } } - self.storage = storage self.viewModifiers = collectedModifiers } } extension Module { - func storeModule>(into repository: inout Repository) { + func storeModule(into spezi: Spezi) { guard let value = self as? Value else { - repository[SpeziLogger.self].warning("Could not store \(Self.self) in the SpeziStorage as the `Value` typealias was modified.") + spezi.logger.warning("Could not store \(Self.self) in the SpeziStorage as the `Value` typealias was modified.") return } - repository[Self.self] = value + spezi.storage[Self.self] = value } } diff --git a/Sources/Spezi/Spezi/SpeziAppDelegate.swift b/Sources/Spezi/Spezi/SpeziAppDelegate.swift index 86019011..39f2d8c6 100644 --- a/Sources/Spezi/Spezi/SpeziAppDelegate.swift +++ b/Sources/Spezi/Spezi/SpeziAppDelegate.swift @@ -9,28 +9,6 @@ import SwiftUI -#if os(iOS) || os(visionOS) -typealias ApplicationDelegate = UIApplicationDelegate & UISceneDelegate -/// Platform agnostic ApplicationDelegateAdaptor. -/// -/// Type-alias for the `UIApplicationDelegateAdaptor`. -public typealias ApplicationDelegateAdaptor = UIApplicationDelegateAdaptor -#elseif os(macOS) -typealias ApplicationDelegate = NSApplicationDelegate -/// Platform agnostic ApplicationDelegateAdaptor. -/// -/// Type-alias for the `NSApplicationDelegateAdaptor`. -public typealias ApplicationDelegateAdaptor = NSApplicationDelegateAdaptor -#elseif os(watchOS) -typealias ApplicationDelegate = WKApplicationDelegate -/// Platform agnostic ApplicationDelegateAdaptor. -/// -/// Type-alias for the `WKApplicationDelegateAdaptor`. -public typealias ApplicationDelegateAdaptor = WKApplicationDelegateAdaptor -#endif - - - /// Configure the Spezi-based application using the ``SpeziAppDelegate/configuration`` property. /// /// Set up the Spezi framework in your `App` instance of your SwiftUI application using the ``SpeziAppDelegate`` and the `@ApplicationDelegateAdaptor` property wrapper. @@ -72,9 +50,17 @@ public typealias ApplicationDelegateAdaptor = WKApplicationDelegateAdaptor /// Refer to the ``Configuration`` documentation to learn more about the Spezi configuration. open class SpeziAppDelegate: NSObject, ApplicationDelegate { private(set) static weak var appDelegate: SpeziAppDelegate? - - - private(set) lazy var spezi: AnySpezi = configuration.spezi + + private var _spezi: Spezi? + + var spezi: Spezi { + guard let spezi = _spezi else { + let spezi = Spezi(from: configuration) + self._spezi = spezi + return spezi + } + return spezi + } /// Register your different ``Module``s (or more sophisticated ``Module``s) using the ``SpeziAppDelegate/configuration`` property,. @@ -100,6 +86,7 @@ open class SpeziAppDelegate: NSObject, ApplicationDelegate { } #if os(iOS) || os(visionOS) || os(tvOS) + @available(*, deprecated, message: "Propagate deprecation warning.") open func application( _ application: UIApplication, // The usage of an optional collection is impossible to avoid as the function signature is defined by the `UIApplicationDelegate` @@ -113,7 +100,15 @@ open class SpeziAppDelegate: NSObject, ApplicationDelegate { // and configuration of the respective modules. This might and will cause troubles with Modules that // are only meant to be instantiated once. Therefore, we skip execution of this if running inside the PreviewSimulator. // This is also not a problem, as there is no way to set up an application delegate within a Xcode preview. - spezi.willFinishLaunchingWithOptions(launchOptions: launchOptions ?? [:]) + + precondition(_spezi == nil, "\(#function) was called when Spezi was already initialized. Unable to pass options!") + + var storage = SpeziStorage() + storage[LaunchOptionsKey.self] = launchOptions + self._spezi = Spezi(from: configuration, storage: storage) + + // backwards compatibility + spezi.lifecycleHandler.willFinishLaunchingWithOptions(application, launchOptions: launchOptions ?? [:]) } return true } @@ -129,74 +124,41 @@ open class SpeziAppDelegate: NSObject, ApplicationDelegate { return sceneConfig } + @available(*, deprecated, message: "Propagate deprecation warning.") open func applicationWillTerminate(_ application: UIApplication) { - spezi.applicationWillTerminate() + spezi.lifecycleHandler.applicationWillTerminate(application) } + @available(*, deprecated, message: "Propagate deprecation warning.") open func sceneWillEnterForeground(_ scene: UIScene) { - spezi.sceneWillEnterForeground() + spezi.lifecycleHandler.sceneWillEnterForeground(scene) } + @available(*, deprecated, message: "Propagate deprecation warning.") open func sceneDidBecomeActive(_ scene: UIScene) { - spezi.sceneDidBecomeActive() + spezi.lifecycleHandler.sceneDidBecomeActive(scene) } + @available(*, deprecated, message: "Propagate deprecation warning.") open func sceneWillResignActive(_ scene: UIScene) { - spezi.sceneWillResignActive() + spezi.lifecycleHandler.sceneWillResignActive(scene) } + @available(*, deprecated, message: "Propagate deprecation warning.") open func sceneDidEnterBackground(_ scene: UIScene) { - spezi.sceneDidEnterBackground() + spezi.lifecycleHandler.sceneDidEnterBackground(scene) } #elseif os(macOS) open func applicationWillFinishLaunching(_ notification: Notification) { if !ProcessInfo.processInfo.isPreviewSimulator { - spezi.willFinishLaunchingWithOptions(launchOptions: [:]) - } - } + // see note above for why we don't launch this within the preview simulator! - open func applicationWillTerminate(_ notification: Notification) { - spezi.applicationWillTerminate() - } - - open func applicationWillBecomeActive(_ notification: Notification) { - spezi.sceneWillEnterForeground() // TODO: is that accurate? - } - - open func applicationDidBecomeActive(_ notification: Notification) { - spezi.sceneDidBecomeActive() - } + precondition(_spezi == nil, "\(#function) was called when Spezi was already initialized. Unable to pass options!") - open func applicationWillResignActive(_ notification: Notification) { - spezi.sceneWillResignActive() - } - - open func applicationDidResignActive(_ notification: Notification) { - spezi.sceneDidEnterBackground() - } -#elseif os(watchOS) - open func applicationDidFinishLaunching() { - if !ProcessInfo.processInfo.isPreviewSimulator { - spezi.willFinishLaunchingWithOptions(launchOptions: [:]) + var storage = SpeziStorage() + storage[LaunchOptionsKey.self] = notification.userInfo + self._spezi = Spezi(from: configuration, storage: storage) } } - - // applicationWillTerminate(_:) not supported for WatchKit - - open func applicationWillEnterForeground() { - spezi.sceneWillEnterForeground() - } - - open func applicationDidBecomeActive() { - spezi.sceneDidBecomeActive() - } - - open func applicationWillResignActive() { - spezi.sceneWillResignActive() - } - - open func applicationDidEnterBackground() { - spezi.sceneDidEnterBackground() - } #endif } diff --git a/Sources/Spezi/Spezi/SpeziPropertyWrapper.swift b/Sources/Spezi/Spezi/SpeziPropertyWrapper.swift new file mode 100644 index 00000000..ffe3f0b4 --- /dev/null +++ b/Sources/Spezi/Spezi/SpeziPropertyWrapper.swift @@ -0,0 +1,21 @@ +// +// 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 +// + + +protocol SpeziPropertyWrapper { + func inject(spezi: Spezi) +} + + +extension Module { + func inject(spezi: Spezi) { + for wrapper in retrieveProperties(ofType: SpeziPropertyWrapper.self) { + wrapper.inject(spezi: spezi) + } + } +} diff --git a/Sources/Spezi/Spezi/SpeziSceneDelegate.swift b/Sources/Spezi/Spezi/SpeziSceneDelegate.swift index 26dbb24c..5b14c7ac 100644 --- a/Sources/Spezi/Spezi/SpeziSceneDelegate.swift +++ b/Sources/Spezi/Spezi/SpeziSceneDelegate.swift @@ -11,6 +11,7 @@ import SwiftUI #if os(iOS) || os(visionOS) || os(tvOS) class SpeziSceneDelegate: NSObject, UISceneDelegate { + @available(*, deprecated, message: "Propagate deprecation warning.") func sceneWillEnterForeground(_ scene: UIScene) { guard let delegate = SpeziAppDelegate.appDelegate else { return @@ -18,6 +19,7 @@ class SpeziSceneDelegate: NSObject, UISceneDelegate { delegate.sceneWillEnterForeground(scene) } + @available(*, deprecated, message: "Propagate deprecation warning.") func sceneDidBecomeActive(_ scene: UIScene) { guard let delegate = SpeziAppDelegate.appDelegate else { return @@ -25,6 +27,7 @@ class SpeziSceneDelegate: NSObject, UISceneDelegate { delegate.sceneDidBecomeActive(scene) } + @available(*, deprecated, message: "Propagate deprecation warning.") func sceneWillResignActive(_ scene: UIScene) { guard let delegate = SpeziAppDelegate.appDelegate else { return @@ -32,6 +35,7 @@ class SpeziSceneDelegate: NSObject, UISceneDelegate { delegate.sceneWillResignActive(scene) } + @available(*, deprecated, message: "Propagate deprecation warning.") func sceneDidEnterBackground(_ scene: UIScene) { guard let delegate = SpeziAppDelegate.appDelegate else { return diff --git a/Sources/Spezi/Spezi/View+Spezi.swift b/Sources/Spezi/Spezi/View+Spezi.swift index 75e79b70..afc87d61 100644 --- a/Sources/Spezi/Spezi/View+Spezi.swift +++ b/Sources/Spezi/Spezi/View+Spezi.swift @@ -14,7 +14,7 @@ struct SpeziViewModifier: ViewModifier { let speziViewModifiers: [any ViewModifier] - init(_ anySpezi: AnySpezi) { + init(_ anySpezi: Spezi) { self.speziViewModifiers = anySpezi.viewModifiers } diff --git a/Sources/Spezi/Standard/AnyStandardPropertyWrapper.swift b/Sources/Spezi/Standard/AnyStandardPropertyWrapper.swift index e3505b8b..5f58f4fc 100644 --- a/Sources/Spezi/Standard/AnyStandardPropertyWrapper.swift +++ b/Sources/Spezi/Standard/AnyStandardPropertyWrapper.swift @@ -10,3 +10,12 @@ protocol AnyStandardPropertyWrapper { func inject(standard: S) } + + +extension Module { + func inject(standard: any Standard) { + for standardPropertyWrapper in retrieveProperties(ofType: AnyStandardPropertyWrapper.self) { + standardPropertyWrapper.inject(standard: standard) + } + } +} diff --git a/Sources/Spezi/Standard/Module+Standard.swift b/Sources/Spezi/Standard/Module+Standard.swift index 266111ca..d2262d67 100644 --- a/Sources/Spezi/Standard/Module+Standard.swift +++ b/Sources/Spezi/Standard/Module+Standard.swift @@ -6,14 +6,6 @@ // SPDX-License-Identifier: MIT // -extension Module { - func inject(standard: any Standard) { - for standardPropertyWrapper in retrieveProperties(ofType: AnyStandardPropertyWrapper.self) { - standardPropertyWrapper.inject(standard: standard) - } - } -} - extension Module { /// Defines access to the shared `Standard` actor. diff --git a/Sources/Spezi/Utilities/ApplicationDelegate.swift b/Sources/Spezi/Utilities/ApplicationDelegate.swift new file mode 100644 index 00000000..e589b4c0 --- /dev/null +++ b/Sources/Spezi/Utilities/ApplicationDelegate.swift @@ -0,0 +1,18 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +#if os(iOS) || os(visionOS) || os(tvOS) +typealias ApplicationDelegate = UIApplicationDelegate & UISceneDelegate // swiftlint:disable:this file_types_order +#elseif os(macOS) +typealias ApplicationDelegate = NSApplicationDelegate +#elseif os(watchOS) +typealias ApplicationDelegate = WKApplicationDelegate +#endif diff --git a/Sources/Spezi/Utilities/ApplicationDelegateAdaptor.swift b/Sources/Spezi/Utilities/ApplicationDelegateAdaptor.swift new file mode 100644 index 00000000..9d715021 --- /dev/null +++ b/Sources/Spezi/Utilities/ApplicationDelegateAdaptor.swift @@ -0,0 +1,27 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SwiftUI + + +#if os(iOS) || os(visionOS) || os(tvOS) +/// Platform agnostic ApplicationDelegateAdaptor. +/// +/// Type-alias for the `UIApplicationDelegateAdaptor`. +public typealias ApplicationDelegateAdaptor = UIApplicationDelegateAdaptor // swiftlint:disable:this file_types_order +#elseif os(macOS) +/// Platform agnostic ApplicationDelegateAdaptor. +/// +/// Type-alias for the `NSApplicationDelegateAdaptor`. +public typealias ApplicationDelegateAdaptor = NSApplicationDelegateAdaptor +#elseif os(watchOS) +/// Platform agnostic ApplicationDelegateAdaptor. +/// +/// Type-alias for the `WKApplicationDelegateAdaptor`. +public typealias ApplicationDelegateAdaptor = WKApplicationDelegateAdaptor +#endif diff --git a/Sources/XCTSpezi/DependencyResolution.swift b/Sources/XCTSpezi/DependencyResolution.swift index 8996fc10..a2a4efb4 100644 --- a/Sources/XCTSpezi/DependencyResolution.swift +++ b/Sources/XCTSpezi/DependencyResolution.swift @@ -23,11 +23,19 @@ public func withDependencyResolution( simulateLifecycle: LifecycleSimulationOptions = .disabled, @ModuleBuilder _ modules: () -> ModuleCollection ) { - let spezi = Spezi(standard: standard, modules: modules().elements) + var storage = SpeziStorage() + if case let .launchWithOptions(options) = simulateLifecycle { + storage[LaunchOptionsKey.self] = options + } + + let spezi = Spezi(standard: standard, modules: modules().elements, storage: storage) +#if os(iOS) || os(visionOS) || os(tvOS) if case let .launchWithOptions(options) = simulateLifecycle { - spezi.lifecycleHandler.willFinishLaunchingWithOptions(launchOptions: options) + // maintain backwards compatibility + spezi.lifecycleHandler.willFinishLaunchingWithOptions(UIApplication.shared, launchOptions: options) } +#endif } /// Configure and resolve the dependency tree for a collection of [`Module`](https://swiftpackageindex.com/stanfordspezi/spezi/documentation/spezi/module)s. diff --git a/Tests/SpeziTests/CapabilityTests/Communication/ModuleCommunicationTests.swift b/Tests/SpeziTests/CapabilityTests/Communication/ModuleCommunicationTests.swift index 4961209f..fdca198c 100644 --- a/Tests/SpeziTests/CapabilityTests/Communication/ModuleCommunicationTests.swift +++ b/Tests/SpeziTests/CapabilityTests/Communication/ModuleCommunicationTests.swift @@ -61,7 +61,7 @@ final class ModuleCommunicationTests: XCTestCase { func testSimpleCommunication() throws { let delegate = TestApplicationDelegate() - _ = try XCTUnwrap(delegate.spezi as? Spezi) + _ = delegate.spezi // ensure init XCTAssertEqual(Self.collectModule.nums, [2, 3, 4, 5, 6]) XCTAssertTrue(Self.collectModule.nothingProvided.isEmpty) @@ -75,7 +75,7 @@ final class ModuleCommunicationTests: XCTestCase { _ = Self.collectModule.strings } - _ = try XCTUnwrap(delegate.spezi as? Spezi) + _ = delegate.spezi // ensure init try XCTRuntimePrecondition { Self.provideModule.numMaybe2 = 12 diff --git a/Tests/SpeziTests/CapabilityTests/LifecycleTests/LifecycleTests.swift b/Tests/SpeziTests/CapabilityTests/LifecycleTests/LifecycleTests.swift index 37656fd8..6bd5bc94 100644 --- a/Tests/SpeziTests/CapabilityTests/LifecycleTests/LifecycleTests.swift +++ b/Tests/SpeziTests/CapabilityTests/LifecycleTests/LifecycleTests.swift @@ -11,10 +11,14 @@ import XCTest import XCTRuntimeAssertions +@available(*, deprecated, message: "Propagate deprecation warning") private final class TestLifecycleHandler: Module, LifecycleHandler { let expectationWillFinishLaunchingWithOption: XCTestExpectation let expectationApplicationWillTerminate: XCTestExpectation - + + @Application(\.launchOptions) + var launchOptions + init( expectationWillFinishLaunchingWithOption: XCTestExpectation, @@ -24,78 +28,78 @@ private final class TestLifecycleHandler: Module, LifecycleHandler { self.expectationApplicationWillTerminate = expectationApplicationWillTerminate } - +#if os(iOS) || os(visionOS) || os(tvOS) func willFinishLaunchingWithOptions( - launchOptions: [LaunchOptionsKey: Any] + _ application: UIApplication, + launchOptions: [UIApplication.LaunchOptionsKey: Any] ) { expectationWillFinishLaunchingWithOption.fulfill() } - func applicationWillTerminate() { + func applicationWillTerminate(_ application: UIApplication) { expectationApplicationWillTerminate.fulfill() } +#endif } +@available(*, deprecated, message: "Propagate deprecation warning") private final class EmptyLifecycleHandler: Module, LifecycleHandler { } +@available(*, deprecated, message: "Propagate deprecation warning") private class TestLifecycleHandlerApplicationDelegate: SpeziAppDelegate { - let expectationWillFinishLaunchingWithOption: XCTestExpectation - let expectationApplicationWillTerminate: XCTestExpectation + private let injectedModule: TestLifecycleHandler override var configuration: Configuration { Configuration { - TestLifecycleHandler( - expectationWillFinishLaunchingWithOption: expectationWillFinishLaunchingWithOption, - expectationApplicationWillTerminate: expectationApplicationWillTerminate - ) + injectedModule EmptyLifecycleHandler() } } - - - init( - expectationWillFinishLaunchingWithOption: XCTestExpectation, - expectationApplicationWillTerminate: XCTestExpectation - ) { - self.expectationWillFinishLaunchingWithOption = expectationWillFinishLaunchingWithOption - self.expectationApplicationWillTerminate = expectationApplicationWillTerminate + + init(injectedModule: TestLifecycleHandler) { + self.injectedModule = injectedModule } } final class LifecycleTests: XCTestCase { @MainActor + @available(*, deprecated, message: "Propagate deprecation warning") func testUIApplicationLifecycleMethods() async throws { let expectationWillFinishLaunchingWithOption = XCTestExpectation(description: "WillFinishLaunchingWithOptions") let expectationApplicationWillTerminate = XCTestExpectation(description: "ApplicationWillTerminate") - - let testApplicationDelegate = TestLifecycleHandlerApplicationDelegate( + + let module = TestLifecycleHandler( expectationWillFinishLaunchingWithOption: expectationWillFinishLaunchingWithOption, expectationApplicationWillTerminate: expectationApplicationWillTerminate ) + let testApplicationDelegate = TestLifecycleHandlerApplicationDelegate(injectedModule: module) #if os(iOS) || os(visionOS) || os(tvOS) - let willFinishLaunchingWithOptions = try testApplicationDelegate.application( + let launchOptions = try [UIApplication.LaunchOptionsKey.url: XCTUnwrap(URL(string: "spezi.stanford.edu"))] + let willFinishLaunchingWithOptions = testApplicationDelegate.application( UIApplication.shared, - willFinishLaunchingWithOptions: [UIApplication.LaunchOptionsKey.url: XCTUnwrap(URL(string: "spezi.stanford.edu"))] + willFinishLaunchingWithOptions: launchOptions ) XCTAssertTrue(willFinishLaunchingWithOptions) + wait(for: [expectationWillFinishLaunchingWithOption]) + + XCTAssertTrue(module.launchOptions.keys.allSatisfy { launchOptions[$0] != nil }) #elseif os(macOS) - testApplicationDelegate.applicationWillFinishLaunching(Notification(name: NSApplication.willFinishLaunchingNotification)) - #elseif os(watchOS) - testApplicationDelegate.applicationDidFinishLaunching() + let launchOptions: [AnyHashable: Any] = [:] // TODO: put in some values + testApplicationDelegate.applicationWillFinishLaunching( + Notification(name: NSApplication.willFinishLaunchingNotification, userInfo: launchOptions) + ) + + XCTAssertTrue(module.launchOptions.keys.allSatisfy { launchOptions[$0] != nil }) #endif - wait(for: [expectationWillFinishLaunchingWithOption]) #if os(iOS) || os(visionOS) || os(tvOS) testApplicationDelegate.applicationWillTerminate(UIApplication.shared) wait(for: [expectationApplicationWillTerminate]) - #elseif os(macOS) - testApplicationDelegate.applicationWillTerminate(.init(name: NSApplication.willTerminateNotification)) - wait(for: [expectationApplicationWillTerminate]) #endif } }