Add support for preferences, `@StateObject`, `@EnvironmentObject`, and custom `DynamicProperty` types (#501)
* Pass preferences up the Fiber tree * Working preferences (except for backgroundPreferenceValue) * Initial StateObject support * Fix layout caching bug * Support StateObject/EnvironmentObject/custom DynamicProperty * Add doc comments for bindProperties and updateDynamicProperties * Use immediate scheduling in static HTML and test renderers * Add preferences test * Add state tests and improve testing API * Attempt to fix tests * Fix preference tests * Attempt to fix tests when Dispatch is unavailable * #if out on os(WASI) * Add check for WASI to test * Add check for WASI to TestViewProxy * #if out of import Dispatch when os == WASI * Remove all Dispatch code * Remove address from debugDescription * Move TestViewProxy to TokamakTestRenderer * Add memory address to Fiber.debugDescription * Fix copyright year * Add several missing types to Core.swift * Add missing LayoutValueKey to Core.swift * Fix issues with view trait propagation * Enable App/SceneStorage in DOMFiberRenderer * Address review comments * Revise preference implementation
This commit is contained in:
parent
9d0e2fc067
commit
676760d34b
|
@ -17,12 +17,13 @@
|
||||||
|
|
||||||
extension FiberReconciler.Fiber: CustomDebugStringConvertible {
|
extension FiberReconciler.Fiber: CustomDebugStringConvertible {
|
||||||
public var debugDescription: String {
|
public var debugDescription: String {
|
||||||
|
let memoryAddress = String(format: "%010p", unsafeBitCast(self, to: Int.self))
|
||||||
if case let .view(view, _) = content,
|
if case let .view(view, _) = content,
|
||||||
let text = view as? Text
|
let text = view as? Text
|
||||||
{
|
{
|
||||||
return "Text(\"\(text.storage.rawText)\")"
|
return "Text(\"\(text.storage.rawText)\") (\(memoryAddress))"
|
||||||
}
|
}
|
||||||
return typeInfo?.name ?? "Unknown"
|
return "\(typeInfo?.name ?? "Unknown") (\(memoryAddress))"
|
||||||
}
|
}
|
||||||
|
|
||||||
private func flush(level: Int = 0) -> String {
|
private func flush(level: Int = 0) -> String {
|
||||||
|
|
|
@ -16,7 +16,9 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import OpenCombineShim
|
||||||
|
|
||||||
|
// swiftlint:disable type_body_length
|
||||||
@_spi(TokamakCore)
|
@_spi(TokamakCore)
|
||||||
public extension FiberReconciler {
|
public extension FiberReconciler {
|
||||||
/// A manager for a single `View`.
|
/// A manager for a single `View`.
|
||||||
|
@ -83,22 +85,36 @@ public extension FiberReconciler {
|
||||||
/// Parent references are `unowned` (as opposed to `weak`)
|
/// Parent references are `unowned` (as opposed to `weak`)
|
||||||
/// because the parent will always exist if a child does.
|
/// because the parent will always exist if a child does.
|
||||||
/// If the parent is released, the child is released with it.
|
/// If the parent is released, the child is released with it.
|
||||||
unowned var parent: Fiber?
|
@_spi(TokamakCore)
|
||||||
|
public unowned var parent: Fiber?
|
||||||
|
|
||||||
/// The nearest parent that can be mounted on.
|
/// The nearest parent that can be mounted on.
|
||||||
unowned var elementParent: Fiber?
|
unowned var elementParent: Fiber?
|
||||||
|
|
||||||
|
/// The nearest parent that receives preferences.
|
||||||
|
unowned var preferenceParent: Fiber?
|
||||||
|
|
||||||
/// The cached type information for the underlying `View`.
|
/// The cached type information for the underlying `View`.
|
||||||
var typeInfo: TypeInfo?
|
var typeInfo: TypeInfo?
|
||||||
|
|
||||||
/// Boxes that store `State` data.
|
/// Boxes that store `State` data.
|
||||||
var state: [PropertyInfo: MutableStorage] = [:]
|
var state: [PropertyInfo: MutableStorage] = [:]
|
||||||
|
|
||||||
|
/// Subscribed `Cancellable`s keyed with the property contained the observable.
|
||||||
|
///
|
||||||
|
/// Each time properties are bound, a new subscription could be created.
|
||||||
|
/// When the subscription is overridden, the old cancellable is released.
|
||||||
|
var subscriptions: [PropertyInfo: AnyCancellable] = [:]
|
||||||
|
|
||||||
|
/// Storage for `PreferenceKey` values as they are passed up the tree.
|
||||||
|
var preferences: _PreferenceStore?
|
||||||
|
|
||||||
/// The computed dimensions and origin.
|
/// The computed dimensions and origin.
|
||||||
var geometry: ViewGeometry?
|
var geometry: ViewGeometry?
|
||||||
|
|
||||||
/// The WIP node if this is current, or the current node if this is WIP.
|
/// The WIP node if this is current, or the current node if this is WIP.
|
||||||
weak var alternate: Fiber?
|
@_spi(TokamakCore)
|
||||||
|
public weak var alternate: Fiber?
|
||||||
|
|
||||||
var createAndBindAlternate: (() -> Fiber?)?
|
var createAndBindAlternate: (() -> Fiber?)?
|
||||||
|
|
||||||
|
@ -129,6 +145,7 @@ public extension FiberReconciler {
|
||||||
element: Renderer.ElementType?,
|
element: Renderer.ElementType?,
|
||||||
parent: Fiber?,
|
parent: Fiber?,
|
||||||
elementParent: Fiber?,
|
elementParent: Fiber?,
|
||||||
|
preferenceParent: Fiber?,
|
||||||
elementIndex: Int?,
|
elementIndex: Int?,
|
||||||
traits: _ViewTraitStore?,
|
traits: _ViewTraitStore?,
|
||||||
reconciler: FiberReconciler<Renderer>?
|
reconciler: FiberReconciler<Renderer>?
|
||||||
|
@ -138,17 +155,24 @@ public extension FiberReconciler {
|
||||||
sibling = nil
|
sibling = nil
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.elementParent = elementParent
|
self.elementParent = elementParent
|
||||||
|
self.preferenceParent = preferenceParent
|
||||||
typeInfo = TokamakCore.typeInfo(of: V.self)
|
typeInfo = TokamakCore.typeInfo(of: V.self)
|
||||||
|
|
||||||
let environment = parent?.outputs.environment ?? .init(.init())
|
let environment = parent?.outputs.environment ?? .init(.init())
|
||||||
state = bindProperties(to: &view, typeInfo, environment.environment)
|
bindProperties(to: &view, typeInfo, environment.environment)
|
||||||
|
var updateView = view
|
||||||
let viewInputs = ViewInputs(
|
let viewInputs = ViewInputs(
|
||||||
content: view,
|
content: view,
|
||||||
|
updateContent: { $0(&updateView) },
|
||||||
environment: environment,
|
environment: environment,
|
||||||
traits: traits
|
traits: traits,
|
||||||
|
preferenceStore: preferences
|
||||||
)
|
)
|
||||||
outputs = V._makeView(viewInputs)
|
outputs = V._makeView(viewInputs)
|
||||||
|
if let preferenceStore = outputs.preferenceStore {
|
||||||
|
preferences = preferenceStore
|
||||||
|
}
|
||||||
|
view = updateView
|
||||||
content = content(for: view)
|
content = content(for: view)
|
||||||
|
|
||||||
if let element = element {
|
if let element = element {
|
||||||
|
@ -175,6 +199,8 @@ public extension FiberReconciler {
|
||||||
let alternate = Fiber(
|
let alternate = Fiber(
|
||||||
bound: alternateView,
|
bound: alternateView,
|
||||||
state: self.state,
|
state: self.state,
|
||||||
|
subscriptions: self.subscriptions,
|
||||||
|
preferences: self.preferences,
|
||||||
layout: self.layout,
|
layout: self.layout,
|
||||||
alternate: self,
|
alternate: self,
|
||||||
outputs: self.outputs,
|
outputs: self.outputs,
|
||||||
|
@ -182,6 +208,7 @@ public extension FiberReconciler {
|
||||||
element: self.element,
|
element: self.element,
|
||||||
parent: self.parent?.alternate,
|
parent: self.parent?.alternate,
|
||||||
elementParent: self.elementParent?.alternate,
|
elementParent: self.elementParent?.alternate,
|
||||||
|
preferenceParent: self.preferenceParent?.alternate,
|
||||||
reconciler: reconciler
|
reconciler: reconciler
|
||||||
)
|
)
|
||||||
self.alternate = alternate
|
self.alternate = alternate
|
||||||
|
@ -205,6 +232,8 @@ public extension FiberReconciler {
|
||||||
init<V: View>(
|
init<V: View>(
|
||||||
bound view: V,
|
bound view: V,
|
||||||
state: [PropertyInfo: MutableStorage],
|
state: [PropertyInfo: MutableStorage],
|
||||||
|
subscriptions: [PropertyInfo: AnyCancellable],
|
||||||
|
preferences: _PreferenceStore?,
|
||||||
layout: AnyLayout!,
|
layout: AnyLayout!,
|
||||||
alternate: Fiber,
|
alternate: Fiber,
|
||||||
outputs: ViewOutputs,
|
outputs: ViewOutputs,
|
||||||
|
@ -212,6 +241,7 @@ public extension FiberReconciler {
|
||||||
element: Renderer.ElementType?,
|
element: Renderer.ElementType?,
|
||||||
parent: FiberReconciler<Renderer>.Fiber?,
|
parent: FiberReconciler<Renderer>.Fiber?,
|
||||||
elementParent: Fiber?,
|
elementParent: Fiber?,
|
||||||
|
preferenceParent: Fiber?,
|
||||||
reconciler: FiberReconciler<Renderer>?
|
reconciler: FiberReconciler<Renderer>?
|
||||||
) {
|
) {
|
||||||
self.alternate = alternate
|
self.alternate = alternate
|
||||||
|
@ -221,9 +251,12 @@ public extension FiberReconciler {
|
||||||
sibling = nil
|
sibling = nil
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.elementParent = elementParent
|
self.elementParent = elementParent
|
||||||
|
self.preferenceParent = preferenceParent
|
||||||
self.typeInfo = typeInfo
|
self.typeInfo = typeInfo
|
||||||
self.outputs = outputs
|
self.outputs = outputs
|
||||||
self.state = state
|
self.state = state
|
||||||
|
self.subscriptions = subscriptions
|
||||||
|
self.preferences = preferences
|
||||||
if element != nil {
|
if element != nil {
|
||||||
self.layout = layout
|
self.layout = layout
|
||||||
}
|
}
|
||||||
|
@ -234,36 +267,98 @@ public extension FiberReconciler {
|
||||||
to content: inout T,
|
to content: inout T,
|
||||||
_ typeInfo: TypeInfo?,
|
_ typeInfo: TypeInfo?,
|
||||||
_ environment: EnvironmentValues
|
_ environment: EnvironmentValues
|
||||||
) -> [PropertyInfo: MutableStorage] {
|
) {
|
||||||
guard let typeInfo = typeInfo else { return [:] }
|
var erased: Any = content
|
||||||
|
bindProperties(to: &erased, typeInfo, environment)
|
||||||
|
// swiftlint:disable:next force_cast
|
||||||
|
content = erased as! T
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collect `DynamicProperty`s and link their state changes to the reconciler.
|
||||||
|
private func bindProperties(
|
||||||
|
to content: inout Any,
|
||||||
|
_ typeInfo: TypeInfo?,
|
||||||
|
_ environment: EnvironmentValues
|
||||||
|
) {
|
||||||
|
guard let typeInfo = typeInfo else { return }
|
||||||
|
|
||||||
var state: [PropertyInfo: MutableStorage] = [:]
|
|
||||||
for property in typeInfo.properties where property.type is DynamicProperty.Type {
|
for property in typeInfo.properties where property.type is DynamicProperty.Type {
|
||||||
var value = property.get(from: content)
|
var value = property.get(from: content)
|
||||||
|
// Bind nested properties.
|
||||||
|
bindProperties(to: &value, TokamakCore.typeInfo(of: property.type), environment)
|
||||||
|
// Create boxes for `@State` and other mutable properties.
|
||||||
if var storage = value as? WritableValueStorage {
|
if var storage = value as? WritableValueStorage {
|
||||||
let box = self.state[property] ?? MutableStorage(
|
let box = self.state[property] ?? MutableStorage(
|
||||||
initialValue: storage.anyInitialValue,
|
initialValue: storage.anyInitialValue,
|
||||||
onSet: { [weak self] in
|
onSet: { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.reconciler?.reconcile(from: self)
|
self.reconciler?.fiberChanged(self)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
state[property] = box
|
state[property] = box
|
||||||
storage.getter = { box.value }
|
storage.getter = { box.value }
|
||||||
storage.setter = { box.setValue($0, with: $1) }
|
storage.setter = { box.setValue($0, with: $1) }
|
||||||
value = storage
|
value = storage
|
||||||
|
// Create boxes for `@StateObject` and other immutable properties.
|
||||||
|
} else if var storage = value as? ValueStorage {
|
||||||
|
let box = self.state[property] ?? MutableStorage(
|
||||||
|
initialValue: storage.anyInitialValue,
|
||||||
|
onSet: {}
|
||||||
|
)
|
||||||
|
state[property] = box
|
||||||
|
storage.getter = { box.value }
|
||||||
|
value = storage
|
||||||
|
// Read from the environment.
|
||||||
} else if var environmentReader = value as? EnvironmentReader {
|
} else if var environmentReader = value as? EnvironmentReader {
|
||||||
environmentReader.setContent(from: environment)
|
environmentReader.setContent(from: environment)
|
||||||
value = environmentReader
|
value = environmentReader
|
||||||
}
|
}
|
||||||
|
// Subscribe to observable properties.
|
||||||
|
if let observed = value as? ObservedProperty {
|
||||||
|
subscriptions[property] = observed.objectWillChange.sink { [weak self] _ in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.reconciler?.fiberChanged(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
property.set(value: value, on: &content)
|
property.set(value: value, on: &content)
|
||||||
}
|
}
|
||||||
if var environmentReader = content as? EnvironmentReader {
|
if var environmentReader = content as? EnvironmentReader {
|
||||||
environmentReader.setContent(from: environment)
|
environmentReader.setContent(from: environment)
|
||||||
// swiftlint:disable:next force_cast
|
content = environmentReader
|
||||||
content = environmentReader as! T
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call `update()` on each `DynamicProperty` in the type.
|
||||||
|
private func updateDynamicProperties(
|
||||||
|
of content: inout Any,
|
||||||
|
_ typeInfo: TypeInfo?
|
||||||
|
) {
|
||||||
|
guard let typeInfo = typeInfo else { return }
|
||||||
|
for property in typeInfo.properties where property.type is DynamicProperty.Type {
|
||||||
|
var value = property.get(from: content)
|
||||||
|
// Update nested properties.
|
||||||
|
updateDynamicProperties(of: &value, TokamakCore.typeInfo(of: property.type))
|
||||||
|
// swiftlint:disable:next force_cast
|
||||||
|
var dynamicProperty = value as! DynamicProperty
|
||||||
|
dynamicProperty.update()
|
||||||
|
property.set(value: dynamicProperty, on: &content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update each `DynamicProperty` in our content.
|
||||||
|
func updateDynamicProperties() {
|
||||||
|
guard let content = content else { return }
|
||||||
|
switch content {
|
||||||
|
case .app(var app, let visit):
|
||||||
|
updateDynamicProperties(of: &app, typeInfo)
|
||||||
|
self.content = .app(app, visit: visit)
|
||||||
|
case .scene(var scene, let visit):
|
||||||
|
updateDynamicProperties(of: &scene, typeInfo)
|
||||||
|
self.content = .scene(scene, visit: visit)
|
||||||
|
case .view(var view, let visit):
|
||||||
|
updateDynamicProperties(of: &view, typeInfo)
|
||||||
|
self.content = .view(view, visit: visit)
|
||||||
}
|
}
|
||||||
return state
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func update<V: View>(
|
func update<V: View>(
|
||||||
|
@ -276,14 +371,20 @@ public extension FiberReconciler {
|
||||||
self.elementIndex = elementIndex
|
self.elementIndex = elementIndex
|
||||||
|
|
||||||
let environment = parent?.outputs.environment ?? .init(.init())
|
let environment = parent?.outputs.environment ?? .init(.init())
|
||||||
state = bindProperties(to: &view, typeInfo, environment.environment)
|
bindProperties(to: &view, typeInfo, environment.environment)
|
||||||
content = content(for: view)
|
var updateView = view
|
||||||
let inputs = ViewInputs(
|
let inputs = ViewInputs(
|
||||||
content: view,
|
content: view,
|
||||||
|
updateContent: {
|
||||||
|
$0(&updateView)
|
||||||
|
},
|
||||||
environment: environment,
|
environment: environment,
|
||||||
traits: traits
|
traits: traits,
|
||||||
|
preferenceStore: preferences
|
||||||
)
|
)
|
||||||
outputs = V._makeView(inputs)
|
outputs = V._makeView(inputs)
|
||||||
|
view = updateView
|
||||||
|
content = content(for: view)
|
||||||
|
|
||||||
if element != nil {
|
if element != nil {
|
||||||
layout = (view as? _AnyLayout)?._erased() ?? DefaultLayout.shared
|
layout = (view as? _AnyLayout)?._erased() ?? DefaultLayout.shared
|
||||||
|
@ -308,13 +409,26 @@ public extension FiberReconciler {
|
||||||
// `App`s are always the root, so they can have no parent.
|
// `App`s are always the root, so they can have no parent.
|
||||||
parent = nil
|
parent = nil
|
||||||
elementParent = nil
|
elementParent = nil
|
||||||
|
preferenceParent = nil
|
||||||
element = rootElement
|
element = rootElement
|
||||||
typeInfo = TokamakCore.typeInfo(of: A.self)
|
typeInfo = TokamakCore.typeInfo(of: A.self)
|
||||||
state = bindProperties(to: &app, typeInfo, rootEnvironment)
|
bindProperties(to: &app, typeInfo, rootEnvironment)
|
||||||
|
var updateApp = app
|
||||||
outputs = .init(
|
outputs = .init(
|
||||||
inputs: .init(content: app, environment: .init(rootEnvironment), traits: .init())
|
inputs: .init(
|
||||||
|
content: app,
|
||||||
|
updateContent: {
|
||||||
|
$0(&updateApp)
|
||||||
|
},
|
||||||
|
environment: .init(rootEnvironment),
|
||||||
|
traits: .init(),
|
||||||
|
preferenceStore: preferences
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
if let preferenceStore = outputs.preferenceStore {
|
||||||
|
preferences = preferenceStore
|
||||||
|
}
|
||||||
|
app = updateApp
|
||||||
content = content(for: app)
|
content = content(for: app)
|
||||||
|
|
||||||
layout = .init(RootLayout(renderer: reconciler.renderer))
|
layout = .init(RootLayout(renderer: reconciler.renderer))
|
||||||
|
@ -326,6 +440,8 @@ public extension FiberReconciler {
|
||||||
let alternate = Fiber(
|
let alternate = Fiber(
|
||||||
bound: alternateApp,
|
bound: alternateApp,
|
||||||
state: self.state,
|
state: self.state,
|
||||||
|
subscriptions: self.subscriptions,
|
||||||
|
preferences: self.preferences,
|
||||||
layout: self.layout,
|
layout: self.layout,
|
||||||
alternate: self,
|
alternate: self,
|
||||||
outputs: self.outputs,
|
outputs: self.outputs,
|
||||||
|
@ -341,6 +457,8 @@ public extension FiberReconciler {
|
||||||
init<A: App>(
|
init<A: App>(
|
||||||
bound app: A,
|
bound app: A,
|
||||||
state: [PropertyInfo: MutableStorage],
|
state: [PropertyInfo: MutableStorage],
|
||||||
|
subscriptions: [PropertyInfo: AnyCancellable],
|
||||||
|
preferences: _PreferenceStore?,
|
||||||
layout: AnyLayout?,
|
layout: AnyLayout?,
|
||||||
alternate: Fiber,
|
alternate: Fiber,
|
||||||
outputs: SceneOutputs,
|
outputs: SceneOutputs,
|
||||||
|
@ -355,9 +473,12 @@ public extension FiberReconciler {
|
||||||
sibling = nil
|
sibling = nil
|
||||||
parent = nil
|
parent = nil
|
||||||
elementParent = nil
|
elementParent = nil
|
||||||
|
preferenceParent = nil
|
||||||
self.typeInfo = typeInfo
|
self.typeInfo = typeInfo
|
||||||
self.outputs = outputs
|
self.outputs = outputs
|
||||||
self.state = state
|
self.state = state
|
||||||
|
self.subscriptions = subscriptions
|
||||||
|
self.preferences = preferences
|
||||||
self.layout = layout
|
self.layout = layout
|
||||||
content = content(for: app)
|
content = content(for: app)
|
||||||
}
|
}
|
||||||
|
@ -367,6 +488,7 @@ public extension FiberReconciler {
|
||||||
element: Renderer.ElementType?,
|
element: Renderer.ElementType?,
|
||||||
parent: Fiber?,
|
parent: Fiber?,
|
||||||
elementParent: Fiber?,
|
elementParent: Fiber?,
|
||||||
|
preferenceParent: Fiber?,
|
||||||
environment: EnvironmentBox?,
|
environment: EnvironmentBox?,
|
||||||
reconciler: FiberReconciler<Renderer>?
|
reconciler: FiberReconciler<Renderer>?
|
||||||
) {
|
) {
|
||||||
|
@ -376,18 +498,27 @@ public extension FiberReconciler {
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.elementParent = elementParent
|
self.elementParent = elementParent
|
||||||
self.element = element
|
self.element = element
|
||||||
|
self.preferenceParent = preferenceParent
|
||||||
typeInfo = TokamakCore.typeInfo(of: S.self)
|
typeInfo = TokamakCore.typeInfo(of: S.self)
|
||||||
|
|
||||||
let environment = environment ?? parent?.outputs.environment ?? .init(.init())
|
let environment = environment ?? parent?.outputs.environment ?? .init(.init())
|
||||||
state = bindProperties(to: &scene, typeInfo, environment.environment)
|
bindProperties(to: &scene, typeInfo, environment.environment)
|
||||||
|
var updateScene = scene
|
||||||
outputs = S._makeScene(
|
outputs = S._makeScene(
|
||||||
.init(
|
.init(
|
||||||
content: scene,
|
content: scene,
|
||||||
|
updateContent: {
|
||||||
|
$0(&updateScene)
|
||||||
|
},
|
||||||
environment: environment,
|
environment: environment,
|
||||||
traits: .init()
|
traits: .init(),
|
||||||
|
preferenceStore: preferences
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if let preferenceStore = outputs.preferenceStore {
|
||||||
|
preferences = preferenceStore
|
||||||
|
}
|
||||||
|
scene = updateScene
|
||||||
content = content(for: scene)
|
content = content(for: scene)
|
||||||
|
|
||||||
if element != nil {
|
if element != nil {
|
||||||
|
@ -401,6 +532,8 @@ public extension FiberReconciler {
|
||||||
let alternate = Fiber(
|
let alternate = Fiber(
|
||||||
bound: alternateScene,
|
bound: alternateScene,
|
||||||
state: self.state,
|
state: self.state,
|
||||||
|
subscriptions: self.subscriptions,
|
||||||
|
preferences: self.preferences,
|
||||||
layout: self.layout,
|
layout: self.layout,
|
||||||
alternate: self,
|
alternate: self,
|
||||||
outputs: self.outputs,
|
outputs: self.outputs,
|
||||||
|
@ -408,6 +541,7 @@ public extension FiberReconciler {
|
||||||
element: self.element,
|
element: self.element,
|
||||||
parent: self.parent?.alternate,
|
parent: self.parent?.alternate,
|
||||||
elementParent: self.elementParent?.alternate,
|
elementParent: self.elementParent?.alternate,
|
||||||
|
preferenceParent: self.preferenceParent?.alternate,
|
||||||
reconciler: reconciler
|
reconciler: reconciler
|
||||||
)
|
)
|
||||||
self.alternate = alternate
|
self.alternate = alternate
|
||||||
|
@ -431,6 +565,8 @@ public extension FiberReconciler {
|
||||||
init<S: Scene>(
|
init<S: Scene>(
|
||||||
bound scene: S,
|
bound scene: S,
|
||||||
state: [PropertyInfo: MutableStorage],
|
state: [PropertyInfo: MutableStorage],
|
||||||
|
subscriptions: [PropertyInfo: AnyCancellable],
|
||||||
|
preferences: _PreferenceStore?,
|
||||||
layout: AnyLayout!,
|
layout: AnyLayout!,
|
||||||
alternate: Fiber,
|
alternate: Fiber,
|
||||||
outputs: SceneOutputs,
|
outputs: SceneOutputs,
|
||||||
|
@ -438,6 +574,7 @@ public extension FiberReconciler {
|
||||||
element: Renderer.ElementType?,
|
element: Renderer.ElementType?,
|
||||||
parent: FiberReconciler<Renderer>.Fiber?,
|
parent: FiberReconciler<Renderer>.Fiber?,
|
||||||
elementParent: Fiber?,
|
elementParent: Fiber?,
|
||||||
|
preferenceParent: Fiber?,
|
||||||
reconciler: FiberReconciler<Renderer>?
|
reconciler: FiberReconciler<Renderer>?
|
||||||
) {
|
) {
|
||||||
self.alternate = alternate
|
self.alternate = alternate
|
||||||
|
@ -447,9 +584,12 @@ public extension FiberReconciler {
|
||||||
sibling = nil
|
sibling = nil
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.elementParent = elementParent
|
self.elementParent = elementParent
|
||||||
|
self.preferenceParent = preferenceParent
|
||||||
self.typeInfo = typeInfo
|
self.typeInfo = typeInfo
|
||||||
self.outputs = outputs
|
self.outputs = outputs
|
||||||
self.state = state
|
self.state = state
|
||||||
|
self.subscriptions = subscriptions
|
||||||
|
self.preferences = preferences
|
||||||
if element != nil {
|
if element != nil {
|
||||||
self.layout = layout
|
self.layout = layout
|
||||||
}
|
}
|
||||||
|
@ -462,13 +602,19 @@ public extension FiberReconciler {
|
||||||
typeInfo = TokamakCore.typeInfo(of: S.self)
|
typeInfo = TokamakCore.typeInfo(of: S.self)
|
||||||
|
|
||||||
let environment = parent?.outputs.environment ?? .init(.init())
|
let environment = parent?.outputs.environment ?? .init(.init())
|
||||||
state = bindProperties(to: &scene, typeInfo, environment.environment)
|
bindProperties(to: &scene, typeInfo, environment.environment)
|
||||||
content = content(for: scene)
|
var updateScene = scene
|
||||||
outputs = S._makeScene(.init(
|
outputs = S._makeScene(.init(
|
||||||
content: scene,
|
content: scene,
|
||||||
|
updateContent: {
|
||||||
|
$0(&updateScene)
|
||||||
|
},
|
||||||
environment: environment,
|
environment: environment,
|
||||||
traits: .init()
|
traits: .init(),
|
||||||
|
preferenceStore: preferences
|
||||||
))
|
))
|
||||||
|
scene = updateScene
|
||||||
|
content = content(for: scene)
|
||||||
|
|
||||||
if element != nil {
|
if element != nil {
|
||||||
layout = (scene as? _AnyLayout)?._erased() ?? DefaultLayout.shared
|
layout = (scene as? _AnyLayout)?._erased() ?? DefaultLayout.shared
|
||||||
|
|
|
@ -29,12 +29,12 @@ extension FiberReconciler {
|
||||||
var sibling: Result?
|
var sibling: Result?
|
||||||
var newContent: Renderer.ElementType.Content?
|
var newContent: Renderer.ElementType.Content?
|
||||||
var elementIndices: [ObjectIdentifier: Int]
|
var elementIndices: [ObjectIdentifier: Int]
|
||||||
|
var nextTraits: _ViewTraitStore
|
||||||
|
|
||||||
// For reducing
|
// For reducing
|
||||||
var lastSibling: Result?
|
var lastSibling: Result?
|
||||||
var nextExisting: Fiber?
|
var nextExisting: Fiber?
|
||||||
var nextExistingAlternate: Fiber?
|
var nextExistingAlternate: Fiber?
|
||||||
var pendingTraits: _ViewTraitStore
|
|
||||||
|
|
||||||
init(
|
init(
|
||||||
fiber: Fiber?,
|
fiber: Fiber?,
|
||||||
|
@ -44,7 +44,7 @@ extension FiberReconciler {
|
||||||
alternateChild: Fiber?,
|
alternateChild: Fiber?,
|
||||||
newContent: Renderer.ElementType.Content? = nil,
|
newContent: Renderer.ElementType.Content? = nil,
|
||||||
elementIndices: [ObjectIdentifier: Int],
|
elementIndices: [ObjectIdentifier: Int],
|
||||||
pendingTraits: _ViewTraitStore
|
nextTraits: _ViewTraitStore
|
||||||
) {
|
) {
|
||||||
self.fiber = fiber
|
self.fiber = fiber
|
||||||
self.visitChildren = visitChildren
|
self.visitChildren = visitChildren
|
||||||
|
@ -53,7 +53,7 @@ extension FiberReconciler {
|
||||||
nextExistingAlternate = alternateChild
|
nextExistingAlternate = alternateChild
|
||||||
self.newContent = newContent
|
self.newContent = newContent
|
||||||
self.elementIndices = elementIndices
|
self.elementIndices = elementIndices
|
||||||
self.pendingTraits = pendingTraits
|
self.nextTraits = nextTraits
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,12 +61,13 @@ extension FiberReconciler {
|
||||||
Self.reduce(
|
Self.reduce(
|
||||||
into: &partialResult,
|
into: &partialResult,
|
||||||
nextValue: nextScene,
|
nextValue: nextScene,
|
||||||
createFiber: { scene, element, parent, elementParent, _, _, reconciler in
|
createFiber: { scene, element, parent, elementParent, preferenceParent, _, _, reconciler in
|
||||||
Fiber(
|
Fiber(
|
||||||
&scene,
|
&scene,
|
||||||
element: element,
|
element: element,
|
||||||
parent: parent,
|
parent: parent,
|
||||||
elementParent: elementParent,
|
elementParent: elementParent,
|
||||||
|
preferenceParent: preferenceParent,
|
||||||
environment: nil,
|
environment: nil,
|
||||||
reconciler: reconciler
|
reconciler: reconciler
|
||||||
)
|
)
|
||||||
|
@ -82,12 +83,16 @@ extension FiberReconciler {
|
||||||
Self.reduce(
|
Self.reduce(
|
||||||
into: &partialResult,
|
into: &partialResult,
|
||||||
nextValue: nextView,
|
nextValue: nextView,
|
||||||
createFiber: { view, element, parent, elementParent, elementIndex, traits, reconciler in
|
createFiber: {
|
||||||
|
view, element,
|
||||||
|
parent, elementParent, preferenceParent, elementIndex,
|
||||||
|
traits, reconciler in
|
||||||
Fiber(
|
Fiber(
|
||||||
&view,
|
&view,
|
||||||
element: element,
|
element: element,
|
||||||
parent: parent,
|
parent: parent,
|
||||||
elementParent: elementParent,
|
elementParent: elementParent,
|
||||||
|
preferenceParent: preferenceParent,
|
||||||
elementIndex: elementIndex,
|
elementIndex: elementIndex,
|
||||||
traits: traits,
|
traits: traits,
|
||||||
reconciler: reconciler
|
reconciler: reconciler
|
||||||
|
@ -114,6 +119,7 @@ extension FiberReconciler {
|
||||||
Renderer.ElementType?,
|
Renderer.ElementType?,
|
||||||
Fiber?,
|
Fiber?,
|
||||||
Fiber?,
|
Fiber?,
|
||||||
|
Fiber?,
|
||||||
Int?,
|
Int?,
|
||||||
_ViewTraitStore,
|
_ViewTraitStore,
|
||||||
FiberReconciler?
|
FiberReconciler?
|
||||||
|
@ -123,6 +129,7 @@ extension FiberReconciler {
|
||||||
) {
|
) {
|
||||||
// Create the node and its element.
|
// Create the node and its element.
|
||||||
var nextValue = nextValue
|
var nextValue = nextValue
|
||||||
|
|
||||||
let resultChild: Result
|
let resultChild: Result
|
||||||
if let existing = partialResult.nextExisting {
|
if let existing = partialResult.nextExisting {
|
||||||
// If a fiber already exists, simply update it with the new view.
|
// If a fiber already exists, simply update it with the new view.
|
||||||
|
@ -136,7 +143,7 @@ extension FiberReconciler {
|
||||||
existing,
|
existing,
|
||||||
&nextValue,
|
&nextValue,
|
||||||
key.map { partialResult.elementIndices[$0, default: 0] },
|
key.map { partialResult.elementIndices[$0, default: 0] },
|
||||||
partialResult.pendingTraits
|
partialResult.nextTraits
|
||||||
)
|
)
|
||||||
resultChild = Result(
|
resultChild = Result(
|
||||||
fiber: existing,
|
fiber: existing,
|
||||||
|
@ -146,7 +153,7 @@ extension FiberReconciler {
|
||||||
alternateChild: existing.alternate?.child,
|
alternateChild: existing.alternate?.child,
|
||||||
newContent: newContent,
|
newContent: newContent,
|
||||||
elementIndices: partialResult.elementIndices,
|
elementIndices: partialResult.elementIndices,
|
||||||
pendingTraits: existing.element != nil ? .init() : partialResult.pendingTraits
|
nextTraits: existing.element != nil ? .init() : partialResult.nextTraits
|
||||||
)
|
)
|
||||||
partialResult.nextExisting = existing.sibling
|
partialResult.nextExisting = existing.sibling
|
||||||
partialResult.nextExistingAlternate = partialResult.nextExistingAlternate?.sibling
|
partialResult.nextExistingAlternate = partialResult.nextExistingAlternate?.sibling
|
||||||
|
@ -154,6 +161,9 @@ extension FiberReconciler {
|
||||||
let elementParent = partialResult.fiber?.element != nil
|
let elementParent = partialResult.fiber?.element != nil
|
||||||
? partialResult.fiber
|
? partialResult.fiber
|
||||||
: partialResult.fiber?.elementParent
|
: partialResult.fiber?.elementParent
|
||||||
|
let preferenceParent = partialResult.fiber?.preferences != nil
|
||||||
|
? partialResult.fiber
|
||||||
|
: partialResult.fiber?.preferenceParent
|
||||||
let key: ObjectIdentifier?
|
let key: ObjectIdentifier?
|
||||||
if let elementParent = elementParent {
|
if let elementParent = elementParent {
|
||||||
key = ObjectIdentifier(elementParent)
|
key = ObjectIdentifier(elementParent)
|
||||||
|
@ -166,10 +176,12 @@ extension FiberReconciler {
|
||||||
partialResult.nextExistingAlternate?.element,
|
partialResult.nextExistingAlternate?.element,
|
||||||
partialResult.fiber,
|
partialResult.fiber,
|
||||||
elementParent,
|
elementParent,
|
||||||
|
preferenceParent,
|
||||||
key.map { partialResult.elementIndices[$0, default: 0] },
|
key.map { partialResult.elementIndices[$0, default: 0] },
|
||||||
partialResult.pendingTraits,
|
partialResult.nextTraits,
|
||||||
partialResult.fiber?.reconciler
|
partialResult.fiber?.reconciler
|
||||||
)
|
)
|
||||||
|
|
||||||
// If a fiber already exists for an alternate, link them.
|
// If a fiber already exists for an alternate, link them.
|
||||||
if let alternate = partialResult.nextExistingAlternate {
|
if let alternate = partialResult.nextExistingAlternate {
|
||||||
fiber.alternate = alternate
|
fiber.alternate = alternate
|
||||||
|
@ -182,7 +194,7 @@ extension FiberReconciler {
|
||||||
child: nil,
|
child: nil,
|
||||||
alternateChild: fiber.alternate?.child,
|
alternateChild: fiber.alternate?.child,
|
||||||
elementIndices: partialResult.elementIndices,
|
elementIndices: partialResult.elementIndices,
|
||||||
pendingTraits: fiber.element != nil ? .init() : partialResult.pendingTraits
|
nextTraits: fiber.element != nil ? .init() : partialResult.nextTraits
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Get the last child element we've processed, and add the new child as its sibling.
|
// Get the last child element we've processed, and add the new child as its sibling.
|
||||||
|
|
|
@ -39,6 +39,13 @@ public final class FiberReconciler<Renderer: FiberRenderer> {
|
||||||
private let caches: Caches
|
private let caches: Caches
|
||||||
|
|
||||||
private var isReconciling = false
|
private var isReconciling = false
|
||||||
|
/// The identifiers for each `Fiber` that changed state during the last run loop.
|
||||||
|
///
|
||||||
|
/// The reconciler loop starts at the root of the `View` hierarchy
|
||||||
|
/// to ensure all preference values are passed down correctly.
|
||||||
|
/// To help mitigate performance issues related to this, we only perform reconcile
|
||||||
|
/// checks when we reach a changed `Fiber`.
|
||||||
|
private var changedFibers = Set<ObjectIdentifier>()
|
||||||
public var afterReconcileActions = [() -> ()]()
|
public var afterReconcileActions = [() -> ()]()
|
||||||
|
|
||||||
struct RootView<Content: View>: View {
|
struct RootView<Content: View>: View {
|
||||||
|
@ -105,13 +112,14 @@ public final class FiberReconciler<Renderer: FiberRenderer> {
|
||||||
element: renderer.rootElement,
|
element: renderer.rootElement,
|
||||||
parent: nil,
|
parent: nil,
|
||||||
elementParent: nil,
|
elementParent: nil,
|
||||||
|
preferenceParent: nil,
|
||||||
elementIndex: 0,
|
elementIndex: 0,
|
||||||
traits: nil,
|
traits: nil,
|
||||||
reconciler: self
|
reconciler: self
|
||||||
)
|
)
|
||||||
// Start by building the initial tree.
|
// Start by building the initial tree.
|
||||||
alternate = current.createAndBindAlternate?()
|
alternate = current.createAndBindAlternate?()
|
||||||
reconcile(from: current)
|
fiberChanged(current)
|
||||||
}
|
}
|
||||||
|
|
||||||
public init<A: App>(_ renderer: Renderer, _ app: A) {
|
public init<A: App>(_ renderer: Renderer, _ app: A) {
|
||||||
|
@ -135,19 +143,20 @@ public final class FiberReconciler<Renderer: FiberRenderer> {
|
||||||
)
|
)
|
||||||
// Start by building the initial tree.
|
// Start by building the initial tree.
|
||||||
alternate = current.createAndBindAlternate?()
|
alternate = current.createAndBindAlternate?()
|
||||||
reconcile(from: current)
|
fiberChanged(current)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A visitor that performs each pass used by the `FiberReconciler`.
|
/// A visitor that performs each pass used by the `FiberReconciler`.
|
||||||
final class ReconcilerVisitor: AppVisitor, SceneVisitor, ViewVisitor {
|
final class ReconcilerVisitor: AppVisitor, SceneVisitor, ViewVisitor {
|
||||||
let root: Fiber
|
let root: Fiber
|
||||||
let reconcileRoot: Fiber
|
/// Any `Fiber`s that changed state during the last run loop.
|
||||||
|
let changedFibers: Set<ObjectIdentifier>
|
||||||
unowned let reconciler: FiberReconciler
|
unowned let reconciler: FiberReconciler
|
||||||
var mutations = [Mutation<Renderer>]()
|
var mutations = [Mutation<Renderer>]()
|
||||||
|
|
||||||
init(root: Fiber, reconcileRoot: Fiber, reconciler: FiberReconciler) {
|
init(root: Fiber, changedFibers: Set<ObjectIdentifier>, reconciler: FiberReconciler) {
|
||||||
self.root = root
|
self.root = root
|
||||||
self.reconcileRoot = reconcileRoot
|
self.changedFibers = changedFibers
|
||||||
self.reconciler = reconciler
|
self.reconciler = reconciler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,13 +182,6 @@ public final class FiberReconciler<Renderer: FiberRenderer> {
|
||||||
} else {
|
} else {
|
||||||
alternateRoot = root.createAndBindAlternate?()
|
alternateRoot = root.createAndBindAlternate?()
|
||||||
}
|
}
|
||||||
let alternateReconcileRoot: Fiber?
|
|
||||||
if let alternate = reconcileRoot.alternate {
|
|
||||||
alternateReconcileRoot = alternate
|
|
||||||
} else {
|
|
||||||
alternateReconcileRoot = reconcileRoot.createAndBindAlternate?()
|
|
||||||
}
|
|
||||||
guard let alternateReconcileRoot = alternateReconcileRoot else { return }
|
|
||||||
let rootResult = TreeReducer.Result(
|
let rootResult = TreeReducer.Result(
|
||||||
fiber: alternateRoot, // The alternate is the WIP node.
|
fiber: alternateRoot, // The alternate is the WIP node.
|
||||||
visitChildren: visitChildren,
|
visitChildren: visitChildren,
|
||||||
|
@ -187,14 +189,14 @@ public final class FiberReconciler<Renderer: FiberRenderer> {
|
||||||
child: alternateRoot?.child,
|
child: alternateRoot?.child,
|
||||||
alternateChild: root.child,
|
alternateChild: root.child,
|
||||||
elementIndices: [:],
|
elementIndices: [:],
|
||||||
pendingTraits: .init()
|
nextTraits: .init()
|
||||||
)
|
)
|
||||||
reconciler.caches.clear()
|
reconciler.caches.clear()
|
||||||
for pass in reconciler.passes {
|
for pass in reconciler.passes {
|
||||||
pass.run(
|
pass.run(
|
||||||
in: reconciler,
|
in: reconciler,
|
||||||
root: rootResult,
|
root: rootResult,
|
||||||
reconcileRoot: alternateReconcileRoot,
|
changedFibers: changedFibers,
|
||||||
caches: reconciler.caches
|
caches: reconciler.caches
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -211,18 +213,31 @@ public final class FiberReconciler<Renderer: FiberRenderer> {
|
||||||
afterReconcileActions.append(action)
|
afterReconcileActions.append(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
func reconcile(from updateRoot: Fiber) {
|
/// Called by any `Fiber` that experiences a state change.
|
||||||
isReconciling = true
|
///
|
||||||
let root: Fiber
|
/// Reconciliation only runs after every change during the current run loop has been performed.
|
||||||
if renderer.useDynamicLayout {
|
func fiberChanged(_ fiber: Fiber) {
|
||||||
// We need to re-layout from the top down when using dynamic layout.
|
guard let alternate = fiber.alternate ?? fiber.createAndBindAlternate?()
|
||||||
root = current
|
else { return }
|
||||||
} else {
|
let shouldSchedule = changedFibers.isEmpty
|
||||||
root = updateRoot
|
changedFibers.insert(ObjectIdentifier(alternate))
|
||||||
|
if shouldSchedule {
|
||||||
|
renderer.schedule { [weak self] in
|
||||||
|
self?.reconcile()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform each `FiberReconcilerPass` given the `changedFibers`.
|
||||||
|
///
|
||||||
|
/// A `reconcile()` call is queued from `fiberChanged` once per run loop.
|
||||||
|
func reconcile() {
|
||||||
|
isReconciling = true
|
||||||
|
let changedFibers = changedFibers
|
||||||
|
self.changedFibers.removeAll()
|
||||||
// Create a list of mutations.
|
// Create a list of mutations.
|
||||||
let visitor = ReconcilerVisitor(root: root, reconcileRoot: updateRoot, reconciler: self)
|
let visitor = ReconcilerVisitor(root: current, changedFibers: changedFibers, reconciler: self)
|
||||||
switch root.content {
|
switch current.content {
|
||||||
case let .view(_, visit):
|
case let .view(_, visit):
|
||||||
visit(visitor)
|
visit(visitor)
|
||||||
case let .scene(_, visit):
|
case let .scene(_, visit):
|
||||||
|
@ -240,15 +255,9 @@ public final class FiberReconciler<Renderer: FiberRenderer> {
|
||||||
// Essentially, making the work in progress tree the current,
|
// Essentially, making the work in progress tree the current,
|
||||||
// and leaving the current available to be the work in progress
|
// and leaving the current available to be the work in progress
|
||||||
// on our next update.
|
// on our next update.
|
||||||
if root === current {
|
let alternate = alternate
|
||||||
let alternate = alternate
|
self.alternate = current
|
||||||
self.alternate = current
|
current = alternate
|
||||||
current = alternate
|
|
||||||
} else {
|
|
||||||
let child = root.child
|
|
||||||
root.child = root.alternate?.child
|
|
||||||
root.alternate?.child = child
|
|
||||||
}
|
|
||||||
|
|
||||||
isReconciling = false
|
isReconciling = false
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,33 @@ public protocol FiberRenderer {
|
||||||
proposal: ProposedViewSize,
|
proposal: ProposedViewSize,
|
||||||
in environment: EnvironmentValues
|
in environment: EnvironmentValues
|
||||||
) -> CGSize
|
) -> CGSize
|
||||||
|
|
||||||
|
/// Run `action` on the next run loop.
|
||||||
|
///
|
||||||
|
/// Called by the `FiberReconciler` to perform reconciliation after all changed Fibers are collected.
|
||||||
|
///
|
||||||
|
/// For example, take the following sample `View`:
|
||||||
|
///
|
||||||
|
/// struct DuelOfTheStates: View {
|
||||||
|
/// @State private var hits1 = 0
|
||||||
|
/// @State private var hits2 = 0
|
||||||
|
///
|
||||||
|
/// var body: some View {
|
||||||
|
/// Button("Hit") {
|
||||||
|
/// hits1 += 1
|
||||||
|
/// hits2 += 2
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// When the button is pressed, both `hits1` and `hits2` are updated.
|
||||||
|
/// If reconciliation was done on every state change, we would needlessly run it twice,
|
||||||
|
/// once for `hits1` and again for `hits2`.
|
||||||
|
///
|
||||||
|
/// Instead, we create a list of changed fibers
|
||||||
|
/// (in this case just `DuelOfTheStates` as both properties were on it),
|
||||||
|
/// and reconcile after all changes have been collected.
|
||||||
|
func schedule(_ action: @escaping () -> ())
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension FiberRenderer {
|
public extension FiberRenderer {
|
||||||
|
|
|
@ -141,8 +141,9 @@ public struct LayoutSubview: Equatable {
|
||||||
)
|
)
|
||||||
cache.sizeThatFits[request] = size
|
cache.sizeThatFits[request] = size
|
||||||
if let alternate = fiber.alternate {
|
if let alternate = fiber.alternate {
|
||||||
caches.updateLayoutCache(for: alternate) { cache in
|
caches.updateLayoutCache(for: alternate) { alternateCache in
|
||||||
cache.sizeThatFits[request] = size
|
alternateCache.cache = cache.cache
|
||||||
|
alternateCache.sizeThatFits[request] = size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return size
|
return size
|
||||||
|
@ -246,6 +247,6 @@ public struct LayoutSubview: Equatable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func == (lhs: LayoutSubview, rhs: LayoutSubview) -> Bool {
|
public static func == (lhs: LayoutSubview, rhs: LayoutSubview) -> Bool {
|
||||||
lhs.id == rhs.id
|
lhs.storage === rhs.storage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,7 +117,6 @@ public extension StackLayout {
|
||||||
|
|
||||||
/// The aggregate minimum size of each `View` with a given priority.
|
/// The aggregate minimum size of each `View` with a given priority.
|
||||||
var prioritySize = [Double: CGFloat]()
|
var prioritySize = [Double: CGFloat]()
|
||||||
|
|
||||||
let measuredSubviews = subviews.enumerated().map { index, view -> MeasuredSubview in
|
let measuredSubviews = subviews.enumerated().map { index, view -> MeasuredSubview in
|
||||||
priorityCount[view.priority, default: 0] += 1
|
priorityCount[view.priority, default: 0] += 1
|
||||||
|
|
||||||
|
|
|
@ -61,9 +61,9 @@ extension FiberReconciler {
|
||||||
}
|
}
|
||||||
|
|
||||||
func clear() {
|
func clear() {
|
||||||
elementIndices = [:]
|
elementIndices.removeAll()
|
||||||
layoutSubviews = [:]
|
layoutSubviews.removeAll()
|
||||||
mutations = []
|
mutations.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
func layoutCache(for fiber: Fiber) -> LayoutCache? {
|
func layoutCache(for fiber: Fiber) -> LayoutCache? {
|
||||||
|
@ -123,14 +123,14 @@ protocol FiberReconcilerPass {
|
||||||
/// - Parameter root: The node to start the pass from.
|
/// - Parameter root: The node to start the pass from.
|
||||||
/// The top of the `View` hierarchy when `useDynamicLayout` is enabled.
|
/// The top of the `View` hierarchy when `useDynamicLayout` is enabled.
|
||||||
/// Otherwise, the same as `reconcileRoot`.
|
/// Otherwise, the same as `reconcileRoot`.
|
||||||
/// - Parameter reconcileRoot: The topmost node that needs reconciliation.
|
/// - Parameter reconcileRoot: A list of topmost nodes that need reconciliation.
|
||||||
/// When `useDynamicLayout` is enabled, this can be used to limit
|
/// When `useDynamicLayout` is enabled, this can be used to limit
|
||||||
/// the number of operations performed during reconciliation.
|
/// the number of operations performed during reconciliation.
|
||||||
/// - Parameter caches: The shared cache data for this and other passes.
|
/// - Parameter caches: The shared cache data for this and other passes.
|
||||||
func run<R: FiberRenderer>(
|
func run<R: FiberRenderer>(
|
||||||
in reconciler: FiberReconciler<R>,
|
in reconciler: FiberReconciler<R>,
|
||||||
root: FiberReconciler<R>.TreeReducer.Result,
|
root: FiberReconciler<R>.TreeReducer.Result,
|
||||||
reconcileRoot: FiberReconciler<R>.Fiber,
|
changedFibers: Set<ObjectIdentifier>,
|
||||||
caches: FiberReconciler<R>.Caches
|
caches: FiberReconciler<R>.Caches
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ struct LayoutPass: FiberReconcilerPass {
|
||||||
func run<R>(
|
func run<R>(
|
||||||
in reconciler: FiberReconciler<R>,
|
in reconciler: FiberReconciler<R>,
|
||||||
root: FiberReconciler<R>.TreeReducer.Result,
|
root: FiberReconciler<R>.TreeReducer.Result,
|
||||||
reconcileRoot: FiberReconciler<R>.Fiber,
|
changedFibers: Set<ObjectIdentifier>,
|
||||||
caches: FiberReconciler<R>.Caches
|
caches: FiberReconciler<R>.Caches
|
||||||
) where R: FiberRenderer {
|
) where R: FiberRenderer {
|
||||||
guard let root = root.fiber else { return }
|
guard let root = root.fiber else { return }
|
||||||
|
|
|
@ -63,7 +63,7 @@ struct ReconcilePass: FiberReconcilerPass {
|
||||||
func run<R>(
|
func run<R>(
|
||||||
in reconciler: FiberReconciler<R>,
|
in reconciler: FiberReconciler<R>,
|
||||||
root: FiberReconciler<R>.TreeReducer.Result,
|
root: FiberReconciler<R>.TreeReducer.Result,
|
||||||
reconcileRoot: FiberReconciler<R>.Fiber,
|
changedFibers: Set<ObjectIdentifier>,
|
||||||
caches: FiberReconciler<R>.Caches
|
caches: FiberReconciler<R>.Caches
|
||||||
) where R: FiberRenderer {
|
) where R: FiberRenderer {
|
||||||
var node = root
|
var node = root
|
||||||
|
@ -71,12 +71,17 @@ struct ReconcilePass: FiberReconcilerPass {
|
||||||
// Enabled when we reach the `reconcileRoot`.
|
// Enabled when we reach the `reconcileRoot`.
|
||||||
var shouldReconcile = false
|
var shouldReconcile = false
|
||||||
|
|
||||||
// Traits that should be attached to the nearest rendered child.
|
|
||||||
var pendingTraits = _ViewTraitStore()
|
|
||||||
|
|
||||||
while true {
|
while true {
|
||||||
if node.fiber === reconcileRoot || node.fiber?.alternate === reconcileRoot {
|
if !shouldReconcile {
|
||||||
shouldReconcile = true
|
if let fiber = node.fiber,
|
||||||
|
changedFibers.contains(ObjectIdentifier(fiber))
|
||||||
|
{
|
||||||
|
shouldReconcile = true
|
||||||
|
} else if let alternate = node.fiber?.alternate,
|
||||||
|
changedFibers.contains(ObjectIdentifier(alternate))
|
||||||
|
{
|
||||||
|
shouldReconcile = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this fiber has an element, set its `elementIndex`
|
// If this fiber has an element, set its `elementIndex`
|
||||||
|
@ -94,27 +99,23 @@ struct ReconcilePass: FiberReconcilerPass {
|
||||||
caches.mutations.append(mutation)
|
caches.mutations.append(mutation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure the `TreeReducer` can access any necessary state.
|
||||||
|
node.elementIndices = caches.elementIndices
|
||||||
// Pass view traits down to the nearest element fiber.
|
// Pass view traits down to the nearest element fiber.
|
||||||
if let traits = node.fiber?.outputs.traits,
|
if let traits = node.fiber?.outputs.traits,
|
||||||
!traits.values.isEmpty
|
!traits.values.isEmpty
|
||||||
{
|
{
|
||||||
if node.fiber?.element == nil {
|
node.nextTraits.values.merge(traits.values, uniquingKeysWith: { $1 })
|
||||||
pendingTraits = traits
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Clear the pending traits once they have been applied to the target.
|
|
||||||
if node.fiber?.element != nil && !pendingTraits.values.isEmpty {
|
|
||||||
pendingTraits = .init()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the `TreeReducer` can access any necessary state.
|
// Update `DynamicProperty`s before accessing the `View`'s body.
|
||||||
node.elementIndices = caches.elementIndices
|
node.fiber?.updateDynamicProperties()
|
||||||
node.pendingTraits = pendingTraits
|
|
||||||
|
|
||||||
// Compute the children of the node.
|
// Compute the children of the node.
|
||||||
let reducer = FiberReconciler<R>.TreeReducer.SceneVisitor(initialResult: node)
|
let reducer = FiberReconciler<R>.TreeReducer.SceneVisitor(initialResult: node)
|
||||||
node.visitChildren(reducer)
|
node.visitChildren(reducer)
|
||||||
|
|
||||||
|
node.fiber?.preferences?.reset()
|
||||||
|
|
||||||
if reconciler.renderer.useDynamicLayout,
|
if reconciler.renderer.useDynamicLayout,
|
||||||
let fiber = node.fiber
|
let fiber = node.fiber
|
||||||
{
|
{
|
||||||
|
@ -124,7 +125,7 @@ struct ReconcilePass: FiberReconcilerPass {
|
||||||
let parentKey = ObjectIdentifier(elementParent)
|
let parentKey = ObjectIdentifier(elementParent)
|
||||||
let subview = LayoutSubview(
|
let subview = LayoutSubview(
|
||||||
id: ObjectIdentifier(fiber),
|
id: ObjectIdentifier(fiber),
|
||||||
traits: node.fiber?.outputs.traits,
|
traits: fiber.outputs.traits,
|
||||||
fiber: fiber,
|
fiber: fiber,
|
||||||
element: element,
|
element: element,
|
||||||
caches: caches
|
caches: caches
|
||||||
|
@ -177,6 +178,15 @@ struct ReconcilePass: FiberReconcilerPass {
|
||||||
propagateCacheInvalidation(for: fiber, in: reconciler, caches: caches)
|
propagateCacheInvalidation(for: fiber, in: reconciler, caches: caches)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let preferences = node.fiber?.preferences {
|
||||||
|
if let action = node.fiber?.outputs.preferenceAction {
|
||||||
|
action(preferences)
|
||||||
|
}
|
||||||
|
if let parentPreferences = node.fiber?.preferenceParent?.preferences {
|
||||||
|
parentPreferences.merge(preferences)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var alternateSibling = node.fiber?.alternate?.sibling
|
var alternateSibling = node.fiber?.alternate?.sibling
|
||||||
// The alternate had siblings that no longer exist.
|
// The alternate had siblings that no longer exist.
|
||||||
while alternateSibling != nil {
|
while alternateSibling != nil {
|
||||||
|
|
|
@ -21,10 +21,17 @@ import Foundation
|
||||||
public struct ViewInputs<V> {
|
public struct ViewInputs<V> {
|
||||||
public let content: V
|
public let content: V
|
||||||
|
|
||||||
|
/// Mutate the underlying content with the given inputs.
|
||||||
|
///
|
||||||
|
/// Used to inject values such as environment values, traits, and preferences into the `View` type.
|
||||||
|
public let updateContent: ((inout V) -> ()) -> ()
|
||||||
|
|
||||||
@_spi(TokamakCore)
|
@_spi(TokamakCore)
|
||||||
public let environment: EnvironmentBox
|
public let environment: EnvironmentBox
|
||||||
|
|
||||||
public let traits: _ViewTraitStore?
|
public let traits: _ViewTraitStore?
|
||||||
|
|
||||||
|
public let preferenceStore: _PreferenceStore?
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Data used to reconcile and render a `View` and its children.
|
/// Data used to reconcile and render a `View` and its children.
|
||||||
|
@ -33,7 +40,12 @@ public struct ViewOutputs {
|
||||||
/// This is stored as a reference to avoid copying the environment when unnecessary.
|
/// This is stored as a reference to avoid copying the environment when unnecessary.
|
||||||
let environment: EnvironmentBox
|
let environment: EnvironmentBox
|
||||||
|
|
||||||
let preferences: _PreferenceStore
|
let preferenceStore: _PreferenceStore?
|
||||||
|
|
||||||
|
/// An action to perform after all preferences values have been reduced.
|
||||||
|
///
|
||||||
|
/// Called when walking back up the tree in the `ReconcilePass`.
|
||||||
|
let preferenceAction: ((_PreferenceStore) -> ())?
|
||||||
|
|
||||||
let traits: _ViewTraitStore?
|
let traits: _ViewTraitStore?
|
||||||
}
|
}
|
||||||
|
@ -51,13 +63,15 @@ public extension ViewOutputs {
|
||||||
init<V>(
|
init<V>(
|
||||||
inputs: ViewInputs<V>,
|
inputs: ViewInputs<V>,
|
||||||
environment: EnvironmentValues? = nil,
|
environment: EnvironmentValues? = nil,
|
||||||
preferences: _PreferenceStore? = nil,
|
preferenceStore: _PreferenceStore? = nil,
|
||||||
|
preferenceAction: ((_PreferenceStore) -> ())? = nil,
|
||||||
traits: _ViewTraitStore? = nil
|
traits: _ViewTraitStore? = nil
|
||||||
) {
|
) {
|
||||||
// Only replace the `EnvironmentBox` when we change the environment.
|
// Only replace the `EnvironmentBox` when we change the environment.
|
||||||
// Otherwise the same box can be reused.
|
// Otherwise the same box can be reused.
|
||||||
self.environment = environment.map(EnvironmentBox.init) ?? inputs.environment
|
self.environment = environment.map(EnvironmentBox.init) ?? inputs.environment
|
||||||
self.preferences = preferences ?? .init()
|
self.preferenceStore = preferenceStore
|
||||||
|
self.preferenceAction = preferenceAction
|
||||||
self.traits = traits ?? inputs.traits
|
self.traits = traits ?? inputs.traits
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,8 +88,10 @@ public extension ModifiedContent where Content: View, Modifier: ViewModifier {
|
||||||
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
|
static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
|
||||||
Modifier._makeView(.init(
|
Modifier._makeView(.init(
|
||||||
content: inputs.content.modifier,
|
content: inputs.content.modifier,
|
||||||
|
updateContent: { _ in },
|
||||||
environment: inputs.environment,
|
environment: inputs.environment,
|
||||||
traits: inputs.traits
|
traits: inputs.traits,
|
||||||
|
preferenceStore: inputs.preferenceStore
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,20 +15,24 @@
|
||||||
// Created by Carson Katri on 2/11/22.
|
// Created by Carson Katri on 2/11/22.
|
||||||
//
|
//
|
||||||
|
|
||||||
enum WalkWorkResult<Success> {
|
@_spi(TokamakCore)
|
||||||
|
public enum WalkWorkResult<Success> {
|
||||||
case `continue`
|
case `continue`
|
||||||
case `break`(with: Success)
|
case `break`(with: Success)
|
||||||
case pause
|
case pause
|
||||||
}
|
}
|
||||||
|
|
||||||
enum WalkResult<Renderer: FiberRenderer, Success> {
|
@_spi(TokamakCore)
|
||||||
|
public enum WalkResult<Renderer: FiberRenderer, Success> {
|
||||||
case success(Success)
|
case success(Success)
|
||||||
case finished
|
case finished
|
||||||
case paused(at: FiberReconciler<Renderer>.Fiber)
|
case paused(at: FiberReconciler<Renderer>.Fiber)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Walk a fiber tree from `root` until the `work` predicate returns `false`.
|
||||||
|
@_spi(TokamakCore)
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func walk<Renderer: FiberRenderer>(
|
public func walk<Renderer: FiberRenderer>(
|
||||||
_ root: FiberReconciler<Renderer>.Fiber,
|
_ root: FiberReconciler<Renderer>.Fiber,
|
||||||
_ work: @escaping (FiberReconciler<Renderer>.Fiber) throws -> Bool
|
_ work: @escaping (FiberReconciler<Renderer>.Fiber) throws -> Bool
|
||||||
) rethrows -> WalkResult<Renderer, ()> {
|
) rethrows -> WalkResult<Renderer, ()> {
|
||||||
|
@ -38,7 +42,17 @@ func walk<Renderer: FiberRenderer>(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parent-first depth-first traversal of a `Fiber` tree.
|
/// Parent-first depth-first traversal of a `Fiber` tree.
|
||||||
func walk<Renderer: FiberRenderer, Success>(
|
/// `work` is called with each `Fiber` in the tree as they are entered.
|
||||||
|
///
|
||||||
|
/// Traversal uses the following process:
|
||||||
|
/// 1. Perform work on the current `Fiber`.
|
||||||
|
/// 2. If the `Fiber` has a child, repeat from (1) with the child.
|
||||||
|
/// 3. If the `Fiber` does not have a sibling, walk up until we find a `Fiber` that does have one.
|
||||||
|
/// 4. Walk across to the sibling.
|
||||||
|
///
|
||||||
|
/// When the `root` is reached, the loop exits.
|
||||||
|
@_spi(TokamakCore)
|
||||||
|
public func walk<Renderer: FiberRenderer, Success>(
|
||||||
_ root: FiberReconciler<Renderer>.Fiber,
|
_ root: FiberReconciler<Renderer>.Fiber,
|
||||||
_ work: @escaping (FiberReconciler<Renderer>.Fiber) throws -> WalkWorkResult<Success>
|
_ work: @escaping (FiberReconciler<Renderer>.Fiber) throws -> WalkWorkResult<Success>
|
||||||
) rethrows -> WalkResult<Renderer, Success> {
|
) rethrows -> WalkResult<Renderer, Success> {
|
||||||
|
|
|
@ -25,12 +25,41 @@ public extension PreferenceKey where Self.Value: ExpressibleByNilLiteral {
|
||||||
static var defaultValue: Value { nil }
|
static var defaultValue: Value { nil }
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct _PreferenceValue<Key> where Key: PreferenceKey {
|
final class _PreferenceValueStorage: CustomDebugStringConvertible {
|
||||||
/// Every value the `Key` has had.
|
/// Every value the `Key` has had.
|
||||||
var valueList: [Key.Value]
|
var valueList: [Any]
|
||||||
|
|
||||||
|
var debugDescription: String {
|
||||||
|
valueList.debugDescription
|
||||||
|
}
|
||||||
|
|
||||||
|
init<Key: PreferenceKey>(_ key: Key.Type = Key.self) {
|
||||||
|
valueList = []
|
||||||
|
}
|
||||||
|
|
||||||
|
init(valueList: [Any]) {
|
||||||
|
self.valueList = valueList
|
||||||
|
}
|
||||||
|
|
||||||
|
func merge(_ other: _PreferenceValueStorage) {
|
||||||
|
valueList.append(contentsOf: other.valueList)
|
||||||
|
}
|
||||||
|
|
||||||
|
func reset() {
|
||||||
|
valueList = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct _PreferenceValue<Key> where Key: PreferenceKey {
|
||||||
|
var storage: _PreferenceValueStorage
|
||||||
|
|
||||||
|
init(storage: _PreferenceValueStorage) {
|
||||||
|
self.storage = storage
|
||||||
|
}
|
||||||
|
|
||||||
/// The latest value.
|
/// The latest value.
|
||||||
public var value: Key.Value {
|
public var value: Key.Value {
|
||||||
reduce(valueList)
|
reduce(storage.valueList.compactMap { $0 as? Key.Value })
|
||||||
}
|
}
|
||||||
|
|
||||||
func reduce(_ values: [Key.Value]) -> Key.Value {
|
func reduce(_ values: [Key.Value]) -> Key.Value {
|
||||||
|
@ -48,30 +77,80 @@ public extension _PreferenceValue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class _PreferenceStore {
|
public final class _PreferenceStore: CustomDebugStringConvertible {
|
||||||
|
/// The values of the `_PreferenceStore` on the last update.
|
||||||
|
private var previousValues: [ObjectIdentifier: _PreferenceValueStorage]
|
||||||
/// The backing values of the `_PreferenceStore`.
|
/// The backing values of the `_PreferenceStore`.
|
||||||
private var values: [String: Any]
|
private var values: [ObjectIdentifier: _PreferenceValueStorage]
|
||||||
|
|
||||||
weak var parent: _PreferenceStore?
|
weak var parent: _PreferenceStore?
|
||||||
|
|
||||||
public init(values: [String: Any] = [:]) {
|
public var debugDescription: String {
|
||||||
|
"Preferences (\(ObjectIdentifier(self))): \(values)"
|
||||||
|
}
|
||||||
|
|
||||||
|
init(values: [ObjectIdentifier: _PreferenceValueStorage] = [:]) {
|
||||||
|
previousValues = [:]
|
||||||
self.values = values
|
self.values = values
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieve a late-binding token for `key`, or save the default value if it does not yet exist.
|
||||||
public func value<Key>(forKey key: Key.Type = Key.self) -> _PreferenceValue<Key>
|
public func value<Key>(forKey key: Key.Type = Key.self) -> _PreferenceValue<Key>
|
||||||
where Key: PreferenceKey
|
where Key: PreferenceKey
|
||||||
{
|
{
|
||||||
values[String(reflecting: key)] as? _PreferenceValue<Key>
|
let keyID = ObjectIdentifier(key)
|
||||||
?? _PreferenceValue(valueList: [Key.defaultValue])
|
let storage: _PreferenceValueStorage
|
||||||
|
if let existing = values[keyID] {
|
||||||
|
storage = existing
|
||||||
|
} else {
|
||||||
|
storage = .init(key)
|
||||||
|
values[keyID] = storage
|
||||||
|
}
|
||||||
|
return _PreferenceValue(storage: storage)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve the value `Key` had on the last update.
|
||||||
|
///
|
||||||
|
/// Used to check if the value changed during the last update.
|
||||||
|
func previousValue<Key>(forKey key: Key.Type = Key.self) -> _PreferenceValue<Key>
|
||||||
|
where Key: PreferenceKey
|
||||||
|
{
|
||||||
|
_PreferenceValue(storage: previousValues[ObjectIdentifier(key)] ?? .init(key))
|
||||||
}
|
}
|
||||||
|
|
||||||
public func insert<Key>(_ value: Key.Value, forKey key: Key.Type = Key.self)
|
public func insert<Key>(_ value: Key.Value, forKey key: Key.Type = Key.self)
|
||||||
where Key: PreferenceKey
|
where Key: PreferenceKey
|
||||||
{
|
{
|
||||||
let previousValues = self.value(forKey: key).valueList
|
let keyID = ObjectIdentifier(key)
|
||||||
values[String(reflecting: key)] = _PreferenceValue<Key>(valueList: previousValues + [value])
|
if !values.keys.contains(keyID) {
|
||||||
|
values[keyID] = .init(key)
|
||||||
|
}
|
||||||
|
values[keyID]?.valueList.append(value)
|
||||||
parent?.insert(value, forKey: key)
|
parent?.insert(value, forKey: key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func merge(_ other: _PreferenceStore) {
|
||||||
|
for (key, otherStorage) in other.values {
|
||||||
|
if let storage = values[key] {
|
||||||
|
storage.merge(otherStorage)
|
||||||
|
} else {
|
||||||
|
values[key] = .init(valueList: otherStorage.valueList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copies `values` to `previousValues`, and clears `values`.
|
||||||
|
///
|
||||||
|
/// Each reconcile pass the preferences are collected from scratch, so we need to
|
||||||
|
/// clear out the old values.
|
||||||
|
func reset() {
|
||||||
|
previousValues = values.mapValues {
|
||||||
|
_PreferenceValueStorage(valueList: $0.valueList)
|
||||||
|
}
|
||||||
|
for storage in values.values {
|
||||||
|
storage.reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A protocol that allows a `View` to read values from the current `_PreferenceStore`.
|
/// A protocol that allows a `View` to read values from the current `_PreferenceStore`.
|
||||||
|
|
|
@ -25,12 +25,26 @@ public struct _PreferenceActionModifier<Key>: _PreferenceWritingModifierProtocol
|
||||||
|
|
||||||
public func body(_ content: Content, with preferenceStore: inout _PreferenceStore) -> AnyView {
|
public func body(_ content: Content, with preferenceStore: inout _PreferenceStore) -> AnyView {
|
||||||
let value = preferenceStore.value(forKey: Key.self)
|
let value = preferenceStore.value(forKey: Key.self)
|
||||||
let previousValue = value.reduce(value.valueList.dropLast())
|
let previousValue = value.reduce((value.storage.valueList as? [Key.Value] ?? []).dropLast())
|
||||||
if previousValue != value.value {
|
if previousValue != value.value {
|
||||||
action(value.value)
|
action(value.value)
|
||||||
}
|
}
|
||||||
return content.view
|
return content.view
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
|
||||||
|
.init(
|
||||||
|
inputs: inputs,
|
||||||
|
preferenceStore: inputs.preferenceStore ?? .init(),
|
||||||
|
preferenceAction: {
|
||||||
|
let value = $0.value(forKey: Key.self).value
|
||||||
|
let previousValue = $0.previousValue(forKey: Key.self).value
|
||||||
|
if value != previousValue {
|
||||||
|
inputs.content.action(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension View {
|
public extension View {
|
||||||
|
|
|
@ -21,11 +21,11 @@ public struct _DelayedPreferenceView<Key, Content>: View, _PreferenceReadingView
|
||||||
where Key: PreferenceKey, Content: View
|
where Key: PreferenceKey, Content: View
|
||||||
{
|
{
|
||||||
@State
|
@State
|
||||||
private var resolvedValue: _PreferenceValue<Key> = _PreferenceValue(
|
private var resolvedValue: _PreferenceValue<Key> = _PreferenceValue(storage: .init(Key.self))
|
||||||
valueList: [Key.defaultValue]
|
|
||||||
)
|
|
||||||
public let transform: (_PreferenceValue<Key>) -> Content
|
public let transform: (_PreferenceValue<Key>) -> Content
|
||||||
|
|
||||||
|
private var valueReference: _PreferenceValue<Key>?
|
||||||
|
|
||||||
public init(transform: @escaping (_PreferenceValue<Key>) -> Content) {
|
public init(transform: @escaping (_PreferenceValue<Key>) -> Content) {
|
||||||
self.transform = transform
|
self.transform = transform
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,18 @@ public struct _DelayedPreferenceView<Key, Content>: View, _PreferenceReadingView
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
transform(resolvedValue)
|
transform(valueReference ?? resolvedValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
|
||||||
|
let preferenceStore = inputs.preferenceStore ?? .init()
|
||||||
|
inputs.updateContent {
|
||||||
|
$0.valueReference = preferenceStore.value(forKey: Key.self)
|
||||||
|
}
|
||||||
|
return .init(
|
||||||
|
inputs: inputs,
|
||||||
|
preferenceStore: preferenceStore
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +81,7 @@ public extension View {
|
||||||
) -> some View
|
) -> some View
|
||||||
where Key: PreferenceKey, T: View
|
where Key: PreferenceKey, T: View
|
||||||
{
|
{
|
||||||
Key._delay { self.overlay(transform($0.value)) }
|
Key._delay { self.overlay($0._force(transform)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
func backgroundPreferenceValue<Key, T>(
|
func backgroundPreferenceValue<Key, T>(
|
||||||
|
@ -79,6 +90,6 @@ public extension View {
|
||||||
) -> some View
|
) -> some View
|
||||||
where Key: PreferenceKey, T: View
|
where Key: PreferenceKey, T: View
|
||||||
{
|
{
|
||||||
Key._delay { self.background(transform($0.value)) }
|
Key._delay { self.background($0._force(transform)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,18 @@ public struct _PreferenceTransformModifier<Key>: _PreferenceWritingModifierProto
|
||||||
preferenceStore.insert(newValue, forKey: Key.self)
|
preferenceStore.insert(newValue, forKey: Key.self)
|
||||||
return content.view
|
return content.view
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
|
||||||
|
.init(
|
||||||
|
inputs: inputs,
|
||||||
|
preferenceStore: inputs.preferenceStore ?? .init(),
|
||||||
|
preferenceAction: {
|
||||||
|
var value = $0.value(forKey: Key.self).value
|
||||||
|
inputs.content.transform(&value)
|
||||||
|
$0.insert(value, forKey: Key.self)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension View {
|
public extension View {
|
||||||
|
|
|
@ -27,6 +27,14 @@ public struct _PreferenceWritingModifier<Key>: _PreferenceWritingModifierProtoco
|
||||||
preferenceStore.insert(value, forKey: Key.self)
|
preferenceStore.insert(value, forKey: Key.self)
|
||||||
return content.view
|
return content.view
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func _makeView(_ inputs: ViewInputs<Self>) -> ViewOutputs {
|
||||||
|
.init(
|
||||||
|
inputs: inputs,
|
||||||
|
preferenceStore: inputs.preferenceStore ?? .init(),
|
||||||
|
preferenceAction: { $0.insert(inputs.content.value, forKey: Key.self) }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension _PreferenceWritingModifier: Equatable where Key.Value: Equatable {
|
extension _PreferenceWritingModifier: Equatable where Key.Value: Equatable {
|
||||||
|
|
|
@ -19,6 +19,8 @@ import TokamakCore
|
||||||
|
|
||||||
// MARK: Environment & State
|
// MARK: Environment & State
|
||||||
|
|
||||||
|
public typealias DynamicProperty = TokamakCore.DynamicProperty
|
||||||
|
|
||||||
public typealias Environment = TokamakCore.Environment
|
public typealias Environment = TokamakCore.Environment
|
||||||
public typealias EnvironmentKey = TokamakCore.EnvironmentKey
|
public typealias EnvironmentKey = TokamakCore.EnvironmentKey
|
||||||
public typealias EnvironmentObject = TokamakCore.EnvironmentObject
|
public typealias EnvironmentObject = TokamakCore.EnvironmentObject
|
||||||
|
@ -180,8 +182,16 @@ public typealias View = TokamakCore.View
|
||||||
public typealias AnyView = TokamakCore.AnyView
|
public typealias AnyView = TokamakCore.AnyView
|
||||||
public typealias EmptyView = TokamakCore.EmptyView
|
public typealias EmptyView = TokamakCore.EmptyView
|
||||||
|
|
||||||
|
// MARK: Layout
|
||||||
|
|
||||||
public typealias Layout = TokamakCore.Layout
|
public typealias Layout = TokamakCore.Layout
|
||||||
public typealias AnyLayout = TokamakCore.AnyLayout
|
public typealias AnyLayout = TokamakCore.AnyLayout
|
||||||
|
public typealias LayoutProperties = TokamakCore.LayoutProperties
|
||||||
|
public typealias LayoutSubviews = TokamakCore.LayoutSubviews
|
||||||
|
public typealias LayoutSubview = TokamakCore.LayoutSubview
|
||||||
|
public typealias LayoutValueKey = TokamakCore.LayoutValueKey
|
||||||
|
public typealias ProposedViewSize = TokamakCore.ProposedViewSize
|
||||||
|
public typealias ViewSpacing = TokamakCore.ViewSpacing
|
||||||
|
|
||||||
// MARK: Toolbars
|
// MARK: Toolbars
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import JavaScriptKit
|
import JavaScriptKit
|
||||||
|
import OpenCombineJS
|
||||||
@_spi(TokamakCore)
|
@_spi(TokamakCore)
|
||||||
import TokamakCore
|
import TokamakCore
|
||||||
@_spi(TokamakStaticHTML)
|
@_spi(TokamakStaticHTML)
|
||||||
|
@ -88,6 +89,8 @@ public struct DOMFiberRenderer: FiberRenderer {
|
||||||
public var defaultEnvironment: EnvironmentValues {
|
public var defaultEnvironment: EnvironmentValues {
|
||||||
var environment = EnvironmentValues()
|
var environment = EnvironmentValues()
|
||||||
environment[_ColorSchemeKey.self] = .light
|
environment[_ColorSchemeKey.self] = .light
|
||||||
|
environment._defaultAppStorage = LocalStorage.standard
|
||||||
|
_DefaultSceneStorageProvider.default = SessionStorage.standard
|
||||||
return environment
|
return environment
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,6 +120,10 @@ public struct DOMFiberRenderer: FiberRenderer {
|
||||||
_ = reference.style.setProperty("width", "100vw")
|
_ = reference.style.setProperty("width", "100vw")
|
||||||
_ = reference.style.setProperty("height", "100vh")
|
_ = reference.style.setProperty("height", "100vh")
|
||||||
_ = reference.style.setProperty("position", "relative")
|
_ = reference.style.setProperty("position", "relative")
|
||||||
|
} else {
|
||||||
|
let style = document.createElement!("style").object!
|
||||||
|
style.innerHTML = .string(TokamakStaticHTML.tokamakStyles)
|
||||||
|
_ = document.head.appendChild(style)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,6 +306,11 @@ public struct DOMFiberRenderer: FiberRenderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let scheduler = JSScheduler()
|
||||||
|
public func schedule(_ action: @escaping () -> ()) {
|
||||||
|
scheduler.schedule(options: nil, action)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension _PrimitiveButtonStyleBody: DOMNodeConvertible {
|
extension _PrimitiveButtonStyleBody: DOMNodeConvertible {
|
||||||
|
|
|
@ -253,4 +253,8 @@ public struct StaticHTMLFiberRenderer: FiberRenderer {
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func schedule(_ action: @escaping () -> ()) {
|
||||||
|
action()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -176,4 +176,8 @@ public struct TestFiberRenderer: FiberRenderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func schedule(_ action: @escaping () -> ()) {
|
||||||
|
action()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
// Copyright 2022 Tokamak contributors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
// Created by Carson Katri on 6/30/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@_spi(TokamakCore)
|
||||||
|
import TokamakCore
|
||||||
|
|
||||||
|
/// A proxy for an identified view in the `TestFiberRenderer`.
|
||||||
|
///
|
||||||
|
/// The properties are evaluated on access,
|
||||||
|
/// so you will never unintentionally access an `alternate` value.
|
||||||
|
@dynamicMemberLookup
|
||||||
|
public struct TestViewProxy<V: View> {
|
||||||
|
/// The id to lookup.
|
||||||
|
let id: AnyHashable
|
||||||
|
|
||||||
|
/// The active reconciler instance to search in.
|
||||||
|
let reconciler: FiberReconciler<TestFiberRenderer>
|
||||||
|
|
||||||
|
/// Searches for a `Fiber` representing `id`.
|
||||||
|
///
|
||||||
|
/// - Note: This returns the child of the `identified(by:)` modifier,
|
||||||
|
/// not the `IdentifiedView` itself.
|
||||||
|
@_spi(TokamakCore)
|
||||||
|
public var fiber: FiberReconciler<TestFiberRenderer>.Fiber? {
|
||||||
|
let id = AnyHashable(id)
|
||||||
|
let result = TokamakCore.walk(
|
||||||
|
reconciler.current
|
||||||
|
) { fiber -> WalkWorkResult<FiberReconciler<TestFiberRenderer>.Fiber?> in
|
||||||
|
guard case let .view(view, _) = fiber.content,
|
||||||
|
!(view is AnyOptional),
|
||||||
|
(view as? IdentifiedViewProtocol)?.id == AnyHashable(id),
|
||||||
|
let child = fiber.child
|
||||||
|
else { return WalkWorkResult.continue }
|
||||||
|
return WalkWorkResult.break(with: child)
|
||||||
|
}
|
||||||
|
guard case let .success(fiber) = result else { return nil }
|
||||||
|
return fiber
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `fiber`'s content casted to `V`.
|
||||||
|
public var view: V? {
|
||||||
|
guard case let .view(view, _) = fiber?.content else { return nil }
|
||||||
|
return view as? V
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access properties from the `view` without specifying `.view` manually.
|
||||||
|
public subscript<T>(dynamicMember member: KeyPath<V, T>) -> T? {
|
||||||
|
self.view?[keyPath: member]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An erased `IdentifiedView`.
|
||||||
|
protocol IdentifiedViewProtocol {
|
||||||
|
var id: AnyHashable { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A wrapper that identifies a `View` in a test.
|
||||||
|
struct IdentifiedView<Content: View>: View, IdentifiedViewProtocol {
|
||||||
|
let id: AnyHashable
|
||||||
|
let content: Content
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension View {
|
||||||
|
/// Identifies a `View` in a test.
|
||||||
|
///
|
||||||
|
/// You can access this view from the `FiberReconciler` with `findView(id:as:)`.
|
||||||
|
func identified<ID: Hashable>(by id: ID) -> some View {
|
||||||
|
IdentifiedView(id: id, content: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension FiberReconciler where Renderer == TestFiberRenderer {
|
||||||
|
/// Find the `View` identified by `ID`.
|
||||||
|
///
|
||||||
|
/// - Note: This returns a proxy to the child of the `identified(by:)` modifier,
|
||||||
|
/// not the `IdentifiedView` itself.
|
||||||
|
func findView<ID: Hashable, V: View>(
|
||||||
|
id: ID,
|
||||||
|
as type: V.Type = V.self
|
||||||
|
) -> TestViewProxy<V> {
|
||||||
|
TestViewProxy<V>(id: id, reconciler: self)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
// Copyright 2021 Tokamak contributors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
// Created by Carson Katri on 6/29/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
@_spi(TokamakCore) @testable import TokamakCore
|
||||||
|
import TokamakTestRenderer
|
||||||
|
|
||||||
|
private enum TestKey: PreferenceKey {
|
||||||
|
static let defaultValue: Int = 0
|
||||||
|
static func reduce(value: inout Int, nextValue: () -> Int) {
|
||||||
|
value += nextValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class PreferenceTests: XCTestCase {
|
||||||
|
func testPreferenceAction() {
|
||||||
|
struct TestView: View {
|
||||||
|
public var body: some View {
|
||||||
|
Text("")
|
||||||
|
.preference(key: TestKey.self, value: 2)
|
||||||
|
.preference(key: TestKey.self, value: 3)
|
||||||
|
.onPreferenceChange(TestKey.self) {
|
||||||
|
XCTAssertEqual($0, 5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let reconciler = TestFiberRenderer(.root, size: .init(width: 500, height: 500))
|
||||||
|
.render(TestView())
|
||||||
|
reconciler.fiberChanged(reconciler.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testOverlay() {
|
||||||
|
struct TestView: View {
|
||||||
|
var body: some View {
|
||||||
|
Text("")
|
||||||
|
.preference(key: TestKey.self, value: 2)
|
||||||
|
.preference(key: TestKey.self, value: 3)
|
||||||
|
.overlayPreferenceValue(TestKey.self) {
|
||||||
|
Text("\($0)")
|
||||||
|
.identified(by: "overlay")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let reconciler = TestFiberRenderer(.root, size: .init(width: 500, height: 500))
|
||||||
|
.render(TestView())
|
||||||
|
|
||||||
|
XCTAssertEqual(reconciler.findView(id: "overlay").view, Text("5"))
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,40 +20,6 @@ import XCTest
|
||||||
@_spi(TokamakCore) @testable import TokamakCore
|
@_spi(TokamakCore) @testable import TokamakCore
|
||||||
import TokamakTestRenderer
|
import TokamakTestRenderer
|
||||||
|
|
||||||
extension FiberReconciler {
|
|
||||||
/// Expect a `Fiber` to represent a particular `View` type.
|
|
||||||
func expect<V>(
|
|
||||||
_ fiber: Fiber?,
|
|
||||||
represents viewType: V.Type,
|
|
||||||
_ message: String? = nil
|
|
||||||
) where V: View {
|
|
||||||
guard case let .view(view, _) = fiber?.content else {
|
|
||||||
return XCTAssert(false, "Fiber does not exit")
|
|
||||||
}
|
|
||||||
if let message = message {
|
|
||||||
XCTAssert(type(of: view) == viewType, message)
|
|
||||||
} else {
|
|
||||||
XCTAssert(type(of: view) == viewType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Expect a `Fiber` to represent a `View` matching`testView`.
|
|
||||||
func expect<V>(
|
|
||||||
_ fiber: Fiber?,
|
|
||||||
equals testView: V,
|
|
||||||
_ message: String? = nil
|
|
||||||
) where V: View & Equatable {
|
|
||||||
guard case let .view(fiberView, _) = fiber?.content else {
|
|
||||||
return XCTAssert(false, "Fiber does not exit")
|
|
||||||
}
|
|
||||||
if let message = message {
|
|
||||||
XCTAssertEqual(fiberView as? V, testView, message)
|
|
||||||
} else {
|
|
||||||
XCTAssertEqual(fiberView as? V, testView)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class VisitorTests: XCTestCase {
|
final class VisitorTests: XCTestCase {
|
||||||
func testCounter() {
|
func testCounter() {
|
||||||
struct TestView: View {
|
struct TestView: View {
|
||||||
|
@ -64,16 +30,19 @@ final class VisitorTests: XCTestCase {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
Text("\(count)")
|
Text("\(count)")
|
||||||
|
.identified(by: "count")
|
||||||
HStack {
|
HStack {
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
Button("Decrement") {
|
Button("Decrement") {
|
||||||
count -= 1
|
count -= 1
|
||||||
}
|
}
|
||||||
|
.identified(by: "decrement")
|
||||||
}
|
}
|
||||||
if count < 5 {
|
if count < 5 {
|
||||||
Button("Increment") {
|
Button("Increment") {
|
||||||
count += 1
|
count += 1
|
||||||
}
|
}
|
||||||
|
.identified(by: "increment")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,75 +53,28 @@ final class VisitorTests: XCTestCase {
|
||||||
Counter()
|
Counter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let reconciler = TestFiberRenderer(.root, size: .init(width: 500, height: 500))
|
let reconciler = TestFiberRenderer(.root, size: .zero).render(TestView())
|
||||||
.render(TestView())
|
|
||||||
var hStack: FiberReconciler<TestFiberRenderer>.Fiber? {
|
let incrementButton = reconciler.findView(id: "increment", as: Button<Text>.self)
|
||||||
reconciler.current // RootView
|
let countText = reconciler.findView(id: "count", as: Text.self)
|
||||||
.child? // LayoutView
|
let decrementButton = reconciler.findView(id: "decrement", as: Button<Text>.self)
|
||||||
.child? // ModifiedContent
|
|
||||||
.child? // _ViewModifier_Content
|
|
||||||
.child? // TestView
|
|
||||||
.child? // Counter
|
|
||||||
.child? // VStack
|
|
||||||
.child? // TupleView
|
|
||||||
.child?.sibling? // HStack
|
|
||||||
.child // TupleView
|
|
||||||
}
|
|
||||||
var text: FiberReconciler<TestFiberRenderer>.Fiber? {
|
|
||||||
reconciler.current // RootView
|
|
||||||
.child? // LayoutView
|
|
||||||
.child? // ModifiedContent
|
|
||||||
.child? // _ViewModifier_Content
|
|
||||||
.child? // TestView
|
|
||||||
.child? // Counter
|
|
||||||
.child? // VStack
|
|
||||||
.child? // TupleView
|
|
||||||
.child // Text
|
|
||||||
}
|
|
||||||
var decrementButton: FiberReconciler<TestFiberRenderer>.Fiber? {
|
|
||||||
hStack?
|
|
||||||
.child? // Optional
|
|
||||||
.child // Button
|
|
||||||
}
|
|
||||||
var incrementButton: FiberReconciler<TestFiberRenderer>.Fiber? {
|
|
||||||
hStack?
|
|
||||||
.child?.sibling? // Optional
|
|
||||||
.child // Button
|
|
||||||
}
|
|
||||||
func decrement() {
|
|
||||||
guard case let .view(view, _) = decrementButton?.content
|
|
||||||
else { return }
|
|
||||||
(view as? Button<Text>)?.action()
|
|
||||||
}
|
|
||||||
func increment() {
|
|
||||||
guard case let .view(view, _) = incrementButton?.content
|
|
||||||
else { return }
|
|
||||||
(view as? Button<Text>)?.action()
|
|
||||||
}
|
|
||||||
// The decrement button is removed when count is < 0
|
// The decrement button is removed when count is < 0
|
||||||
XCTAssertNil(decrementButton, "'Decrement' should be hidden when count <= 0")
|
XCTAssertNil(decrementButton.view, "'Decrement' should be hidden when count <= 0")
|
||||||
|
XCTAssertNotNil(incrementButton.view, "'Increment' should be visible when count < 5")
|
||||||
// Count up to 5
|
// Count up to 5
|
||||||
for i in 0..<5 {
|
for i in 0..<5 {
|
||||||
reconciler.expect(text, equals: Text("\(i)"))
|
XCTAssertEqual(countText.view, Text("\(i)"))
|
||||||
increment()
|
incrementButton.action?()
|
||||||
}
|
}
|
||||||
XCTAssertNil(incrementButton, "'Increment' should be hidden when count >= 5")
|
XCTAssertNil(incrementButton.view, "'Increment' should be hidden when count >= 5")
|
||||||
reconciler.expect(
|
XCTAssertNotNil(decrementButton.view, "'Decrement' should be visible when count > 0")
|
||||||
decrementButton,
|
|
||||||
represents: Button<Text>.self,
|
|
||||||
"'Decrement' should be visible when count > 0"
|
|
||||||
)
|
|
||||||
// Count down to 0.
|
// Count down to 0.
|
||||||
for i in 0..<5 {
|
for i in 0..<5 {
|
||||||
reconciler.expect(text, equals: Text("\(5 - i)"))
|
XCTAssertEqual(countText.view, Text("\(5 - i)"))
|
||||||
decrement()
|
decrementButton.action?()
|
||||||
}
|
}
|
||||||
XCTAssertNil(decrementButton, "'Decrement' should be hidden when count <= 0")
|
XCTAssertNil(decrementButton.view, "'Decrement' should be hidden when count <= 0")
|
||||||
reconciler.expect(
|
XCTAssertNotNil(incrementButton.view, "'Increment' should be visible when count < 5")
|
||||||
incrementButton,
|
|
||||||
represents: Button<Text>.self,
|
|
||||||
"'Increment' should be visible when count < 5"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testForEach() {
|
func testForEach() {
|
||||||
|
@ -163,61 +85,143 @@ final class VisitorTests: XCTestCase {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
Button("Add Item") { count += 1 }
|
Button("Add Item") { count += 1 }
|
||||||
|
.identified(by: "addItem")
|
||||||
ForEach(Array(0..<count), id: \.self) { i in
|
ForEach(Array(0..<count), id: \.self) { i in
|
||||||
Text("Item \(i)")
|
Text("Item \(i)")
|
||||||
|
.identified(by: i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let reconciler = TestFiberRenderer(
|
let reconciler = TestFiberRenderer(.root, size: .zero).render(TestView())
|
||||||
.root,
|
|
||||||
size: .init(width: 500, height: 500),
|
let addItemButton = reconciler.findView(id: "addItem", as: Button<Text>.self)
|
||||||
useDynamicLayout: true
|
XCTAssertNotNil(addItemButton)
|
||||||
)
|
for i in 0..<10 {
|
||||||
.render(TestView())
|
addItemButton.action?()
|
||||||
var addItemFiber: FiberReconciler<TestFiberRenderer>.Fiber? {
|
XCTAssertEqual(reconciler.findView(id: i).view, Text("Item \(i)"))
|
||||||
reconciler.current // RootView
|
|
||||||
.child? // LayoutView
|
|
||||||
.child? // ModifiedContent
|
|
||||||
.child? // _ViewModifier_Content
|
|
||||||
.child? // TestView
|
|
||||||
.child? // VStack
|
|
||||||
.child? // TupleView
|
|
||||||
.child // Button
|
|
||||||
}
|
}
|
||||||
var forEachFiber: FiberReconciler<TestFiberRenderer>.Fiber? {
|
}
|
||||||
reconciler.current // RootView
|
|
||||||
.child? // LayoutView
|
func testDynamicProperties() {
|
||||||
.child? // ModifiedContent
|
enum DynamicPropertyTest: Hashable {
|
||||||
.child? // _ViewModifier_Content
|
case state
|
||||||
.child? // TestView
|
case environment
|
||||||
.child? // VStack
|
case stateObject
|
||||||
.child? // TupleView
|
case observedObject
|
||||||
.child?.sibling // ForEach
|
case environmentObject
|
||||||
}
|
}
|
||||||
func item(at index: Int) -> FiberReconciler<TestFiberRenderer>.Fiber? {
|
struct TestView: View {
|
||||||
var node = forEachFiber?.child
|
var body: some View {
|
||||||
for _ in 0..<index {
|
TestState()
|
||||||
node = node?.sibling
|
TestEnvironment()
|
||||||
|
TestStateObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TestState: View {
|
||||||
|
@State
|
||||||
|
private var count = 0
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button("\(count)") { count += 1 }
|
||||||
|
.identified(by: DynamicPropertyTest.state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum TestKey: EnvironmentKey {
|
||||||
|
static let defaultValue = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TestEnvironment: View {
|
||||||
|
@Environment(\.self)
|
||||||
|
var values
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Text("\(values[TestKey.self])")
|
||||||
|
.identified(by: DynamicPropertyTest.environment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TestStateObject: View {
|
||||||
|
final class Count: ObservableObject {
|
||||||
|
@Published
|
||||||
|
var count = 0
|
||||||
|
|
||||||
|
func increment() {
|
||||||
|
count += 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@StateObject
|
||||||
|
private var count = Count()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
Button("\(count.count)") {
|
||||||
|
count.increment()
|
||||||
|
}
|
||||||
|
.identified(by: DynamicPropertyTest.stateObject)
|
||||||
|
TestObservedObject(count: count)
|
||||||
|
TestEnvironmentObject()
|
||||||
|
}
|
||||||
|
.environmentObject(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TestObservedObject: View {
|
||||||
|
@ObservedObject
|
||||||
|
var count: Count
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Text("\(count.count)")
|
||||||
|
.identified(by: DynamicPropertyTest.observedObject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TestEnvironmentObject: View {
|
||||||
|
@EnvironmentObject
|
||||||
|
var count: Count
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Text("\(count.count)")
|
||||||
|
.identified(by: DynamicPropertyTest.environmentObject)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return node
|
|
||||||
}
|
}
|
||||||
func addItem() {
|
|
||||||
guard case let .view(view, _) = addItemFiber?.content
|
let reconciler = TestFiberRenderer(.root, size: .zero).render(TestView())
|
||||||
else { return }
|
|
||||||
(view as? Button<Text>)?.action()
|
// State
|
||||||
}
|
let button = reconciler.findView(id: DynamicPropertyTest.state, as: Button<Text>.self)
|
||||||
reconciler.expect(addItemFiber, represents: Button<Text>.self)
|
XCTAssertEqual(button.label, Text("0"))
|
||||||
reconciler.expect(forEachFiber, represents: ForEach<[Int], Int, Text>.self)
|
button.action?()
|
||||||
addItem()
|
XCTAssertEqual(button.label, Text("1"))
|
||||||
reconciler.expect(item(at: 0), equals: Text("Item 0"))
|
|
||||||
XCTAssertEqual(reconciler.renderer.rootElement.children[0].children.count, 2)
|
// Environment
|
||||||
addItem()
|
XCTAssertEqual(
|
||||||
reconciler.expect(item(at: 1), equals: Text("Item 1"))
|
reconciler.findView(id: DynamicPropertyTest.environment).view,
|
||||||
XCTAssertEqual(reconciler.renderer.rootElement.children[0].children.count, 3)
|
Text("5")
|
||||||
addItem()
|
)
|
||||||
reconciler.expect(item(at: 2), equals: Text("Item 2"))
|
|
||||||
XCTAssertEqual(reconciler.renderer.rootElement.children[0].children.count, 4)
|
// StateObject
|
||||||
|
let stateObjectButton = reconciler.findView(
|
||||||
|
id: DynamicPropertyTest.stateObject,
|
||||||
|
as: Button<Text>.self
|
||||||
|
)
|
||||||
|
XCTAssertEqual(stateObjectButton.label, Text("0"))
|
||||||
|
stateObjectButton.action?()
|
||||||
|
stateObjectButton.action?()
|
||||||
|
XCTAssertEqual(stateObjectButton.label, Text("5"))
|
||||||
|
|
||||||
|
XCTAssertEqual(
|
||||||
|
reconciler.findView(id: DynamicPropertyTest.observedObject).view,
|
||||||
|
Text("5")
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(
|
||||||
|
reconciler.findView(id: DynamicPropertyTest.environmentObject).view,
|
||||||
|
Text("5")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue