Refine reconciler prototype code
This commit is contained in:
parent
093c0b6881
commit
60e7062d66
|
@ -6,7 +6,7 @@
|
|||
//
|
||||
|
||||
|
||||
struct AnyEquatable: Equatable {
|
||||
public struct AnyEquatable: Equatable {
|
||||
let value: Any
|
||||
private let equals: (Any) -> Bool
|
||||
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// Diffable.swift
|
||||
// Gluon
|
||||
//
|
||||
// Created by Max Desiatov on 28/11/2018.
|
||||
//
|
||||
|
||||
protocol Diffable {
|
||||
func diffKeyPaths(other: Self) -> [PartialKeyPath<Self>]
|
||||
}
|
||||
|
||||
struct Props: Equatable, Diffable {
|
||||
func diffKeyPaths(other: Props) -> [PartialKeyPath<Props>] {
|
||||
var result = [PartialKeyPath<Props>]()
|
||||
if height != other.height {
|
||||
result.append(\Props.height)
|
||||
}
|
||||
|
||||
if width != other.width {
|
||||
result.append(\Props.width)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
var height: Double
|
||||
var width: Float
|
||||
|
||||
static let kp = [\Props.height, \Props.width]
|
||||
}
|
|
@ -5,70 +5,30 @@
|
|||
// Created by Max Desiatov on 06/11/2018.
|
||||
//
|
||||
|
||||
protocol Diffable {
|
||||
func diffKeyPaths(other: Self) -> [PartialKeyPath<Self>]
|
||||
}
|
||||
|
||||
protocol BaseComponent: Equatable {
|
||||
}
|
||||
public struct Hooks {
|
||||
var currentReconciler: StackReconciler?
|
||||
var currentComponent: MountedCompositeComponent?
|
||||
|
||||
public struct Component<P: Equatable>: Equatable {
|
||||
let id: String
|
||||
let base: (P) -> Node
|
||||
|
||||
/// The generated uuid might incur some app start up time penalty. It would
|
||||
/// be great if there was a nice way to generate a unique static string.
|
||||
/// One approach could be using `"\(#file)\(#line)"` as a default value,
|
||||
/// but that leaks absolute filepaths, which is not ideal from a security
|
||||
/// perspective.
|
||||
public init(id: String = UUID().uuidString, base: @escaping (P) -> Node) {
|
||||
self.id = id
|
||||
self.base = base
|
||||
}
|
||||
|
||||
public static func ==(lhs: Component<P>, rhs: Component<P>) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public struct Node {
|
||||
let component: Any
|
||||
let props: Any
|
||||
|
||||
/// Closure with a component rerendered with new props
|
||||
let rerenderIfNeeded: (Node) -> Node?
|
||||
|
||||
public init<P: Equatable>(_ component: Component<P>, _ props: P) {
|
||||
self.component = component
|
||||
self.props = props
|
||||
rerenderIfNeeded = {
|
||||
guard let newComponent = $0.component as? Component<P>,
|
||||
newComponent == component,
|
||||
let newProps = $0.props as? P,
|
||||
newProps != props else { return nil }
|
||||
|
||||
return component.base(newProps)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Props: Equatable, Diffable {
|
||||
func diffKeyPaths(other: Props) -> [PartialKeyPath<Props>] {
|
||||
var result = [PartialKeyPath<Props>]()
|
||||
if height != other.height {
|
||||
result.append(\Props.height)
|
||||
public func state<T>(_ initial: T,
|
||||
id: Int = #line) -> (T, (T) -> ()) {
|
||||
guard let component = currentComponent,
|
||||
let reconciler = currentReconciler else {
|
||||
fatalError("""
|
||||
attempt to use `state` hook outside of a `render` function,
|
||||
or `render` is not called from a renderer
|
||||
""")
|
||||
}
|
||||
|
||||
if width != other.width {
|
||||
result.append(\Props.width)
|
||||
}
|
||||
let current = currentComponent?.state[id] as? T ?? initial
|
||||
|
||||
return result
|
||||
return (current, { [weak reconciler, weak component] new in
|
||||
// avoiding an indirect reference cycle here: this closure can be
|
||||
// owned by callbacks owned by node's target, which is strongly referenced
|
||||
// from node. Same with the reconciler.
|
||||
guard let component = component,
|
||||
let reconciler = reconciler else { return }
|
||||
|
||||
reconciler.queue(state: new, for: component, id: id) })
|
||||
}
|
||||
|
||||
var height: Double
|
||||
var width: Float
|
||||
|
||||
static let kp = [\Props.height, \Props.width]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
//
|
||||
// MountedComponent.swift
|
||||
// Gluon
|
||||
//
|
||||
// Created by Max Desiatov on 28/11/2018.
|
||||
//
|
||||
|
||||
class MountedComponent {
|
||||
let key: String?
|
||||
let props: AnyEquatable
|
||||
let children: AnyEquatable
|
||||
|
||||
fileprivate init(key: String?, props: AnyEquatable, children: AnyEquatable) {
|
||||
self.key = key
|
||||
self.props = props
|
||||
self.children = children
|
||||
}
|
||||
|
||||
static func make(_ node: Node) -> MountedComponent {
|
||||
switch node.type {
|
||||
case let .composite(type):
|
||||
return MountedCompositeComponent(node, type)
|
||||
case let .base(type):
|
||||
return MountedBaseComponent(node, type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class MountedCompositeComponent: MountedComponent {
|
||||
let type: AnyCompositeComponent.Type
|
||||
var state = [Int: Any]()
|
||||
|
||||
fileprivate init(_ node: Node, _ type: AnyCompositeComponent.Type) {
|
||||
self.type = type
|
||||
|
||||
super.init(key: node.key, props: node.props, children: node.children)
|
||||
}
|
||||
}
|
||||
|
||||
final class MountedBaseComponent: MountedComponent {
|
||||
let type: AnyBaseComponent.Type
|
||||
var target: Any?
|
||||
|
||||
fileprivate init(_ node: Node, _ type: AnyBaseComponent.Type) {
|
||||
self.type = type
|
||||
|
||||
super.init(key: node.key, props: node.props, children: node.children)
|
||||
}
|
||||
}
|
|
@ -5,51 +5,76 @@
|
|||
// Created by Max Desiatov on 28/11/2018.
|
||||
//
|
||||
|
||||
private let _hooks = Hooks()
|
||||
import Dispatch
|
||||
|
||||
extension Component {
|
||||
// TODO: this won't work in multi-threaded scenarios
|
||||
private var _hooks = Hooks()
|
||||
|
||||
extension CompositeComponent {
|
||||
public static var hooks: Hooks {
|
||||
return _hooks
|
||||
}
|
||||
}
|
||||
|
||||
public struct Hooks {
|
||||
public func state<T>(_ initial: T,
|
||||
id: Int = #line) -> (T, (T) -> ()) {
|
||||
return (initial, { _ in })
|
||||
}
|
||||
}
|
||||
|
||||
struct Pair<T: Hashable, U: Hashable>: Hashable {
|
||||
let first: T
|
||||
let second: U
|
||||
}
|
||||
|
||||
final class NodeReference {
|
||||
let key: String?
|
||||
let props: AnyEquatable
|
||||
let children: AnyEquatable
|
||||
let type: _Component.Type
|
||||
|
||||
init(node: Node) {
|
||||
self.key = node.key
|
||||
self.props = node.props
|
||||
self.children = node.children
|
||||
self.type = node.type
|
||||
}
|
||||
}
|
||||
|
||||
final class StackReconciler {
|
||||
/// A map from a fully qualified component type name and its state hook id
|
||||
/// to a current state value.
|
||||
var state = [Pair<String, Int>: Any]()
|
||||
private var queuedState = [(MountedCompositeComponent, Int, Any)]()
|
||||
|
||||
let root: NodeReference
|
||||
private let rootComponent: MountedComponent
|
||||
private let rootTarget: Any
|
||||
private weak var renderer: Renderer!
|
||||
|
||||
func reconcile(node reference: NodeReference, with node: Node) {
|
||||
init(node: Node, target: Any, renderer: Renderer) {
|
||||
self.renderer = renderer
|
||||
rootTarget = target
|
||||
|
||||
rootComponent = MountedComponent.make(node)
|
||||
if let component = rootComponent as? MountedBaseComponent,
|
||||
let type = component.type as? RendererBaseComponent.Type {
|
||||
component.target = renderer.mountTarget(to: target, with: type)
|
||||
}
|
||||
}
|
||||
|
||||
init(root: Node) {
|
||||
self.root = NodeReference(node: root)
|
||||
func queue(state: Any, for node: MountedCompositeComponent, id: Int) {
|
||||
let scheduleReconcile = queuedState.isEmpty
|
||||
|
||||
queuedState.append((node, id, state))
|
||||
|
||||
guard scheduleReconcile else { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.updateStateAndReconcile()
|
||||
}
|
||||
}
|
||||
|
||||
private func render(composite type: AnyCompositeComponent.Type,
|
||||
component: MountedCompositeComponent) -> Node? {
|
||||
_hooks.currentReconciler = self
|
||||
_hooks.currentComponent = component
|
||||
|
||||
let result = type.render(props: component.props,
|
||||
children: component.children)
|
||||
|
||||
_hooks.currentComponent = nil
|
||||
_hooks.currentReconciler = nil
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private func updateStateAndReconcile() {
|
||||
for (component, id, state) in queuedState {
|
||||
guard let renderedNode = render(composite: component.type,
|
||||
component: component) else {
|
||||
assertionFailure("""
|
||||
state update scheduled for a component that's not composite or
|
||||
has props and children types that don't match
|
||||
""")
|
||||
continue
|
||||
}
|
||||
component.state[id] = state
|
||||
reconcile(node: component, with: renderedNode)
|
||||
}
|
||||
}
|
||||
|
||||
private func reconcile(node reference: MountedComponent, with node: Node) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,19 +14,15 @@ extension Never: Equatable {
|
|||
}
|
||||
}
|
||||
|
||||
/// Type-erased version of Component to work around
|
||||
/// the lack of generalized existentials in Swift
|
||||
public protocol _Component {
|
||||
}
|
||||
|
||||
/// Conforming to this protocol, but implementing support for these new types
|
||||
/// in a renderer would make that renderer skip unknown types of children.
|
||||
public protocol ChildrenType {
|
||||
}
|
||||
|
||||
/// You should never directly conform to this protocol, use `HostComponent`
|
||||
/// for host components and `Component` for composite components.
|
||||
public protocol BaseComponent: _Component {
|
||||
public protocol AnyBaseComponent {
|
||||
}
|
||||
|
||||
public protocol BaseComponent: AnyBaseComponent {
|
||||
associatedtype Props: Equatable
|
||||
associatedtype Children: ChildrenType & Equatable
|
||||
|
||||
|
@ -34,14 +30,32 @@ public protocol BaseComponent: _Component {
|
|||
var props: Props { get }
|
||||
}
|
||||
|
||||
public protocol Component: _Component {
|
||||
public protocol AnyCompositeComponent {
|
||||
static func render(props: AnyEquatable, children: AnyEquatable) -> Node?
|
||||
}
|
||||
|
||||
public protocol CompositeComponent: AnyCompositeComponent {
|
||||
associatedtype Props: Equatable
|
||||
associatedtype Children: Equatable
|
||||
|
||||
static func render(props: Props, children: Children) -> Node
|
||||
}
|
||||
|
||||
public protocol LeafComponent: Component where Children == Never {
|
||||
public extension CompositeComponent {
|
||||
static func render(props: AnyEquatable, children: AnyEquatable) -> Node? {
|
||||
guard let props = props as? Props,
|
||||
let children = children as? Children else {
|
||||
assertionFailure("""
|
||||
incorrect types of `props` and `children` arguments passed to
|
||||
`AnyComponent.render`
|
||||
""")
|
||||
return nil
|
||||
}
|
||||
return render(props: props, children: children)
|
||||
}
|
||||
}
|
||||
|
||||
public protocol LeafComponent: CompositeComponent where Children == Never {
|
||||
static func render(props: Props) -> Node
|
||||
}
|
||||
|
||||
|
@ -51,6 +65,34 @@ public extension LeafComponent {
|
|||
}
|
||||
}
|
||||
|
||||
enum ComponentType: Equatable {
|
||||
static func == (lhs: ComponentType, rhs: ComponentType) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.base(ltype), .base(rtype)):
|
||||
return ltype == rtype
|
||||
case let (.composite(ltype), .composite(rtype)):
|
||||
return ltype == rtype
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
case base(AnyBaseComponent.Type)
|
||||
case composite(AnyCompositeComponent.Type)
|
||||
|
||||
var composite: AnyCompositeComponent.Type? {
|
||||
guard case let .composite(type) = self else { return nil }
|
||||
|
||||
return type
|
||||
}
|
||||
|
||||
var base: AnyBaseComponent.Type? {
|
||||
guard case let .base(type) = self else { return nil }
|
||||
|
||||
return type
|
||||
}
|
||||
}
|
||||
|
||||
public struct Node: Equatable {
|
||||
/// Equatable can't be automatically derived for `type` property?
|
||||
public static func == (lhs: Node, rhs: Node) -> Bool {
|
||||
|
@ -63,7 +105,7 @@ public struct Node: Equatable {
|
|||
let key: String?
|
||||
let props: AnyEquatable
|
||||
let children: AnyEquatable
|
||||
let type: _Component.Type
|
||||
let type: ComponentType
|
||||
}
|
||||
|
||||
extension BaseComponent {
|
||||
|
@ -73,7 +115,7 @@ extension BaseComponent {
|
|||
return Node(key: key,
|
||||
props: AnyEquatable(props),
|
||||
children: AnyEquatable(children),
|
||||
type: self.self)
|
||||
type: .base(self))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -125,41 +167,6 @@ public struct StackView: BaseComponent {
|
|||
public let children: [Node]
|
||||
}
|
||||
|
||||
// MARK: Legacy
|
||||
|
||||
private protocol ComponentType: BaseComponentType {
|
||||
associatedtype Props: Equatable
|
||||
var props: Props { get }
|
||||
|
||||
init(props: Props, children: [Node])
|
||||
}
|
||||
|
||||
private protocol BaseComponentType {
|
||||
var children: [Node] { get }
|
||||
|
||||
init?(props: AnyEquatable, children: [Node])
|
||||
}
|
||||
|
||||
private protocol StatefulComponent: ComponentType {
|
||||
associatedtype State: Default
|
||||
|
||||
var state: State { get }
|
||||
|
||||
init(props: Props, state: State, children: [Node])
|
||||
}
|
||||
|
||||
extension StatefulComponent {
|
||||
init(props: Props, children: [Node]) {
|
||||
self.init(props: props, state: State(), children: children)
|
||||
}
|
||||
}
|
||||
|
||||
extension StatefulComponent {
|
||||
func setState(setter: (inout State) -> ()) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// well, this gets problematic:
|
||||
// 1. `props` needs to be `var` for renderer to update them from node updates,
|
||||
// but this means `StatefulComponent` implementor is compelled to modify
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
protocol Renderer {
|
||||
func mountTarget(to parent: Any, with component: RendererBaseComponent) -> Any
|
||||
func update(target: Any, with component: RendererBaseComponent)
|
||||
func umount(target: Any, from parent: Any, with component: RendererBaseComponent)
|
||||
protocol Renderer: class {
|
||||
func mountTarget(to parent: Any, with component: RendererBaseComponent.Type) -> Any
|
||||
func update(target: Any, with component: RendererBaseComponent.Type)
|
||||
func umount(target: Any, from parent: Any, with component: RendererBaseComponent.Type)
|
||||
}
|
||||
|
||||
protocol RendererBaseComponent {
|
||||
|
|
Loading…
Reference in New Issue