diff --git a/Sources/TokamakCore/App/AppStorage.swift b/Sources/TokamakCore/App/AppStorage.swift index 3fd89ff62..c89ab99e9 100644 --- a/Sources/TokamakCore/App/AppStorage.swift +++ b/Sources/TokamakCore/App/AppStorage.swift @@ -17,7 +17,7 @@ import OpenCombine -@propertyWrapper public struct AppStorage: ObservedProperty { +@propertyWrapper public struct AppStorage: DynamicProperty { let provider: _StorageProvider? @Environment(\._defaultAppStorage) var defaultProvider: _StorageProvider? var unwrappedProvider: _StorageProvider { @@ -51,7 +51,7 @@ import OpenCombine } } -extension AppStorage: DynamicProperty {} +extension AppStorage: ObservedProperty {} extension AppStorage { public init(wrappedValue: Value, diff --git a/Sources/TokamakCore/App/Scenes/SceneStorage.swift b/Sources/TokamakCore/App/Scenes/SceneStorage.swift index aa576d900..e361e9d33 100644 --- a/Sources/TokamakCore/App/Scenes/SceneStorage.swift +++ b/Sources/TokamakCore/App/Scenes/SceneStorage.swift @@ -23,7 +23,7 @@ public enum _DefaultSceneStorageProvider { public static var `default`: _StorageProvider! } -@propertyWrapper public struct SceneStorage: ObservedProperty { +@propertyWrapper public struct SceneStorage: DynamicProperty { let key: String let defaultValue: Value let store: (_StorageProvider, String, Value) -> () @@ -51,6 +51,8 @@ public enum _DefaultSceneStorageProvider { } } +extension SceneStorage: ObservedProperty {} + extension SceneStorage { public init(wrappedValue: Value, _ key: String) where Value == Bool { diff --git a/Sources/TokamakCore/DynamicProperty.swift b/Sources/TokamakCore/DynamicProperty.swift index 9abcd94d2..8735c2d8c 100644 --- a/Sources/TokamakCore/DynamicProperty.swift +++ b/Sources/TokamakCore/DynamicProperty.swift @@ -15,5 +15,43 @@ // Created by Carson Katri on 7/17/20. // -// FIXME: Match SwiftUI implementation -protocol DynamicProperty {} +import Runtime + +public protocol DynamicProperty { + mutating func update() +} + +extension DynamicProperty { + public mutating func update() {} +} + +extension TypeInfo { + /// Extract all `DynamicProperty` from a type, recursively. + /// This is necessary as a `DynamicProperty` can be nested. + /// `EnvironmentValues` can also be injected at this point. + func dynamicProperties(_ environment: EnvironmentValues, + source: inout Any, + shouldUpdate: Bool) -> [PropertyInfo] { + var dynamicProps = [PropertyInfo]() + for prop in properties where prop.type is DynamicProperty.Type { + dynamicProps.append(prop) + // swiftlint:disable force_try + let propInfo = try! typeInfo(of: prop.type) + propInfo.injectEnvironment(from: environment, into: &source) + var extracted = try! prop.get(from: source) + dynamicProps.append( + contentsOf: propInfo.dynamicProperties(environment, + source: &extracted, + shouldUpdate: shouldUpdate) + ) + // swiftlint:disable:next force_cast + var extractedDynamicProp = extracted as! DynamicProperty + if shouldUpdate { + extractedDynamicProp.update() + } + try! prop.set(value: extractedDynamicProp, on: &source) + // swiftlint:enable force_try + } + return dynamicProps + } +} diff --git a/Sources/TokamakCore/Environment/Environment.swift b/Sources/TokamakCore/Environment/Environment.swift index 2e8511a7b..a4064eb50 100644 --- a/Sources/TokamakCore/Environment/Environment.swift +++ b/Sources/TokamakCore/Environment/Environment.swift @@ -23,7 +23,7 @@ protocol EnvironmentReader { mutating func setContent(from values: EnvironmentValues) } -@propertyWrapper public struct Environment: EnvironmentReader { +@propertyWrapper public struct Environment: DynamicProperty { enum Content { case keyPath(KeyPath) case value(Value) @@ -50,3 +50,5 @@ protocol EnvironmentReader { } } } + +extension Environment: EnvironmentReader {} diff --git a/Sources/TokamakCore/Environment/EnvironmentObject.swift b/Sources/TokamakCore/Environment/EnvironmentObject.swift index 46edc7961..9bacbebb7 100644 --- a/Sources/TokamakCore/Environment/EnvironmentObject.swift +++ b/Sources/TokamakCore/Environment/EnvironmentObject.swift @@ -17,8 +17,7 @@ import OpenCombine -@propertyWrapper public struct EnvironmentObject: ObservedProperty, - EnvironmentReader +@propertyWrapper public struct EnvironmentObject: DynamicProperty where ObjectType: ObservableObject { @dynamicMemberLookup public struct Wrapper { internal let root: ObjectType @@ -63,6 +62,8 @@ import OpenCombine public init() {} } +extension EnvironmentObject: ObservedProperty, EnvironmentReader {} + extension ObservableObject { static var environmentStore: WritableKeyPath { \.[ObjectIdentifier(self)] diff --git a/Sources/TokamakCore/StackReconciler.swift b/Sources/TokamakCore/StackReconciler.swift index bab12255a..51b022d32 100644 --- a/Sources/TokamakCore/StackReconciler.swift +++ b/Sources/TokamakCore/StackReconciler.swift @@ -142,11 +142,17 @@ public final class StackReconciler { body bodyKeypath: ReferenceWritableKeyPath, Any>, result: KeyPath, (Any) -> T>) -> T { let info = try! typeInfo(of: compositeElement.elementType) + info.injectEnvironment(from: compositeElement.environmentValues, + into: &compositeElement[keyPath: bodyKeypath]) let needsSubscriptions = compositeElement.subscriptions.isEmpty var stateIdx = 0 - for property in info.properties { + let dynamicProps = info.dynamicProperties(compositeElement.environmentValues, + source: &compositeElement[keyPath: bodyKeypath], + shouldUpdate: true) + for property in dynamicProps { + // Setup state/subscriptions if property.type is ValueStorage.Type { setupState(id: stateIdx, for: property, of: compositeElement, body: bodyKeypath) stateIdx += 1 diff --git a/Sources/TokamakCore/State/Binding.swift b/Sources/TokamakCore/State/Binding.swift index 2d784d190..8b8ab6d41 100644 --- a/Sources/TokamakCore/State/Binding.swift +++ b/Sources/TokamakCore/State/Binding.swift @@ -21,7 +21,7 @@ typealias Updater = (inout T) -> () view's state in-place synchronously, but only schedule an update with the renderer at a later time. */ -@propertyWrapper public struct Binding { +@propertyWrapper public struct Binding: DynamicProperty { public var wrappedValue: Value { get { get() } nonmutating set { set(newValue) } diff --git a/Sources/TokamakCore/State/ObservedObject.swift b/Sources/TokamakCore/State/ObservedObject.swift index 0f8b58585..76455a625 100644 --- a/Sources/TokamakCore/State/ObservedObject.swift +++ b/Sources/TokamakCore/State/ObservedObject.swift @@ -22,7 +22,7 @@ protocol ObservedProperty: DynamicProperty { } @propertyWrapper -public struct ObservedObject: ObservedProperty where ObjectType: ObservableObject { +public struct ObservedObject: DynamicProperty where ObjectType: ObservableObject { @dynamicMemberLookup public struct Wrapper { let root: ObjectType @@ -52,4 +52,4 @@ public struct ObservedObject: ObservedProperty where ObjectType: Obs } } -extension ObservedObject: DynamicProperty {} +extension ObservedObject: ObservedProperty {} diff --git a/Sources/TokamakCore/State/State.swift b/Sources/TokamakCore/State/State.swift index dec16b5d0..b5d00c36d 100644 --- a/Sources/TokamakCore/State/State.swift +++ b/Sources/TokamakCore/State/State.swift @@ -14,14 +14,13 @@ // // Created by Max Desiatov on 08/04/2020. // - protocol ValueStorage { var getter: (() -> Any)? { get set } var setter: ((Any) -> ())? { get set } var anyInitialValue: Any { get } } -@propertyWrapper public struct State { +@propertyWrapper public struct State: DynamicProperty { private let initialValue: Value var anyInitialValue: Any { initialValue } @@ -48,7 +47,6 @@ protocol ValueStorage { } extension State: ValueStorage {} -extension State: DynamicProperty {} extension State where Value: ExpressibleByNilLiteral { @inlinable public init() { self.init(wrappedValue: nil) }