Add `ref` and `effect` hooks, `Updatable` protocol (#41)
This also cleans up and refines some of the `Example` code and removes unused `key` property from `AnyNode`, which greatly improves error-reporting of Swift type-checker when wrong props are passed to `node` function. Resolves #10 Resolves #9 Resolves #16 * Add Ref class and RefComponent protocol * Add AnyRef protocol, add ref to AnyNode.init * Cleanup hooks, add Target class to Gluon module * Fix tests, avoid optionality in Target class * Improve doc comments on Hooks.effect overload * Implement effects scheduling with finalizers * Add HookedComponents protocol, Hooks as class * Cleanup after rebased on `master` * Fix tests failing after Hooks refactoring * Remove `key` argument from AnyNode.init, cleanups * Efficient Reduceable state fully working w/ tests * Fix use of comments for Equatable AnyNode operator * Cleanup comments in Hooks and MHC * Add TimerCounter, fix host not unmounting children * Rename Reduceable to Updatable, fix effects bugs * Fix TestRenderer compilation issues * Remove `print` remnants in Timer Example code
This commit is contained in:
parent
3715feefef
commit
22cce69d3a
|
@ -13,10 +13,11 @@
|
|||
607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; };
|
||||
607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; };
|
||||
607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; };
|
||||
D11DB6432219C03000013FC3 /* Timer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D11DB6422219C03000013FC3 /* Timer.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 */; };
|
||||
D1DEEC2922009E8000C525EE /* NavRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1DEEC2822009E8000C525EE /* NavRouter.swift */; };
|
||||
D1DEEC2922009E8000C525EE /* ModalRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1DEEC2822009E8000C525EE /* ModalRouter.swift */; };
|
||||
D1E6BCD221AD4CD6002769E3 /* GluonTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E6BCD121AD4CD6002769E3 /* GluonTest.swift */; };
|
||||
D1F2C3272214407B008358DC /* TableModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F2C3262214407B008358DC /* TableModal.swift */; };
|
||||
D1F7185322159E09004E5951 /* Controls.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F7185222159E09004E5951 /* Controls.swift */; };
|
||||
|
@ -52,10 +53,11 @@
|
|||
8CADEBB8BFF6F2621CA49E8F /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
|
||||
C1B4A8D80770145FDAFEAE64 /* Pods_Gluon_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Gluon_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
D1DEEC2822009E8000C525EE /* NavRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavRouter.swift; sourceTree = "<group>"; };
|
||||
D1DEEC2822009E8000C525EE /* ModalRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalRouter.swift; sourceTree = "<group>"; };
|
||||
D1E6BCD121AD4CD6002769E3 /* GluonTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GluonTest.swift; sourceTree = "<group>"; };
|
||||
D1F2C3262214407B008358DC /* TableModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableModal.swift; sourceTree = "<group>"; };
|
||||
D1F7185222159E09004E5951 /* Controls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Controls.swift; sourceTree = "<group>"; };
|
||||
|
@ -171,15 +173,16 @@
|
|||
D1F7185122159D6E004E5951 /* Examples */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D1BFAF7A22158B4000845EA0 /* List.swift */,
|
||||
D1DEEC2822009E8000C525EE /* NavRouter.swift */,
|
||||
D1F2C3262214407B008358DC /* TableModal.swift */,
|
||||
D1BFAF782215800A00845EA0 /* Counter.swift */,
|
||||
D1F7185E2215A5D0004E5951 /* Constraints.swift */,
|
||||
D1F7185222159E09004E5951 /* Controls.swift */,
|
||||
D1F7185422159EAD004E5951 /* DatePickers.swift */,
|
||||
D1F7185C2215A4A1004E5951 /* LayerProps.swift */,
|
||||
D1F7185E2215A5D0004E5951 /* Constraints.swift */,
|
||||
D1BFAF7A22158B4000845EA0 /* List.swift */,
|
||||
D1F718602215A617004E5951 /* Modals.swift */,
|
||||
D1DEEC2822009E8000C525EE /* ModalRouter.swift */,
|
||||
D1F2C3262214407B008358DC /* TableModal.swift */,
|
||||
D11DB6422219C03000013FC3 /* Timer.swift */,
|
||||
);
|
||||
path = Examples;
|
||||
sourceTree = "<group>";
|
||||
|
@ -324,8 +327,9 @@
|
|||
D1F2C3272214407B008358DC /* TableModal.swift in Sources */,
|
||||
607FACD81AFB9204008FA782 /* ViewController.swift in Sources */,
|
||||
D1F7185F2215A5D0004E5951 /* Constraints.swift in Sources */,
|
||||
D11DB6432219C03000013FC3 /* Timer.swift in Sources */,
|
||||
D1F7185322159E09004E5951 /* Controls.swift in Sources */,
|
||||
D1DEEC2922009E8000C525EE /* NavRouter.swift in Sources */,
|
||||
D1DEEC2922009E8000C525EE /* ModalRouter.swift in Sources */,
|
||||
D1BFAF772215795900845EA0 /* Router.swift in Sources */,
|
||||
607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */,
|
||||
D1F7185D2215A4A1004E5951 /* LayerProps.swift in Sources */,
|
||||
|
|
|
@ -17,7 +17,7 @@ struct Constraints: LeafComponent {
|
|||
return StackView.node(.init(
|
||||
axis: .vertical,
|
||||
distribution: .fillEqually,
|
||||
Edges.equal(to: .parent)
|
||||
Edges.equal(to: .safeArea)
|
||||
), [
|
||||
Slider.node(.init(
|
||||
value: left.value,
|
||||
|
|
|
@ -89,7 +89,7 @@ struct Controls: LeafComponent {
|
|||
alignment: .center,
|
||||
axis: .vertical,
|
||||
distribution: .fillEqually,
|
||||
Edges.equal(to: .parent)
|
||||
Edges.equal(to: .safeArea)
|
||||
),
|
||||
children
|
||||
)
|
||||
|
|
|
@ -20,7 +20,7 @@ struct Counter: LeafComponent {
|
|||
alignment: .center,
|
||||
axis: .vertical,
|
||||
distribution: .fillEqually,
|
||||
Edges.equal(to: .parent)
|
||||
Edges.equal(to: .safeArea)
|
||||
), [
|
||||
Button.node(
|
||||
.init(onPress: Handler { count.set { $0 + 1 } }),
|
||||
|
|
|
@ -27,7 +27,7 @@ struct DatePickers: LeafComponent {
|
|||
return StackView.node(.init(
|
||||
axis: .vertical,
|
||||
distribution: .fillEqually,
|
||||
Edges.equal(to: .parent)
|
||||
Edges.equal(to: .safeArea)
|
||||
), [
|
||||
Label.node(
|
||||
labelProps,
|
||||
|
@ -35,7 +35,7 @@ struct DatePickers: LeafComponent {
|
|||
),
|
||||
Label.node(
|
||||
labelProps,
|
||||
"This picker doesn't animate state changes in the next picker"
|
||||
"This picker doesn't animate state changes in the next picker:"
|
||||
),
|
||||
DatePicker.node(
|
||||
.init(
|
||||
|
@ -46,7 +46,7 @@ struct DatePickers: LeafComponent {
|
|||
),
|
||||
Label.node(
|
||||
labelProps,
|
||||
"This picker animates state changes in the previous picker"
|
||||
"This picker animates state changes in the previous picker:"
|
||||
),
|
||||
DatePicker.node(
|
||||
.init(
|
||||
|
|
|
@ -18,7 +18,7 @@ struct LayerProps: LeafComponent {
|
|||
StackView.node(.init(
|
||||
axis: .vertical,
|
||||
distribution: .fillEqually,
|
||||
Edges.equal(to: .parent)
|
||||
Edges.equal(to: .safeArea)
|
||||
), [
|
||||
Slider.node(.init(
|
||||
value: state.value,
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import Gluon
|
||||
|
||||
struct NavRouter: NavigationRouter {
|
||||
struct ModalRouter: NavigationRouter {
|
||||
enum Route {
|
||||
case first
|
||||
case second
|
|
@ -16,7 +16,7 @@ struct NavigationModal: PureLeafComponent {
|
|||
static func render(props: Props) -> AnyNode {
|
||||
return props.isPresented.value ?
|
||||
ModalPresenter.node(
|
||||
NavigationPresenter<NavRouter>.node(
|
||||
NavigationPresenter<ModalRouter>.node(
|
||||
.init(
|
||||
initial: .first,
|
||||
prefersLargeTitles: true,
|
||||
|
@ -82,7 +82,7 @@ struct Modals: LeafComponent {
|
|||
alignment: .center,
|
||||
axis: .vertical,
|
||||
distribution: .fillEqually,
|
||||
Edges.equal(to: .parent)
|
||||
Edges.equal(to: .safeArea)
|
||||
),
|
||||
[
|
||||
Button.node(
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
//
|
||||
// Timer.swift
|
||||
// Gluon_Example
|
||||
//
|
||||
// Created by Max Desiatov on 17/02/2019.
|
||||
// Copyright © 2019 Gluon. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Gluon
|
||||
|
||||
struct TimerCounter: LeafComponent {
|
||||
typealias Props = Null
|
||||
|
||||
static func render(props: Null, hooks: Hooks) -> AnyNode {
|
||||
let count = hooks.state(0)
|
||||
let timer = hooks.ref(type: Timer.self)
|
||||
let interval = hooks.state(1.0)
|
||||
|
||||
hooks.effect(interval.value) { () -> () -> () in
|
||||
timer.value = Timer.scheduledTimer(
|
||||
withTimeInterval: interval.value,
|
||||
repeats: true
|
||||
) { _ in
|
||||
count.set { $0 + 1 }
|
||||
}
|
||||
return {
|
||||
timer.value?.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
return StackView.node(
|
||||
.init(
|
||||
alignment: .center,
|
||||
axis: .vertical,
|
||||
distribution: .fillEqually,
|
||||
Edges.equal(to: .safeArea)
|
||||
), [
|
||||
Label.node(
|
||||
.init(alignment: .center),
|
||||
"Adjust timer interval in seconds: \(interval.value)"
|
||||
),
|
||||
Stepper.node(
|
||||
.init(
|
||||
value: interval.value,
|
||||
valueHandler: Handler(interval.set)
|
||||
)
|
||||
),
|
||||
Label.node(.init(alignment: .center), "\(count.value)"),
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ enum AppRoute: String, CaseIterable {
|
|||
case modals = "Modal Presentation"
|
||||
case datePicker = "Date Picker"
|
||||
case layerProps = "Layer Props"
|
||||
case timer
|
||||
}
|
||||
|
||||
extension AppRoute: CustomStringConvertible {
|
||||
|
@ -53,12 +54,14 @@ struct Router: NavigationRouter {
|
|||
result = DatePickers.node()
|
||||
case .layerProps:
|
||||
result = LayerProps.node()
|
||||
case .timer:
|
||||
result = TimerCounter.node()
|
||||
}
|
||||
|
||||
return NavigationItem.node(
|
||||
.init(title: route.description),
|
||||
View.node(
|
||||
.init(Style(backgroundColor: .white, Edges.equal(to: .safeArea))),
|
||||
.init(Style(backgroundColor: .white, Edges.equal(to: .parent))),
|
||||
result
|
||||
)
|
||||
)
|
||||
|
|
|
@ -41,13 +41,16 @@
|
|||
D163F9D621F27F8C00BA464B /* GluonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D163F9D521F27F8C00BA464B /* GluonViewController.swift */; };
|
||||
D16FCB7C21FCB3870033C5C0 /* TableViewBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16FCB7B21FCB3870033C5C0 /* TableViewBox.swift */; };
|
||||
D16FCB7D21FCB38D0033C5C0 /* ListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16FCB7921FCB3160033C5C0 /* ListView.swift */; };
|
||||
D16FCB8121FDD2550033C5C0 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = D16FCB8021FDD2550033C5C0 /* Weak.swift */; };
|
||||
D1A1A7692210679B0094EA4F /* Target.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A1A7682210679B0094EA4F /* Target.swift */; };
|
||||
D1A1A76B22106D300094EA4F /* Effect.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A1A76A22106D300094EA4F /* Effect.swift */; };
|
||||
D1BDBBE621E0F07800FBBCDF /* MountedNull.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1BDBBE521E0F07800FBBCDF /* MountedNull.swift */; };
|
||||
D1C3A72121F0C47200C6B884 /* XAxisConstraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1C3A72021F0C47200C6B884 /* XAxisConstraint.swift */; };
|
||||
D1C3A72421F0DE5300C6B884 /* Insets.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1C3A72221F0DC6300C6B884 /* Insets.swift */; };
|
||||
D1CB7D2921E136010075C0C3 /* SegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1CB7D2821E136010075C0C3 /* SegmentedControl.swift */; };
|
||||
D1CB7D2B21E136520075C0C3 /* SegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1CB7D2A21E136520075C0C3 /* SegmentedControl.swift */; };
|
||||
D1CB7D2E21E29CC80075C0C3 /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1CB7D2D21E29CC80075C0C3 /* NavigationController.swift */; };
|
||||
D1D792EB220F79140042A632 /* State.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D792EA220F79130042A632 /* State.swift */; };
|
||||
D1D792ED220F79B30042A632 /* Ref.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D792EC220F79B30042A632 /* Ref.swift */; };
|
||||
D1E8755521EF2ED200D4A7BA /* HooksTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E8755421EF2ED200D4A7BA /* HooksTests.swift */; };
|
||||
D1EC92F021F5B8000026C207 /* ListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1EC92EF21F5B8000026C207 /* ListView.swift */; };
|
||||
D1EF77522205C579008829EC /* Constraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1EF77512205C579008829EC /* Constraint.swift */; };
|
||||
|
@ -99,7 +102,6 @@
|
|||
OBJ_120 /* Null.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_71 /* Null.swift */; };
|
||||
OBJ_121 /* Renderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_72 /* Renderer.swift */; };
|
||||
OBJ_122 /* StackReconciler.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_73 /* StackReconciler.swift */; };
|
||||
OBJ_123 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_74 /* Store.swift */; };
|
||||
OBJ_124 /* Unique.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_75 /* Unique.swift */; };
|
||||
OBJ_132 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; };
|
||||
OBJ_144 /* TestRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_11 /* TestRenderer.swift */; };
|
||||
|
@ -197,13 +199,16 @@
|
|||
D163F9D521F27F8C00BA464B /* GluonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GluonViewController.swift; sourceTree = "<group>"; };
|
||||
D16FCB7921FCB3160033C5C0 /* ListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListView.swift; sourceTree = "<group>"; };
|
||||
D16FCB7B21FCB3870033C5C0 /* TableViewBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewBox.swift; sourceTree = "<group>"; };
|
||||
D16FCB8021FDD2550033C5C0 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = "<group>"; };
|
||||
D1A1A7682210679B0094EA4F /* Target.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Target.swift; sourceTree = "<group>"; };
|
||||
D1A1A76A22106D300094EA4F /* Effect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Effect.swift; sourceTree = "<group>"; };
|
||||
D1BDBBE521E0F07800FBBCDF /* MountedNull.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MountedNull.swift; sourceTree = "<group>"; };
|
||||
D1C3A72021F0C47200C6B884 /* XAxisConstraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XAxisConstraint.swift; sourceTree = "<group>"; };
|
||||
D1C3A72221F0DC6300C6B884 /* Insets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Insets.swift; sourceTree = "<group>"; };
|
||||
D1CB7D2821E136010075C0C3 /* SegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedControl.swift; sourceTree = "<group>"; };
|
||||
D1CB7D2A21E136520075C0C3 /* SegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedControl.swift; sourceTree = "<group>"; };
|
||||
D1CB7D2D21E29CC80075C0C3 /* NavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = "<group>"; };
|
||||
D1D792EA220F79130042A632 /* State.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = State.swift; sourceTree = "<group>"; };
|
||||
D1D792EC220F79B30042A632 /* Ref.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ref.swift; sourceTree = "<group>"; };
|
||||
D1E8755421EF2ED200D4A7BA /* HooksTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HooksTests.swift; sourceTree = "<group>"; };
|
||||
D1EC92EF21F5B8000026C207 /* ListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListView.swift; sourceTree = "<group>"; };
|
||||
D1EF77512205C579008829EC /* Constraint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constraint.swift; sourceTree = "<group>"; };
|
||||
|
@ -287,7 +292,6 @@
|
|||
OBJ_71 /* Null.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Null.swift; sourceTree = "<group>"; };
|
||||
OBJ_72 /* Renderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Renderer.swift; sourceTree = "<group>"; };
|
||||
OBJ_73 /* StackReconciler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackReconciler.swift; sourceTree = "<group>"; };
|
||||
OBJ_74 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
|
||||
OBJ_75 /* Unique.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Unique.swift; sourceTree = "<group>"; };
|
||||
OBJ_79 /* ReconcilerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReconcilerTests.swift; sourceTree = "<group>"; };
|
||||
OBJ_8 /* Package.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Package.xcconfig; sourceTree = "<group>"; };
|
||||
|
@ -395,6 +399,17 @@
|
|||
path = Host;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D1D792E7220F784A0042A632 /* Hooks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D1D792EA220F79130042A632 /* State.swift */,
|
||||
OBJ_45 /* Hooks.swift */,
|
||||
D1D792EC220F79B30042A632 /* Ref.swift */,
|
||||
D1A1A76A22106D300094EA4F /* Effect.swift */,
|
||||
);
|
||||
path = Hooks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D1EF77502205C579008829EC /* Constraint */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -493,15 +508,14 @@
|
|||
OBJ_41 /* AnyNode.swift */,
|
||||
OBJ_42 /* Components.swift */,
|
||||
OBJ_43 /* Default.swift */,
|
||||
OBJ_45 /* Hooks.swift */,
|
||||
D1D792E7220F784A0042A632 /* Hooks */,
|
||||
OBJ_46 /* Components */,
|
||||
OBJ_67 /* MountedComponents */,
|
||||
OBJ_71 /* Null.swift */,
|
||||
OBJ_72 /* Renderer.swift */,
|
||||
OBJ_73 /* StackReconciler.swift */,
|
||||
OBJ_74 /* Store.swift */,
|
||||
OBJ_75 /* Unique.swift */,
|
||||
D16FCB8021FDD2550033C5C0 /* Weak.swift */,
|
||||
D1A1A7682210679B0094EA4F /* Target.swift */,
|
||||
);
|
||||
name = Gluon;
|
||||
path = Sources/Gluon;
|
||||
|
@ -845,8 +859,10 @@
|
|||
buildActionMask = 0;
|
||||
files = (
|
||||
D1EF776E2205C74D008829EC /* SizeConstraint.swift in Sources */,
|
||||
D1D792ED220F79B30042A632 /* Ref.swift in Sources */,
|
||||
D1EF77602205C685008829EC /* Right.swift in Sources */,
|
||||
D1EF775E2205C679008829EC /* Left.swift in Sources */,
|
||||
D1D792EB220F79140042A632 /* State.swift in Sources */,
|
||||
OBJ_93 /* AnyEquatable.swift in Sources */,
|
||||
OBJ_94 /* AnyNode.swift in Sources */,
|
||||
D1EF77582205C60D008829EC /* Height.swift in Sources */,
|
||||
|
@ -872,12 +888,14 @@
|
|||
D1EF776A2205C717008829EC /* FirstBaseline.swift in Sources */,
|
||||
OBJ_107 /* TabPresenter.swift in Sources */,
|
||||
D1BDBBE621E0F07800FBBCDF /* MountedNull.swift in Sources */,
|
||||
D1A1A7692210679B0094EA4F /* Target.swift in Sources */,
|
||||
OBJ_108 /* Color.swift in Sources */,
|
||||
OBJ_109 /* Event.swift in Sources */,
|
||||
D1CB7D2E21E29CC80075C0C3 /* NavigationController.swift in Sources */,
|
||||
OBJ_110 /* Rectangle.swift in Sources */,
|
||||
OBJ_111 /* Second.swift in Sources */,
|
||||
OBJ_112 /* Style.swift in Sources */,
|
||||
D1A1A76B22106D300094EA4F /* Effect.swift in Sources */,
|
||||
D1C3A72421F0DE5300C6B884 /* Insets.swift in Sources */,
|
||||
D1EF77702205F6FA008829EC /* Center.swift in Sources */,
|
||||
OBJ_113 /* TextAlignment.swift in Sources */,
|
||||
|
@ -885,7 +903,6 @@
|
|||
D1CB7D2921E136010075C0C3 /* SegmentedControl.swift in Sources */,
|
||||
OBJ_115 /* StackView.swift in Sources */,
|
||||
OBJ_116 /* View.swift in Sources */,
|
||||
D16FCB8121FDD2550033C5C0 /* Weak.swift in Sources */,
|
||||
D1EF77542205C583008829EC /* Edges.swift in Sources */,
|
||||
OBJ_117 /* MountedComponent.swift in Sources */,
|
||||
D1EF77852208BB10008829EC /* TabItem.swift in Sources */,
|
||||
|
@ -896,7 +913,6 @@
|
|||
OBJ_120 /* Null.swift in Sources */,
|
||||
OBJ_121 /* Renderer.swift in Sources */,
|
||||
OBJ_122 /* StackReconciler.swift in Sources */,
|
||||
OBJ_123 /* Store.swift in Sources */,
|
||||
OBJ_124 /* Unique.swift in Sources */,
|
||||
D1EF77682205C6F6008829EC /* CenterY.swift in Sources */,
|
||||
D1EF77862208BB13008829EC /* NavigationItem.swift in Sources */,
|
||||
|
|
|
@ -341,7 +341,12 @@ struct State<T> {
|
|||
func set(_ value: T)
|
||||
|
||||
// or update the state with a pure function
|
||||
func set(_ updater: (T) -> T)
|
||||
func set(_ transformer: @escaping (T) -> T)
|
||||
|
||||
// or efficiently update the state in place with a mutating function
|
||||
// (helps avoiding expensive memory allocations when state contains
|
||||
// large arrays/dictionaries or other copy-on-write value)
|
||||
func set(_ updater: @escaping (inout T) -> ())
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -6,15 +6,16 @@
|
|||
//
|
||||
|
||||
public struct AnyNode: Equatable {
|
||||
/// Equatable can't be automatically derived for `type` property?
|
||||
// Equatable can't be automatically derived for `type` property?
|
||||
public static func ==(lhs: AnyNode, rhs: AnyNode) -> Bool {
|
||||
return lhs.key == rhs.key &&
|
||||
return
|
||||
lhs.ref === rhs.ref &&
|
||||
lhs.type == rhs.type &&
|
||||
lhs.children == rhs.children &&
|
||||
lhs.props == rhs.props
|
||||
}
|
||||
|
||||
let key: String?
|
||||
let ref: AnyObject?
|
||||
public let props: AnyEquatable
|
||||
public let children: AnyEquatable
|
||||
let type: ComponentType
|
||||
|
@ -39,7 +40,7 @@ public struct AnyNode: Equatable {
|
|||
extension Null {
|
||||
public static func node() -> AnyNode {
|
||||
return AnyNode(
|
||||
key: nil,
|
||||
ref: nil,
|
||||
props: AnyEquatable(Null()),
|
||||
children: AnyEquatable(Null()),
|
||||
type: .null
|
||||
|
@ -47,12 +48,6 @@ extension Null {
|
|||
}
|
||||
}
|
||||
|
||||
extension Component {
|
||||
public static func node(_ props: Props, _ children: Children) -> AnyNode {
|
||||
return node(key: nil, props, children)
|
||||
}
|
||||
}
|
||||
|
||||
extension Component where Children == Null {
|
||||
public static func node(_ props: Props) -> AnyNode {
|
||||
return node(props, Null())
|
||||
|
@ -94,23 +89,41 @@ extension Component where Props: Default, Props.DefaultValue == Props,
|
|||
}
|
||||
|
||||
extension HostComponent {
|
||||
public static func node(key: String?,
|
||||
_ props: Props,
|
||||
_ children: Children) -> AnyNode {
|
||||
return AnyNode(key: key,
|
||||
props: AnyEquatable(props),
|
||||
children: AnyEquatable(children),
|
||||
type: .host(self))
|
||||
public static func node(_ props: Props, _ children: Children) -> AnyNode {
|
||||
return AnyNode(
|
||||
ref: nil,
|
||||
props: AnyEquatable(props),
|
||||
children: AnyEquatable(children),
|
||||
type: .host(self)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension CompositeComponent {
|
||||
public static func node(key: String?,
|
||||
_ props: Props,
|
||||
_ children: Children) -> AnyNode {
|
||||
return AnyNode(key: key,
|
||||
props: AnyEquatable(props),
|
||||
children: AnyEquatable(children),
|
||||
type: .composite(self))
|
||||
public static func node(
|
||||
_ props: Props,
|
||||
_ children: Children
|
||||
) -> AnyNode {
|
||||
return AnyNode(
|
||||
ref: nil,
|
||||
props: AnyEquatable(props),
|
||||
children: AnyEquatable(children),
|
||||
type: .composite(self)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension RefComponent {
|
||||
public static func node(
|
||||
ref: Ref<RefType>,
|
||||
_ props: Props,
|
||||
_ children: Children
|
||||
) -> AnyNode {
|
||||
return AnyNode(
|
||||
ref: ref,
|
||||
props: AnyEquatable(props),
|
||||
children: AnyEquatable(children),
|
||||
type: .host(self)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,10 @@ public protocol AnyHostComponent {}
|
|||
|
||||
public protocol HostComponent: AnyHostComponent, Component {}
|
||||
|
||||
public protocol RefComponent: HostComponent {
|
||||
associatedtype RefType
|
||||
}
|
||||
|
||||
/// Type-erased version of `CompositeComponent` to work around
|
||||
/// [PAT restrictions](http://www.russbishop.net/swift-associated-types). Users
|
||||
/// of Gluon shouldn't ever need to conform to this protocol directly, use
|
||||
|
@ -29,9 +33,10 @@ public protocol Component {
|
|||
associatedtype Props: Equatable
|
||||
associatedtype Children: Equatable
|
||||
|
||||
static func node(key: String?,
|
||||
_ props: Props,
|
||||
_ children: Children) -> AnyNode
|
||||
static func node(
|
||||
_ props: Props,
|
||||
_ children: Children
|
||||
) -> AnyNode
|
||||
}
|
||||
|
||||
public protocol CompositeComponent: AnyCompositeComponent, Component {
|
||||
|
|
|
@ -44,7 +44,7 @@ public struct NavigationPresenter<T: NavigationRouter>: LeafComponent {
|
|||
public static func render(props: Props, hooks: Hooks) -> AnyNode {
|
||||
let stack = hooks.state([props.initial])
|
||||
|
||||
let pop = { stack.set(Array(stack.value.dropLast())) }
|
||||
let pop = { stack.set { $0.remove(at: $0.count - 1) } }
|
||||
|
||||
return NavigationController.node(
|
||||
.init(
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
//
|
||||
// Hooks.swift
|
||||
// Gluon
|
||||
//
|
||||
// Created by Max Desiatov on 06/11/2018.
|
||||
//
|
||||
|
||||
public struct State<T> {
|
||||
public let value: T
|
||||
let setter: Handler<T>
|
||||
|
||||
init(_ value: T, _ setter: @escaping (T) -> ()) {
|
||||
self.value = value
|
||||
self.setter = Handler(setter)
|
||||
}
|
||||
|
||||
public func set(_ value: T) {
|
||||
setter.value(value)
|
||||
}
|
||||
|
||||
public func set(_ updater: (T) -> T) {
|
||||
setter.value(updater(value))
|
||||
}
|
||||
}
|
||||
|
||||
extension State: Equatable where T: Equatable {}
|
||||
|
||||
public struct Hooks {
|
||||
var currentState: ((Any) -> (Any, Int))?
|
||||
var queueState: ((_ state: Any, _ index: Int) -> ())?
|
||||
|
||||
public func state<T>(_ initial: T) -> State<T> {
|
||||
guard let currentState = currentState,
|
||||
let queueState = queueState else {
|
||||
fatalError("""
|
||||
attempt to use `state` hook outside of a `render` function,
|
||||
or `render` is not called from a renderer
|
||||
""")
|
||||
}
|
||||
|
||||
let (value, index) = currentState(initial)
|
||||
|
||||
return State(value as? T ?? initial) { queueState($0, index) }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
//
|
||||
// Effect.swift
|
||||
// Gluon
|
||||
//
|
||||
// Created by Max Desiatov on 10/02/2019.
|
||||
//
|
||||
|
||||
extension Hooks {
|
||||
/// Schedule an effect to be executed on every call to `render`.
|
||||
public func effect(closure: @escaping () -> ()) {
|
||||
scheduleEffect(nil, { closure(); return nil })
|
||||
}
|
||||
|
||||
/** Schedule an effect to be executed on every call to `render`. The effect
|
||||
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 () -> () -> ()) {
|
||||
scheduleEffect(nil, closure)
|
||||
}
|
||||
|
||||
/** Schedule an effect to be executed on calls to `render` when `observed`
|
||||
value has changed from the previous call to `render`.
|
||||
|
||||
You can use this overload of `effect` to control when exactly the effect
|
||||
`closure` is executed. For example, always pass `Null()` as `observed` so
|
||||
that the effect is executed only once on the initial rendering.
|
||||
|
||||
Another use case is an effect that schedules a repeated timer with a specific
|
||||
interval. You wouldn't want to reschedule the timer on every call to
|
||||
component's `render` if the interval hasn't changed. Pass the interval as
|
||||
`observed`, which will be compared to the previous value and trigger effect
|
||||
execution (updating the timer interval or creating a new timer) when the
|
||||
interval has changed.
|
||||
*/
|
||||
public func effect<T: Equatable>(_ observed: T, closure: @escaping () -> ()) {
|
||||
scheduleEffect(AnyEquatable(observed), { closure(); return nil })
|
||||
}
|
||||
|
||||
/** Schedule an effect to be executed on calls to `render` when `observed`
|
||||
value has changed from the previous call to `render`. The effect
|
||||
closure should return a cleanup closure to be executed before the next
|
||||
call to `render` or when a component is unmounted.
|
||||
|
||||
You can use this overload of `effect` to control when exactly the effect
|
||||
`closure` is executed with additional cleanup. For example, always pass
|
||||
`Null()` as `observed` so that the effect is executed only once on the
|
||||
initial rendering.
|
||||
|
||||
Another use case is an effect that subscribes to updates on a user model
|
||||
fetched from the network and you need to correctly unsubscribe from updates
|
||||
when a component is unmounted. You wouldn't to unsubscribe and resubscribe
|
||||
every time a component rerendered if an observed user ID hasn't changed.
|
||||
Pass the ID as `observed`, which will be compared to the previous value and
|
||||
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 () -> () -> ())
|
||||
where T: Equatable {
|
||||
scheduleEffect(AnyEquatable(observed), closure)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
//
|
||||
// Hooks.swift
|
||||
// Gluon
|
||||
//
|
||||
// Created by Max Desiatov on 06/11/2018.
|
||||
//
|
||||
|
||||
typealias Finalizer = (() -> ())?
|
||||
typealias Effect = () -> Finalizer
|
||||
|
||||
protocol HookedComponent: class {
|
||||
/// State cells of this component indexed by order of `hooks.state` calls
|
||||
var state: [Any] { get set }
|
||||
|
||||
/// Effect cells of this component indexed by order of `hooks.effect` calls
|
||||
var effects: [(observed: AnyEquatable?, Effect)] { get set }
|
||||
|
||||
/// Finalizer cells of this component received from `Effect` evaluation.
|
||||
/// Indices in this array exactly match indices in `effects` array.
|
||||
var effectFinalizers: [Finalizer] { get set }
|
||||
|
||||
/// Ref cells of this component indexed by order of `hooks.ref` calls
|
||||
var refs: [AnyObject] { get set }
|
||||
}
|
||||
|
||||
/** Functions implemented directly in this class are parts of internal
|
||||
implementation of `Hooks`. The public API is defined in extensions of `Hooks`
|
||||
located in separate files in the same directory.
|
||||
*/
|
||||
public final class Hooks {
|
||||
/** Closure assigned by the reconciler before every `render` call. Queues
|
||||
a state update with this reconciler.
|
||||
*/
|
||||
let queueState: (_ index: Int, _ updater: Updater<Any>) -> ()
|
||||
|
||||
weak var component: HookedComponent?
|
||||
|
||||
private var stateIndex = 0
|
||||
|
||||
private var effectIndex = 0
|
||||
private(set) var scheduledEffects = Set<Int>()
|
||||
|
||||
private var refIndex = 0
|
||||
|
||||
init(
|
||||
component: HookedComponent,
|
||||
queueState: @escaping (_ index: Int, _ updater: Updater<Any>) -> ()
|
||||
) {
|
||||
self.component = component
|
||||
self.queueState = queueState
|
||||
}
|
||||
|
||||
/** For a given initial state return a current value of this state
|
||||
(initialized from `initial` if current was absent) and its index.
|
||||
*/
|
||||
func currentState(_ initial: Any) -> (current: Any, index: Int) {
|
||||
defer { stateIndex += 1 }
|
||||
|
||||
guard let component = component else {
|
||||
assertionFailure("hooks.state should only be called within `render`")
|
||||
return (initial, stateIndex)
|
||||
}
|
||||
|
||||
if component.state.count > stateIndex {
|
||||
return (component.state[stateIndex], stateIndex)
|
||||
} else {
|
||||
component.state.append(initial)
|
||||
return (initial, stateIndex)
|
||||
}
|
||||
}
|
||||
|
||||
/** Schedules effect exection with the current reconciler accessed via
|
||||
`component`.
|
||||
*/
|
||||
func scheduleEffect(
|
||||
_ observed: AnyEquatable?,
|
||||
_ effect: @escaping Effect
|
||||
) {
|
||||
defer { effectIndex += 1 }
|
||||
|
||||
guard let component = component else {
|
||||
assertionFailure("hooks.effect should only be called within `render`")
|
||||
return
|
||||
}
|
||||
|
||||
if component.effects.count > effectIndex {
|
||||
guard component.effects[effectIndex].0 != observed else { return }
|
||||
|
||||
component.effects[effectIndex].0 = observed
|
||||
component.effects[effectIndex].1 = effect
|
||||
scheduledEffects.insert(effectIndex)
|
||||
} else {
|
||||
component.effects.append((observed, effect))
|
||||
scheduledEffects.insert(effectIndex)
|
||||
}
|
||||
}
|
||||
|
||||
/** For a given initial value return a current ref
|
||||
(initialized from `initial` if current was absent)
|
||||
*/
|
||||
func ref<T>(_ initial: Ref<T>) -> Ref<T> {
|
||||
defer { stateIndex += 1 }
|
||||
|
||||
guard let component = component else {
|
||||
assertionFailure("hooks.state should only be called within `render`")
|
||||
return initial
|
||||
}
|
||||
|
||||
if component.refs.count > refIndex {
|
||||
guard let result = component.refs[refIndex] as? Ref<T> else {
|
||||
assertionFailure(
|
||||
"""
|
||||
unexpected ref type during rendering, possible Rules of Hooks violation
|
||||
"""
|
||||
)
|
||||
return initial
|
||||
}
|
||||
return result
|
||||
} else {
|
||||
component.refs.append(initial)
|
||||
return initial
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// Ref.swift
|
||||
// Gluon
|
||||
//
|
||||
// Created by Max Desiatov on 09/02/2019.
|
||||
//
|
||||
|
||||
public final class Ref<T> {
|
||||
public var value: T
|
||||
|
||||
init(_ value: T) {
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
extension Hooks {
|
||||
public func ref<T>(type: T.Type) -> Ref<T?> {
|
||||
return ref(Ref(nil))
|
||||
}
|
||||
|
||||
public func ref<T>(_ initial: T) -> Ref<T> {
|
||||
return ref(Ref(initial))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
//
|
||||
// State.swift
|
||||
// Gluon
|
||||
//
|
||||
// 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
|
||||
component's state in-place synchronously, but only schedule an update with
|
||||
Gluon at a later time. A call to `render` is only scheduled on the component
|
||||
that obtained this state with `hooks.state`.
|
||||
*/
|
||||
public struct State<T> {
|
||||
public let value: T
|
||||
|
||||
/// A closure stored as `Handler` to enable `Equatable` implementation on
|
||||
/// `State` derived by the compiler.
|
||||
let updateHandler: Handler<Updater<T>>
|
||||
|
||||
init(_ value: T, _ updater: @escaping (Updater<T>) -> ()) {
|
||||
self.value = value
|
||||
updateHandler = Handler(updater)
|
||||
}
|
||||
|
||||
/// set the state to a specified value
|
||||
public func set(_ value: T) {
|
||||
updateHandler.value { $0 = value }
|
||||
}
|
||||
|
||||
/// update the state with a pure function
|
||||
public func set(_ transformer: @escaping (T) -> T) {
|
||||
updateHandler.value { $0 = transformer($0) }
|
||||
}
|
||||
|
||||
/// efficiently update the state in place with a mutating function
|
||||
/// (helps avoiding expensive memory allocations when state contains
|
||||
/// large arrays/dictionaries or other copy-on-write value)
|
||||
public func set(_ updater: @escaping (inout T) -> ()) {
|
||||
updateHandler.value(updater)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
render contains `initial` as a value and values passed to `count.set`
|
||||
on subsequent updates:
|
||||
*/
|
||||
public func state<T>(_ initial: T) -> State<T> {
|
||||
let (value, index) = currentState(initial)
|
||||
|
||||
let queueState = self.queueState
|
||||
return State(value as? T ?? initial) { (updater: Updater<T>) in
|
||||
queueState(index) {
|
||||
// There's no easy way to downcast elements of `[Any]` to `T`
|
||||
// and apply `inout` updater without creating copies, working around
|
||||
// that with pointers.
|
||||
withUnsafeMutablePointer(to: &$0) {
|
||||
$0.withMemoryRebound(to: T.self, capacity: 1) { updater(&$0[0]) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,7 +26,7 @@ public class MountedComponent<R: Renderer> {
|
|||
}
|
||||
|
||||
extension AnyNode {
|
||||
func makeMountedComponent<R: Renderer>(_ parentTarget: R.Target)
|
||||
func makeMountedComponent<R: Renderer>(_ parentTarget: R.TargetType)
|
||||
-> MountedComponent<R> {
|
||||
switch type {
|
||||
case let .host(type):
|
||||
|
|
|
@ -5,8 +5,10 @@
|
|||
// Created by Max Desiatov on 03/12/2018.
|
||||
//
|
||||
|
||||
import Dispatch
|
||||
|
||||
final class MountedCompositeComponent<R: Renderer>: MountedComponent<R>,
|
||||
Hashable {
|
||||
HookedComponent, Hashable {
|
||||
static func ==(lhs: MountedCompositeComponent<R>,
|
||||
rhs: MountedCompositeComponent<R>) -> Bool {
|
||||
return lhs === rhs
|
||||
|
@ -17,13 +19,18 @@ final class MountedCompositeComponent<R: Renderer>: MountedComponent<R>,
|
|||
}
|
||||
|
||||
private var mountedChildren = [MountedComponent<R>]()
|
||||
private let parentTarget: R.Target
|
||||
private let parentTarget: R.TargetType
|
||||
let type: AnyCompositeComponent.Type
|
||||
|
||||
// HookedComponent implementation
|
||||
var state = [Any]()
|
||||
var effects = [(observed: AnyEquatable?, Effect)]()
|
||||
var effectFinalizers = [Finalizer]()
|
||||
var refs = [AnyObject]()
|
||||
|
||||
init(_ node: AnyNode,
|
||||
_ type: AnyCompositeComponent.Type,
|
||||
_ parentTarget: R.Target) {
|
||||
_ parentTarget: R.TargetType) {
|
||||
self.type = type
|
||||
self.parentTarget = parentTarget
|
||||
|
||||
|
@ -41,8 +48,12 @@ final class MountedCompositeComponent<R: Renderer>: MountedComponent<R>,
|
|||
|
||||
override func unmount(with reconciler: StackReconciler<R>) {
|
||||
mountedChildren.forEach { $0.unmount(with: reconciler) }
|
||||
// FIXME: Should call `hooks.effect` finalizers here after `hooks.effect`
|
||||
// is implemented
|
||||
|
||||
DispatchQueue.main.async {
|
||||
for f in self.effectFinalizers {
|
||||
f?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func update(with reconciler: StackReconciler<R>) {
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
components by `StackReconciler`.
|
||||
*/
|
||||
public final class MountedHostComponent<R: Renderer>: MountedComponent<R> {
|
||||
private var managedChildren = [Weak<R.Target>: MountedComponent<R>]()
|
||||
private var mountedChildren = [MountedComponent<R>]()
|
||||
|
||||
/** Target of a closest ancestor host component. As a parent of this component
|
||||
|
@ -17,18 +16,18 @@ public final class MountedHostComponent<R: Renderer>: MountedComponent<R> {
|
|||
around the target of a host component to its closests descendent host
|
||||
comoponents. Thus, a parent target is not always the same as a target of
|
||||
a parent component. */
|
||||
private let parentTarget: R.Target
|
||||
private let parentTarget: R.TargetType
|
||||
|
||||
/** Target of this host component supplied by a renderer after mounting has
|
||||
completed.
|
||||
*/
|
||||
private var target: R.Target?
|
||||
private var target: R.TargetType?
|
||||
|
||||
public let type: AnyHostComponent.Type
|
||||
|
||||
init(_ node: AnyNode,
|
||||
_ type: AnyHostComponent.Type,
|
||||
_ parentTarget: R.Target) {
|
||||
_ parentTarget: R.TargetType) {
|
||||
self.type = type
|
||||
self.parentTarget = parentTarget
|
||||
|
||||
|
@ -63,29 +62,33 @@ public final class MountedHostComponent<R: Renderer>: MountedComponent<R> {
|
|||
override func unmount(with reconciler: StackReconciler<R>) {
|
||||
guard let target = target else { return }
|
||||
|
||||
reconciler.renderer?.unmount(target: target, with: self)
|
||||
reconciler.renderer?.unmount(target: target, with: self) {
|
||||
self.mountedChildren.forEach { $0.unmount(with: reconciler) }
|
||||
}
|
||||
}
|
||||
|
||||
override func update(with reconciler: StackReconciler<R>) {
|
||||
guard let target = target else { return }
|
||||
|
||||
target.node = node
|
||||
reconciler.renderer?.update(target: target,
|
||||
with: self)
|
||||
|
||||
switch node.children.value {
|
||||
case var nodes as [AnyNode]:
|
||||
switch (mountedChildren.isEmpty, nodes.isEmpty) {
|
||||
// existing children, new children array is empty, unmount all existing
|
||||
// if existing children present and new children array is empty
|
||||
// then unmount all existing children
|
||||
case (false, true):
|
||||
mountedChildren.forEach { $0.unmount(with: reconciler) }
|
||||
mountedChildren = []
|
||||
|
||||
// no existing children, mount all new
|
||||
// if no existing children then mount all new children
|
||||
case (true, false):
|
||||
mountedChildren = nodes.map { $0.makeMountedComponent(target) }
|
||||
mountedChildren.forEach { $0.mount(with: reconciler) }
|
||||
|
||||
// both arrays have items, reconcile by types and keys
|
||||
// if both arrays have items then reconcile by types and keys
|
||||
case (false, false):
|
||||
var newChildren = [MountedComponent<R>]()
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ public protocol Renderer: class {
|
|||
probably create its own type hierarchy to be able to reason about
|
||||
all possible target types available on a specific platform.
|
||||
*/
|
||||
associatedtype Target: AnyObject
|
||||
associatedtype TargetType: Target
|
||||
|
||||
/// Reconciler instance used by this renderer.
|
||||
var reconciler: StackReconciler<Self>? { get }
|
||||
|
@ -39,8 +39,10 @@ public protocol Renderer: class {
|
|||
target.
|
||||
- returns: The newly created target.
|
||||
*/
|
||||
func mountTarget(to parent: Target,
|
||||
with component: MountedHost) -> Target?
|
||||
func mountTarget(
|
||||
to parent: TargetType,
|
||||
with component: MountedHost
|
||||
) -> TargetType?
|
||||
|
||||
/** Function called by a reconciler when an existing target instance should be
|
||||
updated.
|
||||
|
@ -55,8 +57,10 @@ public protocol Renderer: class {
|
|||
children can be different from children passed on
|
||||
previous updates or on target creation.
|
||||
*/
|
||||
func update(target: Target,
|
||||
with component: MountedHost)
|
||||
func update(
|
||||
target: TargetType,
|
||||
with component: MountedHost
|
||||
)
|
||||
|
||||
/** Function called by a reconciler when an existing target instance should be
|
||||
unmounted: removed from the parent and most likely destroyed.
|
||||
|
@ -64,12 +68,15 @@ public protocol Renderer: class {
|
|||
- parameter component: Type of the host component that renders to the
|
||||
updated target.
|
||||
*/
|
||||
func unmount(target: Target,
|
||||
with component: MountedHost)
|
||||
func unmount(
|
||||
target: TargetType,
|
||||
with component: MountedHost,
|
||||
completion: @escaping () -> ()
|
||||
)
|
||||
}
|
||||
|
||||
extension Renderer {
|
||||
public func mount(with node: AnyNode, to parent: Target) -> Mounted {
|
||||
public func mount(with node: AnyNode, to parent: TargetType) -> Mounted {
|
||||
let result: Mounted = node.makeMountedComponent(parent)
|
||||
if let reconciler = reconciler {
|
||||
result.mount(with: reconciler)
|
||||
|
|
|
@ -10,12 +10,11 @@ import Dispatch
|
|||
public final class StackReconciler<R: Renderer> {
|
||||
private var queuedRerenders = Set<MountedCompositeComponent<R>>()
|
||||
|
||||
public let rootTarget: R.Target
|
||||
public let rootTarget: R.TargetType
|
||||
private let rootComponent: MountedComponent<R>
|
||||
private(set) weak var renderer: R?
|
||||
private var hooks = Hooks()
|
||||
|
||||
public init(node: AnyNode, target: R.Target, renderer: R) {
|
||||
public init(node: AnyNode, target: R.TargetType, renderer: R) {
|
||||
self.renderer = renderer
|
||||
rootTarget = target
|
||||
|
||||
|
@ -24,12 +23,12 @@ public final class StackReconciler<R: Renderer> {
|
|||
rootComponent.mount(with: self)
|
||||
}
|
||||
|
||||
func queue(state: Any,
|
||||
func queue(updater: (inout Any) -> (),
|
||||
for component: MountedCompositeComponent<R>,
|
||||
id: Int) {
|
||||
let scheduleReconcile = queuedRerenders.isEmpty
|
||||
|
||||
component.state[id] = state
|
||||
updater(&component.state[id])
|
||||
queuedRerenders.insert(component)
|
||||
|
||||
guard scheduleReconcile else { return }
|
||||
|
@ -48,26 +47,14 @@ public final class StackReconciler<R: Renderer> {
|
|||
}
|
||||
|
||||
func render(component: MountedCompositeComponent<R>) -> AnyNode {
|
||||
var stateIndex = 0
|
||||
hooks.currentState = { [weak component] in
|
||||
defer { stateIndex += 1 }
|
||||
|
||||
guard let component = component else { return ($0, stateIndex) }
|
||||
|
||||
if component.state.count > stateIndex {
|
||||
return (component.state[stateIndex], stateIndex)
|
||||
} else {
|
||||
component.state.append($0)
|
||||
return ($0, stateIndex)
|
||||
}
|
||||
}
|
||||
|
||||
// Avoiding an indirect reference cycle here: this closure can be
|
||||
// owned by callbacks owned by node's target, which is strongly referenced
|
||||
// by the reconciler.
|
||||
hooks.queueState = { [weak self, weak component] in
|
||||
let hooks = Hooks(
|
||||
component: component
|
||||
) { [weak self, weak component] id, updater in
|
||||
guard let component = component else { return }
|
||||
self?.queue(state: $0, for: component, id: $1)
|
||||
self?.queue(updater: updater, for: component, id: id)
|
||||
}
|
||||
|
||||
let result = component.type.render(
|
||||
|
@ -76,8 +63,20 @@ public final class StackReconciler<R: Renderer> {
|
|||
hooks: hooks
|
||||
)
|
||||
|
||||
hooks.currentState = nil
|
||||
hooks.queueState = nil
|
||||
DispatchQueue.main.async {
|
||||
for i in hooks.scheduledEffects {
|
||||
if component.effectFinalizers.count > i {
|
||||
component.effectFinalizers[i]?()
|
||||
component.effectFinalizers[i] = component.effects[i].1()
|
||||
} else {
|
||||
component.effectFinalizers.append(component.effects[i].1())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// clean up `component` reference to enable assertions when hooks are called
|
||||
// outside of `render`
|
||||
hooks.component = nil
|
||||
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
//
|
||||
// Store.swift
|
||||
// Gluon
|
||||
//
|
||||
// Created by Max Desiatov on 12/10/2018.
|
||||
//
|
||||
|
||||
/// Generic undo-redo manager that doesn't require rollback implementation in
|
||||
/// `apply` of a wrapped store. It saves previous snapshots of state in a stack
|
||||
/// and undo/redo actions allow traversing the state history. This might be
|
||||
/// not very efficient in terms of memory, but only requires an initial
|
||||
/// state and can recreate subsequent snapshots by calling `apply` with
|
||||
/// a sequence of actions. This means you can add undo-redo history to
|
||||
/// any `Store`.
|
||||
public struct History<S: Store>: Store {
|
||||
public enum Action {
|
||||
case branch(S.Action)
|
||||
case undo
|
||||
case redo
|
||||
}
|
||||
|
||||
private var wrapped: S
|
||||
private var history = [S.State]()
|
||||
private var historyIndex: Int
|
||||
|
||||
public var state: S.State {
|
||||
return history[historyIndex]
|
||||
}
|
||||
|
||||
public mutating func apply(action: Action) {
|
||||
switch action {
|
||||
case let .branch(wrappedAction):
|
||||
wrapped.apply(action: wrappedAction)
|
||||
history.removeSubrange(historyIndex..<history.count)
|
||||
history.append(wrapped.state)
|
||||
historyIndex = history.count - 1
|
||||
case .undo:
|
||||
guard historyIndex > 0 else { return }
|
||||
|
||||
historyIndex -= 1
|
||||
case .redo:
|
||||
guard historyIndex < history.count - 1 else { return }
|
||||
|
||||
historyIndex += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public protocol Store {
|
||||
associatedtype State
|
||||
associatedtype Action
|
||||
|
||||
var state: State { get }
|
||||
|
||||
mutating func apply(action: Action)
|
||||
}
|
||||
|
||||
// FIXME: need context working for its implementation
|
||||
// public final class StoreProvider<S>: Component<StoreProvider.Props, S>
|
||||
// where S: Store & StateType {
|
||||
// struct Props: Equatable {
|
||||
// /// FIXME: should store comparison be optimised this way?
|
||||
// let store: Unique<S>
|
||||
// }
|
||||
//
|
||||
// func dispatch(action: S.Action) {
|
||||
// setState { $0.apply(action: action) }
|
||||
// }
|
||||
//
|
||||
// public func render() -> Node {
|
||||
// // FIXME: create a context here
|
||||
// return Node(children)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//typealias Dispatch<Action> = (Action) -> ()
|
||||
//typealias Dispatcher<S: Store> =
|
||||
// (state: S.State, dispatch: Dispatch<S.Action>)
|
||||
|
||||
// FIXME: when contexts are available read state and dispatch
|
||||
// from the context and pass it to the mapper
|
||||
// func storeAccess<S: Store>(_ mapper: (Dispatcher<S>) -> Node) {
|
||||
//
|
||||
// }
|
|
@ -0,0 +1,14 @@
|
|||
//
|
||||
// Target.swift
|
||||
// Gluon
|
||||
//
|
||||
// Created by Max Desiatov on 10/02/2019.
|
||||
//
|
||||
|
||||
open class Target {
|
||||
public internal(set) var node: AnyNode
|
||||
|
||||
public init(node: AnyNode) {
|
||||
self.node = node
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
//
|
||||
// Weak.swift
|
||||
// Gluon
|
||||
//
|
||||
// Created by Max Desiatov on 27/01/2019.
|
||||
//
|
||||
|
||||
public struct Weak<T: AnyObject> {
|
||||
weak var value: T?
|
||||
|
||||
public init(value: T) {
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
extension Weak: Equatable {
|
||||
public static func ==(lhs: Weak<T>, rhs: Weak<T>) -> Bool {
|
||||
return lhs.value === rhs.value
|
||||
}
|
||||
}
|
||||
|
||||
extension Weak: Hashable {
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
guard let value = value else { return }
|
||||
|
||||
hasher.combine(ObjectIdentifier(value))
|
||||
}
|
||||
}
|
|
@ -34,13 +34,12 @@ public final class TestRenderer: Renderer {
|
|||
public func update(
|
||||
target: TestView,
|
||||
with component: TestRenderer.MountedHost
|
||||
) {
|
||||
target.node = component.node
|
||||
}
|
||||
) {}
|
||||
|
||||
public func unmount(
|
||||
target: TestView,
|
||||
with component: TestRenderer.MountedHost
|
||||
with component: TestRenderer.MountedHost,
|
||||
completion: () -> ()
|
||||
) {
|
||||
target.removeFromSuperview()
|
||||
}
|
||||
|
|
|
@ -10,13 +10,10 @@ import Gluon
|
|||
/// A class that `TestRenderer` uses as a target.
|
||||
/// When rendering to a `TestView` instance it is possible
|
||||
/// to examine its `subviews` and `props` for testing.
|
||||
public final class TestView {
|
||||
public final class TestView: Target {
|
||||
/// Subviews of this test view.
|
||||
public private(set) var subviews: [TestView]
|
||||
|
||||
/// Props assigned to this test view.
|
||||
public internal(set) var node: AnyNode
|
||||
|
||||
/// Parent `TestView` instance that owns this instance as a child
|
||||
private weak var parent: TestView?
|
||||
|
||||
|
@ -30,8 +27,8 @@ public final class TestView {
|
|||
*/
|
||||
init(_ node: AnyNode,
|
||||
_ subviews: [TestView] = []) {
|
||||
self.node = node
|
||||
self.subviews = subviews
|
||||
super.init(node: node)
|
||||
}
|
||||
|
||||
/** Add a subview to this test view.
|
||||
|
|
|
@ -104,6 +104,9 @@ final class GluonTableView: UITableView, Default {
|
|||
|
||||
final class TableViewBox<T: CellProvider>: ViewBox<GluonTableView> {
|
||||
private let dataSource: DataSource<T>
|
||||
|
||||
// this delegate stays as a constant and doesn't create a reference cycle
|
||||
// swiftlint:disable:next weak_delegate
|
||||
private let delegate: Delegate<T>
|
||||
|
||||
var props: ListView<T>.Props {
|
||||
|
|
|
@ -11,7 +11,7 @@ import UIKit
|
|||
class ViewControllerBox<T: UIViewController>: UITarget {
|
||||
let containerViewController: T
|
||||
|
||||
init(_ viewController: T, _ node: AnyNode?) {
|
||||
init(_ viewController: T, _ node: AnyNode) {
|
||||
containerViewController = viewController
|
||||
super.init(node: node)
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ extension ModalPresenter: UIHostComponent {
|
|||
// FIXME: update presentation-related props on the target here
|
||||
}
|
||||
|
||||
static func unmount(target: UITarget) {
|
||||
target.viewController.dismiss(animated: true)
|
||||
static func unmount(target: UITarget, completion: @escaping () -> ()) {
|
||||
target.viewController.dismiss(animated: true) { completion() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,18 @@ final class GluonNavigationController: UINavigationController {
|
|||
}
|
||||
}
|
||||
|
||||
// FIXME: is this reliable enough? Will this work for
|
||||
// `GluonNavigationController` without a navigation bar? Can you even create
|
||||
// one without a navigation bar?
|
||||
extension GluonNavigationController: UINavigationBarDelegate {
|
||||
func navigationBar(
|
||||
_ navigationBar: UINavigationBar,
|
||||
didPop item: UINavigationItem
|
||||
) {
|
||||
onPop()
|
||||
}
|
||||
}
|
||||
|
||||
extension NavigationController: UIHostComponent {
|
||||
static func mountTarget(to parent: UITarget,
|
||||
component: UIKitRenderer.MountedHost,
|
||||
|
@ -55,8 +67,8 @@ extension NavigationController: UIHostComponent {
|
|||
// FIXME: this `case` handler is duplicated with `UIViewComponent`,
|
||||
// should this be generalised as a protocol?
|
||||
case let box as ViewControllerBox<UIViewController>
|
||||
where parent.node?.isSubtypeOf(ModalPresenter.self) ?? false:
|
||||
guard let props = parent.node?.props.value as? ModalPresenter.Props else {
|
||||
where parent.node.isSubtypeOf(ModalPresenter.self):
|
||||
guard let props = parent.node.props.value as? ModalPresenter.Props else {
|
||||
propsAssertionFailure()
|
||||
return nil
|
||||
}
|
||||
|
@ -69,7 +81,7 @@ extension NavigationController: UIHostComponent {
|
|||
}
|
||||
case let box as ViewBox<UIView>:
|
||||
result.viewController.willMove(toParent: box.viewController)
|
||||
// FIXME: replace with constraints
|
||||
// FIXME: replace with auto layout constraints
|
||||
result.viewController.view.frame = box.view.frame
|
||||
box.view.addSubview(result.viewController.view)
|
||||
box.viewController.addChild(result.viewController)
|
||||
|
@ -84,5 +96,7 @@ extension NavigationController: UIHostComponent {
|
|||
|
||||
static func update(target: UITarget, node: AnyNode) {}
|
||||
|
||||
static func unmount(target: UITarget) {}
|
||||
static func unmount(target: UITarget, completion: () -> ()) {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ extension NavigationItem: UIHostComponent {
|
|||
}
|
||||
|
||||
guard
|
||||
let parentProps = parent.node?.props.value as? NavigationController.Props
|
||||
let parentProps = parent.node.props.value as? NavigationController.Props
|
||||
else {
|
||||
propsAssertionFailure()
|
||||
return nil
|
||||
|
@ -67,5 +67,5 @@ extension NavigationItem: UIHostComponent {
|
|||
item.largeTitleDisplayMode = .init(mode: props.titleMode)
|
||||
}
|
||||
|
||||
static func unmount(target: UITarget) {}
|
||||
static func unmount(target: UITarget, completion: () -> ()) { completion() }
|
||||
}
|
||||
|
|
|
@ -21,10 +21,9 @@ protocol UIHostComponent: AnyHostComponent {
|
|||
_ renderer: UIKitRenderer
|
||||
) -> UITarget?
|
||||
|
||||
static func update(target: UITarget,
|
||||
node: AnyNode)
|
||||
static func update(target: UITarget, node: AnyNode)
|
||||
|
||||
static func unmount(target: UITarget)
|
||||
static func unmount(target: UITarget, completion: @escaping () -> ())
|
||||
}
|
||||
|
||||
extension UIHostComponent {
|
||||
|
|
|
@ -96,11 +96,11 @@ extension UIViewComponent where Target == Target.DefaultValue,
|
|||
let target = Target.defaultValue
|
||||
let result: ViewBox<Target>
|
||||
|
||||
let parentRequiresViewController = parent.node?.isSubtypeOf(
|
||||
let parentRequiresViewController = parent.node.isSubtypeOf(
|
||||
ModalPresenter.self,
|
||||
or: NavigationController.self,
|
||||
or: AnyTabPresenter.self
|
||||
) ?? false
|
||||
)
|
||||
|
||||
// UIViewController parent target can't present a bare `ViewBox` target,
|
||||
// it needs to be wrapped with `ContainerViewController` first.
|
||||
|
@ -126,8 +126,8 @@ extension UIViewComponent where Target == Target.DefaultValue,
|
|||
case let box as ViewBox<GluonTableCell>:
|
||||
box.view.addSubview(target)
|
||||
case let box as ViewControllerBox<GluonNavigationController>
|
||||
where parent.node?.isSubtypeOf(NavigationController.self) ?? false:
|
||||
guard let props = parent.node?.props.value
|
||||
where parent.node.isSubtypeOf(NavigationController.self):
|
||||
guard let props = parent.node.props.value
|
||||
as? NavigationController.Props else {
|
||||
propsAssertionFailure()
|
||||
return nil
|
||||
|
@ -138,9 +138,9 @@ extension UIViewComponent where Target == Target.DefaultValue,
|
|||
animated: props.pushAnimated
|
||||
)
|
||||
case let box as ViewControllerBox<UIViewController>
|
||||
where parent.node?.isSubtypeOf(ModalPresenter.self) ?? false:
|
||||
where parent.node.isSubtypeOf(ModalPresenter.self):
|
||||
guard
|
||||
let props = parent.node?.props.value as? ModalPresenter.Props
|
||||
let props = parent.node.props.value as? ModalPresenter.Props
|
||||
else {
|
||||
propsAssertionFailure()
|
||||
return nil
|
||||
|
@ -150,7 +150,7 @@ extension UIViewComponent where Target == Target.DefaultValue,
|
|||
animated: props.presentAnimated,
|
||||
completion: nil)
|
||||
case let box as ViewControllerBox<UIViewController>
|
||||
where parent.node?.isSubtypeOf(NavigationItem.self) ?? false:
|
||||
where parent.node.isSubtypeOf(NavigationItem.self):
|
||||
box.viewController.view.addSubview(target)
|
||||
default:
|
||||
parentAssertionFailure()
|
||||
|
@ -182,12 +182,14 @@ extension UIViewComponent where Target == Target.DefaultValue,
|
|||
update(view: target, props, children)
|
||||
}
|
||||
|
||||
static func unmount(target: UITarget) {
|
||||
static func unmount(target: UITarget, completion: () -> ()) {
|
||||
switch target {
|
||||
case let target as ViewBox<Target>:
|
||||
target.view.removeFromSuperview()
|
||||
default:
|
||||
targetAssertionFailure()
|
||||
}
|
||||
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,13 +28,7 @@ struct HackyProvider: SimpleCellProvider {
|
|||
typealias Model = [[Int]]
|
||||
}
|
||||
|
||||
public class UITarget {
|
||||
let node: AnyNode?
|
||||
|
||||
init(node: AnyNode?) {
|
||||
self.node = node
|
||||
}
|
||||
|
||||
class UITarget: Target {
|
||||
var viewController: UIViewController {
|
||||
fatalError("viewController should be overriden in UITarget subclass")
|
||||
}
|
||||
|
@ -74,8 +68,10 @@ final class UIKitRenderer: Renderer {
|
|||
self)
|
||||
}
|
||||
|
||||
func update(target: UITarget,
|
||||
with component: UIKitRenderer.MountedHost) {
|
||||
func update(
|
||||
target: UITarget,
|
||||
with component: UIKitRenderer.MountedHost
|
||||
) {
|
||||
guard let rendererComponent = component.type as? UIHostComponent.Type else {
|
||||
typeAssertionFailure(for: component.type)
|
||||
return
|
||||
|
@ -85,13 +81,16 @@ final class UIKitRenderer: Renderer {
|
|||
node: component.node)
|
||||
}
|
||||
|
||||
func unmount(target: UITarget,
|
||||
with component: UIKitRenderer.MountedHost) {
|
||||
func unmount(
|
||||
target: UITarget,
|
||||
with component: UIKitRenderer.MountedHost,
|
||||
completion: @escaping () -> ()
|
||||
) {
|
||||
guard let rendererComponent = component.type as? UIHostComponent.Type else {
|
||||
typeAssertionFailure(for: component.type)
|
||||
return
|
||||
}
|
||||
|
||||
rendererComponent.unmount(target: target)
|
||||
rendererComponent.unmount(target: target, completion: completion)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,22 @@ import XCTest
|
|||
|
||||
@testable import Gluon
|
||||
|
||||
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)
|
||||
|
@ -22,14 +38,18 @@ struct Test: LeafComponent {
|
|||
static func render(props: Null, hooks: Hooks) -> AnyNode {
|
||||
let state1 = hooks.custom()
|
||||
let state2 = hooks.custom()
|
||||
let state3 = hooks.custom()
|
||||
|
||||
return StackView.node([
|
||||
Button.node(.init(onPress: Handler { state1.set { $0 + 1 } }),
|
||||
Button.node(.init(onPress: Handler { state1.set { $0 += 1 } }),
|
||||
"Increment"),
|
||||
Label.node("\(state1.value)"),
|
||||
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)"),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
@ -41,20 +61,33 @@ final class HooksTests: XCTestCase {
|
|||
|
||||
let stack = root.subviews[0]
|
||||
|
||||
guard let buttonProps = stack.subviews[0].props(Button.Props.self),
|
||||
let buttonHandler = buttonProps.handlers[.touchUpInside]?.value else {
|
||||
guard
|
||||
let button1Props = stack.subviews[0].props(Button.Props.self),
|
||||
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
|
||||
else {
|
||||
XCTAssert(false, "components have no handlers")
|
||||
return
|
||||
}
|
||||
|
||||
buttonHandler(())
|
||||
button1Handler(())
|
||||
|
||||
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("42"))
|
||||
XCTAssertEqual(stack.subviews[3].node.children, AnyEquatable("44"))
|
||||
XCTAssertEqual(stack.subviews[5].node.children, AnyEquatable("45"))
|
||||
|
||||
e.fulfill()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue