Skip to content

Commit

Permalink
Restructure @Dependency property and use BFS for DependencyManager (#…
Browse files Browse the repository at this point in the history
…112)

# Restructure `@Dependency` property and use BFS for DependencyManager

## ♻️ Current situation & Problem
The `Module` and dependency system is very powerful and useful
infrastructure of the Spezi framework ecosystem allowing it to be
modularized. There was immense thought put into it to generalize the
concept of dependency declaration between modules. With recent PRs it
has become even more powerful support a lot of different use cases like
dynamism (e.g., like modeling Bluetooth devices as modules).

However, it some scenarios the `@Dependency` property lacks the
expressiveness as required in certain use cases. For example, it is
currently always required to either specify a default value (either
manually or through `DefaultInitializable`) or declare it as an optional
dependency. Simply, declaring a required dependency that is assumed to
be present, cannot be modeled. For example, a module A, might configure
a module B and C, where B has a required dependency to C (as it knows it
is always configured by A and therefore can safely assume that C is
present). This is currently not possible with either a) having to unwrap
a optional dependency all the time or b) declaring a dependency with a
default value which might be not always be possible if there are
initializer arguments required.

Secondly, you currently cannot have a guarantee that a given module
**instance** you provide to the `@Dependency` property wrapper is
actually loaded as it is always used as a default argument and a module
declared on the outside takes precedence.

The changes of this PR are listed in the next section.

## ⚙️ Release Notes 
This PR introduces several changes to the dependency system.
* You can specify **required** modules without providing a default value
like below:
  * `@Depenency(Example.self) var example`
* You can manually request loading a module in any case by calling the
special initializer `_example = Dependency(load: myInstance)` or using
any array based initializers.
* The PR makes certain initializers to require to specify the Module
type as we are reaching a point where Swift has troubles resolving the
different non-argument initializers:
* Instead of specifying `@Dependency var example: Example?` you have to
specify `@Dependency(Example.self) var example: Example?.
* Instead of specifying `@Dependency var example: Example` (for
`DefaultInitializable`), you will specify `@Dependency(Example.self) var
example` for a required dependency. The default value will automatically
be created.
* Lastly, this PR changes the DependencyManager from using a depth-first
search to a breath-first search to avoid unexpected initialization of
default values.

Old initializers are marked deprecated and shall be removed in the next
major version release.

## 📚 Documentation
This PR updates documentation to refer to the most recent versions of
the `@Dependency` property wrapper.


## ✅ Testing
New unit tests were added to cover new cases. We added some verification
that deprecated initializers still work as expected.

## 📝 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).
  • Loading branch information
Supereg authored Aug 13, 2024
1 parent d87e3d8 commit 6599bac
Show file tree
Hide file tree
Showing 32 changed files with 814 additions and 287 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
[]
}
Expand Down
6 changes: 6 additions & 0 deletions Sources/Spezi/Capabilities/Lifecycle/LifecycleHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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`.
Expand All @@ -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`.
Expand All @@ -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`.
Expand All @@ -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`.
Expand All @@ -123,6 +128,7 @@ public protocol LifecycleHandler {
or other platform-specific mechanisms as a replacement.
"""
)
@MainActor
func applicationWillTerminate(_ application: UIApplication)
#endif
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand All @@ -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.
Expand All @@ -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
}
Expand All @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 6599bac

Please sign in to comment.