Skip to content

Commit

Permalink
Support dynamic initialization of optional dependencies (#96)
Browse files Browse the repository at this point in the history
# Support dynamic initialization of optional dependencies

## ♻️ Current situation & Problem
Optional dependencies are already supported by Spezi. However, they lack
the ability to dynamically supply a default value. Currently, you can
only declare an optional dependency and check if it was configured e.g.
by the user. But it wasn't possible to provide a default value (e.g.,
behind some runtime check) yourself.


## ⚙️ Release Notes 
* Add support to dynamically provide a default value for optional
dependencies.


## 📚 Documentation
Documentation was added and updated.


## ✅ Testing
Extensive testing was added for the new initializer, testing all
possible combinations of initialization.


## 📝 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 Jan 11, 2024
1 parent bf2f55b commit c4bf0e9
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 7 deletions.
29 changes: 24 additions & 5 deletions Sources/Spezi/Dependencies/DependencyPropertyWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public class _DependencyPropertyWrapper<Value>: DependencyDeclaration { // swift
self.dependencies = dependencies
}

/// Declare a dependency to a module that can provide a default value on its own.
public convenience init() where Value: Module & DefaultInitializable {
// this init is placed here directly, otherwise Swift has problems resolving this init
self.init(wrappedValue: Value())
Expand All @@ -67,6 +68,8 @@ public class _DependencyPropertyWrapper<Value>: DependencyDeclaration { // swift


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)))
}
Expand All @@ -79,10 +82,20 @@ extension _DependencyPropertyWrapper: SingleModuleDependency where Value: Module


extension _DependencyPropertyWrapper: OptionalModuleDependency where Value: AnyOptional, Value.Wrapped: Module {
/// Create a empty, optional dependency.
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)))
}


func wrappedValue<WrappedValue>(as value: WrappedValue.Type) -> WrappedValue {
guard let value = dependencies.singleOptionalDependencyRetrieval(for: Value.Wrapped.self) as? WrappedValue else {
Expand All @@ -98,12 +111,13 @@ extension _DependencyPropertyWrapper: ModuleArrayDependency where Value == [any
self.init(DependencyCollection())
}

/// Creates the `@Dependency` property wrapper from an instantiated ``DependencyCollection``, enabling the use of a custom ``DependencyBuilder`` enforcing certain type constraints on the passed, nested ``Dependency``s.
/// - Parameters:
/// - dependencies: The ``DependencyCollection`` to be wrapped.
/// Create a dependency from a ``DependencyCollection``.
///
/// Creates the `@Dependency` property wrapper from an instantiated ``DependencyCollection``,
/// enabling the use of a custom ``DependencyBuilder`` enforcing certain type constraints on the passed, nested ``Dependency``s.
///
/// ### Usage
///
///
/// The `ExampleModule` is initialized with nested ``Module/Dependency``s (``Module``s) enforcing certain type constraints via the `SomeCustomDependencyBuilder`.
/// Spezi automatically injects declared ``Dependency``s within the passed ``Dependency``s in the initializer, enabling proper nesting of ``Module``s.
///
Expand All @@ -118,10 +132,15 @@ extension _DependencyPropertyWrapper: ModuleArrayDependency where Value == [any
/// ```
///
/// See ``DependencyCollection/init(for:singleEntry:)`` for a continued example, specifically the implementation of the `SomeCustomDependencyBuilder` result builder.
///
/// - Parameters:
/// - dependencies: The ``DependencyCollection``.
public convenience init(using dependencies: DependencyCollection) {
self.init(dependencies)
}


/// Create a dependency from a list of dependencies.
/// - Parameter dependencies: The result builder to build the dependency tree.
public convenience init(@DependencyBuilder _ dependencies: () -> DependencyCollection) {
self.init(dependencies())
}
Expand Down
10 changes: 9 additions & 1 deletion Sources/Spezi/Spezi/SpeziAppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,15 @@ open class SpeziAppDelegate: NSObject, UIApplicationDelegate, UISceneDelegate {
// swiftlint:disable:next discouraged_optional_collection
willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
spezi.willFinishLaunchingWithOptions(application, launchOptions: launchOptions ?? [:])
if !ProcessInfo.processInfo.isPreviewSimulator {
// If you are running an Xcode Preview and you have your global SwiftUI `App` defined with
// the `@UIApplicationDelegateAdaptor` property wrapper, it will still instantiate the App Delegate
// and call this willFinishLaunchingWithOptions delegate method. This results in an instantiation of Spezi
// and configuration of the respective modules. This might and will cause troubles with Modules that
// are only meant to be instantiated once. Therefore, we skip execution of this if running inside the PreviewSimulator.
// This is also not a problem, as there is no way to set up an application delegate within a Xcode preview.
spezi.willFinishLaunchingWithOptions(application, launchOptions: launchOptions ?? [:])
}
return true
}

Expand Down
57 changes: 56 additions & 1 deletion Tests/SpeziTests/DependenciesTests/DependencyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,17 @@ private final class TestModule2: Module {
@Dependency var testModule3: TestModule3
}

private final class TestModule3: Module, DefaultInitializable {}
private final class TestModule3: Module, DefaultInitializable {
let state: Int

convenience init() {
self.init(state: 0)
}

init(state: Int) {
self.state = state
}
}

private final class TestModule4: Module {
@Dependency var testModule5 = TestModule5()
Expand Down Expand Up @@ -54,6 +64,16 @@ private final class OptionalModuleDependency: Module {
@Dependency var testModule3: TestModule3?
}

private final class OptionalDependencyWithRuntimeDefault: Module {
@Dependency var testModule3: TestModule3?

init(defaultValue: Int?) {
if let defaultValue {
_testModule3 = Dependency(wrappedValue: TestModule3(state: defaultValue))
}
}
}


final class DependencyTests: XCTestCase {
func testModuleDependencyChain() throws {
Expand Down Expand Up @@ -216,4 +236,39 @@ final class DependencyTests: XCTestCase {
let module = try XCTUnwrap(modules[1] as? OptionalModuleDependency)
XCTAssert(module.testModule3 === module3)
}

func testOptionalDependencyWithDynamicRuntimeDefaultValue() throws {
let nonPresent = DependencyManager.resolve([
OptionalDependencyWithRuntimeDefault(defaultValue: nil) // stays optional
])

let dut1 = try XCTUnwrap(nonPresent[0] as? OptionalDependencyWithRuntimeDefault)
XCTAssertNil(dut1.testModule3)

let configured = DependencyManager.resolve([
TestModule3(state: 1),
OptionalDependencyWithRuntimeDefault(defaultValue: nil)
])

let dut2 = try XCTUnwrap(configured[1] as? OptionalDependencyWithRuntimeDefault)
let dut2Module = try XCTUnwrap(dut2.testModule3)
XCTAssertEqual(dut2Module.state, 1)

let defaulted = DependencyManager.resolve([
OptionalDependencyWithRuntimeDefault(defaultValue: 2)
])

let dut3 = try XCTUnwrap(defaulted[1] as? OptionalDependencyWithRuntimeDefault)
let dut3Module = try XCTUnwrap(dut3.testModule3)
XCTAssertEqual(dut3Module.state, 2)

let configuredAndDefaulted = DependencyManager.resolve([
TestModule3(state: 4),
OptionalDependencyWithRuntimeDefault(defaultValue: 3)
])

let dut4 = try XCTUnwrap(configuredAndDefaulted[1] as? OptionalDependencyWithRuntimeDefault)
let dut4Module = try XCTUnwrap(dut4.testModule3)
XCTAssertEqual(dut4Module.state, 4)
}
}

0 comments on commit c4bf0e9

Please sign in to comment.