Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support dynamic initialization of optional dependencies #96

Merged
merged 3 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
}