Prototyped struct-based and class-based components
This commit is contained in:
parent
4dc78ea44c
commit
0baf0c32ea
|
@ -79,7 +79,9 @@
|
|||
D360275A0620D06017CFB567 /* Pods */,
|
||||
7C9590EA0938024ADB1C1A08 /* Frameworks */,
|
||||
);
|
||||
indentWidth = 2;
|
||||
sourceTree = "<group>";
|
||||
tabWidth = 2;
|
||||
};
|
||||
607FACD11AFB9204008FA782 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
//
|
||||
// RequiredInit.swift
|
||||
// FBSnapshotTestCase
|
||||
//
|
||||
// Created by Max Desiatov on 07/10/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol Default {
|
||||
init()
|
||||
}
|
||||
|
||||
struct Unique<T>: Equatable {
|
||||
private let uuid = UUID()
|
||||
private let boxed: T
|
||||
|
||||
init(_ boxed: T) {
|
||||
self.boxed = boxed
|
||||
}
|
||||
|
||||
static func == (lhs: Unique<T>, rhs: Unique<T>) -> Bool {
|
||||
return lhs.uuid == rhs.uuid
|
||||
}
|
||||
}
|
||||
|
||||
struct NoProps: Equatable, Default {
|
||||
}
|
||||
|
||||
private protocol ComponentType {
|
||||
}
|
||||
|
||||
extension String: ComponentType {
|
||||
}
|
||||
|
||||
class BaseComponent<Props: Equatable>: ComponentType {
|
||||
private(set) var props: Props
|
||||
private(set) var children: [Node]
|
||||
|
||||
required init(props: Props, children: [Node]) {
|
||||
self.props = props
|
||||
self.children = children
|
||||
}
|
||||
|
||||
static func node(_ props: Props, childrenFactory: () -> [Node]) -> Node {
|
||||
let children = childrenFactory()
|
||||
return Node {
|
||||
self.init(props: props, children: children)
|
||||
}
|
||||
}
|
||||
|
||||
static func node(_ props: Props, childFactory: () -> Node) -> Node {
|
||||
// applying `childFactory` here to avoid `@escaping` attribute
|
||||
let child = childFactory()
|
||||
return Node { self.init(props: props, children: [child]) }
|
||||
}
|
||||
|
||||
static func node(_ props: Props) -> Node {
|
||||
return Node { self.init(props: props, children: []) }
|
||||
}
|
||||
}
|
||||
|
||||
extension BaseComponent where Props: Default {
|
||||
static func node(childrenFactory: () -> [Node]) -> Node {
|
||||
return self.node(Props(), childrenFactory: childrenFactory)
|
||||
}
|
||||
|
||||
static func node(childFactory: () -> Node) -> Node {
|
||||
return self.node(Props(), childFactory: childFactory)
|
||||
}
|
||||
|
||||
static func node() -> Node {
|
||||
return Node { self.init(props: Props(), children: []) }
|
||||
}
|
||||
}
|
||||
|
||||
struct Node {
|
||||
fileprivate let factory: () -> ComponentType
|
||||
}
|
||||
|
||||
extension Node: ExpressibleByStringLiteral {
|
||||
init(stringLiteral: String) {
|
||||
factory = { stringLiteral }
|
||||
}
|
||||
}
|
||||
|
||||
extension Node {
|
||||
init(_ string: String) {
|
||||
factory = { string }
|
||||
}
|
||||
}
|
||||
|
||||
class View: BaseComponent<NoProps> {
|
||||
}
|
||||
|
||||
class Label: BaseComponent<Label.Props> {
|
||||
struct Props: Equatable, Default {
|
||||
let fontColor = UIColor.black
|
||||
}
|
||||
}
|
||||
|
||||
class Button: BaseComponent<Button.Props> {
|
||||
struct Props: Equatable {
|
||||
let backgroundColor = UIColor.white
|
||||
let fontColor = UIColor.black
|
||||
let onPress: Unique<() -> ()>
|
||||
}
|
||||
}
|
||||
|
||||
protocol CompositeComponent {
|
||||
func render() -> Node
|
||||
}
|
||||
|
||||
protocol StateType: Default & Equatable {
|
||||
}
|
||||
|
||||
class StatefulComponent<Props: Equatable, State: StateType>: BaseComponent<Props> {
|
||||
private(set) var state: State
|
||||
|
||||
required init(props: Props, children: [Node]) {
|
||||
state = State()
|
||||
super.init(props: props, children: children)
|
||||
}
|
||||
|
||||
func setState(setter: (inout State) -> ()) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
typealias Component<P: Equatable, S: StateType> =
|
||||
StatefulComponent<P, S> & CompositeComponent
|
|
@ -8,105 +8,8 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
protocol Default {
|
||||
init()
|
||||
}
|
||||
|
||||
protocol NodeType: Equatable {
|
||||
}
|
||||
|
||||
struct AnyNode: NodeType {
|
||||
let value: Any
|
||||
private let equals: (Any) -> Bool
|
||||
|
||||
public init<E: NodeType>(_ value: E) {
|
||||
self.value = value
|
||||
self.equals = { ($0 as? E) == value }
|
||||
}
|
||||
|
||||
public static func == (lhs: AnyNode, rhs: AnyNode) -> Bool {
|
||||
return lhs.equals(rhs.value) || rhs.equals(lhs.value)
|
||||
}
|
||||
}
|
||||
|
||||
extension NodeType {
|
||||
var wrap: AnyNode {
|
||||
return AnyNode(self)
|
||||
}
|
||||
}
|
||||
|
||||
extension UIControl.State: Hashable {
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
class BaseComponent<Node: NodeType> {
|
||||
var node: Node
|
||||
|
||||
init(node: Node) {
|
||||
self.node = node
|
||||
}
|
||||
}
|
||||
|
||||
struct Unique<T>: Equatable {
|
||||
private let uuid = UUID()
|
||||
private let boxed: T
|
||||
|
||||
init(_ boxed: T) {
|
||||
self.boxed = boxed
|
||||
}
|
||||
|
||||
static func == (lhs: Unique<T>, rhs: Unique<T>) -> Bool {
|
||||
return lhs.uuid == rhs.uuid
|
||||
}
|
||||
}
|
||||
|
||||
final class View: BaseComponent<View.Node> {
|
||||
struct Node: NodeType {
|
||||
let children: [AnyNode]
|
||||
init(children: () -> [AnyNode]) {
|
||||
self.children = children()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class Label: BaseComponent<Label.Node> {
|
||||
struct Node: NodeType {
|
||||
let children: String
|
||||
}
|
||||
}
|
||||
|
||||
final class Button: BaseComponent<Button.Node> {
|
||||
struct Node: NodeType {
|
||||
let backgroundColor = UIColor.white
|
||||
let onPress: Unique<() -> ()>
|
||||
let children: String
|
||||
}
|
||||
}
|
||||
|
||||
class Component<Node: NodeType, State: Default>: BaseComponent<Node> {
|
||||
private(set) var state: State
|
||||
|
||||
init(node: Node, state: State) {
|
||||
self.state = state
|
||||
super.init(node: node)
|
||||
}
|
||||
|
||||
func setState(setter: (inout State) -> ()) {
|
||||
|
||||
}
|
||||
|
||||
func render() -> AnyNode {
|
||||
fatalError("Component subclass should override render()")
|
||||
}
|
||||
}
|
||||
|
||||
struct NoProps: NodeType {
|
||||
}
|
||||
|
||||
final class Test: Component<NoProps, Test.State> {
|
||||
struct State: Default {
|
||||
final class Counter: Component<NoProps, Counter.State> {
|
||||
struct State: StateType {
|
||||
var counter = 0
|
||||
}
|
||||
|
||||
|
@ -116,16 +19,35 @@ final class Test: Component<NoProps, Test.State> {
|
|||
|
||||
lazy var onPressHandler = { Unique { self.onPress() } }()
|
||||
|
||||
override func render() -> AnyNode {
|
||||
return AnyNode(View.Node {
|
||||
[
|
||||
Button.Node(onPress: onPressHandler, children: "Tap Me").wrap,
|
||||
Label.Node(children: "\(state)").wrap
|
||||
]
|
||||
})
|
||||
func render() -> Node {
|
||||
return View.node {
|
||||
[Button.node(.init(onPress: onPressHandler)) { "Press me" },
|
||||
Label.node { Node("\(state.counter)") }]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func render(node: AnyNode, container: UIView) {
|
||||
final class ViewController: UIViewController {
|
||||
private let button = UIButton()
|
||||
private let label = UILabel()
|
||||
|
||||
private var counter = 0
|
||||
|
||||
@objc func onPress() {
|
||||
counter += 1
|
||||
label.text = "\(counter)"
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
label.text = "\(counter)"
|
||||
|
||||
button.addTarget(self, action: #selector(onPress), for: .touchUpInside)
|
||||
|
||||
view.addSubview(button)
|
||||
view.addSubview(label)
|
||||
}
|
||||
}
|
||||
|
||||
// render(node: Test.node(), container: UIView())
|
||||
|
|
|
@ -0,0 +1,149 @@
|
|||
//
|
||||
// ValueTypes.swift
|
||||
// Gluon
|
||||
//
|
||||
// Created by Max Desiatov on 07/10/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct AnyEquatable: Equatable {
|
||||
let value: Any
|
||||
private let equals: (Any) -> Bool
|
||||
|
||||
public init<E: Equatable>(_ value: E) {
|
||||
self.value = value
|
||||
self.equals = { ($0 as? E) == value }
|
||||
}
|
||||
|
||||
public static func == (lhs: AnyEquatable, rhs: AnyEquatable) -> Bool {
|
||||
return lhs.equals(rhs.value) || rhs.equals(lhs.value)
|
||||
}
|
||||
}
|
||||
|
||||
private protocol BaseComponentType {
|
||||
var children: [Node] { get }
|
||||
|
||||
init?(props: AnyEquatable, children: [Node])
|
||||
}
|
||||
|
||||
private struct Node: Equatable {
|
||||
// FIXME: is compiler not being able to derive `Equatable` for this a bug?
|
||||
static func == (lhs: Node, rhs: Node) -> Bool {
|
||||
return lhs.type == rhs.type &&
|
||||
lhs.children == rhs.children &&
|
||||
lhs.props == rhs.props
|
||||
}
|
||||
|
||||
let props: AnyEquatable
|
||||
let children: [Node]
|
||||
let type: BaseComponentType.Type
|
||||
}
|
||||
|
||||
private protocol ComponentType: BaseComponentType {
|
||||
associatedtype Props: Equatable
|
||||
var props: Props { get }
|
||||
|
||||
init(props: Props, children: [Node])
|
||||
}
|
||||
|
||||
extension ComponentType {
|
||||
init?(props: AnyEquatable, children: [Node]) {
|
||||
guard let props = props.value as? Props else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.init(props: props, children: children)
|
||||
}
|
||||
|
||||
static func node(props: Props, children: () -> [Node]) -> Node {
|
||||
return Node(props: AnyEquatable(props), children: children(), type: self.self)
|
||||
}
|
||||
}
|
||||
|
||||
private struct View: ComponentType {
|
||||
var props: Props
|
||||
var children: [Node]
|
||||
|
||||
struct Props: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
private struct Label: ComponentType {
|
||||
var props: Props
|
||||
var children: [Node]
|
||||
|
||||
struct Props: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
private struct Button: ComponentType {
|
||||
var props: Props
|
||||
var children: [Node]
|
||||
|
||||
struct Props: Equatable {
|
||||
let backgroundColor = UIColor.white
|
||||
let fontColor = UIColor.black
|
||||
let onPress: Unique<() -> ()>
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// `props` directly
|
||||
// 2. Same for `state`, but how would you even implement `setState` if there's
|
||||
// no dependency injection point for a renderer?
|
||||
// 3. Maybe `getState` and `setState` could be closures that are assigned by
|
||||
// the renderer? How does a renderer set up a heterogenous state store for
|
||||
// all components then?
|
||||
// 4. Could ability to have stored properties in extensions make this any
|
||||
// better?
|
||||
private struct Test: StatefulComponent {
|
||||
struct Props: Equatable {
|
||||
}
|
||||
var props: Props
|
||||
|
||||
struct State: Default {
|
||||
var counter = 0
|
||||
}
|
||||
var state: State
|
||||
var children: [Node]
|
||||
|
||||
func onPress() {
|
||||
setState { $0.counter += 1 }
|
||||
}
|
||||
|
||||
// getting an error "Closure cannot implicitly capture a mutating self parameter"
|
||||
// if this is uncommented
|
||||
// lazy var onPressHandler = { Unique { onPress() } }()
|
||||
//
|
||||
// override func render() -> AnyNode {
|
||||
// return AnyNode(View.Node {
|
||||
// [
|
||||
// Button.Node(onPress: onPressHandler, children: "Tap Me").wrap,
|
||||
// Label.Node(children: "\(state)").wrap
|
||||
// ]
|
||||
// })
|
||||
// }
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
//
|
||||
// UIKitRenderer.swift
|
||||
// Gluon
|
||||
//
|
||||
// Created by Max Desiatov on 07/10/2018.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
func render(node: Node, container: UIView) {
|
||||
}
|
Loading…
Reference in New Issue