diff --git a/Package.swift b/Package.swift index addb895e..8aad9e1d 100644 --- a/Package.swift +++ b/Package.swift @@ -86,7 +86,7 @@ func swiftLintPlugin() -> [Target.PluginUsage] { func swiftLintPackage() -> [PackageDescription.Package.Dependency] { if ProcessInfo.processInfo.environment["SPEZI_DEVELOPMENT_SWIFTLINT"] != nil { - [.package(url: "https://github.com/realm/SwiftLint.git", .upToNextMinor(from: "0.55.1"))] + [.package(url: "https://github.com/realm/SwiftLint.git", from: "0.55.1")] } else { [] } diff --git a/Sources/Spezi/Capabilities/Lifecycle/LifecycleHandler.swift b/Sources/Spezi/Capabilities/Lifecycle/LifecycleHandler.swift index 3f62a705..0b065ec2 100644 --- a/Sources/Spezi/Capabilities/Lifecycle/LifecycleHandler.swift +++ b/Sources/Spezi/Capabilities/Lifecycle/LifecycleHandler.swift @@ -44,6 +44,7 @@ public protocol LifecycleHandler { to access launchOptions in a platform independent way. """ ) + @MainActor func willFinishLaunchingWithOptions(_ application: UIApplication, launchOptions: [UIApplication.LaunchOptionsKey: Any]) /// Replicates the `sceneWillEnterForeground(_: UIScene)` functionality of the `UISceneDelegate`. @@ -63,6 +64,7 @@ public protocol LifecycleHandler { or other platform-specific mechanisms as a replacement. """ ) + @MainActor func sceneWillEnterForeground(_ scene: UIScene) /// Replicates the `sceneDidBecomeActive(_: UIScene)` functionality of the `UISceneDelegate`. @@ -78,6 +80,7 @@ public protocol LifecycleHandler { or other platform-specific mechanisms as a replacement. """ ) + @MainActor func sceneDidBecomeActive(_ scene: UIScene) /// Replicates the `sceneWillResignActive(_: UIScene)` functionality of the `UISceneDelegate`. @@ -93,6 +96,7 @@ public protocol LifecycleHandler { or other platform-specific mechanisms as a replacement. """ ) + @MainActor func sceneWillResignActive(_ scene: UIScene) /// Replicates the `sceneDidEnterBackground(_: UIScene)` functionality of the `UISceneDelegate`. @@ -108,6 +112,7 @@ public protocol LifecycleHandler { or other platform-specific mechanisms as a replacement. """ ) + @MainActor func sceneDidEnterBackground(_ scene: UIScene) /// Replicates the `applicationWillTerminate(_: UIApplication)` functionality of the `UIApplicationDelegate`. @@ -123,6 +128,7 @@ public protocol LifecycleHandler { or other platform-specific mechanisms as a replacement. """ ) + @MainActor func applicationWillTerminate(_ application: UIApplication) #endif } diff --git a/Sources/Spezi/Capabilities/Notifications/NotificationHandler.swift b/Sources/Spezi/Capabilities/Notifications/NotificationHandler.swift index 2ae91a60..42a6e2c2 100644 --- a/Sources/Spezi/Capabilities/Notifications/NotificationHandler.swift +++ b/Sources/Spezi/Capabilities/Notifications/NotificationHandler.swift @@ -22,6 +22,7 @@ public protocol NotificationHandler { /// - Note: Notification Actions are not supported on `tvOS`. /// /// - Parameter response: The user's response to the notification. + @MainActor func handleNotificationAction(_ response: UNNotificationResponse) async #endif @@ -35,6 +36,7 @@ public protocol NotificationHandler { /// /// - Parameter notification: The notification that is about to be delivered. /// - Returns: The option for notifying the user. Use `[]` to silence the notification. + @MainActor func receiveIncomingNotification(_ notification: UNNotification) async -> UNNotificationPresentationOptions? #if !os(macOS) @@ -51,6 +53,7 @@ public protocol NotificationHandler { /// /// - Parameter remoteNotification: The data of the notification payload. /// - Returns: Return the respective ``BackgroundFetchResult``. + @MainActor func receiveRemoteNotification(_ remoteNotification: [AnyHashable: Any]) async -> BackgroundFetchResult #else /// Handle remote notification when the app is running in background. @@ -62,6 +65,7 @@ public protocol NotificationHandler { /// [`application(_:didReceiveRemoteNotification:)`](https://developer.apple.com/documentation/appkit/nsapplicationdelegate/1428430-application). /// /// - Parameter remoteNotification: The data of the notification payload. + @MainActor func receiveRemoteNotification(_ remoteNotification: [AnyHashable: Any]) #endif } @@ -70,21 +74,25 @@ public protocol NotificationHandler { extension NotificationHandler { #if !os(tvOS) /// Empty default implementation. + @MainActor public func handleNotificationAction(_ response: UNNotificationResponse) async {} #endif /// Empty default implementation. + @MainActor public func receiveIncomingNotification(_ notification: UNNotification) async -> UNNotificationPresentationOptions? { nil } #if !os(macOS) /// Empty default implementation. + @MainActor public func receiveRemoteNotification(_ remoteNotification: [AnyHashable: Any]) async -> BackgroundFetchResult { .noData } #else /// Empty default implementation. + @MainActor public func receiveRemoteNotification(_ remoteNotification: [AnyHashable: Any]) {} #endif } diff --git a/Sources/Spezi/Capabilities/ViewModifier/ViewModifierProvider.swift b/Sources/Spezi/Capabilities/ViewModifier/ViewModifierProvider.swift index 08962aff..31be13f6 100644 --- a/Sources/Spezi/Capabilities/ViewModifier/ViewModifierProvider.swift +++ b/Sources/Spezi/Capabilities/ViewModifier/ViewModifierProvider.swift @@ -11,7 +11,9 @@ import SwiftUI enum ModifierPlacement: Int, Comparable { + /// No specific order requirement. case regular + /// Outermost placement (e.g., @Model-based property wrappers). case outermost static func < (lhs: ModifierPlacement, rhs: ModifierPlacement) -> Bool { diff --git a/Sources/Spezi/Dependencies/DependencyManager.swift b/Sources/Spezi/Dependencies/DependencyManager.swift index 18e2664e..b821b63b 100644 --- a/Sources/Spezi/Dependencies/DependencyManager.swift +++ b/Sources/Spezi/Dependencies/DependencyManager.swift @@ -14,6 +14,8 @@ import XCTRuntimeAssertions public class DependencyManager: Sendable { /// Collection of already initialized modules. private let existingModules: [any Module] + /// We track the top level module instances to resolve the order for initialization. + private let originalModules: [any Module] /// Collection of initialized Modules. /// @@ -28,8 +30,14 @@ public class DependencyManager: Sendable { /// Collection of all modules with dependencies that are not yet processed. private var modulesWithDependencies: [any Module] - /// Recursive search stack to keep track of potential circular dependencies. - private var searchStack: [any Module] = [] + + + /// The current search stack for the module being pushed. + private var currentPushedModule: ModuleReference? + private var searchStacks: [ModuleReference: [any Module.Type]] = [:] + + private var nextTypeOrderIndex: UInt64 = 0 + private var moduleTypeOrder: [ObjectIdentifier: UInt64] = [:] /// A ``DependencyManager`` in Spezi is used to gather information about modules with dependencies. @@ -43,20 +51,62 @@ public class DependencyManager: Sendable { self.modulesWithDependencies = modules.filter { !$0.dependencyDeclarations.isEmpty } + self.originalModules = modules self.existingModules = existingModules } /// Resolves the dependency order. /// - /// After calling `resolve` you can safely access `initializedModules`. + /// After calling `resolve()` you can safely access `initializedModules`. func resolve() { - // Start the dependency resolution on the first module. while let nextModule = modulesWithDependencies.first { push(nextModule) } 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)") + + buildTypeOrder() + + initializedModules.sort { lhs, rhs in + retrieveTypeOrder(for: lhs) < retrieveTypeOrder(for: rhs) + } + } + + private func buildTypeOrder() { + // when this method is called, we already know there is no cycle + + func nextEntry(for module: any Module.Type) { + let id = ObjectIdentifier(module) + guard moduleTypeOrder[id] == nil else { + return // already tracked + } + moduleTypeOrder[id] = nextTypeOrderIndex + nextTypeOrderIndex += 1 + } + + func depthFirstSearch(for module: any Module) { + for declaration in module.dependencyDeclarations { + for dependency in declaration.unsafeInjectedModules { + depthFirstSearch(for: dependency) + } + } + nextEntry(for: type(of: module)) + } + + for module in originalModules { + depthFirstSearch(for: module) + } + } + + private func retrieveTypeOrder(for module: any Module) -> UInt64 { + guard let order = moduleTypeOrder[ObjectIdentifier(type(of: module))] else { + preconditionFailure("Failed to retrieve module order index for module of type \(type(of: module))") + } + return order } private func injectDependencies() { @@ -64,76 +114,61 @@ public class DependencyManager: Sendable { // that wasn't previously injected. for module in initializedModules + existingModules { for dependency in module.dependencyDeclarations { - dependency.inject(from: self) + dependency.inject(from: self, for: module) } } } /// Push a module on the search stack and resolve dependency information. private func push(_ module: any Module) { - searchStack.append(module) + assert(currentPushedModule == nil, "Module already pushed. Did the algorithm turn into an recursive one by accident?") + + currentPushedModule = ModuleReference(module) + searchStacks[ModuleReference(module), default: []] + .append(type(of: module)) + for dependency in module.dependencyDeclarations { dependency.collect(into: self) // leads to calls to `require(_:defaultValue:)` } - finishSearch(module) + + finishSearch(for: module) } /// Communicate a requirement to a `DependencyManager` /// - 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, defaultValue: (() -> M)?) { - // 1. Return if the depending module is found in the `initializedModules` collection. - if initializedModules.contains(where: { type(of: $0) == M.self }) - || existingModules.contains(where: { type(of: $0) == M.self }) { - return - } + func require(_ dependency: M.Type, type dependencyType: DependencyType, defaultValue: (() -> M)?) { + testForSearchStackCycles(M.self) - - // 2. Search for the required module is found in the `dependingModules` collection. - // If not, use the default value calling the `defaultValue` auto-closure. - guard let foundInModulesWithDependencies = modulesWithDependencies.first(where: { type(of: $0) == M.self }) else { + // 1. Check if it is actively requested to load this module. + if case .load = dependencyType { guard let defaultValue else { - // optional dependency. The user didn't supply anything. So we can't deliver anything. - return + return // doesn't make sense, just ignore that } - let newModule = defaultValue() - - implicitlyCreatedModules.insert(ModuleReference(newModule)) + implicitlyCreate(defaultValue) + return + } - guard !newModule.dependencyDeclarations.isEmpty else { - initializedModules.append(newModule) - return - } - - modulesWithDependencies.insert(newModule, at: 0) - push(newModule) - + // 2. Return if the depending module is already initialized or being initialized. + if initializedModules.contains(where: { type(of: $0) == M.self }) + || existingModules.contains(where: { type(of: $0) == M.self }) + || modulesWithDependencies.contains(where: { type(of: $0) == M.self }) { return } - - // Detect circles in the `recursiveSearch` collection. - guard !searchStack.contains(where: { type(of: $0) == M.self }) else { - let dependencyChain = searchStack - .map { String(describing: type(of: $0)) } - .joined(separator: ", ") - - // The last element must exist as we entered the statement using a successful `contains` statement. - // There is not chance to recover here: If there is a crash here, we would fail in the precondition statement in the next line anyways - let lastElement = searchStack.last! // swiftlint:disable:this force_unwrapping - preconditionFailure( - """ - The `DependencyManager` has detected a dependency cycle of your Spezi modules. - The current dependency chain is: \(dependencyChain). The \(String(describing: type(of: lastElement))) required a type already present in the dependency chain. - - Please ensure that the modules you use or develop can not trigger a dependency cycle. - """ - ) + + + // 3. Otherwise, check if there is a default value we can implicitly load + if let defaultValue { + implicitlyCreate(defaultValue) + } else if case .required = dependencyType, + let defaultInit = dependency as? DefaultInitializable.Type, + let module = defaultInit.init() as? M { + implicitlyCreate { + module + } } - - // If there is no cycle, resolve the dependencies of the module found in the `dependingModules`. - push(foundInModulesWithDependencies) } /// Retrieve a resolved dependency for a given type. @@ -142,35 +177,74 @@ 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 = M.self, optional: Bool = false) -> M? { + func retrieve(module: M.Type, type dependencyType: DependencyType, for owner: any Module) -> 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(optional, "Could not located dependency of type \(M.self)!") + 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. + """ + ) return nil } return module } - private func finishSearch(_ dependingModule: any Module) { - guard !searchStack.isEmpty else { - preconditionFailure("Internal logic error in the `DependencyManager`. Search Stack is empty.") - } - let module = searchStack.removeLast() + private func implicitlyCreate(_ module: () -> M) { + let newModule = module() + + implicitlyCreatedModules.insert(ModuleReference(newModule)) - guard module === dependingModule else { - preconditionFailure("Internal logic error in the `DependencyManager`. Search Stack element was not the one we are resolving for.") + if newModule.dependencyDeclarations.isEmpty { + initializedModules.append(newModule) + } else { + saveSearchStack(for: newModule) + + modulesWithDependencies.append(newModule) // appending for BFS } + } + + private func finishSearch(for dependingModule: any Module) { + currentPushedModule = nil + searchStacks.removeValue(forKey: ModuleReference(dependingModule)) let dependingModulesCount = modulesWithDependencies.count modulesWithDependencies.removeAll(where: { $0 === dependingModule }) - precondition( - dependingModulesCount - 1 == modulesWithDependencies.count, - "Unexpected reduction of modules." - ) + precondition(dependingModulesCount - 1 == modulesWithDependencies.count, "Unexpected reduction of modules.") initializedModules.append(dependingModule) } + + private func saveSearchStack(for module: M) { + guard let currentPushedModule, + let searchStack = searchStacks[currentPushedModule] else { + return + } + + // propagate the search stack such that we have it available when we call push for this module + searchStacks[ModuleReference(module)] = searchStack + } + + private func testForSearchStackCycles(_ module: M.Type) { + 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. + """ + ) + } + } } diff --git a/Sources/Spezi/Dependencies/Property/DependencyBuilder.swift b/Sources/Spezi/Dependencies/Property/DependencyBuilder.swift index bcded6ea..22d07d83 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyBuilder.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyBuilder.swift @@ -7,11 +7,13 @@ // -/// A result builder to build a ``DependencyCollection``. +/// A result builder to build a `DependencyCollection`. +/// +/// For more information refer to ``DependencyCollection``. @resultBuilder public enum DependencyBuilder: DependencyCollectionBuilder { - /// An auto-closure expression, providing the default dependency value, building the ``DependencyCollection``. - public static func buildExpression(_ expression: @escaping @autoclosure () -> M) -> DependencyCollection { - DependencyCollection(DependencyContext(defaultValue: expression)) + /// An auto-closure expression, providing the default dependency value, building the `DependencyCollection`. + public static func buildExpression(_ expression: M) -> DependencyCollection { + DependencyCollection(expression) } } diff --git a/Sources/Spezi/Dependencies/Property/DependencyCollection.swift b/Sources/Spezi/Dependencies/Property/DependencyCollection.swift index 2a83d9b2..16cca771 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyCollection.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyCollection.swift @@ -7,9 +7,34 @@ // -/// A collection of dependency declarations. -public struct DependencyCollection: DependencyDeclaration { - let entries: [AnyDependencyContext] +/// A collection of dependencies. +/// +/// This collection contains a collection of Modules that are meant to be declared as the dependencies of another module. +/// +/// The code example below demonstrates how you can easily create your collection of dependencies from multiple different types of ``Module``s. +/// +/// - Tip: You can also use ``append(contentsOf:)`` to combine two collections. +/// +/// ```swift +/// var collection = DependencyCollection(ModuleA(), ModuleB(), ModuleC()) +/// +/// collection.append(ModuleD()) +/// ``` +/// +/// - Note: Use the ``DependencyCollectionBuilder`` if you want to create your own result builder that can build a ``DependencyCollection`` component +/// out of multiple `Module` expressions. +public struct DependencyCollection { + private var entries: [AnyDependencyContext] + + /// Determine if the collection is empty. + public var isEmpty: Bool { + entries.isEmpty + } + + /// The count of entries. + public var count: Int { + entries.count + } init(_ entries: [AnyDependencyContext]) { self.entries = entries @@ -18,56 +43,77 @@ public struct DependencyCollection: DependencyDeclaration { init(_ entries: AnyDependencyContext...) { self.init(entries) } - - /// Creates a ``DependencyCollection`` from a closure resulting in a single generic type conforming to the Spezi ``Module``. - /// - Parameters: - /// - type: The generic type resulting from the passed closure, has to conform to ``Module``. - /// - singleEntry: Closure returning a dependency conforming to ``Module``, stored within the ``DependencyCollection``. + + /// Create an empty collection. + public init() { + self.entries = [] + } + + + /// Create a collection with entries /// - /// ### Usage + /// - Note: You can create your own result builders that build a `DependencyCollection` using the ``DependencyCollectionBuilder``. /// - /// The `SomeCustomDependencyBuilder` enforces certain type constraints (e.g., `SomeTypeConstraint`, more specific than ``Module``) during aggregation of ``Module/Dependency``s (``Module``s) via a result builder. - /// The individual dependency expressions within the result builder conforming to `SomeTypeConstraint` are then transformed to a ``DependencyCollection`` via ``DependencyCollection/init(for:singleEntry:)-6ihsh``. + /// - Parameter entry: The parameter pack of modules. + public init(_ entry: repeat each M) { + self.init() + repeat append(each entry) + } + + /// Create a collection from a single entry closure. /// - /// ```swift - /// @resultBuilder - /// public enum SomeCustomDependencyBuilder: DependencyCollectionBuilder { - /// public static func buildExpression(_ expression: @escaping @autoclosure () -> T) -> DependencyCollection { - /// DependencyCollection(singleEntry: expression) - /// } - /// } - /// ``` + /// - Note: You can create your own result builders that build a `DependencyCollection` using the ``DependencyCollectionBuilder``. /// - /// See `_DependencyPropertyWrapper/init(using:)` for a continued example regarding the usage of the implemented result builder. + /// - Parameters: + /// - type: The generic type resulting from the passed closure, has to conform to ``Module``. + /// - singleEntry: Closure returning a dependency conforming to ``Module``, stored within the ``DependencyCollection``. + @available( + *, deprecated, renamed: "init(_:)", + message: "DependencyCollection entries are now always an explicit request to load and do not require a closure anymore." + ) public init(for type: Dependency.Type = Dependency.self, singleEntry: @escaping (() -> Dependency)) { - self.init(DependencyContext(for: type, defaultValue: singleEntry)) + self.init(singleEntry()) } - /// Creates a ``DependencyCollection`` from a closure resulting in a single generic type conforming to the Spezi ``Module``. + /// Create a collection from a single entry closure. + /// + /// - Note: You can create your own result builders that build a `DependencyCollection` using the ``DependencyCollectionBuilder``. + /// /// - Parameters: /// - type: The generic type resulting from the passed closure, has to conform to ``Module``. /// - singleEntry: Closure returning a dependency conforming to ``Module``, stored within the ``DependencyCollection``. - /// - /// ### Usage - /// - /// The `SomeCustomDependencyBuilder` enforces certain type constraints (e.g., `SomeTypeConstraint`, more specific than ``Module``) during aggregation of ``Module/Dependency``s (``Module``s) via a result builder. - /// The individual dependency expressions within the result builder conforming to `SomeTypeConstraint` are then transformed to a ``DependencyCollection`` via ``DependencyCollection/init(for:singleEntry:)-6nzui``. - /// - /// ```swift - /// @resultBuilder - /// public enum SomeCustomDependencyBuilder: DependencyCollectionBuilder { - /// public static func buildExpression(_ expression: @escaping @autoclosure () -> T) -> DependencyCollection { - /// DependencyCollection(singleEntry: expression) - /// } - /// } - /// ``` - /// - /// See `_DependencyPropertyWrapper/init(using:)` for a continued example regarding the usage of the implemented result builder. + @available( + *, deprecated, renamed: "init(_:)", + message: "DependencyCollection entries are now always an explicit request to load and do not require a closure anymore." + ) public init(for type: Dependency.Type = Dependency.self, singleEntry: @escaping @autoclosure (() -> Dependency)) { - self.init(singleEntry: singleEntry) + self.init(singleEntry()) + } + + + /// Append a collection. + /// - Parameter collection: The dependency collection to append to this one. + public mutating func append(contentsOf collection: DependencyCollection) { + entries.append(contentsOf: collection.entries) + } + + + /// Append a module. + /// - Parameter module: The ``Module`` to append to the collection. + public mutating func append(_ module: M) { + // we always treat modules passed to a Dependency collection as an explicit request to load them, therefore .load + entries.append(DependencyContext(for: M.self, type: .load, defaultValue: { module })) } +} +extension DependencyCollection: DependencyDeclaration { + var unsafeInjectedModules: [any Module] { + entries.flatMap { entry in + entry.unsafeInjectedModules + } + } + func dependencyRelation(to module: DependencyReference) -> DependencyRelation { let relations = entries.map { $0.dependencyRelation(to: module) } @@ -87,9 +133,9 @@ public struct DependencyCollection: DependencyDeclaration { } } - func inject(from dependencyManager: DependencyManager) { + func inject(from dependencyManager: DependencyManager, for module: any Module) { for entry in entries { - entry.inject(from: dependencyManager) + entry.inject(from: dependencyManager, for: module) } } diff --git a/Sources/Spezi/Dependencies/Property/DependencyCollectionBuilder.swift b/Sources/Spezi/Dependencies/Property/DependencyCollectionBuilder.swift index 55afb6f0..0b872e1a 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyCollectionBuilder.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyCollectionBuilder.swift @@ -7,50 +7,67 @@ // -/// A protocol enabling the implementation of a result builder to build a ``DependencyCollection``. -/// Enables the simple construction of a result builder accepting ``Module``s with additional type constraints (useful for DSL implementations). +/// Implement a custom result builder to build a `DependencyCollection`. +/// +/// A ``DependencyCollection`` is a collection of dependencies that can be passed to the ``Module/Dependency`` property wrapper of a ``Module``. +/// This protocol allows you to easily implement a result builder with custom expression, building a `DependencyCollection` component. +/// +/// To create your own result builder, just add adopt the `DependencyCollectionBuilder` protocol and add your custom expressions. +/// The code example below shows the implementation of a `SpecialModuleBuilder` that only allows to build modules of type `SpecialModule`. /// -/// Upon conformance, developers are required to implement a single result builder function transforming an arbitrary ``Module`` type constraint (M in the example below) to a ``DependencyCollection``. -/// All other result builder functions for constructing a ``DependencyCollection`` are provided as a default protocol implementation. /// ```swift -/// static func buildExpression(_ expression: @escaping @autoclosure () -> M) -> DependencyCollection +/// @resultBuilder +/// enum SpecialModuleBuilder: DependencyCollectionBuilder { +/// static func buildExpression(_ expression: M) -> DependencyCollection { +/// DependencyCollection(expression) +/// } +/// } /// ``` /// -/// See ``DependencyCollection/init(for:singleEntry:)-6nzui`` for an example conformance implementation of the ``DependencyCollectionBuilder``. +/// You could then use this result builder to accept only `SpecialModule` conforming modules in, e.g., the initializer of your Spezi module. +/// ```swift +/// final class MyModule: Module { +/// @Dependency private var specials: [any Module] +/// +/// init(@SpecialModuleBuilder modules: () -> DependencyCollection) { +/// _specials = Dependency(using: modules()) +/// } +/// } +/// ``` public protocol DependencyCollectionBuilder {} /// Default protocol implementations of a result builder constructing a ``DependencyCollection``. extension DependencyCollectionBuilder { - /// Build a block of ``DependencyCollection``s. + /// Build a block of `DependencyCollection`s. public static func buildBlock(_ components: DependencyCollection...) -> DependencyCollection { buildArray(components) } - /// Build the first block of an conditional ``DependencyCollection`` component. + /// Build the first block of an conditional `DependencyCollection` component. public static func buildEither(first component: DependencyCollection) -> DependencyCollection { component } - /// Build the second block of an conditional ``DependencyCollection`` component. + /// Build the second block of an conditional `DependencyCollection` component. public static func buildEither(second component: DependencyCollection) -> DependencyCollection { component } - /// Build an optional ``DependencyCollection`` component. + /// Build an optional `DependencyCollection` component. public static func buildOptional(_ component: DependencyCollection?) -> DependencyCollection { component ?? DependencyCollection() } - /// Build an ``DependencyCollection`` component with limited availability. + /// Build an `DependencyCollection` component with limited availability. public static func buildLimitedAvailability(_ component: DependencyCollection) -> DependencyCollection { component } - /// Build an array of ``DependencyCollection`` components. + /// Build an array of `DependencyCollection` components. public static func buildArray(_ components: [DependencyCollection]) -> DependencyCollection { - DependencyCollection(components.reduce(into: []) { result, component in - result.append(contentsOf: component.entries) - }) + components.reduce(into: DependencyCollection()) { partialResult, collection in + partialResult.append(contentsOf: collection) + } } } diff --git a/Sources/Spezi/Dependencies/Property/DependencyContext.swift b/Sources/Spezi/Dependencies/Property/DependencyContext.swift index aabd17e1..041ff82d 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyContext.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyContext.swift @@ -16,15 +16,38 @@ protocol AnyDependencyContext: DependencyDeclaration { } -class DependencyContext: AnyDependencyContext { - let defaultValue: (() -> Dependency)? - private weak var spezi: Spezi? - private var injectedDependency: DynamicReference? - +/// The type of dependency declaration. +enum DependencyType { + /// The dependency was declared as being required. + case required + /// The dependency was declared as being optional. + case optional + /// The dependency was declared to be loaded. + /// + /// This is a required dependency were the `defaultValue` should always be loaded. + case load + + + var isRequired: Bool { + switch self { + case .required, .load: + true + case .optional: + false + } + } var isOptional: Bool { - defaultValue == nil + !isRequired } +} + + +class DependencyContext: AnyDependencyContext { + private let type: DependencyType + private let defaultValue: (() -> Dependency)? + private weak var spezi: Spezi? + private var injectedDependency: DynamicReference? private var dependency: Dependency? { guard let injectedDependency else { @@ -47,7 +70,12 @@ class DependencyContext: AnyDependencyContext { return nil } - init(for type: Dependency.Type = Dependency.self, defaultValue: (() -> Dependency)? = nil) { + var unsafeInjectedModules: [any Module] { + injectedDependency?.element.map { [$0] } ?? [] + } + + init(for dependency: Dependency.Type, type: DependencyType, defaultValue: (() -> Dependency)? = nil) { + self.type = type self.defaultValue = defaultValue } @@ -56,7 +84,8 @@ class DependencyContext: AnyDependencyContext { return .unrelated } - if isOptional { + + if type.isOptional { return .optional } else { return .dependent @@ -64,16 +93,16 @@ class DependencyContext: AnyDependencyContext { } func collect(into dependencyManager: DependencyManager) { - dependencyManager.require(Dependency.self, defaultValue: defaultValue) + dependencyManager.require(Dependency.self, type: type, defaultValue: defaultValue) } - func inject(from dependencyManager: DependencyManager) { - guard let dependency = dependencyManager.retrieve(module: Dependency.self, optional: isOptional) else { + func inject(from dependencyManager: DependencyManager, for module: any Module) { + guard let dependency = dependencyManager.retrieve(module: Dependency.self, type: type, for: module) else { injectedDependency = nil return } - if isOptional { + if type.isOptional { injectedDependency = .weakElement(dependency) } else { injectedDependency = .element(dependency) @@ -117,14 +146,14 @@ class DependencyContext: AnyDependencyContext { ) } guard let dependencyM = dependency as? M else { - preconditionFailure("A injected dependency of type \(type(of: injectedDependency)) didn't match the expected type \(M.self)!") + preconditionFailure("A injected dependency of type \(Swift.type(of: injectedDependency)) didn't match the expected type \(M.self)!") } return dependencyM } func retrieveOptional(dependency: M.Type) -> M? { guard let dependency = self.dependency as? M? else { - preconditionFailure("A injected dependency of type \(type(of: injectedDependency)) didn't match the expected type \(M?.self)!") + preconditionFailure("A injected dependency of type \(Swift.type(of: injectedDependency)) didn't match the expected type \(M?.self)!") } return dependency } diff --git a/Sources/Spezi/Dependencies/Property/DependencyDeclaration.swift b/Sources/Spezi/Dependencies/Property/DependencyDeclaration.swift index 11c6c461..13a942f8 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyDeclaration.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyDeclaration.swift @@ -21,12 +21,20 @@ enum DependencyRelation: Hashable { /// /// This protocol allows to communicate dependency requirements of a ``Module`` to the ``DependencyManager``. protocol DependencyDeclaration { + /// Directly access the injected dependency of the dependency declaration. + /// + /// Unsafe, as there are not extra checks made to guarantee a safe value. + var unsafeInjectedModules: [any Module] { get } + /// Request from the ``DependencyManager`` to collect all dependencies. Mark required by calling `DependencyManager/require(_:defaultValue:)`. @MainActor func collect(into dependencyManager: DependencyManager) /// 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) + func inject(from dependencyManager: DependencyManager, for module: any Module) @MainActor func inject(spezi: Spezi) diff --git a/Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift b/Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift index c68afb08..417eb1da 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift @@ -52,8 +52,35 @@ public class _DependencyPropertyWrapper { // swiftlint:disable:this type_ self.dependencies = dependencies } + fileprivate convenience init(_ context: DependencyContext) { + self.init(DependencyCollection(context)) + } + + /// Create an optional dependency. + /// - Parameter dependencyType: The wrapped type of the optional dependency. + public convenience init(_ dependencyType: T.Type) where Value == T?, T: Module { + self.init(DependencyCollection(DependencyContext(for: T.self, type: .optional))) + } + + /// Create an optional dependency with a default value. + /// - Parameters: + /// - dependencyType: The wrapped type of the optional dependency. + /// - defaultValue: The default value that is used if no instance was supplied otherwise. + public convenience init( + wrappedValue defaultValue: @escaping @autoclosure () -> T, + _ dependencyType: T.Type = T.self + ) where Value == T?, T: Module { + self.init(DependencyContext(for: T.self, type: .optional, defaultValue: defaultValue)) + } + /// Declare a dependency to a module that can provide a default value on its own. - public convenience init() where Value: Module & DefaultInitializable { + @available( + *, deprecated, renamed: "init(_:)", + message: "Please explicitly specify the Module type." + ) + public convenience init() where Value: DefaultInitializable & Module { + // we probably want to remove this init in the next major release + // this init is placed here directly, otherwise Swift has problems resolving this init self.init(wrappedValue: Value()) } @@ -83,6 +110,10 @@ extension _DependencyPropertyWrapper: SpeziPropertyWrapper { extension _DependencyPropertyWrapper: DependencyDeclaration { + var unsafeInjectedModules: [any Module] { + dependencies.unsafeInjectedModules + } + func dependencyRelation(to module: DependencyReference) -> DependencyRelation { dependencies.dependencyRelation(to: module) } @@ -91,8 +122,8 @@ extension _DependencyPropertyWrapper: DependencyDeclaration { dependencies.collect(into: dependencyManager) } - func inject(from dependencyManager: DependencyManager) { - dependencies.inject(from: dependencyManager) + func inject(from dependencyManager: DependencyManager, for module: any Module) { + dependencies.inject(from: dependencyManager, for: module) } func uninjectDependencies(notifying spezi: Spezi) { @@ -106,14 +137,30 @@ extension _DependencyPropertyWrapper: DependencyDeclaration { extension _DependencyPropertyWrapper: SingleModuleDependency where Value: Module { - /// Create a dependency and supply a default value. - /// - Parameter defaultValue: The default value to be used if there is no other instance configured. - public convenience init(wrappedValue defaultValue: @escaping @autoclosure () -> Value) { - self.init(DependencyCollection(DependencyContext(defaultValue: defaultValue))) + /// Create a required dependency. + /// + /// If the dependency conforms to ``DefaultInitializable`` a default value is automatically supplied, if the module is not found to be configured. + /// - Parameter dependencyType: The wrapped type of the dependency. + public convenience init(_ dependencyType: Value.Type) { + self.init(DependencyContext(for: Value.self, type: .required)) } + /// Create a required dependency with a default value. + /// - Parameters: + /// - dependencyType: The wrapped type of the dependency. + /// - defaultValue: The default value that is used if no instance was supplied otherwise. + public convenience init( + wrappedValue defaultValue: @escaping @autoclosure () -> Value, + _ dependencyType: Value.Type = Value.self + ) { + self.init(DependencyContext(for: Value.self, type: .required, defaultValue: defaultValue)) + } - func wrappedValue(as value: WrappedValue.Type) -> WrappedValue { + public convenience init(load dependency: Value, _ dependencyType: Value.Type = Value.self) { + self.init(DependencyContext(for: Value.self, type: .load, defaultValue: { dependency })) + } + + fileprivate func wrappedValue(as value: WrappedValue.Type) -> WrappedValue { dependencies.singleDependencyRetrieval() } } @@ -121,21 +168,13 @@ extension _DependencyPropertyWrapper: SingleModuleDependency where Value: Module extension _DependencyPropertyWrapper: OptionalModuleDependency where Value: AnyOptional, Value.Wrapped: Module { /// Create a empty, optional dependency. + @available(*, deprecated, renamed: "init(_:)", message: "Please specify the Wrapped type of your optional dependency as the first argument.") public convenience init() { - self.init(DependencyCollection(DependencyContext(for: Value.Wrapped.self))) - } - - /// Create a optional dependency but supplying a default value. - /// - /// This allows to dynamically build the dependency tree on runtime. - /// For example, you might only declare a dependency to a Module if a given runtime check succeeds. - /// - Parameter defaultValue: The default value to be used if declared. - public convenience init(wrappedValue defaultValue: @escaping @autoclosure () -> Value.Wrapped) { - self.init(DependencyCollection(DependencyContext(defaultValue: defaultValue))) + self.init(DependencyCollection(DependencyContext(for: Value.Wrapped.self, type: .optional))) } - func wrappedValue(as value: WrappedValue.Type) -> WrappedValue { + fileprivate func wrappedValue(as value: WrappedValue.Type) -> WrappedValue { guard let value = dependencies.singleOptionalDependencyRetrieval(for: Value.Wrapped.self) as? WrappedValue else { preconditionFailure("Failed to convert from Optional<\(Value.Wrapped.self)> to \(WrappedValue.self)") } @@ -145,10 +184,12 @@ extension _DependencyPropertyWrapper: OptionalModuleDependency where Value: AnyO extension _DependencyPropertyWrapper: ModuleArrayDependency where Value == [any Module] { + /// Initialize an empty collection of dependencies. + @_disfavoredOverload public convenience init() { self.init(DependencyCollection()) } - + /// Create a dependency from a ``DependencyCollection``. /// /// Creates the `@Dependency` property wrapper from an instantiated ``DependencyCollection``, @@ -163,7 +204,7 @@ extension _DependencyPropertyWrapper: ModuleArrayDependency where Value == [any /// class ExampleModule: Module { /// @Dependency var dependentModules: [any Module] /// - /// init(@SomeCustomDependencyBuilder _ dependencies: @Sendable () -> DependencyCollection) { + /// init(@SomeCustomDependencyBuilder _ dependencies: () -> DependencyCollection) { /// self._dependentModules = Dependency(using: dependencies()) /// } /// } @@ -184,7 +225,7 @@ extension _DependencyPropertyWrapper: ModuleArrayDependency where Value == [any } - func wrappedValue(as value: WrappedValue.Type) -> WrappedValue { + fileprivate func wrappedValue(as value: WrappedValue.Type) -> WrappedValue { guard let modules = dependencies.retrieveModules() as? WrappedValue else { preconditionFailure("\(WrappedValue.self) doesn't match expected type \(Value.self)") } diff --git a/Sources/Spezi/Dependencies/Property/Module+Dependencies.swift b/Sources/Spezi/Dependencies/Property/Module+Dependencies.swift index f4e78136..19e99fd4 100644 --- a/Sources/Spezi/Dependencies/Property/Module+Dependencies.swift +++ b/Sources/Spezi/Dependencies/Property/Module+Dependencies.swift @@ -10,22 +10,29 @@ extension Module { /// Define dependency to other `Module`s. /// - /// You can use this property wrapper inside your `Module` to define dependencies to other ``Module``s. + /// You can use the `@Dependency` property wrapper inside your ``Module`` to define dependencies to other `Module`s. /// - /// - Note: You can access the contents of `@Dependency` once your ``Module/configure()-5pa83`` method is called (e.g., it must not be used in the `init`). + /// - Note: You can access the contents of `@Dependency` once your ``Module/configure()-5pa83`` method is called. You cannot + /// access it within your `init`. /// - /// The below code sample demonstrates a simple, singular dependence on the `ExampleModuleDependency` module. + /// ### Required Dependencies + /// + /// The below code sample demonstrates a simple, singular dependence on the `ExampleModuleDependency` module. If the dependency + /// is not available (because the user didn't configure it), the application will crash at runtime. /// /// ```swift /// class ExampleModule: Module { - /// @Dependency var exampleDependency = ExampleModuleDependency() + /// @Dependency(ExampleModuleDependency.self) var exampleDependency /// } /// ``` /// - /// Some modules do not need a default value assigned to the property if they provide a default configuration and conform to ``DefaultInitializable``. + /// Some `Module`s can be used without explicit configuration, for example if they provide a default configuration by conforming to the + /// ``DefaultInitializable`` protocol. In those cases, you can provide a default value to the `@Dependency` property wrapper + /// that is used in case the user didn't configure the `Module` separately. + /// /// ```swift /// class ExampleModule: Module { - /// @Dependency var exampleDependency: ExampleModuleDependency + /// @Dependency var exampleDependency = ExampleModuleDependency() /// } /// ``` /// @@ -36,7 +43,7 @@ extension Module { /// /// ```swift /// class ExampleModule: Module { - /// @Dependency var exampleDependency: ExampleModuleDependency? + /// @Dependency(ExampleModuleDependency.self) var exampleDependency: ExampleModuleDependency? /// /// func configure() { /// if let exampleDependency { @@ -50,9 +57,13 @@ extension Module { /// /// In certain circumstances your list of dependencies might not be statically known. Instead you might want to generate /// your list of dependencies within the initializer, based on external factors. - /// The `@Dependency` property wrapper, allows you to define your dependencies using a result-builder-based appraoch. + /// The `@Dependency` property wrapper, allows you to define your dependencies using a result-builder-based approach or by creating + /// a ``DependencyCollection`` yourself. + /// + /// - Tip: If a collection of `Module`s is part of your `Module`'s configuration and you want to impose certain protocol requirements, + /// you can implement your own result builder using the ``DependencyCollectionBuilder`` protocol. Refer to the documentation for more information. /// - /// Below is a short code example that demonstrates this functionality. + /// Below is a short code example that demonstrates how to dynamically create a list of dependencies. /// /// ```swift /// class ExampleModule: Module { @@ -68,5 +79,7 @@ extension Module { /// } /// } /// ``` + /// + /// - Note: Use `Dependency/init(using:)` to initialize the `@Dependency` property wrapper using a ``DependencyCollection``. public typealias Dependency = _DependencyPropertyWrapper } diff --git a/Sources/Spezi/Module/ModuleBuilder.swift b/Sources/Spezi/Module/ModuleBuilder.swift index 2010c62d..df370e47 100644 --- a/Sources/Spezi/Module/ModuleBuilder.swift +++ b/Sources/Spezi/Module/ModuleBuilder.swift @@ -7,7 +7,7 @@ // -/// A function builder used to aggregate multiple ``Module``s +/// A function builder used to aggregate multiple `Modul``s @resultBuilder public enum ModuleBuilder { /// If declared, provides contextual type information for statement expressions to translate them into partial results. diff --git a/Sources/Spezi/Spezi.docc/Module/Module Dependency.md b/Sources/Spezi/Spezi.docc/Module/Module Dependency.md index c9d8f8b8..c2eaa23d 100644 --- a/Sources/Spezi/Spezi.docc/Module/Module Dependency.md +++ b/Sources/Spezi/Spezi.docc/Module/Module Dependency.md @@ -21,6 +21,9 @@ to ensure functionality of a dependency is available at configuration. > Note: Declaring a cyclic dependency will result in a runtime error. +Below is a example of declaring a dependence on the `ExampleModuleDependency` and additionally providing a default value that is used +in the case that the module wasn't already configured by the user (for more information, see section below). + ```swift class ExampleModule: Module { @Dependency var exampleModuleDependency = ExampleModuleDependency() @@ -36,7 +39,8 @@ section. If you declare a dependency to a `Module` that is not configured by the users (e.g., some underlying configuration `Module`s might not event be publicly accessible), is initialized with the instance that was passed to the ``Module/Dependency`` property wrapper. -- Tip: `Module`s can easily provide a default configuration by adopting the ``DefaultInitializable`` protocol. +- Tip: `Module`s can adopt the ``DefaultInitializable`` protocol to opt into being default configurable. This mandates the presence of a + default initializer. Below is a short code example. ```swift @@ -48,7 +52,8 @@ class ExampleModuleDependency: Module, DefaultInitializable { class ExampleModule: Module { - @Dependency var exampleModuleDependency: ExampleModuleDependency + // dependency that uses the default init, if module is not externally configured. + @Dependency var exampleModuleDependency = ExampleModuleDependency() } ``` diff --git a/Sources/Spezi/Spezi.docc/Spezi.md b/Sources/Spezi/Spezi.docc/Spezi.md index e832692d..66116d71 100644 --- a/Sources/Spezi/Spezi.docc/Spezi.md +++ b/Sources/Spezi/Spezi.docc/Spezi.md @@ -82,11 +82,8 @@ You can learn more about the ``Standard`` protocol and when it is advised to cre A ``Module`` defines a software subsystem providing distinct and reusable functionality. Modules can use the constraint mechanism to enforce a set of requirements to the standard used in the Spezi-based software where the module is used. Modules also define dependencies on each other to reuse functionality and can communicate with other modules by offering and collecting information. -They can also conform to different protocols to provide additional access to Spezi features, such as lifecycle management and triggering view updates in SwiftUI using the observable mechanisms in Swift. You can learn more about modules in the documentation. -To simplify the creation of modules, a common set of functionalities typically used by modules is summarized in the ``Module`` protocol, making it an easy one-stop solution to support all these different functionalities and build a capable Spezi module. - ## Topics diff --git a/Sources/Spezi/Spezi/Spezi.swift b/Sources/Spezi/Spezi/Spezi.swift index 06905844..c7d9e318 100644 --- a/Sources/Spezi/Spezi/Spezi.swift +++ b/Sources/Spezi/Spezi/Spezi.swift @@ -102,9 +102,15 @@ public final class Spezi: Sendable { /// /// Any changes to this property will cause a complete re-render of the SwiftUI view hierarchy. See `SpeziViewModifier`. @MainActor var viewModifiers: [any ViewModifier] { - _viewModifiers.reduce(into: []) { partialResult, entry in - partialResult.append(entry.value) - } + _viewModifiers + // View modifiers of inner-most modules are added first due to the dependency order. + // However, we want view modifiers of dependencies to be available for inside view modifiers of the parent + // (e.g., ViewModifier should be able to require the @Environment(...) value of the @Dependency). + // This is why we need to reverse the order here. + .reversed() + .reduce(into: []) { partialResult, entry in + partialResult.append(entry.value) + } } /// A collection of ``Spezi/Spezi`` `LifecycleHandler`s. @@ -117,25 +123,25 @@ public final class Spezi: Sendable { """ ) @_spi(Spezi) - public var lifecycleHandler: [LifecycleHandler] { + @MainActor public var lifecycleHandler: [LifecycleHandler] { modules.compactMap { module in module as? LifecycleHandler } } - var notificationTokenHandler: [NotificationTokenHandler] { + @MainActor var notificationTokenHandler: [NotificationTokenHandler] { modules.compactMap { module in module as? NotificationTokenHandler } } - - var notificationHandler: [NotificationHandler] { + + @MainActor var notificationHandler: [NotificationHandler] { modules.compactMap { module in module as? NotificationHandler } } - var modules: [any Module] { + @MainActor var modules: [any Module] { storage.collect(allOf: (any AnyStoredModules).self) .reduce(into: []) { partialResult, modules in partialResult.append(contentsOf: modules.anyModules) @@ -177,8 +183,10 @@ public final class Spezi: Sendable { ) { self.standard = standard self.storage = consume storage - - self.loadModules([self.standard] + modules, ownership: .spezi) + + self.loadModules(modules, ownership: .spezi) + // load standard separately, such that all module loading takes precedence + self.loadModule(standard, ownership: .spezi) } /// Load a new Module. @@ -228,7 +236,7 @@ public final class Spezi: Sendable { for module in dependencyManager.initializedModules { if requestedModules.contains(ModuleReference(module)) { - // the policy only applies to the request modules, all other are always managed and owned by Spezi + // the policy only applies to the requested modules, all other are always managed and owned by Spezi self.initModule(module, ownership: ownership) } else { self.initModule(module, ownership: .spezi) @@ -315,20 +323,20 @@ public final class Spezi: Sendable { module.storeWeakly(into: self) } - let modifierEntires = 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 { - _viewModifiers.merge(modifierEntires) { _, rhs in - rhs - } - } - // 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) + } + } } } @@ -337,6 +345,7 @@ public final class Spezi: Sendable { keyPath == \.logger // loggers are created per Module. } + @MainActor private func retrieveDependingModules(_ dependency: DependencyReference, considerOptionals: Bool) -> [any Module] { var result: [any Module] = [] diff --git a/Sources/Spezi/Spezi/SpeziAppDelegate.swift b/Sources/Spezi/Spezi/SpeziAppDelegate.swift index 8fe4088a..23363f03 100644 --- a/Sources/Spezi/Spezi/SpeziAppDelegate.swift +++ b/Sources/Spezi/Spezi/SpeziAppDelegate.swift @@ -164,7 +164,7 @@ open class SpeziAppDelegate: NSObject, ApplicationDelegate, Sendable { return .noData } - let result: Set = await withTaskGroup(of: BackgroundFetchResult.self) { group in + let result: Set = await withTaskGroup(of: BackgroundFetchResult.self) { @MainActor group in for handler in handlers { group.addTask { @MainActor in await handler.receiveRemoteNotification(userInfo) @@ -172,9 +172,9 @@ open class SpeziAppDelegate: NSObject, ApplicationDelegate, Sendable { } var result: Set = [] - while let next = await group.next() { + for await fetchResult in group { // don't ask why, but the `for in` or `reduce(into:_:)` versions trigger Swift 6 concurrency warnings, this one doesn't - result.insert(next) + result.insert(fetchResult) } return result } diff --git a/Sources/Spezi/Spezi/SpeziNotificationCenterDelegate.swift b/Sources/Spezi/Spezi/SpeziNotificationCenterDelegate.swift index 9c10a291..b44bd1dd 100644 --- a/Sources/Spezi/Spezi/SpeziNotificationCenterDelegate.swift +++ b/Sources/Spezi/Spezi/SpeziNotificationCenterDelegate.swift @@ -6,12 +6,20 @@ // SPDX-License-Identifier: MIT // -@preconcurrency import UserNotifications +import UserNotifications -class SpeziNotificationCenterDelegate: NSObject, UNUserNotificationCenterDelegate { +class SpeziNotificationCenterDelegate: NSObject { #if !os(tvOS) + @MainActor func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { + // This method HAS to run on the Main Actor. + // This method is generated through Objective-C interoperability, and is originally defined with a completion handler. + // The completion handler MUST be called from the main thread (as this method is called on the main thread). + // However, if you do not annotate with @MainActor, an async method will be executed on the background thread. + // 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 @@ -31,6 +39,7 @@ class SpeziNotificationCenterDelegate: NSObject, UNUserNotificationCenterDelegat } #endif + @MainActor func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification @@ -42,7 +51,7 @@ class SpeziNotificationCenterDelegate: NSObject, UNUserNotificationCenterDelegat } for handler in delegate.spezi.notificationHandler { - group.addTask { @MainActor in + group.addTask { @Sendable @MainActor in await handler.receiveIncomingNotification(notification) } } @@ -50,13 +59,12 @@ class SpeziNotificationCenterDelegate: NSObject, UNUserNotificationCenterDelegat var hasSpecified = false var unionOptions: UNNotificationPresentationOptions = [] - while let options = await group.next() { + for await options in group { guard let options else { continue } - - hasSpecified = true unionOptions.formUnion(options) + hasSpecified = true } if hasSpecified { @@ -69,6 +77,13 @@ class SpeziNotificationCenterDelegate: NSObject, UNUserNotificationCenterDelegat } +#if compiler(<6) +extension SpeziNotificationCenterDelegate: UNUserNotificationCenterDelegate {} +#else +extension SpeziNotificationCenterDelegate: @preconcurrency UNUserNotificationCenterDelegate {} +#endif + + extension SpeziAppDelegate { func setupNotificationDelegate() { guard !spezi.notificationHandler.isEmpty else { diff --git a/Tests/SpeziTests/CapabilityTests/LifecycleTests/LifecycleTests.swift b/Tests/SpeziTests/CapabilityTests/LifecycleTests.swift similarity index 100% rename from Tests/SpeziTests/CapabilityTests/LifecycleTests/LifecycleTests.swift rename to Tests/SpeziTests/CapabilityTests/LifecycleTests.swift diff --git a/Tests/SpeziTests/CapabilityTests/Communication/ModuleCommunicationTests.swift b/Tests/SpeziTests/CapabilityTests/ModuleCommunicationTests.swift similarity index 100% rename from Tests/SpeziTests/CapabilityTests/Communication/ModuleCommunicationTests.swift rename to Tests/SpeziTests/CapabilityTests/ModuleCommunicationTests.swift diff --git a/Tests/SpeziTests/CapabilityTests/Notifications/NotificationsTests.swift b/Tests/SpeziTests/CapabilityTests/NotificationsTests.swift similarity index 100% rename from Tests/SpeziTests/CapabilityTests/Notifications/NotificationsTests.swift rename to Tests/SpeziTests/CapabilityTests/NotificationsTests.swift diff --git a/Tests/SpeziTests/CapabilityTests/ViewModifierTests/ViewModifierTests.swift b/Tests/SpeziTests/CapabilityTests/ViewModifierTests.swift similarity index 100% rename from Tests/SpeziTests/CapabilityTests/ViewModifierTests/ViewModifierTests.swift rename to Tests/SpeziTests/CapabilityTests/ViewModifierTests.swift diff --git a/Tests/SpeziTests/DependenciesTests/DependencyBuilderTests.swift b/Tests/SpeziTests/DependenciesTests/DependencyBuilderTests.swift index da8a3fb2..734a5b00 100644 --- a/Tests/SpeziTests/DependenciesTests/DependencyBuilderTests.swift +++ b/Tests/SpeziTests/DependenciesTests/DependencyBuilderTests.swift @@ -16,8 +16,8 @@ private final class ExampleDependentModule: ExampleTypeConstraint {} @resultBuilder private enum ExampleDependencyBuilder: DependencyCollectionBuilder { /// An auto-closure expression, providing the default dependency value, building the ``DependencyCollection``. - static func buildExpression(_ expression: @escaping @autoclosure () -> L) -> DependencyCollection { - DependencyCollection(singleEntry: expression) + static func buildExpression(_ expression: L) -> DependencyCollection { + DependencyCollection(expression) } } @@ -25,15 +25,37 @@ class ExampleDependencyModule: Module { @Dependency var dependencies: [any Module] - init( - @ExampleDependencyBuilder _ dependencies: () -> DependencyCollection - ) { + init(@ExampleDependencyBuilder _ dependencies: () -> DependencyCollection) { self._dependencies = Dependency(using: dependencies()) } } final class DependencyBuilderTests: XCTestCase { + @MainActor + func testDependencyCollection() { + var collection = DependencyCollection(ExampleDependentModule()) + XCTAssertEqual(collection.count, 1) + XCTAssertFalse(collection.isEmpty) + + collection.append(ExampleDependentModule()) + + XCTAssertEqual(collection.count, 2) + } + + @available(*, deprecated, message: "Propagate deprecation warning.") + @MainActor + func testDeprecatedInits() { + let collection1 = DependencyCollection(singleEntry: ExampleDependentModule()) + let collection2 = DependencyCollection(singleEntry: { + ExampleDependentModule() + }) + + XCTAssertEqual(collection1.count, 1) + XCTAssertEqual(collection2.count, 1) + } + + @MainActor func testDependencyBuilder() throws { let module = ExampleDependencyModule { diff --git a/Tests/SpeziTests/DependenciesTests/DependencyContextTests.swift b/Tests/SpeziTests/DependenciesTests/DependencyContextTests.swift index f03e248e..572afaf7 100644 --- a/Tests/SpeziTests/DependenciesTests/DependencyContextTests.swift +++ b/Tests/SpeziTests/DependenciesTests/DependencyContextTests.swift @@ -14,7 +14,7 @@ private final class ExampleModule: Module {} final class DependencyContextTests: XCTestCase { func testInjectionPreconditionDependencyPropertyWrapper() throws { try XCTRuntimePrecondition { - _ = _DependencyPropertyWrapper(wrappedValue: TestModule()).wrappedValue + _ = _DependencyPropertyWrapper(wrappedValue: TestModule(), TestModule.self).wrappedValue } } diff --git a/Tests/SpeziTests/DependenciesTests/DependencyTests.swift b/Tests/SpeziTests/DependenciesTests/DependencyTests.swift index 41dc794a..66974574 100644 --- a/Tests/SpeziTests/DependenciesTests/DependencyTests.swift +++ b/Tests/SpeziTests/DependenciesTests/DependencyTests.swift @@ -16,7 +16,7 @@ private final class TestModule1: Module { let deinitExpectation: XCTestExpectation @Dependency var testModule2 = TestModule2() - @Dependency var testModule3: TestModule3 + @Dependency(TestModule3.self) var testModule3 @Provide var num: Int = 1 @Provide var nums: [Int] = [9, 10] @@ -42,7 +42,7 @@ private final class TestModuleX: Module { private final class TestModule2: Module { @Dependency var testModule4 = TestModule4() @Dependency var testModule5 = TestModule5() - @Dependency var testModule3: TestModule3 + @Dependency(TestModule3.self) var testModule3 @Provide var num: Int = 2 } @@ -86,7 +86,7 @@ private final class TestModule7: Module { private final class OptionalModuleDependency: Module { - @Dependency var testModule3: TestModule3? + @Dependency(TestModule3.self) var testModule3: TestModule3? @Collect var nums: [Int] } @@ -100,7 +100,7 @@ private final class AllPropertiesModule: Module { } } - @Dependency var testModule3: TestModule3 + @Dependency(TestModule3.self) var testModule3 @Application(\.logger) var logger @Application(\.spezi) var spezi @Collect var nums: [Int] @@ -111,7 +111,7 @@ private final class AllPropertiesModule: Module { } private final class OptionalDependencyWithRuntimeDefault: Module { - @Dependency var testModule3: TestModule3? + @Dependency(TestModule3.self) var testModule3: TestModule3? init(defaultValue: Int?) { if let defaultValue { @@ -121,29 +121,88 @@ private final class OptionalDependencyWithRuntimeDefault: Module { } private final class TestModule8: Module { - @Dependency var testModule1: TestModule1? + @Dependency(TestModule1.self) var testModule1: TestModule1? init() {} } private final class SimpleOptionalModuleDependency: Module { + @Dependency(TestModule6.self) var testModule6: TestModule6? +} + + +private final class ModuleWithRequiredDependency: Module { + final class NestedX: Module, DefaultInitializable { + @Dependency var testModuleX = TestModuleX(12) + + @Dependency var testModule6 = TestModule6() + + init() {} + } + + @Dependency(TestModule6.self) var testModule6 // either specified from the outside, or it takes the default value from the NestedX + + @Dependency(NestedX.self) var nestedX + @Dependency(TestModuleX.self) var testModuleX: TestModuleX // see in init! + + + init() { + // test that we are searching breadth first + _testModuleX = Dependency(load: TestModuleX(42)) + } +} + + +private final class InjectionOfOptionalDefaultValue: Module { + final class NestedX: Module { + @Dependency var testModuleX = TestModuleX(23) + } + @Dependency(TestModuleX.self) var testModuleX: TestModuleX? // make sure optional dependencies get injected with the default value! + @Dependency var nested = NestedX() + + init() {} +} + +private final class TestModuleCircle1: Module { + @Dependency var modules: [any Module] + + init() {} + + init(module: M) { + self._modules = Dependency { + module + } + } +} + +private final class TestModuleCircle2: Module { + @Dependency var module = TestModuleCircle1() +} + + +// Test that deprecated declaration still compile as expected +@available(*, deprecated, message: "Propagate deprecation warning") +private final class DeprecatedDeclarations: Module { + @Dependency var testModule3: TestModule3 @Dependency var testModule6: TestModule6? } +func getModule(_ module: M.Type = M.self, in modules: [any Module], file: StaticString = #filePath, line: UInt = #line) throws -> M { + // swiftlint:disable:previous function_default_parameter_at_end + try XCTUnwrap(modules.first(where: { $0 is M }) as? M, "Could not find module \(M.self) loaded. Available: \(modules)", file: file, line: line) +} + + final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_length @MainActor func testLoadingAdditionalDependency() throws { let spezi = Spezi(standard: DefaultStandard(), modules: [OptionalModuleDependency()]) var modules = spezi.modules - func getModule(_ module: M.Type = M.self) throws -> M { - try XCTUnwrap(modules.first(where: { $0 is M }) as? M) - } - XCTAssertEqual(modules.count, 2) - _ = try getModule(DefaultStandard.self) - var optionalModuleDependency: OptionalModuleDependency = try getModule() + _ = try getModule(DefaultStandard.self, in: modules) + var optionalModuleDependency: OptionalModuleDependency = try getModule(in: modules) XCTAssertNil(optionalModuleDependency.testModule3) @@ -151,9 +210,9 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le modules = spezi.modules XCTAssertEqual(modules.count, 3) - _ = try getModule(DefaultStandard.self) - optionalModuleDependency = try getModule() - var testModule3: TestModule3 = try getModule() + _ = try getModule(DefaultStandard.self, in: modules) + optionalModuleDependency = try getModule(in: modules) + var testModule3: TestModule3 = try getModule(in: modules) XCTAssert(optionalModuleDependency.testModule3 === testModule3) @@ -162,13 +221,13 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le modules = spezi.modules XCTAssertEqual(modules.count, 7) - _ = try getModule(DefaultStandard.self) - let testModule1: TestModule1 = try getModule() - let testModule2: TestModule2 = try getModule() - testModule3 = try getModule() - let testModule4: TestModule4 = try getModule() - let testModule5: TestModule5 = try getModule() - optionalModuleDependency = try getModule() + _ = try getModule(DefaultStandard.self, in: modules) + let testModule1: TestModule1 = try getModule(in: modules) + let testModule2: TestModule2 = try getModule(in: modules) + testModule3 = try getModule(in: modules) + let testModule4: TestModule4 = try getModule(in: modules) + let testModule5: TestModule5 = try getModule(in: modules) + optionalModuleDependency = try getModule(in: modules) XCTAssert(testModule4.testModule5 === testModule5) XCTAssert(testModule2.testModule5 === testModule5) @@ -218,7 +277,7 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertEqual(optionalModule.nums, [3]) spezi.loadModule(module1) - XCTAssertEqual(optionalModule.nums, [3, 5, 4, 2, 1, 9, 10, 11]) + XCTAssertEqual(Set(optionalModule.nums), Set([3, 5, 4, 2, 1, 9, 10, 11])) XCTAssertEqual(spezi.modules.count, 7) @@ -226,12 +285,8 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertEqual(optionalModule.nums, [3]) var modules = spezi.modules - func getModule(_ module: M.Type = M.self) throws -> M { - try XCTUnwrap(modules.first(where: { $0 is M }) as? M) - } - - let optionalModuleLoaded: OptionalModuleDependency = try getModule() - let module3Loaded: TestModule3 = try getModule() + let optionalModuleLoaded: OptionalModuleDependency = try getModule(in: modules) + let module3Loaded: TestModule3 = try getModule(in: modules) XCTAssertNil(modules.first(where: { $0 is TestModule1 })) XCTAssertNil(modules.first(where: { $0 is TestModule2 })) @@ -251,7 +306,7 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertNil(modules.first(where: { $0 is TestModule4 })) XCTAssertNil(modules.first(where: { $0 is TestModule5 })) - XCTAssertNil(try getModule(OptionalModuleDependency.self).testModule3) + XCTAssertNil(try getModule(OptionalModuleDependency.self, in: modules).testModule3) return spezi } @@ -277,7 +332,7 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le let spezi = Spezi(standard: DefaultStandard(), modules: [optionalModule, moduleX, module8]) spezi.loadModule(module1, ownership: .external) // LOAD AS EXTERNAL - XCTAssertEqual(optionalModule.nums, [5, 5, 4, 3, 2, 1, 9, 10, 11]) + XCTAssertEqual(Set(optionalModule.nums), Set([5, 5, 4, 3, 2, 1, 9, 10, 11])) // leaving this scope causes the module1 to deallocate and should automatically unload it from Spezi! XCTAssertEqual(spezi.modules.count, 9) @@ -317,12 +372,12 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertEqual(initializedModules.count, 7) _ = try XCTUnwrap(initializedModules[0] as? TestModule6) - let testModuleMock5 = try XCTUnwrap(initializedModules[1] as? TestModule5) - let testModuleMock4 = try XCTUnwrap(initializedModules[2] as? TestModule4) - let testModuleMock3 = try XCTUnwrap(initializedModules[3] as? TestModule3) - let testModuleMock2 = try XCTUnwrap(initializedModules[4] as? TestModule2) - let testModuleMock1 = try XCTUnwrap(initializedModules[5] as? TestModule1) - _ = try XCTUnwrap(initializedModules[6] as? TestModule7) + let testModuleMock5: TestModule5 = try getModule(in: initializedModules) + let testModuleMock4: TestModule4 = try getModule(in: initializedModules) + let testModuleMock3: TestModule3 = try getModule(in: initializedModules) + let testModuleMock2: TestModule2 = try getModule(in: initializedModules) + let testModuleMock1: TestModule1 = try getModule(in: initializedModules) + _ = try getModule(TestModule7.self, in: initializedModules) XCTAssert(testModuleMock4.testModule5 === testModuleMock5) XCTAssert(testModuleMock2.testModule5 === testModuleMock5) @@ -344,11 +399,11 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertEqual(initializedModules.count, 4) - let testModule5 = try XCTUnwrap(initializedModules[0] as? TestModule5) - let testModule4 = try XCTUnwrap(initializedModules[1] as? TestModule4) - let testModule3 = try XCTUnwrap(initializedModules[2] as? TestModule3) - let testModule2 = try XCTUnwrap(initializedModules[3] as? TestModule2) - + let testModule5: TestModule5 = try getModule(in: initializedModules) + let testModule4: TestModule4 = try getModule(in: initializedModules) + let testModule3: TestModule3 = try getModule(in: initializedModules) + let testModule2: TestModule2 = try getModule(in: initializedModules) + XCTAssert(testModule4.testModule5 === testModule5) XCTAssert(testModule2.testModule5 === testModule5) XCTAssert(testModule2.testModule4 === testModule4) @@ -366,10 +421,10 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertEqual(initializedModules.count, 3) - let testModule5 = try XCTUnwrap(initializedModules[0] as? TestModule5) - let testModule40 = try XCTUnwrap(initializedModules[1] as? TestModule4) - let testModule41 = try XCTUnwrap(initializedModules[2] as? TestModule4) - + let testModule5: TestModule5 = try getModule(in: initializedModules) + let testModule40 = try XCTUnwrap(initializedModules.compactMap { $0 as? TestModule4 }.first) + let testModule41 = try XCTUnwrap(initializedModules.compactMap { $0 as? TestModule4 }.last) + XCTAssert(testModule40 !== testModule41) XCTAssert(testModule40.testModule5 === testModule5) @@ -386,12 +441,12 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertEqual(initializedModules.count, 5) - let testModule5 = try XCTUnwrap(initializedModules[0] as? TestModule5) - let testModule4 = try XCTUnwrap(initializedModules[1] as? TestModule4) - let testModule3 = try XCTUnwrap(initializedModules[2] as? TestModule3) - let testModule20 = try XCTUnwrap(initializedModules[3] as? TestModule2) - let testModule21 = try XCTUnwrap(initializedModules[4] as? TestModule2) - + let testModule5: TestModule5 = try getModule(in: initializedModules) + let testModule4: TestModule4 = try getModule(in: initializedModules) + let testModule3: TestModule3 = try getModule(in: initializedModules) + let testModule20 = try XCTUnwrap(initializedModules.compactMap { $0 as? TestModule2 }.first) + let testModule21 = try XCTUnwrap(initializedModules.compactMap { $0 as? TestModule2 }.last) + XCTAssert(testModule4.testModule5 === testModule5) XCTAssert(testModule20 !== testModule21) @@ -508,14 +563,11 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le let spezi = Spezi(standard: DefaultStandard(), modules: [first, second, TestModule4()]) let modules = spezi.modules - func getModule(_ module: M.Type = M.self) throws -> M { - try XCTUnwrap(modules.first(where: { $0 is M }) as? M) - } XCTAssertEqual(modules.count, 4) // 3 modules + standard - _ = try getModule(DefaultStandard.self) + _ = try getModule(DefaultStandard.self, in: modules) - let testModule4 = try getModule(TestModule4.self) + let testModule4 = try getModule(TestModule4.self, in: modules) XCTAssertTrue(testModule4.testModule5 === first) } @@ -525,13 +577,10 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le let spezi = Spezi(standard: DefaultStandard(), modules: [SimpleOptionalModuleDependency()]) let modules = spezi.modules - func getModule(_ module: M.Type = M.self) throws -> M { - try XCTUnwrap(modules.first(where: { $0 is M }) as? M) - } XCTAssertEqual(modules.count, 2) - _ = try getModule(DefaultStandard.self) - let module = try getModule(SimpleOptionalModuleDependency.self) + _ = try getModule(DefaultStandard.self, in: modules) + let module = try getModule(SimpleOptionalModuleDependency.self, in: modules) XCTAssertNil(module.testModule6) @@ -562,6 +611,138 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertNotNil(module.testModule6) XCTAssertTrue(module.testModule6 === baseModule6) } + + @MainActor + func testModuleLoadingOrderAndRequiredModulesBehavior() throws { + let spezi = Spezi(standard: DefaultStandard(), modules: [ModuleWithRequiredDependency()]) + + let modules = spezi.modules + + let module: ModuleWithRequiredDependency = try getModule(in: modules) + + // This tests that dependencies declared on the "outside" take precedence. We test that we are doing a BFS. + XCTAssertEqual(module.testModuleX.numX, 42) + + // ensures that we are able to retrieve the required module (that the inner default value was injected) + _ = try getModule(TestModule6.self, in: modules) + _ = module.testModule6 + } + + @MainActor + func testInjectionOfOptionalDependencyWithDefaultValue() throws { + let spezi = Spezi(standard: DefaultStandard(), modules: [InjectionOfOptionalDefaultValue()]) + + let modules = spezi.modules + + let module: InjectionOfOptionalDefaultValue = try getModule(in: modules) + + let testX = try XCTUnwrap(module.testModuleX) + XCTAssertEqual(testX.numX, 23) + } + + @MainActor + func testModuleCircle1() throws { + let module2 = TestModuleCircle2() + let module1 = TestModuleCircle1(module: module2) + + try XCTRuntimePrecondition { + _ = DependencyManager.resolve([module1]) + } + } + + @MainActor + func testMissingRequiredModule() throws { + class Module1: Module { + @Dependency(TestModuleX.self) var module + } + + try XCTRuntimePrecondition { + _ = DependencyManager.resolve([Module1()]) + } + } + + @MainActor + func testConfigureCallOrder() throws { + class Order: Module, DefaultInitializable { + @MainActor + var order: [Int] = [] + + required init() {} + } + + class ModuleA: Module { + @Dependency(Order.self) + private var order + + func configure() { + order.order.append(1) + } + } + + class ModuleB: Module { + @Dependency(Order.self) + private var order + + @Dependency(ModuleA.self) + private var module = ModuleA() + + func configure() { + order.order.append(2) + } + } + + class ModuleC: Module { + @Dependency(Order.self) + private var order + + @Dependency(ModuleA.self) + private var moduleA + @Dependency(ModuleB.self) + private var moduleB + + func configure() { + order.order.append(3) + } + } + + class ModuleD: Module { + @Dependency(Order.self) + private var order + + @Dependency(ModuleC.self) + private var module + + func configure() { + order.order.append(4) + } + } + + let spezi = Spezi(standard: DefaultStandard(), modules: [ + ModuleC(), + ModuleB(), + ModuleD(), + ModuleD() + ]) + + let modules = spezi.modules + let order: Order = try getModule(in: modules) + + XCTAssertEqual(order.order, [1, 2, 3, 4, 4]) + } + + @available(*, deprecated, message: "Propagate deprecation warning") + @MainActor + func testDeprecatedInits() throws { + let spezi = Spezi(standard: DefaultStandard(), modules: [DeprecatedDeclarations()]) + + let modules = spezi.modules + + let module: DeprecatedDeclarations = try getModule(in: modules) + + XCTAssertNil(module.testModule6) + XCTAssertEqual(module.testModule3.state, 0) + XCTAssertEqual(module.testModule3.num, 3) + } } // swiftlint:disable:this file_length diff --git a/Tests/SpeziTests/DependenciesTests/DynamicDependenciesTests.swift b/Tests/SpeziTests/DependenciesTests/DynamicDependenciesTests.swift index f49c15ea..3ee6bee4 100644 --- a/Tests/SpeziTests/DependenciesTests/DynamicDependenciesTests.swift +++ b/Tests/SpeziTests/DependenciesTests/DynamicDependenciesTests.swift @@ -39,8 +39,10 @@ private enum DynamicDependenciesTestCase: CaseIterable { var expectedNumberOfModules: Int { switch self { - case .twoDependencies, .duplicatedDependencies: + case .twoDependencies: return 3 + case .duplicatedDependencies: + return 4 case .noDependencies: return 1 } diff --git a/Tests/UITests/TestApp/ModelTests/ModuleWithModel.swift b/Tests/UITests/TestApp/ModelTests/ModuleWithModel.swift index 081c6e02..ab763aee 100644 --- a/Tests/UITests/TestApp/ModelTests/ModuleWithModel.swift +++ b/Tests/UITests/TestApp/ModelTests/ModuleWithModel.swift @@ -24,12 +24,14 @@ class MyModel2 { } -private struct MyModifier: ViewModifier { +private struct MyModifier2: ViewModifier { @Environment(MyModel2.self) var model @Environment(ModuleWithModel.self) var module + nonisolated init() {} + func body(content: Content) -> some View { content .environment(\.customKey, model.message == "Hello World" && module.message == "MODEL") @@ -43,14 +45,9 @@ class ModuleWithModel: Module, EnvironmentAccessible { @Model var model = MyModel2(message: "Hello World") // ensure reordering happens, ViewModifier must be able to access the model from environment - @Modifier fileprivate var modifier: MyModifier + @Modifier fileprivate var modifier = MyModifier2() let message: String = "MODEL" - - @MainActor - init() { - modifier = MyModifier() // @MainActor isolated default values for property wrappers must currently be specified explicitly via isolated init - } } diff --git a/Tests/UITests/TestApp/ViewModifierTests/ModuleWithModifier.swift b/Tests/UITests/TestApp/ViewModifierTests/ModuleWithModifier.swift index cb0b5835..3a771a5f 100644 --- a/Tests/UITests/TestApp/ViewModifierTests/ModuleWithModifier.swift +++ b/Tests/UITests/TestApp/ViewModifierTests/ModuleWithModifier.swift @@ -20,17 +20,54 @@ class MyModel { } -private struct MyModifier: ViewModifier { +struct MyModifier: ViewModifier { + @Observable + class ModifierState { + @MainActor var appeared: Bool = false + + init() {} + } + + // We expect that a EnvironmentAccessible dependency is available inside the environment of a modifier + // that is placed in the parent module. + @Environment(ModuleB.self) private var module: ModuleB? + let model: MyModel + @State private var state = ModifierState() + + @MainActor private var alertBinding: Binding { + Binding { + module == nil && state.appeared + } set: { _ in + } + } + func body(content: Content) -> some View { content .environment(model) + .environment(state) + .alert("Test Failed", isPresented: alertBinding) { + } message: { + Text(verbatim: "ModuleB dependency was not available in the environment of the modifier of the parent!") + } } } +private class ModuleA: Module, DefaultInitializable { + required init() {} +} + +private class ModuleB: Module, EnvironmentAccessible { + @Dependency(ModuleA.self) private var module + + init() {} +} + class ModuleWithModifier: Module { + @Dependency(ModuleB.self) private var moduleB = ModuleB() + @Modifier fileprivate var modelModifier: MyModifier @MainActor diff --git a/Tests/UITests/TestApp/ViewModifierTests/ViewModifierTestView.swift b/Tests/UITests/TestApp/ViewModifierTests/ViewModifierTestView.swift index fd7d74f7..231186f0 100644 --- a/Tests/UITests/TestApp/ViewModifierTests/ViewModifierTestView.swift +++ b/Tests/UITests/TestApp/ViewModifierTests/ViewModifierTestView.swift @@ -15,7 +15,6 @@ import XCTSpezi class MyModelTestCase: TestAppTestCase { let model: MyModel - init(model: MyModel) { self.model = model } @@ -30,9 +29,17 @@ class MyModelTestCase: TestAppTestCase { struct ViewModifierTestView: View { @Environment(MyModel.self) var model + @Environment(MyModifier.ModifierState.self) + var state var body: some View { TestAppView(testCase: MyModelTestCase(model: model)) + .onAppear { + state.appeared = true + } + .onDisappear { + state.appeared = false + } } } diff --git a/Tests/UITests/TestAppUITests/LifecycleHandlerTests.swift b/Tests/UITests/TestAppUITests/LifecycleHandlerTests.swift index 06b8e8e8..28f860db 100644 --- a/Tests/UITests/TestAppUITests/LifecycleHandlerTests.swift +++ b/Tests/UITests/TestAppUITests/LifecycleHandlerTests.swift @@ -21,6 +21,8 @@ final class LifecycleHandlerTests: XCTestCase { app.launchArguments = ["--lifecycleTests"] app.launch() + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0)) + app.buttons["LifecycleHandler"].tap() XCTAssert(app.staticTexts["WillFinishLaunchingWithOptions: 1"].waitForExistence(timeout: 2)) @@ -43,9 +45,11 @@ final class LifecycleHandlerTests: XCTestCase { app.activate() #endif + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0)) + XCTAssert(app.staticTexts["WillFinishLaunchingWithOptions: 1"].waitForExistence(timeout: 2)) - XCTAssert(app.staticTexts["SceneWillEnterForeground: 2"].exists) - XCTAssert(app.staticTexts["SceneDidBecomeActive: 2"].exists) + XCTAssert(app.staticTexts["SceneWillEnterForeground: 2"].waitForExistence(timeout: 2)) + XCTAssert(app.staticTexts["SceneDidBecomeActive: 2"].waitForExistence(timeout: 2)) XCTAssert(app.staticTexts["SceneWillResignActive: 1"].exists) XCTAssert(app.staticTexts["SceneDidEnterBackground: 1"].exists) XCTAssert(app.staticTexts["ApplicationWillTerminate: 0"].exists) diff --git a/Tests/UITests/TestAppUITests/ModelTests.swift b/Tests/UITests/TestAppUITests/ModelTests.swift index 8cf5f543..0ee4056c 100644 --- a/Tests/UITests/TestAppUITests/ModelTests.swift +++ b/Tests/UITests/TestAppUITests/ModelTests.swift @@ -15,6 +15,8 @@ final class ModelTests: XCTestCase { let app = XCUIApplication() app.launch() + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0)) + app.buttons["Model"].tap() XCTAssert(app.staticTexts["Passed"].waitForExistence(timeout: 1)) diff --git a/Tests/UITests/TestAppUITests/ViewModifierTests.swift b/Tests/UITests/TestAppUITests/ViewModifierTests.swift index 73c53a5d..720506e1 100644 --- a/Tests/UITests/TestAppUITests/ViewModifierTests.swift +++ b/Tests/UITests/TestAppUITests/ViewModifierTests.swift @@ -14,9 +14,12 @@ final class ViewModifierTests: XCTestCase { func testViewModifierPropertyWrapper() throws { let app = XCUIApplication() app.launch() - + + XCTAssertTrue(app.wait(for: .runningForeground, timeout: 2.0)) + app.buttons["ViewModifier"].tap() - + + XCTAssertFalse(app.alerts["Test Failed"].waitForExistence(timeout: 1)) XCTAssert(app.staticTexts["Passed"].waitForExistence(timeout: 1)) } }