From 1a1d23e004c7c4a3258f0a339e55a23ece46341d Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 27 Jul 2020 23:55:54 +0100 Subject: [PATCH 01/13] Unify code of `MountedApp`/`MountedCompositeView` --- Sources/TokamakCore/App/_AnyApp.swift | 4 +- Sources/TokamakCore/App/_AnyScene.swift | 4 +- .../TokamakCore/MountedViews/MountedApp.swift | 54 ++++--------------- .../MountedCompositeElement.swift | 14 ++--- .../MountedViews/MountedCompositeView.swift | 48 ++++------------- .../MountedViews/MountedElement.swift | 8 +-- Sources/TokamakCore/StackReconciler.swift | 44 +++++++++++++++ Sources/TokamakDOM/DOMRenderer.swift | 6 +-- 8 files changed, 83 insertions(+), 99 deletions(-) diff --git a/Sources/TokamakCore/App/_AnyApp.swift b/Sources/TokamakCore/App/_AnyApp.swift index 640c9cb86..24dc7a66a 100644 --- a/Sources/TokamakCore/App/_AnyApp.swift +++ b/Sources/TokamakCore/App/_AnyApp.swift @@ -19,13 +19,13 @@ import OpenCombine public struct _AnyApp: App { var app: Any - let appType: Any.Type + let type: Any.Type let bodyClosure: (Any) -> _AnyScene let bodyType: Any.Type init(_ app: A) { self.app = app - appType = A.self + type = A.self // swiftlint:disable:next force_cast bodyClosure = { _AnyScene(($0 as! A).body) } bodyType = A.Body.self diff --git a/Sources/TokamakCore/App/_AnyScene.swift b/Sources/TokamakCore/App/_AnyScene.swift index 41ad7e58d..40657c789 100644 --- a/Sources/TokamakCore/App/_AnyScene.swift +++ b/Sources/TokamakCore/App/_AnyScene.swift @@ -17,11 +17,11 @@ public struct _AnyScene: Scene { let scene: Any - let sceneType: Any.Type + let type: Any.Type init(_ scene: S) { self.scene = scene - sceneType = S.self + type = S.self } public var body: Never { diff --git a/Sources/TokamakCore/MountedViews/MountedApp.swift b/Sources/TokamakCore/MountedViews/MountedApp.swift index 0525a9c6e..314882b67 100644 --- a/Sources/TokamakCore/MountedViews/MountedApp.swift +++ b/Sources/TokamakCore/MountedViews/MountedApp.swift @@ -34,57 +34,25 @@ final class MountedApp: MountedCompositeElement { mountedChildren.forEach { $0.unmount(with: reconciler) } } - func mountChild(_ childBody: S) -> MountedElement { + private func mountChild(_ childBody: S) -> MountedElement { let mountedScene: MountedScene = childBody.makeMountedView(parentTarget, environmentValues) if let title = mountedScene.title { // swiftlint:disable force_cast - (app.appType as! _TitledApp.Type)._setTitle(title) + (app.type as! _TitledApp.Type)._setTitle(title) } return mountedScene.body } override func update(with reconciler: StackReconciler) { - // FIXME: for now without properly handling `Group` mounted composite views have only - // a single element in `mountedChildren`, but this will change when - // fragments are implemented and this switch should be rewritten to compare - // all elements in `mountedChildren` - - // swiftlint:disable:next force_try - let appInfo = try! typeInfo(of: app.appType) - appInfo.injectEnvironment(from: environmentValues, into: &app.app) - - switch (mountedChildren.last, reconciler.render(mountedApp: self)) { - // no mounted children, but children available now - case let (nil, childBody): - let child: MountedElement = mountChild(childBody) - mountedChildren = [child] - child.mount(with: reconciler) - - // some mounted children - case let (wrapper?, childBody): - let childBodyType = (childBody as? AnyView)?.type ?? type(of: childBody) - - // FIXME: no idea if using `mangledName` is reliable, but seems to be the only way to get - // a name of a type constructor in runtime. Should definitely check if these are different - // across modules, otherwise can cause problems with views with same names in different - // modules. - - // new child has the same type as existing child - // swiftlint:disable:next force_try - if try! wrapper.view.typeConstructorName == typeInfo(of: childBodyType).mangledName { - wrapper.scene = _AnyScene(childBody) - wrapper.update(with: reconciler) - } else { - // new child is of a different type, complete rerender, i.e. unmount the old - // wrapper, then mount a new one with the new `childBody` - wrapper.unmount(with: reconciler) - - let child: MountedElement = mountChild(childBody) - mountedChildren = [child] - child.mount(with: reconciler) - } - } + let element = reconciler.render(mountedApp: self) + reconciler.reconcile( + self, + with: element, + getElementType: { ($0 as? _AnyScene)?.type ?? type(of: $0) }, + updateChild: { $0.scene = _AnyScene(element) }, + mountChild: { mountChild($0) } + ) } } @@ -97,7 +65,7 @@ extension App { let any = (injectableApp as? _AnyApp) ?? _AnyApp(injectableApp) // swiftlint:disable force_try - let appInfo = try! typeInfo(of: any.appType) + let appInfo = try! typeInfo(of: any.type) var extractedApp = any.app appInfo.injectEnvironment(from: environmentValues, into: &extractedApp) diff --git a/Sources/TokamakCore/MountedViews/MountedCompositeElement.swift b/Sources/TokamakCore/MountedViews/MountedCompositeElement.swift index 78bd2ec1b..22d68f46f 100644 --- a/Sources/TokamakCore/MountedViews/MountedCompositeElement.swift +++ b/Sources/TokamakCore/MountedViews/MountedCompositeElement.swift @@ -34,17 +34,19 @@ class MountedCompositeElement: MountedElement, Hashable { var subscriptions = [AnyCancellable]() var environmentValues: EnvironmentValues - init(_ app: _AnyApp, - _ parentTarget: R.TargetType, - _ environmentValues: EnvironmentValues) { + init(_ app: _AnyApp, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) { self.parentTarget = parentTarget self.environmentValues = environmentValues super.init(app) } - init(_ view: AnyView, - _ parentTarget: R.TargetType, - _ environmentValues: EnvironmentValues) { + init(_ scene: _AnyScene, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) { + self.parentTarget = parentTarget + self.environmentValues = environmentValues + super.init(scene) + } + + init(_ view: AnyView, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) { self.parentTarget = parentTarget self.environmentValues = environmentValues super.init(view) diff --git a/Sources/TokamakCore/MountedViews/MountedCompositeView.swift b/Sources/TokamakCore/MountedViews/MountedCompositeView.swift index b4c71b443..a6a2d25d7 100644 --- a/Sources/TokamakCore/MountedViews/MountedCompositeView.swift +++ b/Sources/TokamakCore/MountedViews/MountedCompositeView.swift @@ -26,8 +26,7 @@ final class MountedCompositeView: MountedCompositeElement { appearanceAction.appear?() } - let child: MountedElement = childBody.makeMountedView(parentTarget, - environmentValues) + let child: MountedElement = childBody.makeMountedView(parentTarget, environmentValues) mountedChildren = [child] child.mount(with: reconciler) } @@ -41,42 +40,13 @@ final class MountedCompositeView: MountedCompositeElement { } override func update(with reconciler: StackReconciler) { - // FIXME: for now without properly handling `Group` mounted composite views have only - // a single element in `mountedChildren`, but this will change when - // fragments are implemented and this switch should be rewritten to compare - // all elements in `mountedChildren` - switch (mountedChildren.last, reconciler.render(compositeView: self)) { - // no mounted children, but children available now - case let (nil, childBody): - let child: MountedElement = childBody.makeMountedView(parentTarget, - environmentValues) - mountedChildren = [child] - child.mount(with: reconciler) - - // some mounted children - case let (wrapper?, childBody): - let childBodyType = (childBody as? AnyView)?.type ?? type(of: childBody) - - // FIXME: no idea if using `mangledName` is reliable, but seems to be the only way to get - // a name of a type constructor in runtime. Should definitely check if these are different - // across modules, otherwise can cause problems with views with same names in different - // modules. - - // new child has the same type as existing child - // swiftlint:disable:next force_try - if try! wrapper.view.typeConstructorName == typeInfo(of: childBodyType).mangledName { - wrapper.view = AnyView(childBody) - wrapper.update(with: reconciler) - } else { - // new child is of a different type, complete rerender, i.e. unmount the old - // wrapper, then mount a new one with the new `childBody` - wrapper.unmount(with: reconciler) - - let child: MountedElement = childBody.makeMountedView(parentTarget, - environmentValues) - mountedChildren = [child] - child.mount(with: reconciler) - } - } + let element = reconciler.render(compositeView: self) + reconciler.reconcile( + self, + with: element, + getElementType: { ($0 as? AnyView)?.type ?? type(of: $0) }, + updateChild: { $0.view = AnyView(element) }, + mountChild: { $0.makeMountedView(parentTarget, environmentValues) } + ) } } diff --git a/Sources/TokamakCore/MountedViews/MountedElement.swift b/Sources/TokamakCore/MountedViews/MountedElement.swift index 6ef1d8819..167af4710 100644 --- a/Sources/TokamakCore/MountedViews/MountedElement.swift +++ b/Sources/TokamakCore/MountedViews/MountedElement.swift @@ -25,7 +25,7 @@ enum MountedElementKind { } public class MountedElement { - var element: MountedElementKind + private var element: MountedElementKind public internal(set) var app: _AnyApp { get { @@ -67,8 +67,8 @@ public class MountedElement { var elementType: Any.Type { switch element { - case let .app(app): return app.appType - case let .scene(scene): return scene.sceneType + case let .app(app): return app.type + case let .scene(scene): return scene.type case let .view(view): return view.type } } @@ -188,7 +188,7 @@ extension Scene { } else if let groupSelf = anySelf.scene as? GroupScene { return groupSelf.children[0].makeMountedView(parentTarget, environmentValues) } else { - fatalError("Unsupported `Scene` type `\(anySelf.sceneType)`. Please file a bug report.") + fatalError("Unsupported `Scene` type `\(anySelf.type)`. Please file a bug report.") } } } diff --git a/Sources/TokamakCore/StackReconciler.swift b/Sources/TokamakCore/StackReconciler.swift index 51b022d32..d4f2d0e89 100644 --- a/Sources/TokamakCore/StackReconciler.swift +++ b/Sources/TokamakCore/StackReconciler.swift @@ -171,4 +171,48 @@ public final class StackReconciler { func render(mountedApp: MountedApp) -> some Scene { render(compositeElement: mountedApp, body: \.app.app, result: \.app.bodyClosure) } + + func reconcile( + _ mountedElement: MountedCompositeElement, + with element: E, + getElementType: (E) -> Any.Type, + updateChild: (MountedElement) -> (), + mountChild: (E) -> MountedElement + ) { + // FIXME: for now without properly handling `Group` and `TupleView` mounted composite views + // have only a single element in `mountedChildren`, but this will change when + // fragments are implemented and this switch should be rewritten to compare + // all elements in `mountedChildren` + switch (mountedElement.mountedChildren.last, element) { + // no mounted children previously, but children available now + case let (nil, childBody): + let child: MountedElement = mountChild(childBody) + mountedElement.mountedChildren = [child] + child.mount(with: self) + + // some mounted children before and now + case let (mountedChild?, childBody): + let childBodyType = getElementType(childBody) + + // FIXME: no idea if using `mangledName` is reliable, but seems to be the only way to get + // a name of a type constructor in runtime. Should definitely check if these are different + // across modules, otherwise can cause problems with views with same names in different + // modules. + + // new child has the same type as existing child + // swiftlint:disable:next force_try + if try! mountedChild.view.typeConstructorName == typeInfo(of: childBodyType).mangledName { + updateChild(mountedChild) + mountedChild.update(with: self) + } else { + // new child is of a different type, complete rerender, i.e. unmount the old + // wrapper, then mount a new one with the new `childBody` + mountedChild.unmount(with: self) + + let newMountedChild: MountedElement = mountChild(childBody) + mountedElement.mountedChildren = [newMountedChild] + newMountedChild.mount(with: self) + } + } + } } diff --git a/Sources/TokamakDOM/DOMRenderer.swift b/Sources/TokamakDOM/DOMRenderer.swift index 31a33ae12..e9f5ffe9c 100644 --- a/Sources/TokamakDOM/DOMRenderer.swift +++ b/Sources/TokamakDOM/DOMRenderer.swift @@ -30,10 +30,10 @@ extension EnvironmentValues { } } -/** `SpacerContainer` is part of TokamakDOM, as not all renderers will handle flexible - sizing the way browsers do. Their parent element could already know that if a child is +/** `SpacerContainer` is part of TokamakDOM, as not all renderers will handle flexible + sizing the way browsers do. Their parent element could already know that if a child is requesting full width, then it needs to expand. -*/ + */ private extension AnyView { var axes: [SpacerContainerAxis] { var axes = [SpacerContainerAxis]() From 4ad8859a80c8d55a3e4780de1854b9359c87fac3 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 28 Jul 2020 00:02:05 +0100 Subject: [PATCH 02/13] Make generic type name more verbose --- Sources/TokamakCore/StackReconciler.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/TokamakCore/StackReconciler.swift b/Sources/TokamakCore/StackReconciler.swift index d4f2d0e89..cd65fd3fc 100644 --- a/Sources/TokamakCore/StackReconciler.swift +++ b/Sources/TokamakCore/StackReconciler.swift @@ -172,12 +172,12 @@ public final class StackReconciler { render(compositeElement: mountedApp, body: \.app.app, result: \.app.bodyClosure) } - func reconcile( + func reconcile( _ mountedElement: MountedCompositeElement, - with element: E, - getElementType: (E) -> Any.Type, + with element: Element, + getElementType: (Element) -> Any.Type, updateChild: (MountedElement) -> (), - mountChild: (E) -> MountedElement + mountChild: (Element) -> MountedElement ) { // FIXME: for now without properly handling `Group` and `TupleView` mounted composite views // have only a single element in `mountedChildren`, but this will change when From 42e68cb72daa879199cbd80c9c0d77192eec4d43 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 28 Jul 2020 00:36:36 +0100 Subject: [PATCH 03/13] MountedScene work in progress --- Sources/TokamakCore/App/_AnyScene.swift | 5 ++ .../TokamakCore/MountedViews/MountedApp.swift | 3 +- .../MountedViews/MountedElement.swift | 25 ------ .../MountedViews/MountedScene.swift | 81 +++++++++++++++++++ Sources/TokamakCore/StackReconciler.swift | 4 + 5 files changed, 91 insertions(+), 27 deletions(-) create mode 100644 Sources/TokamakCore/MountedViews/MountedScene.swift diff --git a/Sources/TokamakCore/App/_AnyScene.swift b/Sources/TokamakCore/App/_AnyScene.swift index 40657c789..ec0bc3bd2 100644 --- a/Sources/TokamakCore/App/_AnyScene.swift +++ b/Sources/TokamakCore/App/_AnyScene.swift @@ -18,10 +18,15 @@ public struct _AnyScene: Scene { let scene: Any let type: Any.Type + let bodyClosure: (Any) -> _AnyScene + let bodyType: Any.Type init(_ scene: S) { self.scene = scene type = S.self + // swiftlint:disable:next force_cast + bodyClosure = { _AnyScene(($0 as! S).body) } + bodyType = S.Body.self } public var body: Never { diff --git a/Sources/TokamakCore/MountedViews/MountedApp.swift b/Sources/TokamakCore/MountedViews/MountedApp.swift index 314882b67..5210adcd5 100644 --- a/Sources/TokamakCore/MountedViews/MountedApp.swift +++ b/Sources/TokamakCore/MountedViews/MountedApp.swift @@ -35,8 +35,7 @@ final class MountedApp: MountedCompositeElement { } private func mountChild(_ childBody: S) -> MountedElement { - let mountedScene: MountedScene = childBody.makeMountedView(parentTarget, - environmentValues) + let mountedScene: MountedScene = childBody.makeMountedScene(parentTarget, environmentValues) if let title = mountedScene.title { // swiftlint:disable force_cast (app.type as! _TitledApp.Type)._setTitle(title) diff --git a/Sources/TokamakCore/MountedViews/MountedElement.swift b/Sources/TokamakCore/MountedViews/MountedElement.swift index 167af4710..dee03382b 100644 --- a/Sources/TokamakCore/MountedViews/MountedElement.swift +++ b/Sources/TokamakCore/MountedViews/MountedElement.swift @@ -167,28 +167,3 @@ extension View { } } } - -typealias MountedScene = (body: MountedElement, title: String?) - -extension Scene { - func makeMountedView( - _ parentTarget: R.TargetType, - _ environmentValues: EnvironmentValues - ) -> MountedScene { - let anySelf = (self as? _AnyScene) ?? _AnyScene(self) - var title: String? - if let titledSelf = anySelf.scene as? TitledScene, - let text = titledSelf.title { - title = _TextProxy(text).rawText - } - if let viewSelf = anySelf.scene as? ViewContainingScene { - return (body: viewSelf.anyContent.makeMountedView(parentTarget, environmentValues), title) - } else if let deferredSelf = anySelf.scene as? SceneDeferredToRenderer { - return (deferredSelf.deferredBody.makeMountedView(parentTarget, environmentValues), title) - } else if let groupSelf = anySelf.scene as? GroupScene { - return groupSelf.children[0].makeMountedView(parentTarget, environmentValues) - } else { - fatalError("Unsupported `Scene` type `\(anySelf.type)`. Please file a bug report.") - } - } -} diff --git a/Sources/TokamakCore/MountedViews/MountedScene.swift b/Sources/TokamakCore/MountedViews/MountedScene.swift new file mode 100644 index 000000000..c0d0134fc --- /dev/null +++ b/Sources/TokamakCore/MountedViews/MountedScene.swift @@ -0,0 +1,81 @@ +// Copyright 2020 Tokamak contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +final class MountedScene: MountedCompositeElement { + let title: String? + + init( + _ scene: _AnyScene, + _ title: String?, + _ children: [MountedElement], + _ parentTarget: R.TargetType, + _ environmentValues: EnvironmentValues + ) { + self.title = title + super.init(scene, parentTarget, environmentValues) + mountedChildren = children + } + + override func mount(with reconciler: StackReconciler) { + let childBody = reconciler.render(mountedScene: self) + + let child: MountedElement = childBody.makeMountedScene(parentTarget, environmentValues) + mountedChildren = [child] + child.mount(with: reconciler) + } + + override func unmount(with reconciler: StackReconciler) { + mountedChildren.forEach { $0.unmount(with: reconciler) } + } + + override func update(with reconciler: StackReconciler) { + let element = reconciler.render(mountedScene: self) + reconciler.reconcile( + self, + with: element, + getElementType: { ($0 as? _AnyScene)?.type ?? type(of: $0) }, + updateChild: { $0.scene = _AnyScene(element) }, + mountChild: { $0.makeMountedScene(parentTarget, environmentValues) } + ) + } +} + +extension Scene { + func makeMountedScene( + _ parentTarget: R.TargetType, + _ environmentValues: EnvironmentValues + ) -> MountedScene { + let anySelf = (self as? _AnyScene) ?? _AnyScene(self) + var title: String? + if let titledSelf = anySelf.scene as? TitledScene, + let text = titledSelf.title { + title = _TextProxy(text).rawText + } + let children: [MountedElement] + if let viewSelf = anySelf.scene as? ViewContainingScene { + children = [viewSelf.anyContent.makeMountedView(parentTarget, environmentValues)] + } else if let deferredSelf = anySelf.scene as? SceneDeferredToRenderer { + children = [deferredSelf.deferredBody.makeMountedView(parentTarget, environmentValues)] + } else if let groupSelf = anySelf.scene as? GroupScene { + children = groupSelf.children.map { $0.makeMountedScene(parentTarget, environmentValues) } + } else { + fatalError(""" + Unsupported `Scene` type `\(anySelf.type)`. Please file a bug report at \ + https://github.com/swiftwasm/Tokamak/issues/new + """) + } + + return .init(anySelf, title, children, parentTarget, environmentValues) + } +} diff --git a/Sources/TokamakCore/StackReconciler.swift b/Sources/TokamakCore/StackReconciler.swift index cd65fd3fc..5818ea6a4 100644 --- a/Sources/TokamakCore/StackReconciler.swift +++ b/Sources/TokamakCore/StackReconciler.swift @@ -172,6 +172,10 @@ public final class StackReconciler { render(compositeElement: mountedApp, body: \.app.app, result: \.app.bodyClosure) } + func render(mountedScene: MountedScene) -> some Scene { + render(compositeElement: mountedScene, body: \.scene.scene, result: \.scene.bodyClosure) + } + func reconcile( _ mountedElement: MountedCompositeElement, with element: Element, From 190f1657ede6aa98fa6a95b101f7ef6efb2cc61c Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Tue, 28 Jul 2020 15:30:00 +0100 Subject: [PATCH 04/13] Fix build issues --- Sources/TokamakCore/App/_AnyScene.swift | 2 +- Sources/TokamakCore/MountedViews/MountedApp.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/TokamakCore/App/_AnyScene.swift b/Sources/TokamakCore/App/_AnyScene.swift index ec0bc3bd2..dad77adfc 100644 --- a/Sources/TokamakCore/App/_AnyScene.swift +++ b/Sources/TokamakCore/App/_AnyScene.swift @@ -16,7 +16,7 @@ // public struct _AnyScene: Scene { - let scene: Any + var scene: Any let type: Any.Type let bodyClosure: (Any) -> _AnyScene let bodyType: Any.Type diff --git a/Sources/TokamakCore/MountedViews/MountedApp.swift b/Sources/TokamakCore/MountedViews/MountedApp.swift index 5210adcd5..49bfcad56 100644 --- a/Sources/TokamakCore/MountedViews/MountedApp.swift +++ b/Sources/TokamakCore/MountedViews/MountedApp.swift @@ -40,7 +40,7 @@ final class MountedApp: MountedCompositeElement { // swiftlint:disable force_cast (app.type as! _TitledApp.Type)._setTitle(title) } - return mountedScene.body + return mountedScene } override func update(with reconciler: StackReconciler) { From 2099d902dc86d2a0b843f568a66b919dfda81f80 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 29 Jul 2020 10:45:52 +0100 Subject: [PATCH 05/13] Refine _AnyScene code and AnyView docs --- Sources/TokamakCore/App/_AnyScene.swift | 18 +++++++++++++++--- Sources/TokamakCore/Views/AnyView.swift | 18 ++++++++++++++++-- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/Sources/TokamakCore/App/_AnyScene.swift b/Sources/TokamakCore/App/_AnyScene.swift index dad77adfc..fa54cf4fb 100644 --- a/Sources/TokamakCore/App/_AnyScene.swift +++ b/Sources/TokamakCore/App/_AnyScene.swift @@ -16,17 +16,29 @@ // public struct _AnyScene: Scene { + enum BodyResult { + case scene(_AnyScene) + case view(AnyView) + } + var scene: Any + + /// The type of the underlying `scene` let type: Any.Type - let bodyClosure: (Any) -> _AnyScene + let bodyClosure: (Any) -> BodyResult let bodyType: Any.Type init(_ scene: S) { self.scene = scene type = S.self - // swiftlint:disable:next force_cast - bodyClosure = { _AnyScene(($0 as! S).body) } bodyType = S.Body.self + if scene is SceneDeferredToRenderer { + // swiftlint:disable:next force_cast + bodyClosure = { .view(($0 as! SceneDeferredToRenderer).deferredBody) } + } else { + // swiftlint:disable:next force_cast + bodyClosure = { .scene(_AnyScene(($0 as! S).body)) } + } } public var body: Never { diff --git a/Sources/TokamakCore/Views/AnyView.swift b/Sources/TokamakCore/Views/AnyView.swift index f009f929c..8f9995b32 100644 --- a/Sources/TokamakCore/Views/AnyView.swift +++ b/Sources/TokamakCore/Views/AnyView.swift @@ -19,13 +19,27 @@ import Runtime /// A type-erased view. public struct AnyView: View { + /// The type of the underlying `view`. let type: Any.Type + + /** The name of the unapplied generic type of the underlying view. `Button` and + `Button` types are different, but when reconciling the tree of mounted views + they are treated the same, thus the `Button` part of the type (the type constructor) + is stored in this property. + */ let typeConstructorName: String + + /** The type of the `body` of the underlying `view`. Used to cast the result of the applied + `bodyClosure` property. + */ let bodyType: Any.Type + + /// The actual `View` value wrapped within this `AnyView`. var view: Any - // needs to take a fresh version of `view` as an argument, - // otherwise it captures the old view value + /** Type-erased `body` of the underlying `view`. Needs to take a fresh version of `view` as an + argument, otherwise it captures the old view value. + */ let bodyClosure: (Any) -> AnyView public init(_ view: V) where V: View { From 74e935569a825e0c35c8c64e8a45224c485205a6 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 29 Jul 2020 16:17:52 +0100 Subject: [PATCH 06/13] Fix scenes, improve documentation --- Sources/TokamakCore/App/Scenes/Scene.swift | 4 -- .../TokamakCore/App/Scenes/WindowGroup.swift | 2 +- Sources/TokamakCore/App/_AnyScene.swift | 48 ++++++++++++++--- .../TokamakCore/MountedViews/MountedApp.swift | 4 +- .../MountedViews/MountedCompositeView.swift | 2 +- .../MountedViews/MountedElement.swift | 12 +++++ .../MountedViews/MountedScene.swift | 53 ++++++++++++++----- Sources/TokamakCore/StackReconciler.swift | 45 ++++++++++++++-- Sources/TokamakCore/Views/AnyView.swift | 20 +++---- 9 files changed, 145 insertions(+), 45 deletions(-) diff --git a/Sources/TokamakCore/App/Scenes/Scene.swift b/Sources/TokamakCore/App/Scenes/Scene.swift index 01e84d248..585fc30c4 100644 --- a/Sources/TokamakCore/App/Scenes/Scene.swift +++ b/Sources/TokamakCore/App/Scenes/Scene.swift @@ -27,10 +27,6 @@ protocol TitledScene { var title: Text? { get } } -protocol ViewContainingScene { - var anyContent: AnyView { get } -} - protocol ParentScene { var children: [_AnyScene] { get } } diff --git a/Sources/TokamakCore/App/Scenes/WindowGroup.swift b/Sources/TokamakCore/App/Scenes/WindowGroup.swift index e36ebd678..04c325d26 100644 --- a/Sources/TokamakCore/App/Scenes/WindowGroup.swift +++ b/Sources/TokamakCore/App/Scenes/WindowGroup.swift @@ -15,7 +15,7 @@ // Created by Carson Katri on 7/16/20. // -public struct WindowGroup: Scene, TitledScene, ViewContainingScene where Content: View { +public struct WindowGroup: Scene, TitledScene where Content: View { public let id: String public let title: Text? public let content: Content diff --git a/Sources/TokamakCore/App/_AnyScene.swift b/Sources/TokamakCore/App/_AnyScene.swift index fa54cf4fb..f5bbb5793 100644 --- a/Sources/TokamakCore/App/_AnyScene.swift +++ b/Sources/TokamakCore/App/_AnyScene.swift @@ -15,29 +15,61 @@ // Created by Carson Katri on 7/19/20. // +import Runtime + public struct _AnyScene: Scene { + /** The result type of `bodyClosure` allowing to disambiguate between scenes that + produce other scenes or scenes that only produce containing views. + */ enum BodyResult { case scene(_AnyScene) case view(AnyView) } + /** The name of the unapplied generic type of the underlying `view`. `Button` and + `Button` types are different, but when reconciling the tree of mounted views + they are treated the same, thus the `Button` part of the type (the type constructor) + is stored in this property. + */ + let typeConstructorName: String + + /// The actual `Scene` value wrapped within this `_AnyScene`. var scene: Any /// The type of the underlying `scene` let type: Any.Type + + /** Type-erased `body` of the underlying `scene`. Needs to take a fresh version of `scene` as an + argument, otherwise it captures an old value of the `body` property. + */ let bodyClosure: (Any) -> BodyResult + + /** The type of the `body` of the underlying `scene`. Used to cast the result of the applied + `bodyClosure` property. + */ let bodyType: Any.Type init(_ scene: S) { - self.scene = scene - type = S.self - bodyType = S.Body.self - if scene is SceneDeferredToRenderer { - // swiftlint:disable:next force_cast - bodyClosure = { .view(($0 as! SceneDeferredToRenderer).deferredBody) } + if let anyScene = scene as? _AnyScene { + self = anyScene } else { - // swiftlint:disable:next force_cast - bodyClosure = { .scene(_AnyScene(($0 as! S).body)) } + self.scene = scene + type = S.self + bodyType = S.Body.self + if scene is SceneDeferredToRenderer { + // swiftlint:disable:next force_cast + bodyClosure = { .view(($0 as! SceneDeferredToRenderer).deferredBody) } + } else { + // swiftlint:disable:next force_cast + bodyClosure = { .scene(_AnyScene(($0 as! S).body)) } + } + // FIXME: no idea if using `mangledName` is reliable, but seems to be the only way to get + // a name of a type constructor in runtime. Should definitely check if these are different + // across modules, otherwise can cause problems with scenes with same names in different + // modules. + + // swiftlint:disable:next force_try + typeConstructorName = try! typeInfo(of: type).mangledName } } diff --git a/Sources/TokamakCore/MountedViews/MountedApp.swift b/Sources/TokamakCore/MountedViews/MountedApp.swift index 49bfcad56..b77d14056 100644 --- a/Sources/TokamakCore/MountedViews/MountedApp.swift +++ b/Sources/TokamakCore/MountedViews/MountedApp.swift @@ -34,7 +34,7 @@ final class MountedApp: MountedCompositeElement { mountedChildren.forEach { $0.unmount(with: reconciler) } } - private func mountChild(_ childBody: S) -> MountedElement { + private func mountChild(_ childBody: _AnyScene) -> MountedElement { let mountedScene: MountedScene = childBody.makeMountedScene(parentTarget, environmentValues) if let title = mountedScene.title { // swiftlint:disable force_cast @@ -48,7 +48,7 @@ final class MountedApp: MountedCompositeElement { reconciler.reconcile( self, with: element, - getElementType: { ($0 as? _AnyScene)?.type ?? type(of: $0) }, + getElementType: { $0.type }, updateChild: { $0.scene = _AnyScene(element) }, mountChild: { mountChild($0) } ) diff --git a/Sources/TokamakCore/MountedViews/MountedCompositeView.swift b/Sources/TokamakCore/MountedViews/MountedCompositeView.swift index a6a2d25d7..d477097a1 100644 --- a/Sources/TokamakCore/MountedViews/MountedCompositeView.swift +++ b/Sources/TokamakCore/MountedViews/MountedCompositeView.swift @@ -44,7 +44,7 @@ final class MountedCompositeView: MountedCompositeElement { reconciler.reconcile( self, with: element, - getElementType: { ($0 as? AnyView)?.type ?? type(of: $0) }, + getElementType: { $0.type }, updateChild: { $0.view = AnyView(element) }, mountChild: { $0.makeMountedView(parentTarget, environmentValues) } ) diff --git a/Sources/TokamakCore/MountedViews/MountedElement.swift b/Sources/TokamakCore/MountedViews/MountedElement.swift index dee03382b..8910af708 100644 --- a/Sources/TokamakCore/MountedViews/MountedElement.swift +++ b/Sources/TokamakCore/MountedViews/MountedElement.swift @@ -73,6 +73,18 @@ public class MountedElement { } } + var typeConstructorName: String { + switch element { + case .app: fatalError(""" + `App` values aren't supposed to be reconciled, thus the type constructor name is not stored \ + for `App` elements. Please report this crash as a bug at \ + https://github.com/swiftwasm/Tokamak/issues/new + """) + case let .scene(scene): return scene.typeConstructorName + case let .view(view): return view.typeConstructorName + } + } + init(_ app: _AnyApp) { element = .app(app) } diff --git a/Sources/TokamakCore/MountedViews/MountedScene.swift b/Sources/TokamakCore/MountedViews/MountedScene.swift index c0d0134fc..d91e93205 100644 --- a/Sources/TokamakCore/MountedViews/MountedScene.swift +++ b/Sources/TokamakCore/MountedViews/MountedScene.swift @@ -30,7 +30,7 @@ final class MountedScene: MountedCompositeElement { override func mount(with reconciler: StackReconciler) { let childBody = reconciler.render(mountedScene: self) - let child: MountedElement = childBody.makeMountedScene(parentTarget, environmentValues) + let child: MountedElement = childBody.makeMountedElement(parentTarget, environmentValues) mountedChildren = [child] child.mount(with: reconciler) } @@ -44,38 +44,65 @@ final class MountedScene: MountedCompositeElement { reconciler.reconcile( self, with: element, - getElementType: { ($0 as? _AnyScene)?.type ?? type(of: $0) }, - updateChild: { $0.scene = _AnyScene(element) }, - mountChild: { $0.makeMountedScene(parentTarget, environmentValues) } + getElementType: { $0.type }, + updateChild: { + switch element { + case let .scene(scene): + $0.scene = _AnyScene(scene) + case let .view(view): + $0.view = AnyView(view) + } + }, + mountChild: { $0.makeMountedElement(parentTarget, environmentValues) } ) } } -extension Scene { +extension _AnyScene.BodyResult { + var type: Any.Type { + switch self { + case let .scene(scene): + return scene.type + case let .view(view): + return view.type + } + } + + func makeMountedElement( + _ parentTarget: R.TargetType, + _ environmentValues: EnvironmentValues + ) -> MountedElement { + switch self { + case let .scene(scene): + return scene.makeMountedScene(parentTarget, environmentValues) + case let .view(view): + return view.makeMountedView(parentTarget, environmentValues) + } + } +} + +extension _AnyScene { func makeMountedScene( _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues ) -> MountedScene { - let anySelf = (self as? _AnyScene) ?? _AnyScene(self) var title: String? - if let titledSelf = anySelf.scene as? TitledScene, + if let titledSelf = scene as? TitledScene, let text = titledSelf.title { title = _TextProxy(text).rawText } let children: [MountedElement] - if let viewSelf = anySelf.scene as? ViewContainingScene { - children = [viewSelf.anyContent.makeMountedView(parentTarget, environmentValues)] - } else if let deferredSelf = anySelf.scene as? SceneDeferredToRenderer { + if let deferredSelf = scene as? SceneDeferredToRenderer { children = [deferredSelf.deferredBody.makeMountedView(parentTarget, environmentValues)] - } else if let groupSelf = anySelf.scene as? GroupScene { + } else if let groupSelf = scene as? GroupScene { children = groupSelf.children.map { $0.makeMountedScene(parentTarget, environmentValues) } } else { fatalError(""" - Unsupported `Scene` type `\(anySelf.type)`. Please file a bug report at \ + Unsupported `Scene` type `\(type)`. Please file a bug report at \ https://github.com/swiftwasm/Tokamak/issues/new """) } - return .init(anySelf, title, children, parentTarget, environmentValues) + return .init(self, title, children, parentTarget, environmentValues) } } diff --git a/Sources/TokamakCore/StackReconciler.swift b/Sources/TokamakCore/StackReconciler.swift index 5818ea6a4..0db5e7f67 100644 --- a/Sources/TokamakCore/StackReconciler.swift +++ b/Sources/TokamakCore/StackReconciler.swift @@ -18,12 +18,49 @@ import OpenCombine import Runtime +/** A class that reconciles a "raw" tree of element values (such as `App`, `Scene` and `View`, + all coming from `body` or `deferredBody` properties) with a tree of mounted element instances + ('MountedApp', `MountedScene`, `MountedCompositeView` and `MountedHostView` respectively). Any + updates to the former tree are reflected in the latter tree, and then resulting changes are + delegated to the renderer for it to reflect those in its viewport. + + Scheduled updates are stored in a simple stack-like structure and are processed sequentially as + opposed to potentially more sophisticated implementations. [React's fiber + reconciler](https://github.com/acdlite/react-fiber-architecture) is one of those and could be + implemented in the future to improve UI responsiveness under heavy load and potentially even + support multi-threading when it's supported in WebAssembly. + */ public final class StackReconciler { + /** A set of mounted elements that triggered a re-render. These are stored in a `Set` instead of + an array to avoid duplicate re-renders. The actual performance benefits of such de-duplication + haven't been proven in the absence of benchmarks, so this could be updated to a simple `Array` in + the future if that's proven to be more effective. + */ private var queuedRerenders = Set>() + /** A root renderer's target instance. We establish the "host-target" terminology where a "host" + is a primitive `View` that doesn't have any children, and a "target" is an instance of a type + declared by a rendererto which the "host" is rendered to. For example, in the DOM renderer a + "target" is a DOM node, in a hypothetical iOS renderer it would be a `UIView`, and a macOS + renderer would declare an `NSView` as its "target" type. + */ public let rootTarget: R.TargetType + + /** A root of the mounted elements tree to which all other mounted elements are attached to. + */ private let rootElement: MountedElement + + /** A renderer instances to delegate to. Usually the renderer owns the reconciler instance, thus + the reference has to be weak to avoid a reference cycle. + **/ private(set) weak var renderer: R? + + /** A platform-specific implementation of an event loop cycle scheduler. Usually reconciler + updates are scheduled in reponse to user input. To make the updates non-blocking so that app + feels responsive, the actual reconcilliation needs to be scheduled on the next event loop cycle. + Usually it's `DispatchQueue.main.async` on platforms where `Dispatch` is supported, or + `setTimeout` in the DOM environment. + */ private let scheduler: (@escaping () -> ()) -> () public init( @@ -164,15 +201,15 @@ public final class StackReconciler { return compositeElement[keyPath: result](compositeElement[keyPath: bodyKeypath]) } - func render(compositeView: MountedCompositeView) -> some View { + func render(compositeView: MountedCompositeView) -> AnyView { render(compositeElement: compositeView, body: \.view.view, result: \.view.bodyClosure) } - func render(mountedApp: MountedApp) -> some Scene { + func render(mountedApp: MountedApp) -> _AnyScene { render(compositeElement: mountedApp, body: \.app.app, result: \.app.bodyClosure) } - func render(mountedScene: MountedScene) -> some Scene { + func render(mountedScene: MountedScene) -> _AnyScene.BodyResult { render(compositeElement: mountedScene, body: \.scene.scene, result: \.scene.bodyClosure) } @@ -205,7 +242,7 @@ public final class StackReconciler { // new child has the same type as existing child // swiftlint:disable:next force_try - if try! mountedChild.view.typeConstructorName == typeInfo(of: childBodyType).mangledName { + if try! mountedChild.typeConstructorName == typeInfo(of: childBodyType).mangledName { updateChild(mountedChild) mountedChild.update(with: self) } else { diff --git a/Sources/TokamakCore/Views/AnyView.swift b/Sources/TokamakCore/Views/AnyView.swift index 8f9995b32..95d59a224 100644 --- a/Sources/TokamakCore/Views/AnyView.swift +++ b/Sources/TokamakCore/Views/AnyView.swift @@ -22,33 +22,29 @@ public struct AnyView: View { /// The type of the underlying `view`. let type: Any.Type - /** The name of the unapplied generic type of the underlying view. `Button` and + /** The name of the unapplied generic type of the underlying `view`. `Button` and `Button` types are different, but when reconciling the tree of mounted views they are treated the same, thus the `Button` part of the type (the type constructor) is stored in this property. */ let typeConstructorName: String - /** The type of the `body` of the underlying `view`. Used to cast the result of the applied - `bodyClosure` property. - */ - let bodyType: Any.Type - /// The actual `View` value wrapped within this `AnyView`. var view: Any /** Type-erased `body` of the underlying `view`. Needs to take a fresh version of `view` as an - argument, otherwise it captures the old view value. + argument, otherwise it captures an old value of the `body` property. */ let bodyClosure: (Any) -> AnyView + /** The type of the `body` of the underlying `view`. Used to cast the result of the applied + `bodyClosure` property. + */ + let bodyType: Any.Type + public init(_ view: V) where V: View { if let anyView = view as? AnyView { - type = anyView.type - typeConstructorName = anyView.typeConstructorName - bodyType = anyView.bodyType - self.view = anyView.view - bodyClosure = anyView.bodyClosure + self = anyView } else { type = V.self From bd6c7857a55ffe5deec9cb30a4ee2f917e3468d7 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 29 Jul 2020 18:44:53 +0100 Subject: [PATCH 07/13] Fix environment propagation and reflection bugs --- .../TokamakCore/MountedViews/MountedApp.swift | 37 +++++++--------- .../MountedCompositeElement.swift | 11 ++--- .../MountedViews/MountedCompositeView.swift | 5 ++- .../MountedViews/MountedElement.swift | 42 +++++++++---------- .../MountedViews/MountedHostView.swift | 11 +---- .../MountedViews/MountedScene.swift | 28 ++++++++----- Sources/TokamakCore/StackReconciler.swift | 8 ++-- Sources/TokamakDemo/main.swift | 11 ++++- 8 files changed, 75 insertions(+), 78 deletions(-) diff --git a/Sources/TokamakCore/MountedViews/MountedApp.swift b/Sources/TokamakCore/MountedViews/MountedApp.swift index b77d14056..3c8b7f96b 100644 --- a/Sources/TokamakCore/MountedViews/MountedApp.swift +++ b/Sources/TokamakCore/MountedViews/MountedApp.swift @@ -49,33 +49,28 @@ final class MountedApp: MountedCompositeElement { self, with: element, getElementType: { $0.type }, - updateChild: { $0.scene = _AnyScene(element) }, + updateChild: { + $0.environmentValues = environmentValues + $0.scene = _AnyScene(element) + }, mountChild: { mountChild($0) } ) } } -extension App { - func makeMountedApp(_ parentTarget: R.TargetType, - _ environmentValues: EnvironmentValues) - -> MountedApp where R: Renderer { - // Find Environment changes - var injectableApp = self - let any = (injectableApp as? _AnyApp) ?? _AnyApp(injectableApp) - // swiftlint:disable force_try +extension _AnyApp { + func makeMountedApp( + _ parentTarget: R.TargetType, + _ environmentValues: EnvironmentValues + ) -> MountedApp where R: Renderer { + // swiftlint:disable:next force_try + let info = try! typeInfo(of: type) - let appInfo = try! typeInfo(of: any.type) - var extractedApp = any.app + var modified = app + info.injectEnvironment(from: environmentValues, into: &modified) - appInfo.injectEnvironment(from: environmentValues, into: &extractedApp) - - // Set the extractedApp back on the AnyApp after modification - let anyAppInfo = try! typeInfo(of: _AnyApp.self) - try! anyAppInfo.property(named: "app").set(value: extractedApp, on: &injectableApp) - // swiftlint:enable force_try - - // Make MountedView - let anyApp = injectableApp as? _AnyApp ?? _AnyApp(injectableApp) - return MountedApp(anyApp, parentTarget, environmentValues) + var result = self + result.app = modified + return MountedApp(result, parentTarget, environmentValues) } } diff --git a/Sources/TokamakCore/MountedViews/MountedCompositeElement.swift b/Sources/TokamakCore/MountedViews/MountedCompositeElement.swift index 22d68f46f..05efc8958 100644 --- a/Sources/TokamakCore/MountedViews/MountedCompositeElement.swift +++ b/Sources/TokamakCore/MountedViews/MountedCompositeElement.swift @@ -27,28 +27,23 @@ class MountedCompositeElement: MountedElement, Hashable { hasher.combine(ObjectIdentifier(self)) } - var mountedChildren = [MountedElement]() let parentTarget: R.TargetType var state = [Any]() var subscriptions = [AnyCancellable]() - var environmentValues: EnvironmentValues init(_ app: _AnyApp, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) { self.parentTarget = parentTarget - self.environmentValues = environmentValues - super.init(app) + super.init(app, environmentValues) } init(_ scene: _AnyScene, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) { self.parentTarget = parentTarget - self.environmentValues = environmentValues - super.init(scene) + super.init(scene, environmentValues) } init(_ view: AnyView, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) { self.parentTarget = parentTarget - self.environmentValues = environmentValues - super.init(view) + super.init(view, environmentValues) } } diff --git a/Sources/TokamakCore/MountedViews/MountedCompositeView.swift b/Sources/TokamakCore/MountedViews/MountedCompositeView.swift index d477097a1..db2de569d 100644 --- a/Sources/TokamakCore/MountedViews/MountedCompositeView.swift +++ b/Sources/TokamakCore/MountedViews/MountedCompositeView.swift @@ -45,7 +45,10 @@ final class MountedCompositeView: MountedCompositeElement { self, with: element, getElementType: { $0.type }, - updateChild: { $0.view = AnyView(element) }, + updateChild: { + $0.environmentValues = environmentValues + $0.view = AnyView(element) + }, mountChild: { $0.makeMountedView(parentTarget, environmentValues) } ) } diff --git a/Sources/TokamakCore/MountedViews/MountedElement.swift b/Sources/TokamakCore/MountedViews/MountedElement.swift index 8910af708..73313e569 100644 --- a/Sources/TokamakCore/MountedViews/MountedElement.swift +++ b/Sources/TokamakCore/MountedViews/MountedElement.swift @@ -85,16 +85,22 @@ public class MountedElement { } } - init(_ app: _AnyApp) { + var mountedChildren = [MountedElement]() + var environmentValues: EnvironmentValues + + init(_ app: _AnyApp, _ environmentValues: EnvironmentValues) { element = .app(app) + self.environmentValues = environmentValues } - init(_ scene: _AnyScene) { + init(_ scene: _AnyScene, _ environmentValues: EnvironmentValues) { element = .scene(scene) + self.environmentValues = environmentValues } - init(_ view: AnyView) { + init(_ view: AnyView, _ environmentValues: EnvironmentValues) { element = .view(view) + self.environmentValues = environmentValues } func mount(with reconciler: StackReconciler) { @@ -137,41 +143,31 @@ extension TypeInfo { } } -extension View { +extension AnyView { func makeMountedView( _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues ) -> MountedElement { // Find Environment changes var modifiedEnv = environmentValues - var injectableView = self - let any = (injectableView as? AnyView) ?? AnyView(injectableView) // swiftlint:disable force_try // Extract the view from the AnyView for modification - var extractedView = any.view - let viewInfo = try! typeInfo(of: any.type) - if viewInfo - .genericTypes - .filter({ $0 is EnvironmentModifier.Type }).count > 0 { + let viewInfo = try! typeInfo(of: type) + if viewInfo.genericTypes.filter({ $0 is EnvironmentModifier.Type }).count > 0 { // Apply Environment changes: - if let modifier = try? viewInfo + if let modifier = try! viewInfo .property(named: "modifier") - .get(from: any.view) as? EnvironmentModifier { + .get(from: view) as? EnvironmentModifier { modifier.modifyEnvironment(&modifiedEnv) } } + var modifiedView = view + viewInfo.injectEnvironment(from: environmentValues, into: &modifiedView) - viewInfo.injectEnvironment(from: environmentValues, into: &extractedView) - - // Set the extractedView back on the AnyView after modification - let anyViewInfo = try! typeInfo(of: AnyView.self) - try! anyViewInfo.property(named: "view").set(value: extractedView, on: &injectableView) - // swiftlint:enable force_try - - // Make MountedView - let anyView = injectableView as? AnyView ?? AnyView(injectableView) + var anyView = self + anyView.view = modifiedView if anyView.type == EmptyView.self { - return MountedNull(anyView) + return MountedNull(anyView, modifiedEnv) } else if anyView.bodyType == Never.self && !(anyView.type is ViewDeferredToRenderer.Type) { return MountedHostView(anyView, parentTarget, modifiedEnv) } else { diff --git a/Sources/TokamakCore/MountedViews/MountedHostView.swift b/Sources/TokamakCore/MountedViews/MountedHostView.swift index 9ab89c29c..7c6e14e9b 100644 --- a/Sources/TokamakCore/MountedViews/MountedHostView.swift +++ b/Sources/TokamakCore/MountedViews/MountedHostView.swift @@ -21,8 +21,6 @@ import Runtime views by `StackReconciler`. */ public final class MountedHostView: MountedElement { - private var mountedChildren = [MountedElement]() - /** Target of a closest ancestor host view. As a parent of this view might not be a host view, but a composite view, we need to pass around the target of a host view to its closests descendant host @@ -33,15 +31,10 @@ public final class MountedHostView: MountedElement { /// Target of this host view supplied by a renderer after mounting has completed. private var target: R.TargetType? - private let environmentValues: EnvironmentValues - - init(_ view: AnyView, - _ parentTarget: R.TargetType, - _ environmentValues: EnvironmentValues) { + init(_ view: AnyView, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) { self.parentTarget = parentTarget - self.environmentValues = environmentValues - super.init(view) + super.init(view, environmentValues) } override func mount(with reconciler: StackReconciler) { diff --git a/Sources/TokamakCore/MountedViews/MountedScene.swift b/Sources/TokamakCore/MountedViews/MountedScene.swift index d91e93205..239a65a61 100644 --- a/Sources/TokamakCore/MountedViews/MountedScene.swift +++ b/Sources/TokamakCore/MountedViews/MountedScene.swift @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import Runtime + final class MountedScene: MountedCompositeElement { let title: String? @@ -46,6 +48,7 @@ final class MountedScene: MountedCompositeElement { with: element, getElementType: { $0.type }, updateChild: { + $0.environmentValues = environmentValues switch element { case let .scene(scene): $0.scene = _AnyScene(scene) @@ -86,23 +89,28 @@ extension _AnyScene { _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues ) -> MountedScene { + // swiftlint:disable:next force_try + let info = try! typeInfo(of: type) + + var modified = scene + info.injectEnvironment(from: environmentValues, into: &modified) + var title: String? - if let titledSelf = scene as? TitledScene, + if let titledSelf = modified as? TitledScene, let text = titledSelf.title { title = _TextProxy(text).rawText } let children: [MountedElement] - if let deferredSelf = scene as? SceneDeferredToRenderer { - children = [deferredSelf.deferredBody.makeMountedView(parentTarget, environmentValues)] - } else if let groupSelf = scene as? GroupScene { - children = groupSelf.children.map { $0.makeMountedScene(parentTarget, environmentValues) } + if let deferredScene = modified as? SceneDeferredToRenderer { + children = [deferredScene.deferredBody.makeMountedView(parentTarget, environmentValues)] + } else if let groupScene = modified as? GroupScene { + children = groupScene.children.map { $0.makeMountedScene(parentTarget, environmentValues) } } else { - fatalError(""" - Unsupported `Scene` type `\(type)`. Please file a bug report at \ - https://github.com/swiftwasm/Tokamak/issues/new - """) + children = [] } - return .init(self, title, children, parentTarget, environmentValues) + var result = self + result.scene = modified + return .init(result, title, children, parentTarget, environmentValues) } } diff --git a/Sources/TokamakCore/StackReconciler.swift b/Sources/TokamakCore/StackReconciler.swift index 0db5e7f67..cdd6b7a9d 100644 --- a/Sources/TokamakCore/StackReconciler.swift +++ b/Sources/TokamakCore/StackReconciler.swift @@ -74,7 +74,7 @@ public final class StackReconciler { self.scheduler = scheduler rootTarget = target - rootElement = view.makeMountedView(target, environment) + rootElement = AnyView(view).makeMountedView(target, environment) rootElement.mount(with: self) } @@ -90,13 +90,13 @@ public final class StackReconciler { self.scheduler = scheduler rootTarget = target - rootElement = app.makeMountedApp(target, environment) + rootElement = _AnyApp(app).makeMountedApp(target, environment) rootElement.mount(with: self) if let mountedApp = rootElement as? MountedApp { app._phasePublisher.sink { [weak self] phase in - if mountedApp.environmentValues[keyPath: \.scenePhase] != phase { - mountedApp.environmentValues[keyPath: \.scenePhase] = phase + if mountedApp.environmentValues.scenePhase != phase { + mountedApp.environmentValues.scenePhase = phase self?.queueUpdate(for: mountedApp) } }.store(in: &mountedApp.subscriptions) diff --git a/Sources/TokamakDemo/main.swift b/Sources/TokamakDemo/main.swift index db5930c17..c29cbe20e 100644 --- a/Sources/TokamakDemo/main.swift +++ b/Sources/TokamakDemo/main.swift @@ -15,17 +15,24 @@ import TokamakShim @available(OSX 10.16, iOS 14.0, *) -struct TokamakDemoApp: App { +struct CustomScene: Scene { @Environment(\.scenePhase) private var scenePhase var body: some Scene { - print(scenePhase) + print("In CustomScene.body scenePhase is \(scenePhase)") return WindowGroup("Tokamak Demo") { TokamakDemoView() } } } +@available(OSX 10.16, iOS 14.0, *) +struct TokamakDemoApp: App { + var body: some Scene { + CustomScene() + } +} + // If @main was supported for executable Swift Packages, // this would match SwiftUI 100% if #available(OSX 10.16, iOS 14.0, *) { From 201da0a4d1e9be616600fe2916ed5d8ac6e8a58b Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 29 Jul 2020 18:48:48 +0100 Subject: [PATCH 08/13] Rename `MountedNull` to `MountedEmptyView` --- Sources/TokamakCore/MountedViews/MountedElement.swift | 2 +- .../MountedViews/{MountedNull.swift => MountedEmptyView.swift} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename Sources/TokamakCore/MountedViews/{MountedNull.swift => MountedEmptyView.swift} (93%) diff --git a/Sources/TokamakCore/MountedViews/MountedElement.swift b/Sources/TokamakCore/MountedViews/MountedElement.swift index 73313e569..ebf43f939 100644 --- a/Sources/TokamakCore/MountedViews/MountedElement.swift +++ b/Sources/TokamakCore/MountedViews/MountedElement.swift @@ -167,7 +167,7 @@ extension AnyView { var anyView = self anyView.view = modifiedView if anyView.type == EmptyView.self { - return MountedNull(anyView, modifiedEnv) + return MountedEmptyView(anyView, modifiedEnv) } else if anyView.bodyType == Never.self && !(anyView.type is ViewDeferredToRenderer.Type) { return MountedHostView(anyView, parentTarget, modifiedEnv) } else { diff --git a/Sources/TokamakCore/MountedViews/MountedNull.swift b/Sources/TokamakCore/MountedViews/MountedEmptyView.swift similarity index 93% rename from Sources/TokamakCore/MountedViews/MountedNull.swift rename to Sources/TokamakCore/MountedViews/MountedEmptyView.swift index acc358cf1..8cf96ecb8 100644 --- a/Sources/TokamakCore/MountedViews/MountedNull.swift +++ b/Sources/TokamakCore/MountedViews/MountedEmptyView.swift @@ -15,7 +15,7 @@ // Created by Max Desiatov on 05/01/2019. // -final class MountedNull: MountedElement { +final class MountedEmptyView: MountedElement { override func mount(with reconciler: StackReconciler) {} override func unmount(with reconciler: StackReconciler) {} From 0517b1cd5b047c2e106fd0cbfc97af0a55ba1b12 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 29 Jul 2020 18:50:21 +0100 Subject: [PATCH 09/13] Fix doc comments --- Sources/TokamakCore/StackReconciler.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/TokamakCore/StackReconciler.swift b/Sources/TokamakCore/StackReconciler.swift index cdd6b7a9d..4eb227b30 100644 --- a/Sources/TokamakCore/StackReconciler.swift +++ b/Sources/TokamakCore/StackReconciler.swift @@ -50,13 +50,13 @@ public final class StackReconciler { */ private let rootElement: MountedElement - /** A renderer instances to delegate to. Usually the renderer owns the reconciler instance, thus + /** A renderer instance to delegate to. Usually the renderer owns the reconciler instance, thus the reference has to be weak to avoid a reference cycle. **/ private(set) weak var renderer: R? - /** A platform-specific implementation of an event loop cycle scheduler. Usually reconciler - updates are scheduled in reponse to user input. To make the updates non-blocking so that app + /** A platform-specific implementation of an event loop scheduler. Usually reconciler + updates are scheduled in reponse to user input. To make updates non-blocking so that the app feels responsive, the actual reconcilliation needs to be scheduled on the next event loop cycle. Usually it's `DispatchQueue.main.async` on platforms where `Dispatch` is supported, or `setTimeout` in the DOM environment. From 5c556d4c2f728ba564a6a8f7aba1b7bf3cdca687 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 29 Jul 2020 20:20:48 +0100 Subject: [PATCH 10/13] Refactor StackReconiler.render to use closures --- Sources/TokamakCore/StackReconciler.swift | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Sources/TokamakCore/StackReconciler.swift b/Sources/TokamakCore/StackReconciler.swift index 4eb227b30..8cc2e0a43 100644 --- a/Sources/TokamakCore/StackReconciler.swift +++ b/Sources/TokamakCore/StackReconciler.swift @@ -175,9 +175,11 @@ public final class StackReconciler { }.store(in: &compositeElement.subscriptions) } - func render(compositeElement: MountedCompositeElement, - body bodyKeypath: ReferenceWritableKeyPath, Any>, - result: KeyPath, (Any) -> T>) -> T { + func render( + compositeElement: MountedCompositeElement, + body bodyKeypath: ReferenceWritableKeyPath, Any>, + bodyClosure: (MountedCompositeElement) -> T + ) -> T { let info = try! typeInfo(of: compositeElement.elementType) info.injectEnvironment(from: compositeElement.environmentValues, into: &compositeElement[keyPath: bodyKeypath]) @@ -198,19 +200,19 @@ public final class StackReconciler { } } - return compositeElement[keyPath: result](compositeElement[keyPath: bodyKeypath]) + return bodyClosure(compositeElement) } func render(compositeView: MountedCompositeView) -> AnyView { - render(compositeElement: compositeView, body: \.view.view, result: \.view.bodyClosure) + render(compositeElement: compositeView, body: \.view.view) { $0.view.bodyClosure($0.view.view) } } func render(mountedApp: MountedApp) -> _AnyScene { - render(compositeElement: mountedApp, body: \.app.app, result: \.app.bodyClosure) + render(compositeElement: mountedApp, body: \.app.app) { $0.app.bodyClosure($0.app.app) } } func render(mountedScene: MountedScene) -> _AnyScene.BodyResult { - render(compositeElement: mountedScene, body: \.scene.scene, result: \.scene.bodyClosure) + render(compositeElement: mountedScene, body: \.scene.scene) { $0.scene.bodyClosure($0.scene.scene) } } func reconcile( From 3d60370cb12b12c40488a60881127eb4a845affd Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 29 Jul 2020 20:58:10 +0100 Subject: [PATCH 11/13] Work in progress --- README.md | 39 ++++------ Sources/TokamakCore/DynamicProperty.swift | 16 ++-- .../TokamakCore/MountedViews/MountedApp.swift | 21 +++-- .../MountedCompositeElement.swift | 2 +- .../MountedViews/MountedElement.swift | 28 ++++--- .../MountedViews/MountedScene.swift | 10 +-- Sources/TokamakCore/Renderer.swift | 14 ++-- Sources/TokamakCore/StackReconciler.swift | 78 ++++++++----------- Sources/TokamakCore/Target.swift | 50 +----------- Sources/TokamakDOM/DOMNode.swift | 9 +-- Sources/TokamakDOM/DOMRenderer.swift | 27 +------ Sources/TokamakDOM/Views/HTML.swift | 2 +- 12 files changed, 107 insertions(+), 189 deletions(-) diff --git a/README.md b/README.md index 8eea2cd09..8b58c45dd 100644 --- a/README.md +++ b/README.md @@ -48,23 +48,18 @@ struct Counter: View { } } } -``` - -You can then render your view in any DOM node captured with -[JavaScriptKit](https://github.com/kateinoigakukun/JavaScriptKit/), just -pass it as an argument to the `DOMRenderer` initializer together with your view: - -```swift -import JavaScriptKit -import TokamakDOM -let document = JSObjectRef.global.document.object! - -let divElement = document.createElement!("div").object! -let renderer = DOMRenderer(Counter(count: 5, limit: 15), divElement) +struct CounterApp: App { + var body: some Scene { + WindowGroup("Counter Demo") { + Counter(count: 5, limit: 15) + } + } +} -let body = document.body.object! -_ = body.appendChild!(divElement) +// @main attribute is not supported in SwiftPM apps. +// See https://bugs.swift.org/browse/SR-12683 for more details. +CounterApp.main() ``` ### Arbitrary HTML @@ -108,9 +103,9 @@ app. ## Requirements for app developers - macOS 10.15 and Xcode 11.4/11.5/11.6 for macOS. Xcode betas are currently not supported. You can have -those installed, but please make sure you use -[`xcode-select`](https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-HOW_DO_I_SELECT_THE_DEFAULT_VERSION_OF_XCODE_TO_USE_FOR_MY_COMMAND_LINE_TOOLS_) -to point it to a release version of Xcode. + those installed, but please make sure you use + [`xcode-select`](https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-HOW_DO_I_SELECT_THE_DEFAULT_VERSION_OF_XCODE_TO_USE_FOR_MY_COMMAND_LINE_TOOLS_) + to point it to a release version of Xcode. - [Swift 5.2 or later](https://swift.org/download/) for Linux. ## Requirements for app users @@ -128,7 +123,7 @@ Not all of these were tested though, compatibility reports are very welcome! Tokamak relies on [`carton`](https://carton.dev) as a primary build tool. As a part of these steps you'll install `carton` via [Homebrew](https://brew.sh/) on macOS (unfortunately you'll have to build -it manually on Linux). Assuming you already have Homebrew installed, you can create a new Tokamak +it manually on Linux). Assuming you already have Homebrew installed, you can create a new Tokamak app by following these steps: 1. Install `carton`: @@ -156,15 +151,15 @@ carton init --template tokamak ``` 4. Build the project and start the development server, `carton dev` can be kept running -during development: + during development: ``` carton dev ``` 5. Open [http://127.0.0.1:8080/](http://127.0.0.1:8080/) in your browser to see the app -running. You can edit the app source code in your favorite editor and save it, `carton` -will immediately rebuild the app and reload all browser tabs that have the app open. + running. You can edit the app source code in your favorite editor and save it, `carton` + will immediately rebuild the app and reload all browser tabs that have the app open. You can also clone this repository and run `carton dev` in its root directory. This will build the demo app that shows almost all of the currently implemented APIs. diff --git a/Sources/TokamakCore/DynamicProperty.swift b/Sources/TokamakCore/DynamicProperty.swift index 8735c2d8c..ae608c0b2 100644 --- a/Sources/TokamakCore/DynamicProperty.swift +++ b/Sources/TokamakCore/DynamicProperty.swift @@ -29,9 +29,11 @@ extension TypeInfo { /// Extract all `DynamicProperty` from a type, recursively. /// This is necessary as a `DynamicProperty` can be nested. /// `EnvironmentValues` can also be injected at this point. - func dynamicProperties(_ environment: EnvironmentValues, - source: inout Any, - shouldUpdate: Bool) -> [PropertyInfo] { + func dynamicProperties( + _ environment: EnvironmentValues, + source: inout T, + shouldUpdate: Bool + ) -> [PropertyInfo] { var dynamicProps = [PropertyInfo]() for prop in properties where prop.type is DynamicProperty.Type { dynamicProps.append(prop) @@ -40,9 +42,11 @@ extension TypeInfo { propInfo.injectEnvironment(from: environment, into: &source) var extracted = try! prop.get(from: source) dynamicProps.append( - contentsOf: propInfo.dynamicProperties(environment, - source: &extracted, - shouldUpdate: shouldUpdate) + contentsOf: propInfo.dynamicProperties( + environment, + source: &extracted, + shouldUpdate: shouldUpdate + ) ) // swiftlint:disable:next force_cast var extractedDynamicProp = extracted as! DynamicProperty diff --git a/Sources/TokamakCore/MountedViews/MountedApp.swift b/Sources/TokamakCore/MountedViews/MountedApp.swift index 3c8b7f96b..419551fc5 100644 --- a/Sources/TokamakCore/MountedViews/MountedApp.swift +++ b/Sources/TokamakCore/MountedViews/MountedApp.swift @@ -34,11 +34,10 @@ final class MountedApp: MountedCompositeElement { mountedChildren.forEach { $0.unmount(with: reconciler) } } - private func mountChild(_ childBody: _AnyScene) -> MountedElement { - let mountedScene: MountedScene = childBody.makeMountedScene(parentTarget, environmentValues) + private func mountChild(_ scene: S) -> MountedElement { + let mountedScene: MountedScene = scene.makeMountedScene(parentTarget, environmentValues) if let title = mountedScene.title { - // swiftlint:disable force_cast - (app.type as! _TitledApp.Type)._setTitle(title) + R.AppType._setTitle(title) } return mountedScene } @@ -48,7 +47,7 @@ final class MountedApp: MountedCompositeElement { reconciler.reconcile( self, with: element, - getElementType: { $0.type }, + getElementType: { type(of: $0) }, updateChild: { $0.environmentValues = environmentValues $0.scene = _AnyScene(element) @@ -58,19 +57,17 @@ final class MountedApp: MountedCompositeElement { } } -extension _AnyApp { +extension App { func makeMountedApp( _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues - ) -> MountedApp where R: Renderer { + ) -> MountedApp where R: Renderer, R.AppType == Self { // swiftlint:disable:next force_try - let info = try! typeInfo(of: type) + let info = try! typeInfo(of: Self.self) - var modified = app + var modified = self info.injectEnvironment(from: environmentValues, into: &modified) - var result = self - result.app = modified - return MountedApp(result, parentTarget, environmentValues) + return MountedApp(modified, parentTarget, environmentValues) } } diff --git a/Sources/TokamakCore/MountedViews/MountedCompositeElement.swift b/Sources/TokamakCore/MountedViews/MountedCompositeElement.swift index 05efc8958..886b53ea5 100644 --- a/Sources/TokamakCore/MountedViews/MountedCompositeElement.swift +++ b/Sources/TokamakCore/MountedViews/MountedCompositeElement.swift @@ -32,7 +32,7 @@ class MountedCompositeElement: MountedElement, Hashable { var state = [Any]() var subscriptions = [AnyCancellable]() - init(_ app: _AnyApp, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) { + init(_ app: R.AppType, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues) { self.parentTarget = parentTarget super.init(app, environmentValues) } diff --git a/Sources/TokamakCore/MountedViews/MountedElement.swift b/Sources/TokamakCore/MountedViews/MountedElement.swift index ebf43f939..2f9a929b3 100644 --- a/Sources/TokamakCore/MountedViews/MountedElement.swift +++ b/Sources/TokamakCore/MountedViews/MountedElement.swift @@ -17,17 +17,17 @@ import Runtime -/// The container for any of the possible `MountedElement` types -enum MountedElementKind { - case app(_AnyApp) - case scene(_AnyScene) - case view(AnyView) -} - public class MountedElement { - private var element: MountedElementKind + /// The container for any of the possible "raw" element types + enum Kind { + case app(R.AppType) + case scene(_AnyScene) + case view(AnyView) + } - public internal(set) var app: _AnyApp { + private var element: Kind + + public internal(set) var app: R.AppType { get { if case let .app(app) = element { return app @@ -67,7 +67,11 @@ public class MountedElement { var elementType: Any.Type { switch element { - case let .app(app): return app.type + case .app: fatalError(""" + `App` values aren't supposed to be reconciled, thus the type constructor name is not stored \ + for `App` elements. Please report this crash as a bug at \ + https://github.com/swiftwasm/Tokamak/issues/new + """) case let .scene(scene): return scene.type case let .view(view): return view.type } @@ -88,7 +92,7 @@ public class MountedElement { var mountedChildren = [MountedElement]() var environmentValues: EnvironmentValues - init(_ app: _AnyApp, _ environmentValues: EnvironmentValues) { + init(_ app: R.AppType, _ environmentValues: EnvironmentValues) { element = .app(app) self.environmentValues = environmentValues } @@ -117,7 +121,7 @@ public class MountedElement { } extension TypeInfo { - func injectEnvironment(from environmentValues: EnvironmentValues, into element: inout Any) { + func injectEnvironment(from environmentValues: EnvironmentValues, into element: inout E) { // Inject @Environment values // swiftlint:disable force_cast // swiftlint:disable force_try diff --git a/Sources/TokamakCore/MountedViews/MountedScene.swift b/Sources/TokamakCore/MountedViews/MountedScene.swift index 239a65a61..b937deafa 100644 --- a/Sources/TokamakCore/MountedViews/MountedScene.swift +++ b/Sources/TokamakCore/MountedViews/MountedScene.swift @@ -84,15 +84,15 @@ extension _AnyScene.BodyResult { } } -extension _AnyScene { +extension Scene { func makeMountedScene( _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues ) -> MountedScene { // swiftlint:disable:next force_try - let info = try! typeInfo(of: type) + let info = try! typeInfo(of: Self.self) - var modified = scene + var modified = self info.injectEnvironment(from: environmentValues, into: &modified) var title: String? @@ -109,8 +109,6 @@ extension _AnyScene { children = [] } - var result = self - result.scene = modified - return .init(result, title, children, parentTarget, environmentValues) + return .init(_AnyScene(modified), title, children, parentTarget, environmentValues) } } diff --git a/Sources/TokamakCore/Renderer.swift b/Sources/TokamakCore/Renderer.swift index 2f49cbae5..1923d645f 100644 --- a/Sources/TokamakCore/Renderer.swift +++ b/Sources/TokamakCore/Renderer.swift @@ -25,11 +25,13 @@ public protocol Renderer: AnyObject { typealias Mounted = MountedElement typealias MountedHost = MountedHostView + associatedtype AppType: App + /** Views are rendered to platform-specific targets with a renderer. Usually a target is a simple view (`UIView` and `NSView` for `UIKit` and `AppKit` respectively). */ - associatedtype TargetType: Target + associatedtype TargetType /// Reconciler instance used by this renderer. var reconciler: StackReconciler? { get } @@ -41,20 +43,14 @@ public protocol Renderer: AnyObject { - parameter view: The host view that renders to the newly created target. - returns: The newly created target. */ - func mountTarget( - to parent: TargetType, - with host: MountedHost - ) -> TargetType? + func mountTarget(to parent: TargetType, with host: MountedHost) -> TargetType? /** Function called by a reconciler when an existing target instance should be updated. - parameter target: Existing target instance to be updated. - parameter view: The host view that renders to the updated target. */ - func update( - target: TargetType, - with host: MountedHost - ) + func update(target: TargetType, with host: MountedHost) /** Function called by a reconciler when an existing target instance should be unmounted: removed from the parent and most likely destroyed. diff --git a/Sources/TokamakCore/StackReconciler.swift b/Sources/TokamakCore/StackReconciler.swift index 8cc2e0a43..c881448f7 100644 --- a/Sources/TokamakCore/StackReconciler.swift +++ b/Sources/TokamakCore/StackReconciler.swift @@ -48,7 +48,7 @@ public final class StackReconciler { /** A root of the mounted elements tree to which all other mounted elements are attached to. */ - private let rootElement: MountedElement + private let mountedApp: MountedApp /** A renderer instance to delegate to. Usually the renderer owns the reconciler instance, thus the reference has to be weak to avoid a reference cycle. @@ -63,8 +63,8 @@ public final class StackReconciler { */ private let scheduler: (@escaping () -> ()) -> () - public init( - view: V, + public init( + app: R.AppType, target: R.TargetType, environment: EnvironmentValues, renderer: R, @@ -74,33 +74,15 @@ public final class StackReconciler { self.scheduler = scheduler rootTarget = target - rootElement = AnyView(view).makeMountedView(target, environment) + mountedApp = app.makeMountedApp(target, environment) - rootElement.mount(with: self) - } - - public init( - app: A, - target: R.TargetType, - environment: EnvironmentValues, - renderer: R, - scheduler: @escaping (@escaping () -> ()) -> () - ) { - self.renderer = renderer - self.scheduler = scheduler - rootTarget = target - - rootElement = _AnyApp(app).makeMountedApp(target, environment) - - rootElement.mount(with: self) - if let mountedApp = rootElement as? MountedApp { - app._phasePublisher.sink { [weak self] phase in - if mountedApp.environmentValues.scenePhase != phase { - mountedApp.environmentValues.scenePhase = phase - self?.queueUpdate(for: mountedApp) - } - }.store(in: &mountedApp.subscriptions) - } + mountedApp.mount(with: self) + app._phasePublisher.sink { [weak self, weak mountedApp] phase in + if let mountedApp = mountedApp, mountedApp.environmentValues.scenePhase != phase { + mountedApp.environmentValues.scenePhase = phase + self?.queueUpdate(for: mountedApp) + } + }.store(in: &mountedApp.subscriptions) } private func queueStateUpdate( @@ -129,11 +111,11 @@ public final class StackReconciler { queuedRerenders.removeAll() } - private func setupState( + private func setupState( id: Int, for property: PropertyInfo, of compositeElement: MountedCompositeElement, - body bodyKeypath: ReferenceWritableKeyPath, Any> + body bodyKeypath: ReferenceWritableKeyPath, Element> ) { // swiftlint:disable force_try // `ValueStorage` property already filtered out, so safe to assume the value's type @@ -158,10 +140,10 @@ public final class StackReconciler { try! property.set(value: state, on: &compositeElement[keyPath: bodyKeypath]) } - private func setupSubscription( + private func setupSubscription( for property: PropertyInfo, of compositeElement: MountedCompositeElement, - body bodyKeypath: KeyPath, Any> + body bodyKeypath: KeyPath, Element> ) { // `ObservedProperty` property already filtered out, so safe to assume the value's type // swiftlint:disable force_cast @@ -175,11 +157,11 @@ public final class StackReconciler { }.store(in: &compositeElement.subscriptions) } - func render( + func render( compositeElement: MountedCompositeElement, - body bodyKeypath: ReferenceWritableKeyPath, Any>, - bodyClosure: (MountedCompositeElement) -> T - ) -> T { + body bodyKeypath: ReferenceWritableKeyPath, Element>, + bodyClosure: (MountedCompositeElement) -> Body + ) -> Body { let info = try! typeInfo(of: compositeElement.elementType) info.injectEnvironment(from: compositeElement.environmentValues, into: &compositeElement[keyPath: bodyKeypath]) @@ -187,9 +169,11 @@ public final class StackReconciler { let needsSubscriptions = compositeElement.subscriptions.isEmpty var stateIdx = 0 - let dynamicProps = info.dynamicProperties(compositeElement.environmentValues, - source: &compositeElement[keyPath: bodyKeypath], - shouldUpdate: true) + let dynamicProps = info.dynamicProperties( + compositeElement.environmentValues, + source: &compositeElement[keyPath: bodyKeypath], + shouldUpdate: true + ) for property in dynamicProps { // Setup state/subscriptions if property.type is ValueStorage.Type { @@ -204,15 +188,21 @@ public final class StackReconciler { } func render(compositeView: MountedCompositeView) -> AnyView { - render(compositeElement: compositeView, body: \.view.view) { $0.view.bodyClosure($0.view.view) } + render(compositeElement: compositeView, body: \.view.view) { + $0.view.bodyClosure($0.view.view) + } } - func render(mountedApp: MountedApp) -> _AnyScene { - render(compositeElement: mountedApp, body: \.app.app) { $0.app.bodyClosure($0.app.app) } + func render(mountedApp: MountedApp) -> some Scene { + render(compositeElement: mountedApp, body: \.app) { + $0.app.body + } } func render(mountedScene: MountedScene) -> _AnyScene.BodyResult { - render(compositeElement: mountedScene, body: \.scene.scene) { $0.scene.bodyClosure($0.scene.scene) } + render(compositeElement: mountedScene, body: \.scene.scene) { + $0.scene.bodyClosure($0.scene.scene) + } } func reconcile( diff --git a/Sources/TokamakCore/Target.swift b/Sources/TokamakCore/Target.swift index f7cd9f4bc..d96a484f5 100644 --- a/Sources/TokamakCore/Target.swift +++ b/Sources/TokamakCore/Target.swift @@ -15,52 +15,6 @@ // Created by Max Desiatov on 10/02/2019. // -open class Target { - var element: MountedElementKind - public internal(set) var app: _AnyApp { - get { - if case let .app(app) = element { - return app - } else { - fatalError("`Target` has type \(element) not `App`") - } - } - set { - element = .app(newValue) - } - } - - public internal(set) var scene: _AnyScene { - get { - if case let .scene(scene) = element { - return scene - } else { - fatalError("`Target` has type \(element) not `Scene`") - } - } - set { - element = .scene(newValue) - } - } - - public internal(set) var view: AnyView { - get { - if case let .view(view) = element { - return view - } else { - fatalError("`Target` has type \(element) not `View`") - } - } - set { - element = .view(newValue) - } - } - - public init(_ view: V) { - element = .view(AnyView(view)) - } - - public init(_ app: A) { - element = .app(_AnyApp(app)) - } +protocol Target { + var view: AnyView { get set } } diff --git a/Sources/TokamakDOM/DOMNode.swift b/Sources/TokamakDOM/DOMNode.swift index 46761b58e..a9d44dafe 100644 --- a/Sources/TokamakDOM/DOMNode.swift +++ b/Sources/TokamakDOM/DOMNode.swift @@ -18,19 +18,18 @@ import TokamakCore public final class DOMNode: Target { let ref: JSObjectRef private var listeners: [String: JSClosure] + private let view: AnyView? init(_ view: V, _ ref: JSObjectRef, _ listeners: [String: Listener] = [:]) { self.ref = ref self.listeners = [:] - super.init(view) + self.view = AnyView(view) reinstall(listeners) } - init(_ app: A, _ ref: JSObjectRef, _ listeners: [String: Listener] = [:]) { + init(_ ref: JSObjectRef) { self.ref = ref - self.listeners = [:] - super.init(app) - reinstall(listeners) + listeners = [:] } /// Removes all existing event listeners on this DOM node and install new ones from diff --git a/Sources/TokamakDOM/DOMRenderer.swift b/Sources/TokamakDOM/DOMRenderer.swift index e9f5ffe9c..890898d4d 100644 --- a/Sources/TokamakDOM/DOMRenderer.swift +++ b/Sources/TokamakDOM/DOMRenderer.swift @@ -72,33 +72,14 @@ func appendRootStyle(_ rootNode: JSObjectRef) { _ = head.appendChild!(rootStyle) } -public final class DOMRenderer: Renderer { +public final class DOMRenderer: Renderer { + public typealias AppType = A + public private(set) var reconciler: StackReconciler? private let rootRef: JSObjectRef - public init( - _ view: V, - _ ref: JSObjectRef, - _ rootEnvironment: EnvironmentValues? = nil - ) { - rootRef = ref - appendRootStyle(ref) - - reconciler = StackReconciler( - view: view, - target: DOMNode(view, ref), - environment: .defaultEnvironment, - renderer: self, - scheduler: timeoutScheduler - ) - } - - init( - _ app: A, - _ ref: JSObjectRef, - _ rootEnvironment: EnvironmentValues? = nil - ) { + init(_ app: A, _ ref: JSObjectRef, _ rootEnvironment: EnvironmentValues? = nil) { rootRef = ref appendRootStyle(ref) diff --git a/Sources/TokamakDOM/Views/HTML.swift b/Sources/TokamakDOM/Views/HTML.swift index 366cea43d..9685055b4 100644 --- a/Sources/TokamakDOM/Views/HTML.swift +++ b/Sources/TokamakDOM/Views/HTML.swift @@ -37,7 +37,7 @@ extension AnyHTML { """ } - func update(dom: DOMNode) { + func update(dom: DOMNode) { // FIXME: is there a sensible way to diff attributes and listeners to avoid // crossing the JavaScript bridge and touching DOM if not needed? From f8026eeb5f2a17e66947ceeace1555eff1d58a39 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 29 Jul 2020 21:12:16 +0100 Subject: [PATCH 12/13] Fully remove _AnyApp --- Sources/TokamakCore/App/_AnyApp.swift | 54 ------------------- .../MountedViews/MountedElement.swift | 6 +-- Sources/TokamakCore/Renderer.swift | 2 +- Sources/TokamakCore/Target.swift | 2 +- Sources/TokamakDOM/DOMNode.swift | 3 +- Sources/TokamakDOM/DOMRenderer.swift | 2 +- Sources/TokamakDOM/Views/HTML.swift | 2 +- 7 files changed, 7 insertions(+), 64 deletions(-) delete mode 100644 Sources/TokamakCore/App/_AnyApp.swift diff --git a/Sources/TokamakCore/App/_AnyApp.swift b/Sources/TokamakCore/App/_AnyApp.swift deleted file mode 100644 index 24dc7a66a..000000000 --- a/Sources/TokamakCore/App/_AnyApp.swift +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2020 Tokamak contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// Created by Carson Katri on 7/19/20. -// - -import OpenCombine - -public struct _AnyApp: App { - var app: Any - let type: Any.Type - let bodyClosure: (Any) -> _AnyScene - let bodyType: Any.Type - - init(_ app: A) { - self.app = app - type = A.self - // swiftlint:disable:next force_cast - bodyClosure = { _AnyScene(($0 as! A).body) } - bodyType = A.Body.self - } - - public var body: Never { - neverScene("_AnyApp") - } - - public init() { - fatalError("`_AnyApp` cannot be initialized without an underlying `App` type.") - } - - public static func _launch(_ app: Self, - _ rootEnvironment: EnvironmentValues) { - fatalError("`_AnyApp` cannot be launched. Access underlying `app` value.") - } - - public static func _setTitle(_ title: String) { - fatalError("`title` cannot be set for `AnyApp`. Access underlying `app` value.") - } - - public var _phasePublisher: CurrentValueSubject { - fatalError("`_AnyApp` cannot monitor scenePhase. Access underlying `app` value.") - } -} diff --git a/Sources/TokamakCore/MountedViews/MountedElement.swift b/Sources/TokamakCore/MountedViews/MountedElement.swift index 2f9a929b3..8713f88e8 100644 --- a/Sources/TokamakCore/MountedViews/MountedElement.swift +++ b/Sources/TokamakCore/MountedViews/MountedElement.swift @@ -67,11 +67,7 @@ public class MountedElement { var elementType: Any.Type { switch element { - case .app: fatalError(""" - `App` values aren't supposed to be reconciled, thus the type constructor name is not stored \ - for `App` elements. Please report this crash as a bug at \ - https://github.com/swiftwasm/Tokamak/issues/new - """) + case .app: return R.AppType.self case let .scene(scene): return scene.type case let .view(view): return view.type } diff --git a/Sources/TokamakCore/Renderer.swift b/Sources/TokamakCore/Renderer.swift index 1923d645f..c64a0aa4f 100644 --- a/Sources/TokamakCore/Renderer.swift +++ b/Sources/TokamakCore/Renderer.swift @@ -31,7 +31,7 @@ public protocol Renderer: AnyObject { Usually a target is a simple view (`UIView` and `NSView` for `UIKit` and `AppKit` respectively). */ - associatedtype TargetType + associatedtype TargetType: Target /// Reconciler instance used by this renderer. var reconciler: StackReconciler? { get } diff --git a/Sources/TokamakCore/Target.swift b/Sources/TokamakCore/Target.swift index d96a484f5..fcd683a70 100644 --- a/Sources/TokamakCore/Target.swift +++ b/Sources/TokamakCore/Target.swift @@ -15,6 +15,6 @@ // Created by Max Desiatov on 10/02/2019. // -protocol Target { +public protocol Target: AnyObject { var view: AnyView { get set } } diff --git a/Sources/TokamakDOM/DOMNode.swift b/Sources/TokamakDOM/DOMNode.swift index a9d44dafe..99c2bd03c 100644 --- a/Sources/TokamakDOM/DOMNode.swift +++ b/Sources/TokamakDOM/DOMNode.swift @@ -18,7 +18,7 @@ import TokamakCore public final class DOMNode: Target { let ref: JSObjectRef private var listeners: [String: JSClosure] - private let view: AnyView? + public var view: AnyView init(_ view: V, _ ref: JSObjectRef, _ listeners: [String: Listener] = [:]) { self.ref = ref @@ -29,6 +29,7 @@ public final class DOMNode: Target { init(_ ref: JSObjectRef) { self.ref = ref + view = AnyView(EmptyView()) listeners = [:] } diff --git a/Sources/TokamakDOM/DOMRenderer.swift b/Sources/TokamakDOM/DOMRenderer.swift index 890898d4d..a3fcf9581 100644 --- a/Sources/TokamakDOM/DOMRenderer.swift +++ b/Sources/TokamakDOM/DOMRenderer.swift @@ -85,7 +85,7 @@ public final class DOMRenderer: Renderer { reconciler = StackReconciler( app: app, - target: DOMNode(app, ref), + target: DOMNode(ref), environment: .defaultEnvironment, renderer: self, scheduler: timeoutScheduler diff --git a/Sources/TokamakDOM/Views/HTML.swift b/Sources/TokamakDOM/Views/HTML.swift index 9685055b4..366cea43d 100644 --- a/Sources/TokamakDOM/Views/HTML.swift +++ b/Sources/TokamakDOM/Views/HTML.swift @@ -37,7 +37,7 @@ extension AnyHTML { """ } - func update(dom: DOMNode) { + func update(dom: DOMNode) { // FIXME: is there a sensible way to diff attributes and listeners to avoid // crossing the JavaScript bridge and touching DOM if not needed? From 7ca57349a0995c5c0e52cb282b4f0970e922f3c8 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Mon, 28 Sep 2020 18:30:08 +0100 Subject: [PATCH 13/13] Update Sources/TokamakCore/MountedViews/MountedApp.swift Co-authored-by: Jed Fox --- Sources/TokamakCore/MountedViews/MountedApp.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/TokamakCore/MountedViews/MountedApp.swift b/Sources/TokamakCore/MountedViews/MountedApp.swift index 419551fc5..c479a8390 100644 --- a/Sources/TokamakCore/MountedViews/MountedApp.swift +++ b/Sources/TokamakCore/MountedViews/MountedApp.swift @@ -47,7 +47,7 @@ final class MountedApp: MountedCompositeElement { reconciler.reconcile( self, with: element, - getElementType: { type(of: $0) }, + getElementType: type(of:), updateChild: { $0.environmentValues = environmentValues $0.scene = _AnyScene(element)