Define and set RefTarget in TokamakUIKit (#54)

Also add `UIView.animate` animation effect to example code. Also removes redundant `Updatable` protocol.

Resolves #49 

* Define and set RefTarget in TokamakUIKit
* Add animation example
* Rename backgroundColor to currentColor
* Add comments to Animation example
This commit is contained in:
Max Desiatov 2019-02-25 11:50:09 +00:00 committed by GitHub
parent 5fabe139dd
commit 923ffd02fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 153 additions and 99 deletions

View File

@ -15,6 +15,7 @@
A6D5AF87221B131400DBF186 /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6D5AF86221B131400DBF186 /* Image.swift */; };
C449B806DFEE55B6CEE6478C /* libPods-TokamakDemo.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5B96B435A9D67621D318616E /* libPods-TokamakDemo.a */; };
D11DB6432219C03000013FC3 /* Timer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D11DB6422219C03000013FC3 /* Timer.swift */; };
D1BB3D302223F6B400C30062 /* Animation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1BB3D2F2223F6B400C30062 /* Animation.swift */; };
D1BFAF772215795900845EA0 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1BFAF762215795900845EA0 /* Router.swift */; };
D1BFAF792215800A00845EA0 /* Counter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1BFAF782215800A00845EA0 /* Counter.swift */; };
D1BFAF7B22158B4000845EA0 /* List.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1BFAF7A22158B4000845EA0 /* List.swift */; };
@ -43,6 +44,7 @@
A9EEF813955DAEEFE1D52ED4 /* Pods-TokamakDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TokamakDemo.debug.xcconfig"; path = "Pods/Target Support Files/Pods-TokamakDemo/Pods-TokamakDemo.debug.xcconfig"; sourceTree = "<group>"; };
C6DA99382B6892EAB361742F /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = "<group>"; };
D11DB6422219C03000013FC3 /* Timer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timer.swift; sourceTree = "<group>"; };
D1BB3D2F2223F6B400C30062 /* Animation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Animation.swift; sourceTree = "<group>"; };
D1BFAF762215795900845EA0 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = "<group>"; };
D1BFAF782215800A00845EA0 /* Counter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Counter.swift; sourceTree = "<group>"; };
D1BFAF7A22158B4000845EA0 /* List.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = List.swift; sourceTree = "<group>"; };
@ -162,6 +164,7 @@
D1F2C3262214407B008358DC /* TableModal.swift */,
D11DB6422219C03000013FC3 /* Timer.swift */,
A6D5AF86221B131400DBF186 /* Image.swift */,
D1BB3D2F2223F6B400C30062 /* Animation.swift */,
);
path = Components;
sourceTree = "<group>";
@ -283,6 +286,7 @@
D1F7185D2215A4A1004E5951 /* LayerProps.swift in Sources */,
D1BFAF792215800A00845EA0 /* Counter.swift in Sources */,
D1F718612215A617004E5951 /* Modals.swift in Sources */,
D1BB3D302223F6B400C30062 /* Animation.swift in Sources */,
D1F7185522159EAD004E5951 /* DatePickers.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@ -0,0 +1,67 @@
//
// Animation.swift
// TokamakDemo
//
// Created by Max Desiatov on 25/02/2019.
// Copyright © 2019 Tokamak. All rights reserved.
//
import Tokamak
struct Animation: CompositeComponent {
private static let colors: [(Color, String)] = [
(.white, "white"),
(.red, "red"),
(.green, "green"),
(.blue, "blue"),
]
typealias Props = Null
typealias Children = [AnyNode]
static func render(props: Null, children: Children, hooks: Hooks) -> AnyNode {
let previousColor = hooks.state(0)
let currentColor = hooks.state(0)
let ref = hooks.ref(type: UIView.self)
hooks.effect(currentColor.value) {
guard let view = ref.value else { return }
guard currentColor.value != previousColor.value else {
view.backgroundColor = UIColor(colors[currentColor.value].0)
return
}
UIView.animate(withDuration: 0.5, animations: {
view.backgroundColor = UIColor(colors[currentColor.value].0)
}, completion: { _ in
previousColor.set(currentColor.value)
})
}
return View.node(
.init(Style(
Edges.equal(to: .parent),
backgroundColor: .white
)),
StackView.node(.init(
Edges.equal(to: .safeArea),
axis: .vertical,
distribution: .fillEqually
), [
View.node(ref: ref, .init(), children),
SegmentedControl.node(
.init(
value: currentColor.value,
valueHandler: Handler {
// sometimes UISegmentedControl allows deselecting all segments
guard $0 >= 0 else { return }
currentColor.set($0)
}
),
colors.map { $0.1 }
),
])
)
}
}

View File

@ -29,42 +29,18 @@ struct NavigationModal: PureLeafComponent {
}
}
struct SimpleModal: LeafComponent {
struct SimpleModal: PureLeafComponent {
struct Props: Equatable {
let isPresented: State<Bool>
}
private static let colors: [(Color, String)] = [
(.white, "white"),
(.red, "red"),
(.green, "green"),
(.blue, "blue"),
]
static func render(props: Props, hooks: Hooks) -> AnyNode {
let backgroundColor = hooks.state(0)
static func render(props: Props) -> AnyNode {
return props.isPresented.value ? ModalPresenter.node(
View.node(
.init(Style(
Edges.equal(to: .parent),
backgroundColor: colors[backgroundColor.value].0
)),
StackView.node(.init(
Edges.equal(to: .parent),
axis: .vertical,
distribution: .fillEqually
), [
Button.node(.init(
onPress: Handler { props.isPresented.set(false) }
), "Close Modal"),
SegmentedControl.node(
.init(value: backgroundColor.value,
valueHandler: Handler(backgroundColor.set)),
colors.map { $0.1 }
),
])
)
Animation.node(Null(),
Button.node(.init(
Style(Center.equal(to: .parent)),
onPress: Handler { props.isPresented.set(false) }
), "Close Modal"))
) : Null.node()
}
}

View File

@ -17,7 +17,7 @@ struct TimerCounter: LeafComponent {
let timer = hooks.ref(type: Timer.self)
let interval = hooks.state(1.0)
hooks.effect(interval.value) { () -> () -> () in
hooks.finalizedEffect(interval.value) {
timer.value = Timer.scheduledTimer(
withTimeInterval: interval.value,
repeats: true

View File

@ -18,6 +18,7 @@ enum AppRoute: String, CaseIterable {
case layerProps = "Layer Props"
case timer
case image
case animation
}
extension AppRoute: CustomStringConvertible {
@ -59,6 +60,8 @@ struct Router: NavigationRouter {
result = TimerCounter.node()
case .image:
result = ImageExample.node()
case .animation:
result = Animation.node()
}
return NavigationItem.node(

View File

@ -77,6 +77,12 @@ extension Component where Children == [AnyNode] {
}
}
extension Component where Props == Null, Children == [AnyNode] {
public static func node() -> AnyNode {
return node(Null(), [])
}
}
extension Component where Props: Default, Props.DefaultValue == Props,
Children == [AnyNode] {
public static func node(_ child: AnyNode) -> AnyNode {
@ -127,3 +133,23 @@ extension RefComponent {
)
}
}
extension RefComponent where Children == [AnyNode] {
public static func node(
ref: Ref<RefTarget?>,
_ props: Props,
_ child: AnyNode
) -> AnyNode {
return node(ref: ref, props, [child])
}
public static func node(ref: Ref<RefTarget?>, _ props: Props) -> AnyNode {
return node(ref: ref, props, [])
}
}
extension RefComponent where Children == Null {
public static func node(ref: Ref<RefTarget?>, _ props: Props) -> AnyNode {
return node(ref: ref, props, Null())
}
}

View File

@ -15,7 +15,7 @@ extension Hooks {
closure should return a cleanup closure to be executed before the next
call to `render` or when a component is unmounted.
*/
public func effect(closure: @escaping () -> () -> ()) {
public func finalizedEffect(closure: @escaping () -> () -> ()) {
scheduleEffect(nil, closure)
}
@ -55,7 +55,10 @@ extension Hooks {
trigger effect execution (unsubscribing from updates on old user ID and
subscribing for new user ID) when the ID has changed.
*/
public func effect<T>(_ observed: T, closure: @escaping () -> () -> ())
public func finalizedEffect<T>(
_ observed: T,
closure: @escaping () -> () -> ()
)
where T: Equatable {
scheduleEffect(AnyEquatable(observed), closure)
}

View File

@ -5,16 +5,6 @@
// Created by Max Desiatov on 09/02/2019.
//
/// Formalizes an update to a value with a given action. Note that `update`
/// function is mutating here to allow efficient in-place updates. As long as
/// `Updatable` is implemented on a value type, this still allows freezing it
/// into an immutable value when needed.
public protocol Updatable {
associatedtype Action
mutating func update(_ action: Action)
}
typealias Updater<T> = (inout T) -> ()
/** Note that `set` functions are not `mutating`, they never update the
@ -54,14 +44,6 @@ public struct State<T> {
extension State: Equatable where T: Equatable {}
extension State where T: Updatable {
/// For any `Reduceable` state you can dispatch an `Action` to reduce that
/// state to a different value.
public func set(_ action: T.Action) {
updateHandler.value { $0.update(action) }
}
}
extension Hooks {
/** Allows a component to have its own state and to be updated when the state
changes. Returns a very simple state container, which on initial call of

View File

@ -42,6 +42,8 @@ public final class MountedHostComponent<R: Renderer>: MountedComponent<R> {
self.target = target
reconciler.renderer?.update(target: target, with: self)
switch node.children.value {
case let nodes as [AnyNode]:
mountedChildren = nodes.map { $0.makeMountedComponent(target) }

View File

@ -28,8 +28,6 @@ public final class TestRenderer: Renderer {
let result = TestView(component.node)
parent.add(subview: result)
update(target: result, with: component)
return result
}

View File

@ -19,4 +19,8 @@ class ViewBox<T: UIView>: ViewControllerBox<UIViewController> {
super.init(viewController, node)
}
override var refTarget: Any {
return view
}
}

View File

@ -19,4 +19,8 @@ class ViewControllerBox<T: UIViewController>: UITarget {
override var viewController: UIViewController {
return containerViewController
}
override var refTarget: Any {
return containerViewController
}
}

View File

@ -19,6 +19,7 @@ final class TokamakButton: UIButton, Default {
extension Button: UIControlComponent {
typealias Target = TokamakButton
public typealias RefTarget = UIButton
static func update(control box: ControlBox<TokamakButton>,
_ props: Button.Props,

View File

@ -27,6 +27,7 @@ final class TokamakDatePicker: UIDatePicker, Default, ValueStorage {
extension DatePicker: UIValueComponent {
typealias Target = TokamakDatePicker
public typealias RefTarget = UIDatePicker
static func update(valueBox: ValueControlBox<TokamakDatePicker>,
_ props: DatePicker.Props,

View File

@ -28,6 +28,8 @@ extension UIImage.RenderingMode {
}
extension Image: UIViewComponent {
public typealias RefTarget = UIImageView
static func update(
view box: ViewBox<TokamakImage>,
_ props: Image.Props,

View File

@ -32,6 +32,8 @@ extension NSTextAlignment {
}
extension Label: UIViewComponent {
public typealias RefTarget = UILabel
static func update(view box: ViewBox<TokamakLabel>,
_ props: Label.Props,
_ children: String) {

View File

@ -9,6 +9,8 @@ import Tokamak
import UIKit
extension ListView: UIViewComponent {
public typealias RefTarget = UITableView
static func box(
for view: TokamakTableView,
_ viewController: UIViewController,

View File

@ -41,7 +41,6 @@ extension NavigationItem: UIHostComponent {
let viewController = UIViewController()
let result = ViewControllerBox(viewController, component.node)
update(target: result, node: component.node)
parent.containerViewController.pushViewController(
viewController,

View File

@ -25,6 +25,7 @@ final class TokamakSegmentedControl: UISegmentedControl, Default, ValueStorage {
extension SegmentedControl: UIValueComponent {
typealias Target = TokamakSegmentedControl
public typealias RefTarget = UISegmentedControl
static func update(valueBox: ValueControlBox<TokamakSegmentedControl>,
_ props: SegmentedControl.Props,

View File

@ -16,6 +16,7 @@ final class TokamakSlider: UISlider, Default, ValueStorage {
extension Slider: UIValueComponent {
typealias Target = TokamakSlider
public typealias RefTarget = UISlider
static func update(valueBox: ValueControlBox<TokamakSlider>,
_ props: Slider.Props,

View File

@ -60,6 +60,8 @@ extension UIStackView.Distribution {
}
extension StackView: UIViewComponent {
public typealias RefTarget = UIStackView
static func update(view box: ViewBox<TokamakStackView>,
_ props: StackView.Props,
_: [AnyNode]) {

View File

@ -16,6 +16,7 @@ final class TokamakStepper: UIStepper, Default, ValueStorage {
extension Stepper: UIValueComponent {
typealias Target = TokamakStepper
public typealias RefTarget = UIStepper
static func update(valueBox: ValueControlBox<TokamakStepper>,
_ props: Stepper.Props,

View File

@ -27,6 +27,7 @@ final class TokamakSwitch: UISwitch, Default, ValueStorage {
extension Switch: UIValueComponent {
typealias Target = TokamakSwitch
public typealias RefTarget = UISwitch
static func update(valueBox: ValueControlBox<TokamakSwitch>,
_ props: Switch.Props,

View File

@ -15,6 +15,8 @@ final class TokamakView: UIView, Default {
}
extension View: UIViewComponent {
public typealias RefTarget = UIView
static func update(view: ViewBox<TokamakView>,
_ props: View.Props,
_: [AnyNode]) {}

View File

@ -9,7 +9,7 @@ import Tokamak
import UIKit
extension UIColor {
convenience init(_ color: Color) {
public convenience init(_ color: Color) {
switch color.space {
case .sRGB:
self.init(red: CGFloat(color.red),

View File

@ -9,7 +9,7 @@ import Tokamak
import UIKit
extension UIControl.Event {
init(_ value: Event) {
public init(_ value: Event) {
switch value {
case .touchDown:
self = .touchDown

View File

@ -8,7 +8,7 @@
import Tokamak
import UIKit
protocol UIViewComponent: UIHostComponent, HostComponent {
protocol UIViewComponent: UIHostComponent, RefComponent {
associatedtype Target: UIView & Default
static func update(view box: ViewBox<Target>,
@ -94,16 +94,6 @@ extension UIViewComponent where Target == Target.DefaultValue,
component: UIKitRenderer.MountedHost,
_ renderer: UIKitRenderer
) -> UITarget? {
guard let children = component.node.children.value as? Children else {
childrenAssertionFailure()
return nil
}
guard let props = component.node.props.value as? Props else {
propsAssertionFailure()
return nil
}
let target = Target.defaultValue
let result: ViewBox<Target>
@ -167,9 +157,6 @@ extension UIViewComponent where Target == Target.DefaultValue,
parentAssertionFailure()
}
applyStyle(result, props)
update(view: result, props, children)
return result
}

View File

@ -30,7 +30,11 @@ struct HackyProvider: SimpleCellProvider {
class UITarget: Target {
var viewController: UIViewController {
fatalError("viewController should be overriden in UITarget subclass")
fatalError("\(#function) should be overriden in UITarget subclass")
}
var refTarget: Any {
fatalError("\(#function) should be overriden in UITarget subclass")
}
}
@ -79,6 +83,12 @@ final class UIKitRenderer: Renderer {
rendererComponent.update(target: target,
node: component.node)
guard
let componentType = component.type as? AnyRefComponent.Type,
let anyRef = component.node.ref else { return }
componentType.update(ref: anyRef, with: target.refTarget)
}
func unmount(

View File

@ -14,22 +14,6 @@ extension Button: RefComponent {
public typealias RefTarget = TestView
}
extension Int: Updatable {
public enum Action {
case increment
case decrement
}
public mutating func update(_ action: Int.Action) {
switch action {
case .decrement:
self -= 1
case .increment:
self += 1
}
}
}
private extension Hooks {
func custom() -> State<Int> {
return state(42)
@ -42,7 +26,6 @@ struct Test: LeafComponent {
static func render(props: Null, hooks: Hooks) -> AnyNode {
let state1 = hooks.custom()
let state2 = hooks.custom()
let state3 = hooks.custom()
let ref = hooks.ref(type: TestView.self)
return StackView.node([
@ -55,9 +38,6 @@ struct Test: LeafComponent {
Button.node(.init(onPress: Handler { state2.set { $0 + 1 } }),
"Increment"),
Label.node("\(state2.value)"),
Button.node(.init(onPress: Handler { state3.set(.increment) }),
"Increment"),
Label.node("\(state3.value)"),
])
}
}
@ -74,8 +54,6 @@ final class HooksTests: XCTestCase {
let button1Handler = button1Props.handlers[.touchUpInside]?.value,
let button2Props = stack.subviews[2].props(Button.Props.self),
let button2Handler = button2Props.handlers[.touchUpInside]?.value,
let button3Props = stack.subviews[4].props(Button.Props.self),
let button3Handler = button3Props.handlers[.touchUpInside]?.value,
let button1Ref = stack.subviews[0].node.ref as? Ref<TestView?>
else {
XCTAssert(false, "components have no handlers")
@ -89,16 +67,11 @@ final class HooksTests: XCTestCase {
button2Handler(())
button2Handler(())
button3Handler(())
button3Handler(())
button3Handler(())
let e = expectation(description: "rerender")
DispatchQueue.main.async {
XCTAssertEqual(stack.subviews[1].node.children, AnyEquatable("43"))
XCTAssertEqual(stack.subviews[3].node.children, AnyEquatable("44"))
XCTAssertEqual(stack.subviews[5].node.children, AnyEquatable("45"))
e.fulfill()
}