From 84018db509a7bef27f19c221400e4f837e9a45dc Mon Sep 17 00:00:00 2001 From: Carson Katri Date: Sun, 6 Feb 2022 13:57:57 -0500 Subject: [PATCH 01/64] Initial Reconciler using visitor pattern --- Package.swift | 1 + .../AttributeGraph/ViewGraph.swift | 395 ++++++++++++++++++ .../Environment/EnvironmentValues.swift | 2 +- .../Reflection/Models/PropertyInfo.swift | 15 +- Sources/TokamakCore/Views/AnyView.swift | 8 + .../Views/Containers/ForEach.swift | 6 + .../Views/Containers/TupleView.swift | 83 ++++ .../TokamakCore/Views/Controls/Button.swift | 12 +- Sources/TokamakCore/Views/Layout/HStack.swift | 6 +- Sources/TokamakCore/Views/Layout/VStack.swift | 6 +- Sources/TokamakCore/Views/View.swift | 4 + Sources/TokamakCore/Views/ViewBuilder.swift | 22 +- .../TokamakStaticHTML/Views/Text/Text.swift | 4 +- .../TokamakReconcilerTests/VisitorTests.swift | 175 ++++++++ 14 files changed, 725 insertions(+), 14 deletions(-) create mode 100644 Sources/TokamakCore/AttributeGraph/ViewGraph.swift create mode 100644 Tests/TokamakReconcilerTests/VisitorTests.swift diff --git a/Package.swift b/Package.swift index 65f4064bc..b379ee3f2 100644 --- a/Package.swift +++ b/Package.swift @@ -207,6 +207,7 @@ let package = Package( name: "TokamakTestRenderer", dependencies: ["TokamakCore"] ), + .testTarget(name: "TokamakReconcilerTests", dependencies: ["TokamakCore"]), .testTarget( name: "TokamakTests", dependencies: ["TokamakTestRenderer"] diff --git a/Sources/TokamakCore/AttributeGraph/ViewGraph.swift b/Sources/TokamakCore/AttributeGraph/ViewGraph.swift new file mode 100644 index 000000000..823822043 --- /dev/null +++ b/Sources/TokamakCore/AttributeGraph/ViewGraph.swift @@ -0,0 +1,395 @@ +// +// File.swift +// +// +// Created by Carson Katri on 2/3/22. +// + +public protocol ViewVisitor { + func visit(_ view: V) +} + +public extension View { + func _visitChildren(_ visitor: V) { + visitor.visit(body) + } +} + +typealias ViewVisitorF = (V) -> () + +protocol ViewReducer { + associatedtype Result + static var initialResult: Result { get } + static func reduce(partialResult: Result, nextView: V) -> Result +} + +final class ReducerVisitor: ViewVisitor { + var result: R.Result = R.initialResult + + func visit(_ view: V) where V: View { + result = R.reduce(partialResult: result, nextView: view) + } +} + +extension ViewReducer { + typealias Visitor = ReducerVisitor +} + +/// An output from a `Renderer`. +public protocol Element: AnyObject, Equatable { + init(from primitiveView: V) +} + +public protocol GraphRenderer { + associatedtype ElementType: Element + static func isPrimitive(_ view: V) -> Bool where V: View + func commit(_ mutations: [RenderableMutation]) +} + +public enum RenderableMutation { + case insert( + element: Renderer.ElementType, + parent: Renderer.ElementType, + sibling: Renderer.ElementType? + ) + case remove(element: Renderer.ElementType, parent: Renderer.ElementType?) + case replace( + parent: Renderer.ElementType, + previous: Renderer.ElementType, + replacement: Renderer.ElementType + ) + case update(previous: Renderer.ElementType, newElement: Renderer.ElementType) +} + +@_spi(TokamakCore) public final class ViewNode: CustomDebugStringConvertible { + weak var reconciler: Reconciler? + + var view: Any! + var visitView: ((ViewVisitor) -> ())! + var id: Identity? + var element: Renderer.ElementType? + var children: [ViewNode] + unowned var parent: ViewNode? + let typeInfo: TypeInfo? + var state: [PropertyInfo: MutableStorage]! + + final class MutableStorage { + private(set) var value: Any + let onSet: () -> () + + func setValue(_ newValue: Any, with transaction: Transaction) { + value = newValue + onSet() + } + + init(initialValue: Any, onSet: @escaping () -> ()) { + value = initialValue + self.onSet = onSet + } + } + + public enum Identity: Hashable { + case explicit(AnyHashable) + case structural(index: Int) + } + + init( + _ view: inout V, + element: Renderer.ElementType?, + parent: ViewNode?, + reconciler: Reconciler? + ) { + self.reconciler = reconciler + self.element = element + children = [] + self.parent = parent + typeInfo = TokamakCore.typeInfo(of: V.self) + + state = bindProperties(to: &view, typeInfo) + self.view = view + visitView = { [weak self] in + guard let self = self else { return } + // swiftlint:disable:next force_cast + $0.visit(self.view as! V) + } + } + + init( + bound view: Any, + typeInfo: TypeInfo?, + visitView: @escaping (ViewVisitor) -> (), + element: Renderer.ElementType?, + parent: ViewNode?, + reconciler: Reconciler? + ) { + self.view = view + self.reconciler = reconciler + self.element = element + children = [] + self.parent = parent + self.typeInfo = typeInfo + self.visitView = visitView + } + + private func bindProperties( + to view: inout V, + _ typeInfo: TypeInfo? + ) -> [PropertyInfo: MutableStorage] { + guard let typeInfo = typeInfo else { return [:] } + + var state: [PropertyInfo: MutableStorage] = [:] + for property in typeInfo.properties where property.type is DynamicProperty.Type { + var value = property.get(from: view) + if var storage = value as? WritableValueStorage { + let box = MutableStorage(initialValue: storage.anyInitialValue, onSet: { [weak self] in + guard let self = self else { return } + self.reconciler?.reconcile(from: self) + }) + state[property] = box + storage.getter = { box.value } + storage.setter = { box.setValue($0, with: $1) } + value = storage + } + property.set(value: value, on: &view) + } + return state + } + + func clone() -> ViewNode { + .init( + bound: view!, + typeInfo: typeInfo, + visitView: visitView, + element: nil, + parent: nil, + reconciler: reconciler + ) + } + + public var debugDescription: String { + flush() + } + + private func flush(level: Int = 0) -> String { + let spaces = String(repeating: " ", count: level) + let elementDescription: String + if let element = element { + elementDescription = """ + ( + \(spaces) \(String(describing: element)) + \(spaces)) + """ + } else { + elementDescription = "" + } + let childrenDescription: String + if children.isEmpty { + childrenDescription = "" + } else { + childrenDescription = """ + { + \(children.map { $0.flush(level: level + 1) }.joined(separator: "\n")) + \(spaces)} + """ + } + return """ + \(spaces)\(String(describing: type(of: view!)) + .split(separator: "<")[0])\(elementDescription) \(childrenDescription) + """ + } +} + +@_spi(TokamakCore) public extension ViewNode { + func traverse(_ work: (ViewNode) -> Result?) -> Result? { + var stack = children + while true { + guard let next = stack.popLast() else { return nil } + if let result = work(next) { + return result + } + stack.insert(contentsOf: next.children, at: 0) + } + } + + func findView(id: ViewNode.Identity) -> ViewNode? { + traverse { node in + node.id == id ? node : nil + } + } +} + +public final class Reconciler { + var tree: ViewNode! + let renderer: Renderer + + public init(_ renderer: Renderer, _ view: V) { + self.renderer = renderer + let visitor = InitialTreeBuilderVisitor(reconciler: self) + visitor.visit(view) + tree = visitor.root + } + + enum Mutation { + case insert(ViewNode, parent: ViewNode, sibling: ViewNode?) + case remove(ViewNode, parent: ViewNode) + case replace( + parent: ViewNode, + previous: ViewNode, + current: ViewNode + ) + case update(ViewNode, newElement: Renderer.ElementType) + } + + final class InitialTreeBuilderVisitor: ViewVisitor { + var root: ViewNode + var reconciler: Reconciler + + init(reconciler: Reconciler) { + self.reconciler = reconciler + var view = EmptyView() + root = .init(&view, element: nil, parent: nil, reconciler: reconciler) + } + + func visit(_ view: V) where V: View { + root.view = view + // Create a stack of nodes and the accessor for their children. + var accessors: [(ViewNode, (TreeReducer.Visitor) -> ())] = + [(root, { $0.visit(view) })] + while true { + guard let next = accessors.popLast() else { return } // Pop from the stack + // Visit each child, collapsing the result into an array of (ViewNode, Accessor) + let reducer = TreeReducer.Visitor() + next.1(reducer) + for (index, child) in reducer.result.enumerated() { + child.0.parent = next.0 + child.0.reconciler = reconciler + child.0.id = .structural(index: index) + next.0.children.append(child.0) + } + accessors.append(contentsOf: reducer.result) + } + } + } + + struct TreeReducer: ViewReducer { + typealias Result = [(ViewNode, visitChildren: (TreeReducer.Visitor) -> ())] + static var initialResult: Result { [] } + + static func reduce(partialResult: Result, nextView: V) -> Result where V: View { + var nextView = nextView + return partialResult + [(ViewNode( + &nextView, + element: Renderer.isPrimitive(nextView) ? Renderer.ElementType(from: nextView) : nil, + parent: nil, + reconciler: nil + ), nextView._visitChildren)] + } + } + + final class ReconcilerVisitor: ViewVisitor { + var current: ViewNode + var mutations = [Mutation]() + + init(current: ViewNode) { + self.current = current + } + + func visit(_ view: V) where V: View { + var nodeStack = [current] + var accessorStack: [(ViewNode, (TreeReducer.Visitor) -> ())] = + [(current.clone(), { view._visitChildren($0) })] + while true { + guard let nextNode = nodeStack.popLast(), + let nextAccessor = accessorStack.popLast() + else { return } // Pop from the stacks + // Visit each child, collapsing the result into an array of (ViewNode, Accessor) + let reducer = TreeReducer.Visitor() + nextAccessor.1(reducer) + var lastSibling: ViewNode? + for (index, child) in reducer.result + .enumerated() + { // TODO: Reconcile with nextNode.children + child.0.parent = nextAccessor.0 + child.0.reconciler = current.reconciler + child.0.id = .structural(index: index) + nextAccessor.0.children.append(child.0) + if !nextNode.children.indices.contains(index) { + mutations.append(.insert(child.0, parent: nextNode, sibling: lastSibling)) + } else { + let previousChild = nextNode.children[index] + if child.0.typeInfo?.type != previousChild.typeInfo?.type { + mutations + .append(.replace(parent: nextNode, previous: previousChild, current: child.0)) + } else if let newElement = child.0.element, + newElement != previousChild.element + { + mutations.append(.update(previousChild, newElement: newElement)) + } + lastSibling = previousChild + } + } + for removed in nextNode.children.dropFirst(reducer.result.count) { + mutations.append(.remove(removed, parent: nextNode)) + } + accessorStack.append(contentsOf: reducer.result) + nodeStack.append(contentsOf: nextNode.children) + } + } + } + + func reconcile(from current: ViewNode) { + let visitor = ReconcilerVisitor(current: current) + current.visitView(visitor) + // Apply mutations to the tree and the renderer. + var renderableMutations = [RenderableMutation]() + for mutation in visitor.mutations { + switch mutation { + case let .insert(viewNode, parent, sibling): + if let sibling = sibling, + let index = parent.children.firstIndex(where: { $0 === sibling }) + { + parent.children.insert(viewNode, at: index + 1) + } else { + parent.children.insert(viewNode, at: 0) + } + guard let element = viewNode.element, + let parentElement = parent.element + else { continue } + renderableMutations + .append(.insert(element: element, parent: parentElement, sibling: sibling?.element)) + case let .remove(viewNode, parent): + guard let index = parent.children.firstIndex(where: { $0 === viewNode }) else { continue } + parent.children.remove(at: index) + guard let element = viewNode.element else { continue } + renderableMutations.append(.remove(element: element, parent: parent.element)) + case let .replace(parent, previous, current): + guard let index = parent.children.firstIndex(where: { $0 === previous }) else { continue } + parent.children[index] = current + if let parentElement = parent.element, + let previousElement = previous.element, + let currentElement = current.element + { + renderableMutations + .append(.replace( + parent: parentElement, + previous: previousElement, + replacement: currentElement + )) + } + case let .update(viewNode, newElement): + if let previous = viewNode.element { + renderableMutations.append(.update(previous: previous, newElement: newElement)) + } + viewNode.element = newElement + } + } + renderer.commit(renderableMutations) + } +} + +public extension GraphRenderer { + @discardableResult + func render(_ view: V) -> Reconciler { + .init(self, view) + } +} diff --git a/Sources/TokamakCore/Environment/EnvironmentValues.swift b/Sources/TokamakCore/Environment/EnvironmentValues.swift index e54a00f86..dc1ec51d7 100644 --- a/Sources/TokamakCore/Environment/EnvironmentValues.swift +++ b/Sources/TokamakCore/Environment/EnvironmentValues.swift @@ -47,7 +47,7 @@ public struct EnvironmentValues: CustomStringConvertible { @_spi(TokamakCore) public mutating func merge(_ other: Self?) { if let other = other { - values.merge(other.values) { existing, new in + values.merge(other.values) { _, new in new } } diff --git a/Sources/TokamakCore/Reflection/Models/PropertyInfo.swift b/Sources/TokamakCore/Reflection/Models/PropertyInfo.swift index 3f09a164e..323fca340 100644 --- a/Sources/TokamakCore/Reflection/Models/PropertyInfo.swift +++ b/Sources/TokamakCore/Reflection/Models/PropertyInfo.swift @@ -20,7 +20,20 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -public struct PropertyInfo { +public struct PropertyInfo: Hashable { + public static func == (lhs: PropertyInfo, rhs: PropertyInfo) -> Bool { + lhs.name == rhs.name && lhs.type == rhs.type && lhs.isVar == rhs.isVar && lhs.offset == rhs + .offset && lhs.ownerType == rhs.ownerType + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(name) + hasher.combine(ObjectIdentifier(type)) + hasher.combine(isVar) + hasher.combine(offset) + hasher.combine(ObjectIdentifier(ownerType)) + } + public let name: String public let type: Any.Type public let isVar: Bool diff --git a/Sources/TokamakCore/Views/AnyView.swift b/Sources/TokamakCore/Views/AnyView.swift index 47e0263a5..fd9170380 100644 --- a/Sources/TokamakCore/Views/AnyView.swift +++ b/Sources/TokamakCore/Views/AnyView.swift @@ -40,6 +40,8 @@ public struct AnyView: _PrimitiveView { */ let bodyType: Any.Type + let visitChildren: (ViewVisitor, Any) -> () + public init(_ view: V) where V: View { if let anyView = view as? AnyView { self = anyView @@ -52,8 +54,14 @@ public struct AnyView: _PrimitiveView { self.view = view // swiftlint:disable:next force_cast bodyClosure = { AnyView(($0 as! V).body) } + // swiftlint:disable:next force_cast + visitChildren = { $0.visit($1 as! V) } } } + + public func _visitChildren(_ visitor: V) where V: ViewVisitor { + visitChildren(visitor, view) + } } public func mapAnyView(_ anyView: AnyView, transform: (V) -> T) -> T? { diff --git a/Sources/TokamakCore/Views/Containers/ForEach.swift b/Sources/TokamakCore/Views/Containers/ForEach.swift index 88a3eff8a..ef2bf6ef9 100644 --- a/Sources/TokamakCore/Views/Containers/ForEach.swift +++ b/Sources/TokamakCore/Views/Containers/ForEach.swift @@ -48,6 +48,12 @@ public struct ForEach: _PrimitiveView where Data: RandomAcces self.id = id self.content = content } + + public func _visitChildren(_ visitor: V) where V: ViewVisitor { + for element in data { + visitor.visit(content(element)) + } + } } extension ForEach: ForEachProtocol where Data.Index == Int { diff --git a/Sources/TokamakCore/Views/Containers/TupleView.swift b/Sources/TokamakCore/Views/Containers/TupleView.swift index 0f0cfdec0..6945589a5 100644 --- a/Sources/TokamakCore/Views/Containers/TupleView.swift +++ b/Sources/TokamakCore/Views/Containers/TupleView.swift @@ -22,26 +22,46 @@ public struct TupleView: _PrimitiveView { public let value: T let _children: [AnyView] + private let visit: (ViewVisitor) -> () public init(_ value: T) { self.value = value _children = [] + visit = { _ in } } public init(_ value: T, children: [AnyView]) { self.value = value _children = children + visit = { + for child in children { + $0.visit(child) + } + } + } + + public func _visitChildren(_ visitor: V) where V: ViewVisitor { + visit(visitor) } init(_ v1: T1, _ v2: T2) where T == (T1, T2) { value = (v1, v2) _children = [AnyView(v1), AnyView(v2)] + visit = { + $0.visit(v1) + $0.visit(v2) + } } // swiftlint:disable large_tuple init(_ v1: T1, _ v2: T2, _ v3: T3) where T == (T1, T2, T3) { value = (v1, v2, v3) _children = [AnyView(v1), AnyView(v2), AnyView(v3)] + visit = { + $0.visit(v1) + $0.visit(v2) + $0.visit(v3) + } } init(_ v1: T1, _ v2: T2, _ v3: T3, _ v4: T4) @@ -49,6 +69,12 @@ public struct TupleView: _PrimitiveView { { value = (v1, v2, v3, v4) _children = [AnyView(v1), AnyView(v2), AnyView(v3), AnyView(v4)] + visit = { + $0.visit(v1) + $0.visit(v2) + $0.visit(v3) + $0.visit(v4) + } } init( @@ -60,6 +86,13 @@ public struct TupleView: _PrimitiveView { ) where T == (T1, T2, T3, T4, T5) { value = (v1, v2, v3, v4, v5) _children = [AnyView(v1), AnyView(v2), AnyView(v3), AnyView(v4), AnyView(v5)] + visit = { + $0.visit(v1) + $0.visit(v2) + $0.visit(v3) + $0.visit(v4) + $0.visit(v5) + } } init( @@ -72,6 +105,14 @@ public struct TupleView: _PrimitiveView { ) where T == (T1, T2, T3, T4, T5, T6) { value = (v1, v2, v3, v4, v5, v6) _children = [AnyView(v1), AnyView(v2), AnyView(v3), AnyView(v4), AnyView(v5), AnyView(v6)] + visit = { + $0.visit(v1) + $0.visit(v2) + $0.visit(v3) + $0.visit(v4) + $0.visit(v5) + $0.visit(v6) + } } init( @@ -93,6 +134,15 @@ public struct TupleView: _PrimitiveView { AnyView(v6), AnyView(v7), ] + visit = { + $0.visit(v1) + $0.visit(v2) + $0.visit(v3) + $0.visit(v4) + $0.visit(v5) + $0.visit(v6) + $0.visit(v7) + } } init( @@ -116,6 +166,16 @@ public struct TupleView: _PrimitiveView { AnyView(v7), AnyView(v8), ] + visit = { + $0.visit(v1) + $0.visit(v2) + $0.visit(v3) + $0.visit(v4) + $0.visit(v5) + $0.visit(v6) + $0.visit(v7) + $0.visit(v8) + } } init( @@ -141,6 +201,17 @@ public struct TupleView: _PrimitiveView { AnyView(v8), AnyView(v9), ] + visit = { + $0.visit(v1) + $0.visit(v2) + $0.visit(v3) + $0.visit(v4) + $0.visit(v5) + $0.visit(v6) + $0.visit(v7) + $0.visit(v8) + $0.visit(v9) + } } init< @@ -179,6 +250,18 @@ public struct TupleView: _PrimitiveView { AnyView(v9), AnyView(v10), ] + visit = { + $0.visit(v1) + $0.visit(v2) + $0.visit(v3) + $0.visit(v4) + $0.visit(v5) + $0.visit(v6) + $0.visit(v7) + $0.visit(v8) + $0.visit(v9) + $0.visit(v10) + } } } diff --git a/Sources/TokamakCore/Views/Controls/Button.swift b/Sources/TokamakCore/Views/Controls/Button.swift index a675bb86f..9e69c46de 100644 --- a/Sources/TokamakCore/Views/Controls/Button.swift +++ b/Sources/TokamakCore/Views/Controls/Button.swift @@ -65,7 +65,7 @@ public struct Button