From fffb6916fb9df5e3b8aee9ecb9a8b591ba303461 Mon Sep 17 00:00:00 2001 From: Andreas Bauer Date: Thu, 11 Jul 2024 14:56:57 +0200 Subject: [PATCH] Swift 6 compatibility (#108) # Swift 6 compatibility ## :recycle: Current situation & Problem This PR fixes the swift concurrency flag. There weren't any concurrency warnings left when compiling with the latest Swift 6 toolchain that is part of Xcode 16 beta 3. Note, that Xcode 15 will trigger some warnings as some symbols aren't annotated as Sendable in the previous SDK. Further, some sendability checks changed. This PR changes some actor isolations of some types and interfaces. Most importantly, we add `@MainActor` isolation to the `Module/configure()` method. We found, that a lot of modules manipulate `@MainActor` isolated properties in their configure method (those that are relevant for UI components) but cannot do so without spawning a new Task, as the configure method previously didn't have `@MainActor` guarantees. Except for out-of-band module loading, `configure()` would have always been called on the Main Actor (through SwiftUI initialization). Making all module interactions to be isolated to `@MainActor` greatly simplifies some of our internal state handling. ## :gear: Release Notes * Fix swift strict concurrency flag. * Resolve some strict concurrency warnings. ### Breaking Changes * `Module/configure()` is now isolated to `@MainActor`. * `Spezi/loadModule(_:ownership:)` and `Spezi/unloadModule(_:)` are now isolated to `@MainActor`. ## :books: Documentation Minor documentation files. ## :white_check_mark: Testing Test target was updated to latest concurrency changes as well. ## :pencil: Code of Conduct & Contributing Guidelines By submitting creating this pull request, you agree to follow our [Code of Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md): - [x] I agree to follow the [Code of Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md). --- .github/workflows/build-and-test.yml | 21 ++++++ Package.swift | 4 +- .../ProvidePropertyWrapper.swift | 37 ++++++--- .../Communication/StorageValueCollector.swift | 2 + .../Communication/StorageValueProvider.swift | 2 + .../Lifecycle/LifecycleHandler.swift | 18 ++--- .../RegisterRemoteNotificationsAction.swift | 2 +- .../Observable/EnvironmentAccessible.swift | 4 +- .../Observable/ModelPropertyWrapper.swift | 11 ++- .../ModelModifierInitialization.swift | 27 ------- .../ModifierPropertyWrapper.swift | 10 ++- .../ViewModifierInitialization.swift | 22 ------ .../ViewModifier/ViewModifierProvider.swift | 8 +- .../ViewModifier/WrappedViewModifier.swift | 25 ------- .../Dependencies/DependencyManager.swift | 3 +- .../Module+DependencyRelation.swift | 2 +- .../Property/DependencyCollection.swift | 8 +- .../Property/DependencyContext.swift | 29 +++++-- .../Property/DependencyDeclaration.swift | 8 +- .../Property/DependencyPropertyWrapper.swift | 8 +- .../Wrapper/DependencyReference.swift | 29 +++++++ .../{ => Wrapper}/ModuleReference.swift | 9 ++- Sources/Spezi/Module/Module.swift | 8 +- .../KnowledgeSources/LaunchOptionsKey.swift | 7 +- Sources/Spezi/Spezi/Spezi+Preview.swift | 5 +- Sources/Spezi/Spezi/Spezi.swift | 75 ++++++++----------- Sources/Spezi/Spezi/SpeziAppDelegate.swift | 5 +- .../SpeziNotificationCenterDelegate.swift | 1 - .../Spezi/Spezi/SpeziPropertyWrapper.swift | 4 + Sources/Spezi/Spezi/View+Spezi.swift | 2 +- .../ModuleCommunicationTests.swift | 7 +- .../ViewModifierTests/ViewModifierTests.swift | 8 +- .../DependencyBuilderTests.swift | 1 + .../DependenciesTests/DependencyTests.swift | 48 +++++------- .../DynamicDependenciesTests.swift | 39 +--------- .../ModuleTests/ModuleBuilderTests.swift | 6 +- .../SpeziTests/ModuleTests/ModuleTests.swift | 10 +-- .../StandardConstraintTests.swift | 6 +- .../LifecycleHandlerTestModule.swift | 1 + .../TestApp/ModelTests/ModuleWithModel.swift | 9 ++- .../ModuleWithModifier.swift | 7 +- .../UITests/UITests.xcodeproj/project.pbxproj | 22 ------ 42 files changed, 272 insertions(+), 288 deletions(-) delete mode 100644 Sources/Spezi/Capabilities/ViewModifier/ModelModifierInitialization.swift delete mode 100644 Sources/Spezi/Capabilities/ViewModifier/ViewModifierInitialization.swift delete mode 100644 Sources/Spezi/Capabilities/ViewModifier/WrappedViewModifier.swift create mode 100644 Sources/Spezi/Dependencies/Wrapper/DependencyReference.swift rename Sources/Spezi/Dependencies/{ => Wrapper}/ModuleReference.swift (70%) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 48499587..457d3887 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -24,6 +24,16 @@ jobs: scheme: Spezi-Package resultBundle: Spezi-Package-iOS.xcresult artifactname: Spezi-Package-iOS.xcresult + buildandtest_ios_latest: + name: Build and Test Swift Package iOS Latest + uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 + with: + runsonlabels: '["macOS", "self-hosted"]' + scheme: Spezi-Package + xcodeversion: latest + swiftVersion: 6 + resultBundle: Spezi-Package-iOS-Latest.xcresult + artifactname: Spezi-Package-iOS-Latest.xcresult buildandtest_watchos: name: Build and Test Swift Package watchOS uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 @@ -69,6 +79,17 @@ jobs: scheme: TestApp resultBundle: TestApp-iOS.xcresult artifactname: TestApp-iOS.xcresult + buildandtestuitests_ios_latest: + name: Build and Test UI Tests iOS Latest + uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 + with: + runsonlabels: '["macOS", "self-hosted"]' + path: Tests/UITests + scheme: TestApp + xcodeversion: latest + swiftVersion: 6 + resultBundle: TestApp-iOS-Latest.xcresult + artifactname: TestApp-iOS-Latest.xcresult buildandtestuitests_visionos: name: Build and Test UI Tests visionOS uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2 diff --git a/Package.swift b/Package.swift index 2af3e524..f35423f9 100644 --- a/Package.swift +++ b/Package.swift @@ -12,9 +12,9 @@ import class Foundation.ProcessInfo import PackageDescription #if swift(<6) -let swiftConcurrency: SwiftSetting = .enableExperimentalFeature("SwiftConcurrency") +let swiftConcurrency: SwiftSetting = .enableExperimentalFeature("StrictConcurrency") #else -let swiftConcurrency: SwiftSetting = .enableUpcomingFeature("SwiftConcurrency") +let swiftConcurrency: SwiftSetting = .enableUpcomingFeature("StrictConcurrency") #endif diff --git a/Sources/Spezi/Capabilities/Communication/ProvidePropertyWrapper.swift b/Sources/Spezi/Capabilities/Communication/ProvidePropertyWrapper.swift index 18ab6e07..c024f023 100644 --- a/Sources/Spezi/Capabilities/Communication/ProvidePropertyWrapper.swift +++ b/Sources/Spezi/Capabilities/Communication/ProvidePropertyWrapper.swift @@ -16,7 +16,7 @@ import XCTRuntimeAssertions protocol CollectionBasedProvideProperty { func collectArrayElements>(into repository: inout Repository) - func clearValues() + func clearValues(isolated: Bool) } @@ -24,7 +24,7 @@ protocol CollectionBasedProvideProperty { protocol OptionalBasedProvideProperty { func collectOptional>(into repository: inout Repository) - func clearValues() + func clearValues(isolated: Bool) } @@ -69,7 +69,7 @@ public class _ProvidePropertyWrapper { deinit { - clear() + clear(isolated: false) } } @@ -143,15 +143,32 @@ extension _ProvidePropertyWrapper: StorageValueProvider { collected = true } + @MainActor func clear() { + clear(isolated: true) + } + + private func clear(isolated: Bool) { collected = false if let wrapperWithOptional = self as? OptionalBasedProvideProperty { - wrapperWithOptional.clearValues() + wrapperWithOptional.clearValues(isolated: isolated) } else if let wrapperWithArray = self as? CollectionBasedProvideProperty { - wrapperWithArray.clearValues() + wrapperWithArray.clearValues(isolated: isolated) + } else { + performClear(isolated: isolated, of: Value.self) + } + } + + private func performClear(isolated: Bool, of type: V.Type) { + if isolated { + MainActor.assumeIsolated { [spezi, id] in + spezi?.handleCollectedValueRemoval(for: id, of: type) + } } else { - spezi?.handleCollectedValueRemoval(for: id, of: Value.self) + Task { @MainActor [spezi, id] in + spezi?.handleCollectedValueRemoval(for: id, of: type) + } } } } @@ -162,8 +179,8 @@ extension _ProvidePropertyWrapper: CollectionBasedProvideProperty where Value: A repository.setValues(for: id, storedValue.unwrappedArray) } - func clearValues() { - spezi?.handleCollectedValueRemoval(for: id, of: Value.Element.self) + func clearValues(isolated: Bool) { + performClear(isolated: isolated, of: Value.Element.self) } } @@ -175,8 +192,8 @@ extension _ProvidePropertyWrapper: OptionalBasedProvideProperty where Value: Any } } - func clearValues() { - spezi?.handleCollectedValueRemoval(for: id, of: Value.Wrapped.self) + func clearValues(isolated: Bool) { + performClear(isolated: isolated, of: Value.Wrapped.self) } } diff --git a/Sources/Spezi/Capabilities/Communication/StorageValueCollector.swift b/Sources/Spezi/Capabilities/Communication/StorageValueCollector.swift index 8ec8c3ac..7588e1ea 100644 --- a/Sources/Spezi/Capabilities/Communication/StorageValueCollector.swift +++ b/Sources/Spezi/Capabilities/Communication/StorageValueCollector.swift @@ -16,6 +16,7 @@ import SpeziFoundation protocol StorageValueCollector: SpeziPropertyWrapper { /// This method is called to retrieve all the requested values from the given ``SpeziStorage`` repository. /// - Parameter repository: Provides access to the ``SpeziStorage`` repository for read access. + @MainActor func retrieve>(from repository: Repository) } @@ -25,6 +26,7 @@ extension Module { retrieveProperties(ofType: StorageValueCollector.self) } + @MainActor func injectModuleValues>(from repository: Repository) { for collector in storageValueCollectors { collector.retrieve(from: repository) diff --git a/Sources/Spezi/Capabilities/Communication/StorageValueProvider.swift b/Sources/Spezi/Capabilities/Communication/StorageValueProvider.swift index fde95cfb..e179c7e0 100644 --- a/Sources/Spezi/Capabilities/Communication/StorageValueProvider.swift +++ b/Sources/Spezi/Capabilities/Communication/StorageValueProvider.swift @@ -16,6 +16,7 @@ import SpeziFoundation protocol StorageValueProvider: SpeziPropertyWrapper { /// This method is called to collect all provided values into the given ``SpeziStorage`` repository. /// - Parameter repository: Provides access to the ``SpeziStorage`` repository. + @MainActor func collect>(into repository: inout Repository) } @@ -25,6 +26,7 @@ extension Module { retrieveProperties(ofType: StorageValueProvider.self) } + @MainActor func collectModuleValues>(into repository: inout Repository) { for provider in storageValueProviders { provider.collect(into: &repository) diff --git a/Sources/Spezi/Capabilities/Lifecycle/LifecycleHandler.swift b/Sources/Spezi/Capabilities/Lifecycle/LifecycleHandler.swift index f72cd1c7..3f62a705 100644 --- a/Sources/Spezi/Capabilities/Lifecycle/LifecycleHandler.swift +++ b/Sources/Spezi/Capabilities/Lifecycle/LifecycleHandler.swift @@ -138,28 +138,22 @@ public protocol LifecycleHandler { ) 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 + /// Empty default implementation. 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 + /// Empty default implementation. 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 + /// Empty default implementation. 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 + /// Empty default implementation. 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 + /// Empty default implementation. 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 + /// Empty default implementation. public func applicationWillTerminate(_ application: UIApplication) { } #endif } diff --git a/Sources/Spezi/Capabilities/Notifications/RegisterRemoteNotificationsAction.swift b/Sources/Spezi/Capabilities/Notifications/RegisterRemoteNotificationsAction.swift index 07df38e3..e0d3f426 100644 --- a/Sources/Spezi/Capabilities/Notifications/RegisterRemoteNotificationsAction.swift +++ b/Sources/Spezi/Capabilities/Notifications/RegisterRemoteNotificationsAction.swift @@ -10,7 +10,7 @@ import SpeziFoundation import SwiftUI -private final class RemoteNotificationContinuation: DefaultProvidingKnowledgeSource { +private final class RemoteNotificationContinuation: DefaultProvidingKnowledgeSource, Sendable { typealias Anchor = SpeziAnchor static let defaultValue = RemoteNotificationContinuation() diff --git a/Sources/Spezi/Capabilities/Observable/EnvironmentAccessible.swift b/Sources/Spezi/Capabilities/Observable/EnvironmentAccessible.swift index 19422a5d..5e2fd23e 100644 --- a/Sources/Spezi/Capabilities/Observable/EnvironmentAccessible.swift +++ b/Sources/Spezi/Capabilities/Observable/EnvironmentAccessible.swift @@ -32,7 +32,7 @@ public protocol EnvironmentAccessible: AnyObject, Observable {} extension EnvironmentAccessible { - var viewModifierInitialization: any ViewModifierInitialization { - ModelModifierInitialization(model: self) + @MainActor var viewModifier: any ViewModifier { + ModelModifier(model: self) } } diff --git a/Sources/Spezi/Capabilities/Observable/ModelPropertyWrapper.swift b/Sources/Spezi/Capabilities/Observable/ModelPropertyWrapper.swift index 8171c09f..ca64071b 100644 --- a/Sources/Spezi/Capabilities/Observable/ModelPropertyWrapper.swift +++ b/Sources/Spezi/Capabilities/Observable/ModelPropertyWrapper.swift @@ -50,7 +50,11 @@ public class _ModelPropertyWrapper { deinit { - clear() + // mirrors implementation of clear, however in a compiler proven, concurrency safe way (: + collected = false + Task { @MainActor [spezi, id] in + spezi?.handleViewModifierRemoval(for: id) + } } } @@ -61,6 +65,7 @@ extension _ModelPropertyWrapper: SpeziPropertyWrapper { spezi?.handleViewModifierRemoval(for: id) } + func inject(spezi: Spezi) { self.spezi = spezi } @@ -97,7 +102,7 @@ extension Module { extension _ModelPropertyWrapper: ViewModifierProvider { - var viewModifierInitialization: (any ViewModifierInitialization)? { + var viewModifier: (any ViewModifier)? { collected = true guard let storedValue else { @@ -105,7 +110,7 @@ extension _ModelPropertyWrapper: ViewModifierProvider { return nil } - return ModelModifierInitialization(model: storedValue) + return ModelModifier(model: storedValue) } var placement: ModifierPlacement { diff --git a/Sources/Spezi/Capabilities/ViewModifier/ModelModifierInitialization.swift b/Sources/Spezi/Capabilities/ViewModifier/ModelModifierInitialization.swift deleted file mode 100644 index a0f28bbe..00000000 --- a/Sources/Spezi/Capabilities/ViewModifier/ModelModifierInitialization.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// This source file is part of the Stanford Spezi open-source project -// -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - - -import SwiftUI - - -struct ModelModifierInitialization: ViewModifierInitialization, @unchecked Sendable { - // @uncheked Sendable is fine as we are never allowing to mutate the non-Sendable `Model`. - // The `Model` will be passed to the MainActor (and be accessible from the SwiftUI environment). Those interactions - // are out of scope and expected to be handled by the Application developer (typically Model will be fully MainActor isolated anyways). - // We just make sure with this wrapper that no interaction can happen till the Model arrives on the MainActor. - private let model: Model - - init(model: Model) { - self.model = model - } - - func initializeModifier() -> some ViewModifier { - ModelModifier(model: model) - } -} diff --git a/Sources/Spezi/Capabilities/ViewModifier/ModifierPropertyWrapper.swift b/Sources/Spezi/Capabilities/ViewModifier/ModifierPropertyWrapper.swift index c9b9828a..6bed4fbe 100644 --- a/Sources/Spezi/Capabilities/ViewModifier/ModifierPropertyWrapper.swift +++ b/Sources/Spezi/Capabilities/ViewModifier/ModifierPropertyWrapper.swift @@ -48,7 +48,11 @@ public class _ModifierPropertyWrapper { } deinit { - clear() + // mirrors implementation of clear, however in a compiler proven, concurrency safe way (: + collected = false + Task { @MainActor [spezi, id] in + spezi?.handleViewModifierRemoval(for: id) + } } } @@ -97,7 +101,7 @@ extension Module { extension _ModifierPropertyWrapper: ViewModifierProvider { - var viewModifierInitialization: (any ViewModifierInitialization)? { + var viewModifier: (any ViewModifier)? { collected = true guard let storedValue else { @@ -105,6 +109,6 @@ extension _ModifierPropertyWrapper: ViewModifierProvider { return nil } - return WrappedViewModifier(modifier: storedValue) + return storedValue } } diff --git a/Sources/Spezi/Capabilities/ViewModifier/ViewModifierInitialization.swift b/Sources/Spezi/Capabilities/ViewModifier/ViewModifierInitialization.swift deleted file mode 100644 index 9176656d..00000000 --- a/Sources/Spezi/Capabilities/ViewModifier/ViewModifierInitialization.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// This source file is part of the Stanford Spezi open-source project -// -// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - - -import SwiftUI - - -/// Capture the possibility to initialize a `ViewModifier`. -/// -/// With Swift 6, even the ViewModifier initializers are isolated to MainActor. Therefore, we need to delay initialization -/// of ViewModifiers to the point where we are on the MainActor. -protocol ViewModifierInitialization: Sendable { - associatedtype Modifier: ViewModifier - - @MainActor - func initializeModifier() -> Modifier -} diff --git a/Sources/Spezi/Capabilities/ViewModifier/ViewModifierProvider.swift b/Sources/Spezi/Capabilities/ViewModifier/ViewModifierProvider.swift index 60ff4099..08962aff 100644 --- a/Sources/Spezi/Capabilities/ViewModifier/ViewModifierProvider.swift +++ b/Sources/Spezi/Capabilities/ViewModifier/ViewModifierProvider.swift @@ -9,6 +9,7 @@ import OrderedCollections import SwiftUI + enum ModifierPlacement: Int, Comparable { case regular case outermost @@ -28,7 +29,7 @@ protocol ViewModifierProvider { /// The view modifier instance that should be injected into the SwiftUI view hierarchy. /// /// Does nothing if `nil` is provided. - var viewModifierInitialization: (any ViewModifierInitialization)? { get } + @MainActor var viewModifier: (any ViewModifier)? { get } /// Defines the placement order of this view modifier. /// @@ -48,13 +49,14 @@ extension ViewModifierProvider { extension Module { /// All SwiftUI `ViewModifier` the module wants to modify the global view hierarchy with. - var viewModifierEntires: [(UUID, any ViewModifierInitialization)] { + @MainActor + var viewModifierEntires: [(UUID, any ViewModifier)] { retrieveProperties(ofType: ViewModifierProvider.self) .sorted { lhs, rhs in lhs.placement < rhs.placement } .compactMap { provider in - guard let modifier = provider.viewModifierInitialization else { + guard let modifier = provider.viewModifier else { return nil } return (provider.id, modifier) diff --git a/Sources/Spezi/Capabilities/ViewModifier/WrappedViewModifier.swift b/Sources/Spezi/Capabilities/ViewModifier/WrappedViewModifier.swift deleted file mode 100644 index 87fdf484..00000000 --- a/Sources/Spezi/Capabilities/ViewModifier/WrappedViewModifier.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// This source file is part of the Stanford Spezi open-source project -// -// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import SwiftUI - - -struct WrappedViewModifier: ViewModifierInitialization, @unchecked Sendable { - // @uncheked Sendable is fine as we are never allowing to mutate the non-Sendable `Model` till it arrives on the MainActor. - // ViewModifiers must be instantiated on the MainActor and `initializedModifier()` will only release the Modifier once it arrives on the MainActor. - // So this is essentially just a storage to pass around the Modifier between different actors but guarantees that it never leaves the MainActor. - private let modifier: Modifier - - init(modifier: Modifier) { - self.modifier = modifier - } - - func initializeModifier() -> Modifier { - modifier - } -} diff --git a/Sources/Spezi/Dependencies/DependencyManager.swift b/Sources/Spezi/Dependencies/DependencyManager.swift index 94a71057..13a5b6ae 100644 --- a/Sources/Spezi/Dependencies/DependencyManager.swift +++ b/Sources/Spezi/Dependencies/DependencyManager.swift @@ -10,7 +10,8 @@ import XCTRuntimeAssertions /// Gather information about modules with dependencies. -public class DependencyManager { +@MainActor +public class DependencyManager: Sendable { /// Collection of already initialized modules. private let existingModules: [any Module] diff --git a/Sources/Spezi/Dependencies/Module+DependencyRelation.swift b/Sources/Spezi/Dependencies/Module+DependencyRelation.swift index eca22f1c..5ad24aa0 100644 --- a/Sources/Spezi/Dependencies/Module+DependencyRelation.swift +++ b/Sources/Spezi/Dependencies/Module+DependencyRelation.swift @@ -8,7 +8,7 @@ extension Module { - func dependencyRelation(to module: any Module) -> DependencyRelation { + func dependencyRelation(to module: DependencyReference) -> DependencyRelation { let relations = dependencyDeclarations.map { $0.dependencyRelation(to: module) } if relations.contains(.dependent) { diff --git a/Sources/Spezi/Dependencies/Property/DependencyCollection.swift b/Sources/Spezi/Dependencies/Property/DependencyCollection.swift index 24bde4b2..cebb9452 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyCollection.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyCollection.swift @@ -50,7 +50,7 @@ public struct DependencyCollection: DependencyDeclaration { } - func dependencyRelation(to module: any Module) -> DependencyRelation { + func dependencyRelation(to module: DependencyReference) -> DependencyRelation { let relations = entries.map { $0.dependencyRelation(to: module) } if relations.contains(.dependent) { @@ -81,6 +81,12 @@ public struct DependencyCollection: DependencyDeclaration { } } + func nonIsolatedUninjectDependencies(notifying spezi: Spezi) { + for entry in entries { + entry.nonIsolatedUninjectDependencies(notifying: spezi) + } + } + private func singleDependencyContext() -> AnyDependencyContext { guard let dependency = entries.first else { preconditionFailure("DependencyCollection unexpectedly empty!") diff --git a/Sources/Spezi/Dependencies/Property/DependencyContext.swift b/Sources/Spezi/Dependencies/Property/DependencyContext.swift index 54a5bc36..11f9fe0e 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyContext.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyContext.swift @@ -17,11 +17,12 @@ protocol AnyDependencyContext: DependencyDeclaration { class DependencyContext: AnyDependencyContext { - private enum Storage { + @MainActor + private enum StorageReference: Sendable { case dependency(Dependency) case weakDependency(WeaklyStoredModule) - var value: Dependency? { + nonisolated var value: Dependency? { switch self { case let .dependency(module): return module @@ -32,7 +33,7 @@ class DependencyContext: AnyDependencyContext { } let defaultValue: (() -> Dependency)? - private var injectedDependency: Storage? + private var injectedDependency: StorageReference? var isOptional: Bool { @@ -56,10 +57,8 @@ class DependencyContext: AnyDependencyContext { self.defaultValue = defaultValue } - func dependencyRelation(to module: any Module) -> DependencyRelation { - let type = type(of: module) - - guard type == Dependency.self else { + func dependencyRelation(to module: DependencyReference) -> DependencyRelation { + guard module.sameType(as: Dependency.self) else { return .unrelated } @@ -92,7 +91,21 @@ class DependencyContext: AnyDependencyContext { injectedDependency = nil if let dependency { - spezi.handleDependencyUninjection(dependency) + spezi.handleDependencyUninjection(of: dependency) + } + } + + func nonIsolatedUninjectDependencies(notifying spezi: Spezi) { + let injectedDependency = injectedDependency + self.injectedDependency = nil + + if let injectedDependency { + Task { @MainActor in + guard let dependency = injectedDependency.value else { + return + } + spezi.handleDependencyUninjection(of: dependency) + } } } diff --git a/Sources/Spezi/Dependencies/Property/DependencyDeclaration.swift b/Sources/Spezi/Dependencies/Property/DependencyDeclaration.swift index 2305d026..131d824d 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyDeclaration.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyDeclaration.swift @@ -25,17 +25,23 @@ protocol DependencyDeclaration { var injectedDependencies: [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:)`. + @MainActor func inject(from dependencyManager: DependencyManager) /// Remove all dependency injections. + @MainActor func uninjectDependencies(notifying spezi: Spezi) + /// Same as `uninjectDependencies` but called from the non-isolated deinit + func nonIsolatedUninjectDependencies(notifying spezi: Spezi) + /// Determine the dependency relationship to a given module. /// - Parameter module: The module to retrieve the dependency relationship for. /// - Returns: Returns the `DependencyRelation` - func dependencyRelation(to module: any Module) -> DependencyRelation + func dependencyRelation(to module: DependencyReference) -> DependencyRelation } diff --git a/Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift b/Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift index c0f53a28..c81ae2fe 100644 --- a/Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift +++ b/Sources/Spezi/Dependencies/Property/DependencyPropertyWrapper.swift @@ -62,7 +62,7 @@ public class _DependencyPropertyWrapper { // swiftlint:disable:this type_ guard let spezi = spezi else { return } - uninjectDependencies(notifying: spezi) + nonIsolatedUninjectDependencies(notifying: spezi) } } @@ -87,7 +87,7 @@ extension _DependencyPropertyWrapper: DependencyDeclaration { } - func dependencyRelation(to module: any Module) -> DependencyRelation { + func dependencyRelation(to module: DependencyReference) -> DependencyRelation { dependencies.dependencyRelation(to: module) } @@ -102,6 +102,10 @@ extension _DependencyPropertyWrapper: DependencyDeclaration { func uninjectDependencies(notifying spezi: Spezi) { dependencies.uninjectDependencies(notifying: spezi) } + + func nonIsolatedUninjectDependencies(notifying spezi: Spezi) { + dependencies.nonIsolatedUninjectDependencies(notifying: spezi) + } } diff --git a/Sources/Spezi/Dependencies/Wrapper/DependencyReference.swift b/Sources/Spezi/Dependencies/Wrapper/DependencyReference.swift new file mode 100644 index 00000000..4f5d8d30 --- /dev/null +++ b/Sources/Spezi/Dependencies/Wrapper/DependencyReference.swift @@ -0,0 +1,29 @@ +// +// 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 +// + + +struct DependencyReference: Hashable, Sendable { + let reference: ModuleReference + private let typeId: ObjectIdentifier + + init(_ module: M) { + self.reference = ModuleReference(module) + self.typeId = ObjectIdentifier(M.self) + } + + func sameType(as module: M.Type) -> Bool { + typeId == ObjectIdentifier(M.self) + } +} + + +extension Module { + var dependencyReference: DependencyReference { + DependencyReference(self) + } +} diff --git a/Sources/Spezi/Dependencies/ModuleReference.swift b/Sources/Spezi/Dependencies/Wrapper/ModuleReference.swift similarity index 70% rename from Sources/Spezi/Dependencies/ModuleReference.swift rename to Sources/Spezi/Dependencies/Wrapper/ModuleReference.swift index 8816ebd3..b2b4e63f 100644 --- a/Sources/Spezi/Dependencies/ModuleReference.swift +++ b/Sources/Spezi/Dependencies/Wrapper/ModuleReference.swift @@ -7,10 +7,17 @@ // -struct ModuleReference: Hashable { +struct ModuleReference: Hashable, Sendable { private let id: ObjectIdentifier init(_ module: any Module) { self.id = ObjectIdentifier(module) } } + + +extension Module { + var reference: ModuleReference { + ModuleReference(self) + } +} diff --git a/Sources/Spezi/Module/Module.swift b/Sources/Spezi/Module/Module.swift index 0d09e8b5..043f5dc7 100644 --- a/Sources/Spezi/Module/Module.swift +++ b/Sources/Spezi/Module/Module.swift @@ -12,15 +12,17 @@ import SpeziFoundation // note: detailed documentation is provided as an article extension in the DocC bundle /// A `Module` defines a software subsystem that can be configured as part of the ``SpeziAppDelegate/configuration``. public protocol Module: AnyObject, KnowledgeSource { - /// The ``Module/configure()-5pa83`` method is called on the initialization of the Spezi instance to perform a lightweight configuration of the module. + /// Called on the initialization of the Spezi instance to perform a lightweight configuration of the module. /// /// It is advised that longer setup tasks are done in an asynchronous task and started during the call of the configure method. + @MainActor func configure() } extension Module { - // A documentation for this method exists in the `Module` type which SwiftLint doesn't recognize. - // swiftlint:disable:next missing_docs + /// Empty configuration method. + /// + /// No operation. public func configure() {} } diff --git a/Sources/Spezi/Spezi/KnowledgeSources/LaunchOptionsKey.swift b/Sources/Spezi/Spezi/KnowledgeSources/LaunchOptionsKey.swift index bf49decb..eda1c38c 100644 --- a/Sources/Spezi/Spezi/KnowledgeSources/LaunchOptionsKey.swift +++ b/Sources/Spezi/Spezi/KnowledgeSources/LaunchOptionsKey.swift @@ -9,6 +9,7 @@ import SpeziFoundation import SwiftUI + @_spi(Spezi) public struct LaunchOptionsKey: DefaultProvidingKnowledgeSource { public typealias Anchor = SpeziAnchor @@ -23,9 +24,11 @@ public struct LaunchOptionsKey: DefaultProvidingKnowledgeSource { public typealias Value = [Never: Any] #endif + // Unsafe, non-isolated is fine as we have an empty dictionary. // We inherit the type from UIKit, Any is inherently unsafe and also contains objects which might not conform to sendable. - // The dictionary access itself is not unsafe and our empty default value isn't as well. - // So annotating it as non-isolated is fine and passing LaunchOptions Values around actor boundaries is specific to the application. + // Dealing with launch options in a safe way is up to the implementing Module to do so. Ideally we would make + // `Application/launchOptions` to be isolated to the MainActor. However, we can't really do that selectively with the @Application + // property wrapper. Most likely, you would interact with launch options in the `configure()` method which is @MainActor isolated. public static nonisolated(unsafe) let defaultValue: Value = [:] } diff --git a/Sources/Spezi/Spezi/Spezi+Preview.swift b/Sources/Spezi/Spezi/Spezi+Preview.swift index c0f563e8..69a81698 100644 --- a/Sources/Spezi/Spezi/Spezi+Preview.swift +++ b/Sources/Spezi/Spezi/Spezi+Preview.swift @@ -12,7 +12,8 @@ import XCTRuntimeAssertions /// Options to simulate behavior for a ``LifecycleHandler`` in cases where there is no app delegate like in Preview setups. -public enum LifecycleSimulationOptions: @unchecked Sendable { // see discussion in `LaunchOptionsKey` +@MainActor +public enum LifecycleSimulationOptions { /// Simulation is disabled. case disabled #if os(iOS) || os(visionOS) || os(tvOS) @@ -44,6 +45,7 @@ extension View { /// - simulateLifecycle: Options to simulate behavior for ``LifecycleHandler``s. Disabled by default. /// - modules: The ``Module``s used in the Spezi project. /// - Returns: The configured view using the Spezi framework. + @MainActor public func previewWith( standard: S, simulateLifecycle: LifecycleSimulationOptions = .disabled, @@ -84,6 +86,7 @@ extension View { /// - simulateLifecycle: Options to simulate behavior for ``LifecycleHandler``s. Disabled by default. /// - modules: The ``Module``s used in the Spezi project. /// - Returns: The configured view using the Spezi framework. + @MainActor public func previewWith( simulateLifecycle: LifecycleSimulationOptions = .disabled, @ModuleBuilder _ modules: () -> ModuleCollection diff --git a/Sources/Spezi/Spezi/Spezi.swift b/Sources/Spezi/Spezi/Spezi.swift index 230564e7..05ec20f3 100644 --- a/Sources/Spezi/Spezi/Spezi.swift +++ b/Sources/Spezi/Spezi/Spezi.swift @@ -8,7 +8,7 @@ import OrderedCollections -import os +import OSLog import SpeziFoundation import SwiftUI import XCTRuntimeAssertions @@ -119,28 +119,25 @@ struct WeaklyStoredModule: KnowledgeSource { /// - ``registerRemoteNotifications`` /// - ``unregisterRemoteNotifications`` @Observable -public class Spezi { +public final class Spezi: Sendable { static let logger = Logger(subsystem: "edu.stanford.spezi", category: "Spezi") - + @TaskLocal static var moduleInitContext: ModuleDescription? let standard: any Standard - /// Recursive lock for module loading. - private let lock = NSRecursiveLock() - /// A shared repository to store any `KnowledgeSource`s restricted to the ``SpeziAnchor``. /// /// Every `Module` automatically conforms to `KnowledgeSource` and is stored within this storage object. - fileprivate(set) var storage: SpeziStorage + fileprivate(set) nonisolated(unsafe) var storage: SpeziStorage // nonisolated, writes are all isolated to @MainActor, just reads are non-isolated /// Key is either a UUID for `@Modifier` or `@Model` property wrappers, or a `ModuleReference` for `EnvironmentAccessible` modifiers. - private var _viewModifiers: OrderedDictionary = [:] + @MainActor private var _viewModifiers: OrderedDictionary = [:] /// Array of all SwiftUI `ViewModifiers` collected using `_ModifierPropertyWrapper` from the configured ``Module``s. /// /// Any changes to this property will cause a complete re-render of the SwiftUI view hierarchy. See `SpeziViewModifier`. - var viewModifiers: [any ViewModifierInitialization] { + @MainActor var viewModifiers: [any ViewModifier] { _viewModifiers.reduce(into: []) { partialResult, entry in partialResult.append(entry.value) } @@ -159,7 +156,7 @@ public class Spezi { public var lifecycleHandler: [LifecycleHandler] { storage.collect(allOf: LifecycleHandler.self) } - + var notificationTokenHandler: [NotificationTokenHandler] { storage.collect(allOf: NotificationTokenHandler.self) } @@ -173,7 +170,7 @@ public class Spezi { + storage.collect(allOf: (any AnyWeaklyStoredModule).self).compactMap { $0.retrievePurgingIfNil(in: &storage) } } - private var implicitlyCreatedModules: Set { + @MainActor private var implicitlyCreatedModules: Set { get { storage[ImplicitlyCreatedModulesKey.self] } @@ -186,7 +183,8 @@ public class Spezi { } } - + + @MainActor convenience init(from configuration: Configuration, storage: consuming SpeziStorage = SpeziStorage()) { self.init(standard: configuration.standard, modules: configuration.modules.elements, storage: storage) } @@ -199,6 +197,7 @@ public class Spezi { /// - modules: The collection of modules to initialize. /// - storage: Optional, initial storage to inject. @_spi(Spezi) + @MainActor public init( standard: any Standard, modules: [any Module], @@ -226,18 +225,15 @@ public class Spezi { /// ## Topics /// ### Ownership /// - ``ModuleOwnership`` + @MainActor public func loadModule(_ module: any Module, ownership: ModuleOwnership = .spezi) { loadModules([module], ownership: ownership) } - + + @MainActor private func loadModules(_ modules: [any Module], ownership: ModuleOwnership) { precondition(Self.moduleInitContext == nil, "Modules cannot be loaded within the `configure()` method.") - lock.lock() - defer { - lock.unlock() - } - purgeWeaklyReferenced() let requestedModules = Set(modules.map { ModuleReference($0) }) @@ -283,14 +279,10 @@ public class Spezi { /// Unloading a Module will recursively unload its dependencies that were not loaded explicitly. /// /// - Parameter module: The Module to unload. + @MainActor public func unloadModule(_ module: any Module) { precondition(Self.moduleInitContext == nil, "Modules cannot be unloaded within the `configure()` method.") - lock.lock() - defer { - lock.unlock() - } - purgeWeaklyReferenced() guard module.isLoaded(in: self) else { @@ -299,7 +291,7 @@ public class Spezi { logger.debug("Unloading module \(type(of: module)) ...") - let dependents = retrieveDependingModules(module, considerOptionals: false) + let dependents = retrieveDependingModules(module.dependencyReference, considerOptionals: false) precondition(dependents.isEmpty, "Tried to unload Module \(type(of: module)) that is still required by peer Modules: \(dependents)") module.clearModule(from: self) @@ -321,7 +313,7 @@ public class Spezi { // pre-existing Modules. let dependencyManager = DependencyManager([], existing: modules) dependencyManager.resolve() - + module.clear() // automatically removes @Provide values and recursively unloads implicitly created modules } @@ -332,6 +324,7 @@ public class Spezi { /// - Parameters: /// - module: The module to initialize. /// - ownership: Define the type of ownership when loading the module. + @MainActor private func initModule(_ module: any Module, ownership: ModuleOwnership) { precondition(!module.isLoaded(in: self), "Tried to initialize Module \(type(of: module)) that was already loaded!") @@ -362,7 +355,7 @@ public class Spezi { 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.viewModifierInitialization + _viewModifiers[ModuleReference(module)] = observable.viewModifier } } } @@ -372,7 +365,7 @@ public class Spezi { keyPath == \.logger // loggers are created per Module. } - private func retrieveDependingModules(_ dependency: any Module, considerOptionals: Bool) -> [any Module] { + private func retrieveDependingModules(_ dependency: DependencyReference, considerOptionals: Bool) -> [any Module] { var result: [any Module] = [] for module in modules { @@ -391,30 +384,24 @@ public class Spezi { return result } - func handleDependencyUninjection(_ dependency: M) { - lock.lock() - defer { - lock.unlock() - } + @MainActor + func handleDependencyUninjection(of dependency: M) { + let dependencyReference = dependency.dependencyReference - guard implicitlyCreatedModules.contains(ModuleReference(dependency)) else { + guard implicitlyCreatedModules.contains(dependencyReference.reference) else { // we only recursively unload modules that have been created implicitly return } - guard retrieveDependingModules(dependency, considerOptionals: true).isEmpty else { + guard retrieveDependingModules(dependencyReference, considerOptionals: true).isEmpty else { return } unloadModule(dependency) } + @MainActor func handleCollectedValueRemoval(for id: UUID, of type: Value.Type) { - lock.lock() - defer { - lock.unlock() - } - var entries = storage[CollectedModuleValues.self] let removed = entries.removeValue(forKey: id) guard removed != nil else { @@ -428,12 +415,8 @@ public class Spezi { } } + @MainActor func handleViewModifierRemoval(for id: UUID) { - lock.lock() - defer { - lock.unlock() - } - if _viewModifiers[id] != nil { _viewModifiers.removeValue(forKey: id) } @@ -454,6 +437,7 @@ public class Spezi { extension Module { + @MainActor fileprivate func storeModule(into spezi: Spezi) { guard let value = self as? Value else { spezi.logger.warning("Could not store \(Self.self) in the SpeziStorage as the `Value` typealias was modified.") @@ -462,6 +446,7 @@ extension Module { spezi.storage[Self.self] = value } + @MainActor fileprivate func storeWeakly(into spezi: Spezi) { guard self is Value else { spezi.logger.warning("Could not store \(Self.self) in the SpeziStorage as the `Value` typealias was modified.") @@ -471,11 +456,13 @@ extension Module { spezi.storage[WeaklyStoredModule.self] = WeaklyStoredModule(self) } + @MainActor fileprivate func isLoaded(in spezi: Spezi) -> Bool { spezi.storage[Self.self] != nil || spezi.storage[WeaklyStoredModule.self]?.retrievePurgingIfNil(in: &spezi.storage) != nil } + @MainActor fileprivate func clearModule(from spezi: Spezi) { spezi.storage[Self.self] = nil spezi.storage[WeaklyStoredModule.self] = nil diff --git a/Sources/Spezi/Spezi/SpeziAppDelegate.swift b/Sources/Spezi/Spezi/SpeziAppDelegate.swift index 6f46a03b..8fe4088a 100644 --- a/Sources/Spezi/Spezi/SpeziAppDelegate.swift +++ b/Sources/Spezi/Spezi/SpeziAppDelegate.swift @@ -48,7 +48,8 @@ import SwiftUI /// /// The ``Module`` documentation provides more information about the structure of modules. /// Refer to the ``Configuration`` documentation to learn more about the Spezi configuration. -open class SpeziAppDelegate: NSObject, ApplicationDelegate { +@MainActor // need to be made explicit, macOS NSApplicationDelegate has @MainActor individually specified for each method +open class SpeziAppDelegate: NSObject, ApplicationDelegate, Sendable { private(set) static weak var appDelegate: SpeziAppDelegate? static var notificationDelegate: SpeziNotificationCenterDelegate? // swiftlint:disable:this weak_delegate @@ -172,7 +173,7 @@ open class SpeziAppDelegate: NSObject, ApplicationDelegate { var result: Set = [] while let next = await group.next() { - // don't ask why, but the for in or .reduce versions trigger Swift 6 concurrency warnings, this one doesn't + // don't ask why, but the `for in` or `reduce(into:_:)` versions trigger Swift 6 concurrency warnings, this one doesn't result.insert(next) } return result diff --git a/Sources/Spezi/Spezi/SpeziNotificationCenterDelegate.swift b/Sources/Spezi/Spezi/SpeziNotificationCenterDelegate.swift index 174809df..9c10a291 100644 --- a/Sources/Spezi/Spezi/SpeziNotificationCenterDelegate.swift +++ b/Sources/Spezi/Spezi/SpeziNotificationCenterDelegate.swift @@ -11,7 +11,6 @@ class SpeziNotificationCenterDelegate: NSObject, UNUserNotificationCenterDelegate { #if !os(tvOS) - func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { 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. diff --git a/Sources/Spezi/Spezi/SpeziPropertyWrapper.swift b/Sources/Spezi/Spezi/SpeziPropertyWrapper.swift index d29fc9d0..5060448e 100644 --- a/Sources/Spezi/Spezi/SpeziPropertyWrapper.swift +++ b/Sources/Spezi/Spezi/SpeziPropertyWrapper.swift @@ -13,9 +13,11 @@ protocol SpeziPropertyWrapper { /// This call happens right before ``Module/configure()-5pa83`` is called. /// An empty default implementation is provided. /// - Parameter spezi: The global ``Spezi/Spezi`` instance. + @MainActor func inject(spezi: Spezi) /// Clear the property wrapper state before the Module is unloaded. + @MainActor func clear() } @@ -26,12 +28,14 @@ extension SpeziPropertyWrapper { extension Module { + @MainActor func inject(spezi: Spezi) { for wrapper in retrieveProperties(ofType: SpeziPropertyWrapper.self) { wrapper.inject(spezi: spezi) } } + @MainActor func clear() { for wrapper in retrieveProperties(ofType: SpeziPropertyWrapper.self) { wrapper.clear() diff --git a/Sources/Spezi/Spezi/View+Spezi.swift b/Sources/Spezi/Spezi/View+Spezi.swift index 5801a446..f986399c 100644 --- a/Sources/Spezi/Spezi/View+Spezi.swift +++ b/Sources/Spezi/Spezi/View+Spezi.swift @@ -21,7 +21,6 @@ struct SpeziViewModifier: ViewModifier { func body(content: Content) -> some View { spezi.viewModifiers - .map { $0.initializeModifier() } .modify(content) } } @@ -31,6 +30,7 @@ extension View { /// Configure Spezi for your application using a delegate. /// - Parameter delegate: The ``SpeziAppDelegate`` used in the SwiftUI App instance. /// - Returns: The configured view using the Spezi framework. + @MainActor public func spezi(_ delegate: SpeziAppDelegate) -> some View { modifier(SpeziViewModifier(delegate.spezi)) } diff --git a/Tests/SpeziTests/CapabilityTests/Communication/ModuleCommunicationTests.swift b/Tests/SpeziTests/CapabilityTests/Communication/ModuleCommunicationTests.swift index fdca198c..fc0968f9 100644 --- a/Tests/SpeziTests/CapabilityTests/Communication/ModuleCommunicationTests.swift +++ b/Tests/SpeziTests/CapabilityTests/Communication/ModuleCommunicationTests.swift @@ -50,15 +50,17 @@ final class ModuleCommunicationTests: XCTestCase { } } - private static var provideModule = ProvideModule1() - private static var collectModule = CollectModule() + @MainActor private static var provideModule = ProvideModule1() + @MainActor private static var collectModule = CollectModule() + @MainActor override func setUp() { Self.provideModule = ProvideModule1() Self.collectModule = CollectModule() } + @MainActor func testSimpleCommunication() throws { let delegate = TestApplicationDelegate() _ = delegate.spezi // ensure init @@ -68,6 +70,7 @@ final class ModuleCommunicationTests: XCTestCase { XCTAssertEqual(Self.collectModule.strings, ["Hello World"]) } + @MainActor func testIllegalAccess() throws { let delegate = TestApplicationDelegate() diff --git a/Tests/SpeziTests/CapabilityTests/ViewModifierTests/ViewModifierTests.swift b/Tests/SpeziTests/CapabilityTests/ViewModifierTests/ViewModifierTests.swift index 063e5c1b..8cc39661 100644 --- a/Tests/SpeziTests/CapabilityTests/ViewModifierTests/ViewModifierTests.swift +++ b/Tests/SpeziTests/CapabilityTests/ViewModifierTests/ViewModifierTests.swift @@ -23,14 +23,14 @@ final class ViewModifierTests: XCTestCase { let modifiers = testApplicationDelegate.spezi.viewModifiers XCTAssertEqual(modifiers.count, 2) - print(modifiers) let message = modifiers - .compactMap { $0 as? WrappedViewModifier } - .map { $0.initializeModifier().message } + .compactMap { $0 as? TestViewModifier } + .map { $0.message } .joined(separator: " ") XCTAssertEqual(message, "Hello World") } - + + @MainActor func testEmptyRetrieval() { let speziAppDelegate = SpeziAppDelegate() XCTAssert(speziAppDelegate.spezi.viewModifiers.isEmpty) diff --git a/Tests/SpeziTests/DependenciesTests/DependencyBuilderTests.swift b/Tests/SpeziTests/DependenciesTests/DependencyBuilderTests.swift index 7bab8c8d..da8a3fb2 100644 --- a/Tests/SpeziTests/DependenciesTests/DependencyBuilderTests.swift +++ b/Tests/SpeziTests/DependenciesTests/DependencyBuilderTests.swift @@ -34,6 +34,7 @@ class ExampleDependencyModule: Module { final class DependencyBuilderTests: XCTestCase { + @MainActor func testDependencyBuilder() throws { let module = ExampleDependencyModule { ExampleDependentModule() diff --git a/Tests/SpeziTests/DependenciesTests/DependencyTests.swift b/Tests/SpeziTests/DependenciesTests/DependencyTests.swift index 6ff1cae8..4e575488 100644 --- a/Tests/SpeziTests/DependenciesTests/DependencyTests.swift +++ b/Tests/SpeziTests/DependenciesTests/DependencyTests.swift @@ -84,21 +84,6 @@ private final class TestModule7: Module { @Dependency var testModule1 = TestModule1() } -// Swift 6 compiler doesn't allow circular references (even when there is a property wrapper in between) -// Review this in future versions. -#if compiler(<6) -private final class TestModuleCircle1: Module { - @Dependency var testModuleCircle2 = TestModuleCircle2() -} -private final class TestModuleCircle2: Module { - @Dependency var testModuleCircle1 = TestModuleCircle1() -} - -private final class TestModuleItself: Module { - @Dependency var testModuleItself = TestModuleItself() -} -#endif - private final class OptionalModuleDependency: Module { @Dependency var testModule3: TestModule3? @@ -143,6 +128,7 @@ private final class TestModule8: Module { final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_length + @MainActor func testLoadingAdditionalDependency() throws { let spezi = Spezi(standard: DefaultStandard(), modules: [OptionalModuleDependency()]) @@ -191,6 +177,7 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssert(optionalModuleDependency.testModule3 === testModule3) } + @MainActor func testImpossibleUnloading() throws { let module3 = TestModule3() let spezi = Spezi(standard: DefaultStandard(), modules: [TestModule1(), module3]) @@ -201,6 +188,7 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le } } + @MainActor func testMultiLoading() throws { let module = AllPropertiesModule() let spezi = Spezi(standard: DefaultStandard(), modules: [module]) @@ -210,6 +198,7 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le spezi.unloadModule(module) } + @MainActor func testUnloadingDependencies() throws { func runUnloadingTests(deinitExpectation1: XCTestExpectation, deinitExpectation3: XCTestExpectation) throws -> Spezi { let optionalModule = OptionalModuleDependency() @@ -272,7 +261,8 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le wait(for: [deinitExpectation1, deinitExpectation3]) } - func testSelfManagedModules() throws { + @MainActor + func testSelfManagedModules() async throws { let optionalModule = OptionalModuleDependency() let moduleX = TestModuleX(5) let module8 = TestModule8() @@ -296,7 +286,8 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le let spezi = try runModuleTests(deinitExpectation: deinitExpectation) _ = spezi - print(spezi.modules) + try await Task.sleep(for: .milliseconds(50)) // deinit need to get back to MainActor + XCTAssertEqual(spezi.modules.count, 5) XCTAssertNil(module8.testModule1) // tests that optional @Dependency reference modules weakly @@ -310,6 +301,7 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le wait(for: [deinitExpectation]) } + @MainActor func testModuleDependencyChain() throws { let modules: [any Module] = [ TestModule6(), @@ -338,6 +330,7 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssert(testModuleMock1.testModule2.testModule4.testModule5 === testModuleMock5) } + @MainActor func testAlreadyInDependableModules() throws { let modules: [any Module] = [ TestModule2(), @@ -358,6 +351,7 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssert(testModule2.testModule3 === testModule3) } + @MainActor func testModuleDependencyMultipleTimes() throws { let modules: [any Module] = [ TestModule5(), @@ -378,6 +372,7 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssert(testModule41.testModule5 === testModule5) } + @MainActor func testModuleDependencyChainMultipleTimes() throws { let modules: [any Module] = [ TestModule2(), @@ -409,6 +404,7 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le } + @MainActor func testModuleNoDependency() throws { let modules: [any Module] = [TestModule5()] let initializedModules = DependencyManager.resolve(modules) @@ -418,6 +414,7 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le _ = try XCTUnwrap(initializedModules[0] as? TestModule5) } + @MainActor func testModuleNoDependencyMultipleTimes() throws { let modules: [any Module] = [ TestModule5(), @@ -433,18 +430,7 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le _ = try XCTUnwrap(initializedModules[2] as? TestModule5) } -#if compiler(<6) - func testModuleCycle() throws { - let modules: [any Module] = [ - TestModuleCircle1() - ] - - try XCTRuntimePrecondition { - _ = DependencyManager.resolve(modules) - } - } -#endif - + @MainActor func testOptionalDependenceNonPresent() throws { let nonPresent: [any Module] = [ OptionalModuleDependency() @@ -458,6 +444,7 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertNil(module.testModule3) } + @MainActor func testOptionalDependencePresent() throws { let nonPresent: [any Module] = [ OptionalModuleDependency(), @@ -473,6 +460,7 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssert(module.testModule3 === module3) } + @MainActor func testOptionalDependencyWithDynamicRuntimeDefaultValue() throws { let nonPresent = DependencyManager.resolve([ OptionalDependencyWithRuntimeDefault(defaultValue: nil) // stays optional @@ -508,5 +496,3 @@ final class DependencyTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertEqual(dut4Module.state, 4) } } - -// swiftlint:disable:this file_length diff --git a/Tests/SpeziTests/DependenciesTests/DynamicDependenciesTests.swift b/Tests/SpeziTests/DependenciesTests/DynamicDependenciesTests.swift index d0ddd909..f49c15ea 100644 --- a/Tests/SpeziTests/DependenciesTests/DynamicDependenciesTests.swift +++ b/Tests/SpeziTests/DependenciesTests/DynamicDependenciesTests.swift @@ -17,9 +17,6 @@ private enum DynamicDependenciesTestCase: CaseIterable { case twoDependencies case duplicatedDependencies case noDependencies -#if compiler(<6) - case dependencyCircle -#endif var dynamicDependencies: _DependencyPropertyWrapper<[any Module]> { @@ -37,13 +34,6 @@ private enum DynamicDependenciesTestCase: CaseIterable { } case .noDependencies: return .init() -#if compiler(<6) - case .dependencyCircle: - return .init { - TestModuleCircle1() - TestModuleCircle2() - } -#endif } } @@ -53,11 +43,6 @@ private enum DynamicDependenciesTestCase: CaseIterable { return 3 case .noDependencies: return 1 -#if compiler(<6) - case .dependencyCircle: - XCTFail("Should never be called!") - return -1 -#endif } } @@ -76,10 +61,6 @@ private enum DynamicDependenciesTestCase: CaseIterable { XCTAssert(testModule2 !== testModule3) case .noDependencies: XCTAssertEqual(modules.count, 0) -#if compiler(<6) - case .dependencyCircle: - XCTFail("Should never be called!") -#endif } } } @@ -104,32 +85,14 @@ private final class TestModule2: Module {} private final class TestModule3: Module {} -#if compiler(<6) -private final class TestModuleCircle1: Module { - @Dependency var testModuleCircle2 = TestModuleCircle2() -} - -private final class TestModuleCircle2: Module { - @Dependency var testModuleCircle1 = TestModuleCircle1() -} -#endif - final class DynamicDependenciesTests: XCTestCase { + @MainActor func testDynamicDependencies() throws { for dynamicDependenciesTestCase in DynamicDependenciesTestCase.allCases { let modules: [any Module] = [ TestModule1(dynamicDependenciesTestCase) ] - -#if compiler(<6) - guard dynamicDependenciesTestCase != .dependencyCircle else { - try XCTRuntimePrecondition { - _ = DependencyManager.resolve(modules) - } - return - } -#endif let initializedModules = DependencyManager.resolve(modules) XCTAssertEqual(initializedModules.count, dynamicDependenciesTestCase.expectedNumberOfModules) diff --git a/Tests/SpeziTests/ModuleTests/ModuleBuilderTests.swift b/Tests/SpeziTests/ModuleTests/ModuleBuilderTests.swift index 1a7782da..890065d5 100644 --- a/Tests/SpeziTests/ModuleTests/ModuleBuilderTests.swift +++ b/Tests/SpeziTests/ModuleTests/ModuleBuilderTests.swift @@ -77,7 +77,8 @@ final class ModuleBuilderTests: XCTestCase { return modules } - + + @MainActor func testModuleBuilderIf() throws { let expectations = Expectations(xctestCase: self) expectations.loopTestExpectation.expectedFulfillmentCount = 5 @@ -97,7 +98,8 @@ final class ModuleBuilderTests: XCTestCase { try expectations.wait() } - + + @MainActor func testModuleBuilderElse() throws { let expectations = Expectations(xctestCase: self) expectations.conditionalTestExpectation.isInverted = true diff --git a/Tests/SpeziTests/ModuleTests/ModuleTests.swift b/Tests/SpeziTests/ModuleTests/ModuleTests.swift index 15979b1c..4772c039 100644 --- a/Tests/SpeziTests/ModuleTests/ModuleTests.swift +++ b/Tests/SpeziTests/ModuleTests/ModuleTests.swift @@ -35,14 +35,14 @@ final class ModuleTests: XCTestCase { func testModuleFlow() throws { let expectation = XCTestExpectation(description: "Module") expectation.assertForOverFulfill = true - - _ = try XCTUnwrap( - Text("Spezi") - .spezi(TestApplicationDelegate(expectation: expectation)) as? ModifiedContent - ) + + _ = Text("Spezi") + .spezi(TestApplicationDelegate(expectation: expectation)) + wait(for: [expectation]) } + @MainActor func testSpezi() throws { let spezi = Spezi(standard: DefaultStandard(), modules: [DependingTestModule()]) diff --git a/Tests/SpeziTests/StandardTests/StandardConstraintTests.swift b/Tests/SpeziTests/StandardTests/StandardConstraintTests.swift index ab09ca1a..46a59ea8 100644 --- a/Tests/SpeziTests/StandardTests/StandardConstraintTests.swift +++ b/Tests/SpeziTests/StandardTests/StandardConstraintTests.swift @@ -52,15 +52,15 @@ final class StandardConstraintTests: XCTestCase { } } - + @MainActor func testStandardConstraint() async throws { let expectation = XCTestExpectation(description: "Module") expectation.assertForOverFulfill = true - let standardCTestApplicationDelegate = await StandardCTestApplicationDelegate( + let standardCTestApplicationDelegate = StandardCTestApplicationDelegate( expectation: expectation ) - _ = await standardCTestApplicationDelegate.spezi + _ = standardCTestApplicationDelegate.spezi await fulfillment(of: [expectation], timeout: 0.01) } diff --git a/Tests/UITests/TestApp/LifecycleHandler/LifecycleHandlerTestModule.swift b/Tests/UITests/TestApp/LifecycleHandler/LifecycleHandlerTestModule.swift index dd448442..b3ad0b9e 100644 --- a/Tests/UITests/TestApp/LifecycleHandler/LifecycleHandlerTestModule.swift +++ b/Tests/UITests/TestApp/LifecycleHandler/LifecycleHandlerTestModule.swift @@ -48,6 +48,7 @@ final class LifecycleHandlerTestModule: Module, LifecycleHandler { @Modifier var modifier: LifecycleHandlerModifier + @MainActor init() { let model = LifecycleHandlerModel() self.model = model diff --git a/Tests/UITests/TestApp/ModelTests/ModuleWithModel.swift b/Tests/UITests/TestApp/ModelTests/ModuleWithModel.swift index 766a0a11..081c6e02 100644 --- a/Tests/UITests/TestApp/ModelTests/ModuleWithModel.swift +++ b/Tests/UITests/TestApp/ModelTests/ModuleWithModel.swift @@ -38,12 +38,19 @@ private struct MyModifier: ViewModifier { class ModuleWithModel: Module, EnvironmentAccessible { + @Application(\.launchOptions) private var launchOptions + @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: MyModifier 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 a8c0d118..cb0b5835 100644 --- a/Tests/UITests/TestApp/ViewModifierTests/ModuleWithModifier.swift +++ b/Tests/UITests/TestApp/ViewModifierTests/ModuleWithModifier.swift @@ -31,5 +31,10 @@ private struct MyModifier: ViewModifier { class ModuleWithModifier: Module { - @Modifier fileprivate var modelModifier = MyModifier(model: MyModel(message: "Hello World")) + @Modifier fileprivate var modelModifier: MyModifier + + @MainActor + init() { + modelModifier = MyModifier(model: MyModel(message: "Hello World")) + } } diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index a2216734..ed958839 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -182,7 +182,6 @@ 2F6D138E28F5F384007C25D6 /* Sources */, 2F6D138F28F5F384007C25D6 /* Frameworks */, 2F6D139028F5F384007C25D6 /* Resources */, - 2FB4E4A429AC746E00C771C8 /* ShellScript */, ); buildRules = ( ); @@ -278,27 +277,6 @@ }; /* End PBXResourcesBuildPhase section */ -/* Begin PBXShellScriptBuildPhase section */ - 2FB4E4A429AC746E00C771C8 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "export PATH=\"$PATH:/opt/homebrew/bin\"\nif which swiftlint > /dev/null; then\n cd ../../ && swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; - }; -/* End PBXShellScriptBuildPhase section */ - /* Begin PBXSourcesBuildPhase section */ 2F6D138E28F5F384007C25D6 /* Sources */ = { isa = PBXSourcesBuildPhase;