diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7a9117e..517d5f0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,15 +10,22 @@ on: branches: [ "main" ] jobs: - build: + unittest: runs-on: macos-14 steps: - uses: actions/checkout@v4 + - name: Install Dependencies + run: swift package resolve - name: Pick xcode 15.1 run: sudo xcode-select -s '/Applications/Xcode_15.1.app/Contents/Developer' - - name: Build - run: swift build -v - - name: Run tests - run: swift test -v + - name: Run Tests on MacOS + run: | + swift build -v + swift test -v + - name: Run Tests on iPhone 14 Simulator (iOS 16.0) + run: | + xcodebuild test \ + -scheme 'SwiftEnvironment' \ + -destination 'platform=iOS Simulator,name=iPhone 14,OS=16.0' diff --git a/README.md b/README.md index cf38211..5de5ffe 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,53 @@ To resolve dependency manually from GlobalResolver, do this: let myValue = GlobalResolver.resolve(\.myValue) ``` +### UIEnvironment + +`UIEnvirontment` is similar as SwiftUI Environment but for UIKit. It allows UIKit be injected using `EnvironmentValues` just like SwiftUI. It will also allow shared environment on any it subview or child view controller: + +```swift +// inject SomeDependency to window +window.environment(\.myValue, SomeDependency()) +``` + +Then all of it's child view controller and view can access myValue injected from the same window: + +```swift +class MyViewController: UIViewController { + // this will be using the injected value from it's parent (UIViewController or UIWindow if it's a root) + @UIEnvironment(\.myValue) var myValue +} +``` + +even the view and subview can access the value also: + +```swift +class MyView: UIVIiew { + // this will be using the injected value from it's superview or it's viewController if its a root. + @UIEnvironment(\.myValue) var myValue +} +``` + +Same like SwiftUI, if the ViewController is injected, it will use it's own value instead of from its parent. This value then will be inherited to it's child too: + +```swift +// All of its child viewcontroller and view will use this value instead of the one coming from window +myViewController.environment(\.myValue, SomeOtherDependency()) +``` + +Updating the enviroment will be reflect to all inheriting value just like SwiftUI. But this will only work on UIKit to UIKit, not UIKit to SwiftUI. + +If you are presenting a SwiftUI from UIKit, you can inject the value to the SwiftUI also: + +```swift +// this will inject all of the enviroment to the SwiftUI View +let hostingController = UIHostingController( + rootView: MySwiftUIView().inheritEnvironment(from: presentingViewController) + ) +presentingViewController.present(hostingController, animated: true) +``` + +All of the Enviroment will be injected. But keep in mind that updating an Enviroment will not update the SwiftUI Environment, since it will just resolve all value during inherit. ### EnvironmentValue macro diff --git a/Sources/SwiftEnvironment/EnvironmentValuesResolver.swift b/Sources/SwiftEnvironment/EnvironmentValuesResolver.swift deleted file mode 100644 index 038ac2d..0000000 --- a/Sources/SwiftEnvironment/EnvironmentValuesResolver.swift +++ /dev/null @@ -1,114 +0,0 @@ -// -// EnvironmentValuesResolver.swift -// -// -// Created by Nayanda Haberty on 14/3/24. -// - -import Foundation - -public class EnvironmentValuesResolver { - - static var global: EnvironmentValuesResolver = EnvironmentValuesResolver() - - private var environmentValues: EnvironmentValues - private var resolvers: [AnyKeyPath: InstanceResolver] - - init(resolvers: [AnyKeyPath: InstanceResolver] = [:]) { - self.environmentValues = EnvironmentValues() - self.resolvers = resolvers - } - - public func resolve(_ keyPath: KeyPath) -> V { - resolvers[keyPath]?.resolve(for: V.self) ?? environmentValues[keyPath: keyPath] - } - - @discardableResult - public func environment( - _ keyPath: WritableKeyPath, - resolveOn queue: DispatchQueue? = nil, - _ value: @autoclosure @escaping () -> V) -> EnvironmentValuesResolver { - resolvers[keyPath] = SingletonInstanceResolver(queue: queue, resolver: value) - return self - } - - @discardableResult - public func environment( - _ keyPath: WritableKeyPath, - resolveOn queue: DispatchQueue? = nil, - resolver: @escaping () -> V) -> EnvironmentValuesResolver { - resolvers[keyPath] = SingletonInstanceResolver(queue: queue, resolver: resolver) - return self - } - - @discardableResult - public func transient( - _ keyPath: WritableKeyPath, - resolveOn queue: DispatchQueue? = nil, - _ value: @autoclosure @escaping () -> V) -> EnvironmentValuesResolver { - resolvers[keyPath] = TransientInstanceResolver(queue: queue, resolver: value) - return self - } - - @discardableResult - public func transient( - _ keyPath: WritableKeyPath, - resolveOn queue: DispatchQueue? = nil, - resolver: @escaping () -> V) -> EnvironmentValuesResolver { - resolvers[keyPath] = TransientInstanceResolver(queue: queue, resolver: resolver) - return self - } - - @discardableResult - public func weak( - _ keyPath: WritableKeyPath, - resolveOn queue: DispatchQueue? = nil, - _ value: @autoclosure @escaping () -> V) -> EnvironmentValuesResolver { - resolvers[keyPath] = WeakInstanceResolver(queue: queue, resolver: value) - return self - } - - @discardableResult - public func weak( - _ keyPath: WritableKeyPath, - resolveOn queue: DispatchQueue? = nil, - resolver: @escaping () -> V) -> EnvironmentValuesResolver { - resolvers[keyPath] = WeakInstanceResolver(queue: queue, resolver: resolver) - return self - } - - @discardableResult - public func environment( - _ keyPath: WritableKeyPath, - use soureKeyPath: WritableKeyPath) -> EnvironmentValuesResolver { - resolvers[keyPath] = TransientInstanceResolver(queue: nil) { [unowned self] in - (self.resolve(soureKeyPath) as? V) ?? self.environmentValues[keyPath: keyPath] - } - return self - } -} - -extension EnvironmentValuesResolver { - - @inlinable - @discardableResult - public func environment( - _ keyPath1: WritableKeyPath, - _ keyPath2: WritableKeyPath, - use soureKeyPath: WritableKeyPath) -> EnvironmentValuesResolver { - environment(keyPath1, use: soureKeyPath) - .environment(keyPath2, use: soureKeyPath) - } - - @inlinable - @discardableResult - public func environment( - _ keyPath1: WritableKeyPath, - _ keyPath2: WritableKeyPath, - _ keyPath3: WritableKeyPath, - use soureKeyPath: WritableKeyPath) -> EnvironmentValuesResolver { - environment(keyPath1, use: soureKeyPath) - .environment(keyPath2, use: soureKeyPath) - .environment(keyPath3, use: soureKeyPath) - } -} diff --git a/Sources/SwiftEnvironment/GlobalEnvironment.swift b/Sources/SwiftEnvironment/GlobalEnvironment.swift deleted file mode 100644 index c897a49..0000000 --- a/Sources/SwiftEnvironment/GlobalEnvironment.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// GlobalEnvironment.swift -// SwiftEnvironment -// -// Created by Nayanda Haberty on 14/3/24. -// - -import Foundation - -@propertyWrapper -public final class GlobalEnvironment { - - private let resolver: EnvironmentValuesResolver - private let keyPath: KeyPath - - private lazy var _wrappedValue: Value = resolver.resolve(keyPath) - public var wrappedValue: Value { - get { _wrappedValue } - set { _wrappedValue = newValue } - } - - public init(_ keyPath: KeyPath) { - self.keyPath = keyPath - self.resolver = EnvironmentValuesResolver.global - } -} diff --git a/Sources/SwiftEnvironment/InstanceResolver.swift b/Sources/SwiftEnvironment/InstanceResolver.swift deleted file mode 100644 index f5333b6..0000000 --- a/Sources/SwiftEnvironment/InstanceResolver.swift +++ /dev/null @@ -1,85 +0,0 @@ -// -// InstanceResolver.swift -// SwiftEnvironment -// -// Created by Nayanda Haberty on 14/3/24. -// - -import Foundation -import Chary - -// MARK: InstanceResolver - -protocol InstanceResolver { - func resolve(for type: V.Type) -> V? -} - -// MARK: SingletonInstanceResolver - -final class SingletonInstanceResolver: InstanceResolver { - - private(set) var instance: Value? - private var resolver: (() -> Value)? - private let queue: DispatchQueue? - - @inlinable init(queue: DispatchQueue?, resolver: @escaping () -> Value) { - self.resolver = resolver - self.queue = queue - } - - @inlinable func resolve(for type: V.Type) -> V? { - guard let instance else { - // this resolver should not be nil in this line - let resolver = self.resolver! - let newInstance = queue?.safeSync(execute: resolver) ?? resolver() - self.resolver = nil - self.instance = newInstance - return newInstance as? V - } - return instance as? V - } -} - -// MARK: TransientInstanceResolver - -struct TransientInstanceResolver: InstanceResolver { - - private let resolver: () -> Value - private let queue: DispatchQueue? - - @inlinable init(queue: DispatchQueue?, resolver: @escaping () -> Value) { - self.resolver = resolver - self.queue = queue - } - - @inlinable func resolve(for type: V.Type) -> V? { - let instance = queue?.safeSync(execute: resolver) ?? resolver() - guard let kInstance = instance as? V else { - return nil - } - return kInstance - } -} - -// MARK: WeakInstanceResolver - -final class WeakInstanceResolver: InstanceResolver { - - private(set) weak var instance: Value? - private let resolver: () -> Value - private let queue: DispatchQueue? - - @inlinable init(queue: DispatchQueue?, resolver: @escaping () -> Value) { - self.resolver = resolver - self.queue = queue - } - - @inlinable func resolve(for type: V.Type) -> V? { - guard let instance else { - let newInstance = queue?.safeSync(execute: resolver) ?? resolver() - self.instance = newInstance - return newInstance as? V - } - return instance as? V - } -} diff --git a/Sources/SwiftEnvironment/InstanceResolver/InstanceResolver.swift b/Sources/SwiftEnvironment/InstanceResolver/InstanceResolver.swift new file mode 100644 index 0000000..554d17a --- /dev/null +++ b/Sources/SwiftEnvironment/InstanceResolver/InstanceResolver.swift @@ -0,0 +1,14 @@ +// +// InstanceResolver.swift +// SwiftEnvironment +// +// Created by Nayanda Haberty on 14/3/24. +// + +import Foundation +import SwiftUI + +protocol InstanceResolver { + func resolve(for type: V.Type) -> V? + func assign(to view: any View, for keyPath: AnyKeyPath) -> any View +} diff --git a/Sources/SwiftEnvironment/InstanceResolver/SingletonInstanceResolver.swift b/Sources/SwiftEnvironment/InstanceResolver/SingletonInstanceResolver.swift new file mode 100644 index 0000000..3e27b6f --- /dev/null +++ b/Sources/SwiftEnvironment/InstanceResolver/SingletonInstanceResolver.swift @@ -0,0 +1,41 @@ +// +// SingletonInstanceResolver.swift +// SwiftEnvironment +// +// Created by Nayanda Haberty on 5/11/24. +// + +import Foundation +import SwiftUI + +final class SingletonInstanceResolver: InstanceResolver { + + private(set) var instance: Value? + private var resolver: (() -> Value)? + private let queue: DispatchQueue? + + @inlinable init(queue: DispatchQueue?, resolver: @escaping () -> Value) { + self.resolver = resolver + self.queue = queue + } + + @inlinable func resolve(for type: V.Type) -> V? { + guard let instance else { + // this resolver should not be nil in this line + let resolver = self.resolver! + let newInstance = queue?.safeSync(execute: resolver) ?? resolver() + self.resolver = nil + self.instance = newInstance + return newInstance as? V + } + return instance as? V + } + + func assign(to view: any View, for keyPath: AnyKeyPath) -> any View { + guard let writableKeyPath = keyPath as? WritableKeyPath, + let value = resolve(for: Value.self) else { + return view + } + return view.environment(writableKeyPath, value) + } +} diff --git a/Sources/SwiftEnvironment/InstanceResolver/TransientInstanceResolver.swift b/Sources/SwiftEnvironment/InstanceResolver/TransientInstanceResolver.swift new file mode 100644 index 0000000..5672158 --- /dev/null +++ b/Sources/SwiftEnvironment/InstanceResolver/TransientInstanceResolver.swift @@ -0,0 +1,37 @@ +// +// TransientInstanceResolver.swift +// SwiftEnvironment +// +// Created by Nayanda Haberty on 5/11/24. +// + +import Foundation +import Chary +import SwiftUI + +struct TransientInstanceResolver: InstanceResolver { + + private let resolver: () -> Value + private let queue: DispatchQueue? + + @inlinable init(queue: DispatchQueue?, resolver: @escaping () -> Value) { + self.resolver = resolver + self.queue = queue + } + + @inlinable func resolve(for type: V.Type) -> V? { + let instance = queue?.safeSync(execute: resolver) ?? resolver() + guard let kInstance = instance as? V else { + return nil + } + return kInstance + } + + func assign(to view: any View, for keyPath: AnyKeyPath) -> any View { + guard let writableKeyPath = keyPath as? WritableKeyPath, + let value = resolve(for: Value.self) else { + return view + } + return view.environment(writableKeyPath, value) + } +} diff --git a/Sources/SwiftEnvironment/InstanceResolver/WeakInstanceResolver.swift b/Sources/SwiftEnvironment/InstanceResolver/WeakInstanceResolver.swift new file mode 100644 index 0000000..a520e4b --- /dev/null +++ b/Sources/SwiftEnvironment/InstanceResolver/WeakInstanceResolver.swift @@ -0,0 +1,39 @@ +// +// WeakInstanceResolver.swift +// SwiftEnvironment +// +// Created by Nayanda Haberty on 5/11/24. +// + +import Foundation +import Chary +import SwiftUI + +final class WeakInstanceResolver: InstanceResolver { + + private(set) weak var instance: Value? + private let resolver: () -> Value + private let queue: DispatchQueue? + + @inlinable init(queue: DispatchQueue?, resolver: @escaping () -> Value) { + self.resolver = resolver + self.queue = queue + } + + @inlinable func resolve(for type: V.Type) -> V? { + guard let instance else { + let newInstance = queue?.safeSync(execute: resolver) ?? resolver() + self.instance = newInstance + return newInstance as? V + } + return instance as? V + } + + func assign(to view: any View, for keyPath: AnyKeyPath) -> any View { + guard let writableKeyPath = keyPath as? WritableKeyPath, + let value = resolve(for: Value.self) else { + return view + } + return view.environment(writableKeyPath, value) + } +} diff --git a/Sources/SwiftEnvironment/PropertyWrapper/GlobalEnvironment.swift b/Sources/SwiftEnvironment/PropertyWrapper/GlobalEnvironment.swift new file mode 100644 index 0000000..1d9a84c --- /dev/null +++ b/Sources/SwiftEnvironment/PropertyWrapper/GlobalEnvironment.swift @@ -0,0 +1,51 @@ +// +// GlobalEnvironment.swift +// SwiftEnvironment +// +// Created by Nayanda Haberty on 14/3/24. +// + +import Foundation +import Combine + +@propertyWrapper +public final class GlobalEnvironment: PropertyWrapperDiscardable { + + private let resolver: EnvironmentValuesResolving + private let keyPath: KeyPath + private var observerCancellable: AnyCancellable? + + private var injectedValue: Value? + private lazy var resolvedValue: Value = resolveAndObserveValue() + public var wrappedValue: Value { + get { injectedValue ?? resolvedValue } + set { injectedValue = newValue } + } + + public var projectedValue: PropertyWrapperDiscardableControl { + PropertyWrapperDiscardableControl(propertyWrapper: self) + } + + public init(_ keyPath: KeyPath) { + self.keyPath = keyPath + self.resolver = EnvironmentValuesResolver.global + } + + public func discardValueSet() { + injectedValue = nil + } + + private func resolveAndObserveValue() -> Value { + observerCancellable = resolver.environmentValuePublisher(for: keyPath) + .weakAssign(to: \.resolvedValue, on: self) + return resolver.resolve(keyPath) + } +} + +extension Publisher where Failure == Never { + func weakAssign(to keyPath: ReferenceWritableKeyPath, on object: Root) -> AnyCancellable { + sink { [weak object] output in + object?[keyPath: keyPath] = output + } + } +} diff --git a/Sources/SwiftEnvironment/PropertyWrapper/PropertyWrapperDiscardable.swift b/Sources/SwiftEnvironment/PropertyWrapper/PropertyWrapperDiscardable.swift new file mode 100644 index 0000000..fee5065 --- /dev/null +++ b/Sources/SwiftEnvironment/PropertyWrapper/PropertyWrapperDiscardable.swift @@ -0,0 +1,24 @@ +// +// PropertyWrapperDiscardable.swift +// SwiftEnvironment +// +// Created by Nayanda Haberty on 6/11/24. +// + + +public protocol PropertyWrapperDiscardable { + func discardValueSet() + var projectedValue: PropertyWrapperDiscardableControl { get } +} + +public struct PropertyWrapperDiscardableControl { + private let propertyWrapper: PropertyWrapperDiscardable + + init(propertyWrapper: PropertyWrapperDiscardable) { + self.propertyWrapper = propertyWrapper + } + + func discardValueSet() { + propertyWrapper.discardValueSet() + } +} diff --git a/Sources/SwiftEnvironment/PropertyWrapper/UIEnvironment.swift b/Sources/SwiftEnvironment/PropertyWrapper/UIEnvironment.swift new file mode 100644 index 0000000..9d06785 --- /dev/null +++ b/Sources/SwiftEnvironment/PropertyWrapper/UIEnvironment.swift @@ -0,0 +1,50 @@ +// +// UIEnvironment.swift +// SwiftEnvironment +// +// Created by Nayanda Haberty on 5/11/24. +// + +#if canImport(UIKit) +import UIKit + +@propertyWrapper +public final class UIEnvironment: PropertyWrapperDiscardable { + + public static subscript( + _enclosingInstance instance: EnclosingType, + wrapped wrappedKeyPath: KeyPath, + storage storageKeyPath: KeyPath + ) -> Value { + get { + let wrapper = instance[keyPath: storageKeyPath] + return wrapper._wrappedValue ?? instance.resolve(wrapper.keyPath) + } + set { + instance[keyPath: storageKeyPath]._wrappedValue = newValue + } + } + + private let keyPath: KeyPath + + private var _wrappedValue: Value? + + @available(*, unavailable, message: "Enclosing type must be instance of UIResponder") + public var wrappedValue: Value { + get { fatalError() } + set { fatalError() } + } + + public var projectedValue: PropertyWrapperDiscardableControl { + PropertyWrapperDiscardableControl(propertyWrapper: self) + } + + public init(_ keyPath: KeyPath) { + self.keyPath = keyPath + } + + public func discardValueSet() { + _wrappedValue = nil + } +} +#endif diff --git a/Sources/SwiftEnvironment/Resolver/EnvironmentValuesResolver.swift b/Sources/SwiftEnvironment/Resolver/EnvironmentValuesResolver.swift new file mode 100644 index 0000000..0837321 --- /dev/null +++ b/Sources/SwiftEnvironment/Resolver/EnvironmentValuesResolver.swift @@ -0,0 +1,80 @@ +// +// EnvironmentValuesResolver.swift +// +// +// Created by Nayanda Haberty on 14/3/24. +// + +import Foundation +import Combine + +public class EnvironmentValuesResolver: EnvironmentLifeCycledValuesResolving, EnvironmentValuesRepository { + + static var global: EnvironmentValuesResolver = EnvironmentValuesResolver() + + let defaultEnvironmentValues: EnvironmentValues + private(set) var underlyingResolvers: [AnyKeyPath: InstanceResolver] + var resolvers: [AnyKeyPath: InstanceResolver] { underlyingResolvers } + var resolverAssignSubject: PassthroughSubject<(AnyKeyPath, InstanceResolver), Never> = .init() + + init(resolvers: [AnyKeyPath: InstanceResolver] = [:]) { + self.defaultEnvironmentValues = EnvironmentValues() + self.underlyingResolvers = resolvers + } + + public func resolve(_ keyPath: KeyPath) -> V { + underlyingResolvers[keyPath]?.resolve(for: V.self) ?? defaultEnvironmentValues[keyPath: keyPath] + } + + public func environmentValuePublisher(for keyPath: KeyPath) -> AnyPublisher { + resolverAssignSubject + .filter { $0.0 == keyPath } + .compactMap { $0.1.resolve(for: V.self) } + .eraseToAnyPublisher() + } + + @discardableResult + public func environment( + _ keyPath: WritableKeyPath, + resolveOn queue: DispatchQueue?, + resolver: @escaping () -> V) -> Self { + assign(resolver: SingletonInstanceResolver(queue: queue, resolver: resolver), to: keyPath) + return self + } + + @discardableResult + public func transient( + _ keyPath: WritableKeyPath, + resolveOn queue: DispatchQueue?, + resolver: @escaping () -> V) -> Self { + assign(resolver: TransientInstanceResolver(queue: queue, resolver: resolver), to: keyPath) + return self + } + + @discardableResult + public func weak( + _ keyPath: WritableKeyPath, + resolveOn queue: DispatchQueue?, + resolver: @escaping () -> V) -> Self { + assign(resolver: WeakInstanceResolver(queue: queue, resolver: resolver), to: keyPath) + return self + } + + @discardableResult + public func environment( + _ keyPath: WritableKeyPath, + use soureKeyPath: WritableKeyPath) -> Self { + assign( + resolver: TransientInstanceResolver(queue: nil) { [unowned self] in + (self.resolve(soureKeyPath) as? V) ?? self.defaultEnvironmentValues[keyPath: keyPath] + }, + to: keyPath + ) + return self + } + + private func assign(resolver: InstanceResolver, to keyPath: AnyKeyPath) { + underlyingResolvers[keyPath] = resolver + resolverAssignSubject.send((keyPath, resolver)) + } +} diff --git a/Sources/SwiftEnvironment/Resolver/EnvironmentValuesResolverHost.swift b/Sources/SwiftEnvironment/Resolver/EnvironmentValuesResolverHost.swift new file mode 100644 index 0000000..4b03fa2 --- /dev/null +++ b/Sources/SwiftEnvironment/Resolver/EnvironmentValuesResolverHost.swift @@ -0,0 +1,36 @@ +// +// ViewEnvironment.swift +// SwiftEnvironment +// +// Created by Nayanda Haberty on 5/11/24. +// + +import Foundation + +protocol EnvironmentValuesResolverHost: EnvironmentValuesResolving { + var environmentValuesResolver: EnvironmentValuesResolving { get } +} + +extension EnvironmentValuesResolverHost { + + public func resolve(_ keyPath: KeyPath) -> V { + environmentValuesResolver.resolve(keyPath) + } + + @discardableResult + public func environment( + _ keyPath: WritableKeyPath, + resolveOn queue: DispatchQueue?, + resolver: @escaping () -> V) -> Self { + environmentValuesResolver.environment(keyPath, resolveOn: queue, resolver: resolver) + return self + } + + @discardableResult + public func environment( + _ keyPath: WritableKeyPath, + use soureKeyPath: WritableKeyPath) -> Self { + environmentValuesResolver.environment(keyPath, use: soureKeyPath) + return self + } +} diff --git a/Sources/SwiftEnvironment/Resolver/EnvironmentValuesResolving.swift b/Sources/SwiftEnvironment/Resolver/EnvironmentValuesResolving.swift new file mode 100644 index 0000000..a853edc --- /dev/null +++ b/Sources/SwiftEnvironment/Resolver/EnvironmentValuesResolving.swift @@ -0,0 +1,119 @@ +// +// EnvironmentValuesResolving.swift +// SwiftEnvironment +// +// Created by Nayanda Haberty on 5/11/24. +// + +import Foundation +import Combine + +protocol EnvironmentValuesRepository { + var resolvers: [AnyKeyPath: InstanceResolver] { get } +} + +public protocol EnvironmentValuesResolving: AnyObject { + + func resolve(_ keyPath: KeyPath) -> V + + func environmentValuePublisher(for keyPath: KeyPath) -> AnyPublisher + + @discardableResult + func environment( + _ keyPath: WritableKeyPath, + resolveOn queue: DispatchQueue?, + resolver: @escaping () -> V) -> Self + + @discardableResult + func environment( + _ keyPath: WritableKeyPath, + use soureKeyPath: WritableKeyPath) -> Self +} + +public protocol EnvironmentLifeCycledValuesResolving: EnvironmentValuesResolving { + + @discardableResult + func transient( + _ keyPath: WritableKeyPath, + resolveOn queue: DispatchQueue?, + resolver: @escaping () -> V) -> Self + + @discardableResult + func weak( + _ keyPath: WritableKeyPath, + resolveOn queue: DispatchQueue?, + resolver: @escaping () -> V) -> Self +} + +public extension EnvironmentValuesResolving { + + @discardableResult + func environment( + _ keyPath: WritableKeyPath, + resolver: @escaping () -> V) -> Self { + environment(keyPath, resolveOn: nil, resolver: resolver) + } + + @discardableResult + func environment( + _ keyPath: WritableKeyPath, + resolveOn queue: DispatchQueue? = nil, + _ value: @autoclosure @escaping () -> V) -> Self { + environment(keyPath, resolveOn: queue, resolver: value) + } + + @inlinable + @discardableResult + func environment( + _ keyPath1: WritableKeyPath, + _ keyPath2: WritableKeyPath, + use soureKeyPath: WritableKeyPath) -> Self { + environment(keyPath1, use: soureKeyPath) + .environment(keyPath2, use: soureKeyPath) + } + + @inlinable + @discardableResult + func environment( + _ keyPath1: WritableKeyPath, + _ keyPath2: WritableKeyPath, + _ keyPath3: WritableKeyPath, + use soureKeyPath: WritableKeyPath) -> Self { + environment(keyPath1, use: soureKeyPath) + .environment(keyPath2, use: soureKeyPath) + .environment(keyPath3, use: soureKeyPath) + } +} + +public extension EnvironmentLifeCycledValuesResolving { + + @discardableResult + func transient( + _ keyPath: WritableKeyPath, + resolver: @escaping () -> V) -> Self { + transient(keyPath, resolveOn: nil, resolver: resolver) + } + + @discardableResult + func transient( + _ keyPath: WritableKeyPath, + resolveOn queue: DispatchQueue? = nil, + _ value: @autoclosure @escaping () -> V) -> Self { + transient(keyPath, resolveOn: queue, resolver: value) + } + + @discardableResult + func weak( + _ keyPath: WritableKeyPath, + resolver: @escaping () -> V) -> Self { + weak(keyPath, resolveOn: nil, resolver: resolver) + } + + @discardableResult + func weak( + _ keyPath: WritableKeyPath, + resolveOn queue: DispatchQueue? = nil, + _ value: @autoclosure @escaping () -> V) -> Self { + weak(keyPath, resolveOn: queue, resolver: value) + } +} diff --git a/Sources/SwiftEnvironment/GlobalResolver.swift b/Sources/SwiftEnvironment/Resolver/GlobalResolver.swift similarity index 100% rename from Sources/SwiftEnvironment/GlobalResolver.swift rename to Sources/SwiftEnvironment/Resolver/GlobalResolver.swift diff --git a/Sources/SwiftEnvironment/Resolver/InheritEnvironmentValuesResolver.swift b/Sources/SwiftEnvironment/Resolver/InheritEnvironmentValuesResolver.swift new file mode 100644 index 0000000..bb541e1 --- /dev/null +++ b/Sources/SwiftEnvironment/Resolver/InheritEnvironmentValuesResolver.swift @@ -0,0 +1,36 @@ +// +// InheritEnvironmentValuesResolver.swift +// SwiftEnvironment +// +// Created by Nayanda Haberty on 5/11/24. +// + +import Foundation + +public final class InheritEnvironmentValuesResolver: EnvironmentValuesResolver { + + private var parentGetter: () -> EnvironmentValuesResolving? + var parent: EnvironmentValuesResolving? { + parentGetter() + } + + override var resolvers: [AnyKeyPath: InstanceResolver] { + let parentResolvers = (parent as? EnvironmentValuesRepository)?.resolvers ?? [:] + return underlyingResolvers.reduce(into: parentResolvers) { partialResult, pair in + partialResult[pair.key] = pair.value + } + } + + init(resolvers: [AnyKeyPath: InstanceResolver] = [:], parent: @escaping () -> EnvironmentValuesResolving?) { + self.parentGetter = parent + super.init(resolvers: resolvers) + } + + public override func resolve(_ keyPath: KeyPath) -> V { + underlyingResolvers[keyPath]?.resolve(for: V.self) ?? defaultResolve(keyPath) + } + + private func defaultResolve(_ keyPath: KeyPath) -> V { + parent?.resolve(keyPath) ?? defaultEnvironmentValues[keyPath: keyPath] + } +} diff --git a/Sources/SwiftEnvironment/Resolver/UIResponder+EnvironmentValuesResolverHost.swift b/Sources/SwiftEnvironment/Resolver/UIResponder+EnvironmentValuesResolverHost.swift new file mode 100644 index 0000000..0855fda --- /dev/null +++ b/Sources/SwiftEnvironment/Resolver/UIResponder+EnvironmentValuesResolverHost.swift @@ -0,0 +1,41 @@ +// +// UIResponder+EnvironmentValuesResolverHost.swift +// SwiftEnvironment +// +// Created by Nayanda Haberty on 5/11/24. +// + +#if canImport(UIKit) +import Foundation +import UIKit +import Combine + +private var underlyingResolverKey: Void = () + +extension UIResponder: EnvironmentValuesResolverHost { + + var environmentValuesResolver: EnvironmentValuesResolving { + guard let currentEnv = underlyingResolver else { + let newEnv = InheritEnvironmentValuesResolver { [weak self] in + self?.next?.environmentValuesResolver + } + underlyingResolver = newEnv + return newEnv + } + return currentEnv + } + + private var underlyingResolver: EnvironmentValuesResolving? { + get { + objc_getAssociatedObject(self, &underlyingResolverKey) as? EnvironmentValuesResolving + } + set { + objc_setAssociatedObject(self, &underlyingResolverKey, newValue, .OBJC_ASSOCIATION_RETAIN) + } + } + + public func environmentValuePublisher(for keyPath: KeyPath) -> AnyPublisher { + environmentValuesResolver.environmentValuePublisher(for: keyPath) + } +} +#endif diff --git a/Sources/SwiftEnvironment/View+Inherit.swift b/Sources/SwiftEnvironment/View+Inherit.swift new file mode 100644 index 0000000..5cd8da7 --- /dev/null +++ b/Sources/SwiftEnvironment/View+Inherit.swift @@ -0,0 +1,20 @@ +// +// View+Inherit.swift +// SwiftEnvironment +// +// Created by Nayanda Haberty on 6/11/24. +// + +#if canImport(UIKit) +import UIKit +import SwiftUI + +extension View { + func inheritEnvironment(from responder: UIResponder) -> any View { + let resolvers = (responder.environmentValuesResolver as? EnvironmentValuesRepository)?.resolvers ?? [:] + return resolvers.reduce(self) { view, pair in + pair.value.assign(to: view, for: pair.key) + } + } +} +#endif diff --git a/Tests/SwiftEnvironmentTests/DummyDependency.swift b/Tests/SwiftEnvironmentTests/DummyDependency.swift index c724312..7726d81 100644 --- a/Tests/SwiftEnvironmentTests/DummyDependency.swift +++ b/Tests/SwiftEnvironmentTests/DummyDependency.swift @@ -23,3 +23,14 @@ struct DummyStruct { struct DummyClass { let id: UUID } + +typealias DummyEnvironmentKey = EnvironmentValues.DummySwiftEnvironmentKey + +@EnvironmentValue +extension EnvironmentValues { + static let dummy = DummyDependencyStub() + static let secondDummy = DummyDependencyStub() + static let thirdDummy = DummyDependencyStub() + static let fourthDummy = DummyDependencyStub() + static let fifthDummy: String = "dummy" +} diff --git a/Tests/SwiftEnvironmentTests/EnvironmentValueMacroTests.swift b/Tests/SwiftEnvironmentTests/EnvironmentValueMacroTests.swift index 13764c6..46c3512 100644 --- a/Tests/SwiftEnvironmentTests/EnvironmentValueMacroTests.swift +++ b/Tests/SwiftEnvironmentTests/EnvironmentValueMacroTests.swift @@ -5,6 +5,7 @@ // Created by Nayanda Haberty on 15/3/24. // +#if os(macOS) import XCTest import SwiftSyntaxMacrosTestSupport @testable import SwiftEnvironmentMacro @@ -211,3 +212,4 @@ public extension EnvironmentValues { } } """ +#endif diff --git a/Tests/SwiftEnvironmentTests/IntegrationTests.swift b/Tests/SwiftEnvironmentTests/IntegrationTests.swift index 4e0e54d..943bfe3 100644 --- a/Tests/SwiftEnvironmentTests/IntegrationTests.swift +++ b/Tests/SwiftEnvironmentTests/IntegrationTests.swift @@ -102,14 +102,3 @@ final class IntegrationTests: XCTestCase { } } - -typealias DummyEnvironmentKey = EnvironmentValues.DummySwiftEnvironmentKey - -@EnvironmentValue -extension EnvironmentValues { - static let dummy = DummyDependencyStub() - static let secondDummy = DummyDependencyStub() - static let thirdDummy = DummyDependencyStub() - static let fourthDummy = DummyDependencyStub() - static let fifthDummy: String = "dummy" -} diff --git a/Tests/SwiftEnvironmentTests/StubFromProtocolGeneratorMacroTests.swift b/Tests/SwiftEnvironmentTests/StubFromProtocolGeneratorMacroTests.swift index 09a5273..e931a93 100644 --- a/Tests/SwiftEnvironmentTests/StubFromProtocolGeneratorMacroTests.swift +++ b/Tests/SwiftEnvironmentTests/StubFromProtocolGeneratorMacroTests.swift @@ -5,6 +5,7 @@ // Created by Nayanda Haberty on 16/3/24. // +#if os(macOS) import XCTest import SwiftSyntaxMacrosTestSupport @testable import SwiftEnvironmentMacro @@ -460,3 +461,4 @@ struct SomeStub: Some { } } """ +#endif diff --git a/Tests/SwiftEnvironmentTests/StubFromTypeGeneratorTests.swift b/Tests/SwiftEnvironmentTests/StubFromTypeGeneratorTests.swift index 92f94cc..e65407d 100644 --- a/Tests/SwiftEnvironmentTests/StubFromTypeGeneratorTests.swift +++ b/Tests/SwiftEnvironmentTests/StubFromTypeGeneratorTests.swift @@ -5,6 +5,7 @@ // Created by Nayanda Haberty on 1/4/24. // +#if os(macOS) import XCTest import SwiftSyntaxMacrosTestSupport @testable import SwiftEnvironmentMacro @@ -400,3 +401,4 @@ public struct Some { public typealias SomeStub = Some """ +#endif diff --git a/Tests/SwiftEnvironmentTests/UIKitEnvironmentTests.swift b/Tests/SwiftEnvironmentTests/UIKitEnvironmentTests.swift new file mode 100644 index 0000000..1ed3924 --- /dev/null +++ b/Tests/SwiftEnvironmentTests/UIKitEnvironmentTests.swift @@ -0,0 +1,88 @@ +// +// UIKitEnvironmentTests.swift +// SwiftEnvironment +// +// Created by Nayanda Haberty on 6/12/24. +// + +#if canImport(UIKit) +import XCTest +@testable import SwiftEnvironment +import SwiftUI + +final class UIKitEnvironmentTests: XCTestCase { + + private var window: UIWindow! + private var viewControllerUnderTest: ViewControllerUnderTest! + + override func setUp() { + window = UIWindow() + viewControllerUnderTest = ViewControllerUnderTest() + window.rootViewController = viewControllerUnderTest + window.makeKeyAndVisible() + } + + func test_givenNoInjection_whenGet_shouldReturnDefault() { + let dummy1 = viewControllerUnderTest.dummy + let dummy2 = viewControllerUnderTest.viewUnderTest.dummy + let dummy3 = viewControllerUnderTest.subViewUnderTest.dummy + XCTAssertTrue(dummy1 === DummyEnvironmentKey.defaultValue) + XCTAssertTrue(dummy2 === DummyEnvironmentKey.defaultValue) + XCTAssertTrue(dummy3 === DummyEnvironmentKey.defaultValue) + } + + func test_givenWindowInjection_whenGet_shouldAlwaysReturnSameValue() { + window.environment(\.dummy, DummyDependencyStub()) + let dummy1 = viewControllerUnderTest.dummy + let dummy2 = viewControllerUnderTest.viewUnderTest.dummy + let dummy3 = viewControllerUnderTest.subViewUnderTest.dummy + + XCTAssertFalse(dummy1 === DummyEnvironmentKey.defaultValue) + XCTAssertTrue(dummy1 === dummy2) + XCTAssertTrue(dummy2 === dummy3) + } + + func test_givenViewControllerInjection_whenGet_shouldAlwaysReturnSameValue() { + viewControllerUnderTest.environment(\.dummy, DummyDependencyStub()) + let dummy1 = viewControllerUnderTest.dummy + let dummy2 = viewControllerUnderTest.viewUnderTest.dummy + let dummy3 = viewControllerUnderTest.subViewUnderTest.dummy + + XCTAssertFalse(dummy1 === DummyEnvironmentKey.defaultValue) + XCTAssertTrue(dummy1 === dummy2) + XCTAssertTrue(dummy2 === dummy3) + } + + func test_givenViewInjection_whenGet_shouldNotOverridenByViewControllerValue() { + viewControllerUnderTest.viewUnderTest.environment(\.dummy, DummyDependencyStub()) + let dummy1 = viewControllerUnderTest.dummy + let dummy2 = viewControllerUnderTest.viewUnderTest.dummy + let dummy3 = viewControllerUnderTest.subViewUnderTest.dummy + + XCTAssertTrue(dummy1 === DummyEnvironmentKey.defaultValue) + XCTAssertFalse(dummy2 === DummyEnvironmentKey.defaultValue) + XCTAssertTrue(dummy2 === dummy3) + } + +} + +private final class ViewControllerUnderTest: UIViewController { + @UIEnvironment(\.dummy) var dummy: DummyDependencyStub + + lazy var viewUnderTest: ViewUnderTest = { + let view = ViewUnderTest() + self.view.addSubview(view) + return view + }() + + lazy var subViewUnderTest: ViewUnderTest = { + let view = ViewUnderTest() + self.viewUnderTest.addSubview(view) + return view + }() +} + +private final class ViewUnderTest: UIView { + @UIEnvironment(\.dummy) var dummy: DummyDependencyStub +} +#endif