diff --git a/Package.swift b/Package.swift index 392117ea4..ffaf733c7 100644 --- a/Package.swift +++ b/Package.swift @@ -172,14 +172,9 @@ let package = Package( name: "TokamakTests", dependencies: ["TokamakTestRenderer"] ), - // FIXME: re-enable when `ViewDeferredToRenderer` conformance conflicts issue is resolved - // Currently, when multiple modules that have conflicting `ViewDeferredToRenderer` - // implementations are linked in the same binary, only a single one is used with no defined - // behavior for that. We need to replace `ViewDeferredToRenderer` with a different solution - // that isn't prone to these hard to debug errors. - // .testTarget( - // name: "TokamakStaticHTMLTests", - // dependencies: ["TokamakStaticHTML"] - // ), + .testTarget( + name: "TokamakStaticHTMLTests", + dependencies: ["TokamakStaticHTML"] + ), ] ) diff --git a/Sources/TokamakCore/MountedViews/MountedApp.swift b/Sources/TokamakCore/MountedViews/MountedApp.swift index f85c880d9..73bb90a77 100644 --- a/Sources/TokamakCore/MountedViews/MountedApp.swift +++ b/Sources/TokamakCore/MountedViews/MountedApp.swift @@ -30,7 +30,7 @@ final class MountedApp: MountedCompositeElement { // They also have no parents, so the `parent` argument is discarded as well. let childBody = reconciler.render(mountedApp: self) - let child: MountedElement = mountChild(childBody) + let child: MountedElement = mountChild(reconciler.renderer, childBody) mountedChildren = [child] child.mount(before: nil, on: self, with: reconciler) } @@ -39,9 +39,14 @@ final class MountedApp: MountedCompositeElement { mountedChildren.forEach { $0.unmount(with: reconciler) } } - private func mountChild(_ childBody: _AnyScene) -> MountedElement { + /// Mounts a child scene within the app. + /// - Parameters: + /// - renderer: A instance conforming to the `Renderer` protocol to render the mounted scene with. + /// - childBody: The body of the child scene to mount for this app. + /// - Returns: Returns an instance of the `MountedScene` class that's already mounted in this app. + private func mountChild(_ renderer: R, _ childBody: _AnyScene) -> MountedScene { let mountedScene: MountedScene = childBody - .makeMountedScene(parentTarget, environmentValues, self) + .makeMountedScene(renderer, parentTarget, environmentValues, self) if let title = mountedScene.title { // swiftlint:disable force_cast (app.type as! _TitledApp.Type)._setTitle(title) @@ -59,7 +64,7 @@ final class MountedApp: MountedCompositeElement { $0.environmentValues = environmentValues $0.scene = _AnyScene(element) }, - mountChild: { mountChild($0) } + mountChild: { mountChild(reconciler.renderer, $0) } ) } } diff --git a/Sources/TokamakCore/MountedViews/MountedCompositeView.swift b/Sources/TokamakCore/MountedViews/MountedCompositeView.swift index b5a0e2f18..cac366fee 100644 --- a/Sources/TokamakCore/MountedViews/MountedCompositeView.swift +++ b/Sources/TokamakCore/MountedViews/MountedCompositeView.swift @@ -26,6 +26,7 @@ final class MountedCompositeView: MountedCompositeElement { let childBody = reconciler.render(compositeView: self) let child: MountedElement = childBody.makeMountedView( + reconciler.renderer, parentTarget, environmentValues, self @@ -33,7 +34,8 @@ final class MountedCompositeView: MountedCompositeElement { mountedChildren = [child] child.mount(before: sibling, on: self, with: reconciler) - // `_TargetRef` is a composite view, so it's enough to check for it only here + // `_TargetRef` (and `TargetRefType` generic eraser protocol it conforms to) is a composite view, so it's enough + // to check for it only here. if var targetRef = view.view as? TargetRefType { // `_TargetRef` body is not always a host view that has a target, need to traverse // all descendants to find a `MountedHostView` instance. @@ -51,7 +53,7 @@ final class MountedCompositeView: MountedCompositeElement { reconciler.afterCurrentRender(perform: { [weak self] in guard let self = self else { return } - // FIXME: this has to be implemented in a render-specific way, otherwise it's equivalent to + // FIXME: this has to be implemented in a renderer-specific way, otherwise it's equivalent to // `_onMount` and `_onUnmount` at the moment, // see https://github.com/swiftwasm/Tokamak/issues/175 for more details if let appearanceAction = self.view.view as? AppearanceActionType { @@ -89,7 +91,9 @@ final class MountedCompositeView: MountedCompositeElement { $0.environmentValues = environmentValues $0.view = AnyView(element) }, - mountChild: { $0.makeMountedView(parentTarget, environmentValues, self) } + mountChild: { + $0.makeMountedView(reconciler.renderer, parentTarget, environmentValues, self) + } ) } } diff --git a/Sources/TokamakCore/MountedViews/MountedElement.swift b/Sources/TokamakCore/MountedViews/MountedElement.swift index 95b9b8b44..a32e380fc 100644 --- a/Sources/TokamakCore/MountedViews/MountedElement.swift +++ b/Sources/TokamakCore/MountedViews/MountedElement.swift @@ -222,13 +222,14 @@ extension TypeInfo { extension AnyView { func makeMountedView( + _ renderer: R, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues, _ parent: MountedElement? ) -> MountedElement { if type == EmptyView.self { return MountedEmptyView(self, environmentValues, parent) - } else if bodyType == Never.self && !(type is ViewDeferredToRenderer.Type) { + } else if bodyType == Never.self && !renderer.isPrimitiveView(type) { return MountedHostView(self, parentTarget, environmentValues, parent) } else { return MountedCompositeView(self, parentTarget, environmentValues, parent) diff --git a/Sources/TokamakCore/MountedViews/MountedHostView.swift b/Sources/TokamakCore/MountedViews/MountedHostView.swift index 4b120f5a2..8930c79f0 100644 --- a/Sources/TokamakCore/MountedViews/MountedHostView.swift +++ b/Sources/TokamakCore/MountedViews/MountedHostView.swift @@ -45,7 +45,7 @@ public final class MountedHostView: MountedElement { on parent: MountedElement? = nil, with reconciler: StackReconciler ) { - guard let target = reconciler.renderer?.mountTarget( + guard let target = reconciler.renderer.mountTarget( before: sibling, to: parentTarget, with: self @@ -57,7 +57,7 @@ public final class MountedHostView: MountedElement { guard !view.children.isEmpty else { return } mountedChildren = view.children.map { - $0.makeMountedView(target, environmentValues, self) + $0.makeMountedView(reconciler.renderer, target, environmentValues, self) } /* Remember that `GroupView`s are always "flattened", their `target` instances are targets of @@ -74,7 +74,7 @@ public final class MountedHostView: MountedElement { override func unmount(with reconciler: StackReconciler) { guard let target = target else { return } - reconciler.renderer?.unmount( + reconciler.renderer.unmount( target: target, from: parentTarget, with: self @@ -88,7 +88,7 @@ public final class MountedHostView: MountedElement { updateEnvironment() target.view = view - reconciler.renderer?.update(target: target, with: self) + reconciler.renderer.update(target: target, with: self) var childrenViews = view.children @@ -101,7 +101,9 @@ public final class MountedHostView: MountedElement { // if no existing children then mount all new children case (true, false): - mountedChildren = childrenViews.map { $0.makeMountedView(target, environmentValues, self) } + mountedChildren = childrenViews.map { + $0.makeMountedView(reconciler.renderer, target, environmentValues, self) + } mountedChildren.forEach { $0.mount(on: self, with: reconciler) } // if both arrays have items then reconcile by types and keys @@ -124,7 +126,12 @@ public final class MountedHostView: MountedElement { as a "cursor" sibling when mounting. Only then we can dispose of the old mounted child by unmounting it. */ - newChild = childView.makeMountedView(target, environmentValues, self) + newChild = childView.makeMountedView( + reconciler.renderer, + target, + environmentValues, + self + ) newChild.mount(before: mountedChild.firstDescendantTarget, on: self, with: reconciler) mountedChild.unmount(with: reconciler) } @@ -144,7 +151,7 @@ public final class MountedHostView: MountedElement { // mount remaining views for firstChild in childrenViews { let newChild: MountedElement = - firstChild.makeMountedView(target, environmentValues, self) + firstChild.makeMountedView(reconciler.renderer, target, environmentValues, self) newChild.mount(on: self, with: reconciler) newChildren.append(newChild) } diff --git a/Sources/TokamakCore/MountedViews/MountedScene.swift b/Sources/TokamakCore/MountedViews/MountedScene.swift index 64c3173f0..b73ad3bb3 100644 --- a/Sources/TokamakCore/MountedViews/MountedScene.swift +++ b/Sources/TokamakCore/MountedViews/MountedScene.swift @@ -36,7 +36,7 @@ final class MountedScene: MountedCompositeElement { let childBody = reconciler.render(mountedScene: self) let child: MountedElement = childBody - .makeMountedElement(parentTarget, environmentValues, self) + .makeMountedElement(reconciler.renderer, parentTarget, environmentValues, self) mountedChildren = [child] child.mount(before: sibling, on: self, with: reconciler) } @@ -60,7 +60,9 @@ final class MountedScene: MountedCompositeElement { $0.view = AnyView(view) } }, - mountChild: { $0.makeMountedElement(parentTarget, environmentValues, self) } + mountChild: { + $0.makeMountedElement(reconciler.renderer, parentTarget, environmentValues, self) + } ) } } @@ -76,21 +78,23 @@ extension _AnyScene.BodyResult { } func makeMountedElement( + _ renderer: R, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues, _ parent: MountedElement? ) -> MountedElement { switch self { case let .scene(scene): - return scene.makeMountedScene(parentTarget, environmentValues, parent) + return scene.makeMountedScene(renderer, parentTarget, environmentValues, parent) case let .view(view): - return view.makeMountedView(parentTarget, environmentValues, parent) + return view.makeMountedView(renderer, parentTarget, environmentValues, parent) } } } extension _AnyScene { func makeMountedScene( + _ renderer: R, _ parentTarget: R.TargetType, _ environmentValues: EnvironmentValues, _ parent: MountedElement? @@ -104,11 +108,16 @@ extension _AnyScene { let children: [MountedElement] if let deferredScene = scene as? SceneDeferredToRenderer { children = [ - deferredScene.deferredBody.makeMountedView(parentTarget, environmentValues, parent), + deferredScene.deferredBody.makeMountedView( + renderer, + parentTarget, + environmentValues, + parent + ), ] } else if let groupScene = scene as? GroupScene { children = groupScene.children.map { - $0.makeMountedScene(parentTarget, environmentValues, parent) + $0.makeMountedScene(renderer, parentTarget, environmentValues, parent) } } else { children = [] diff --git a/Sources/TokamakCore/Renderer.swift b/Sources/TokamakCore/Renderer.swift index eb59c33ab..c2fa959d1 100644 --- a/Sources/TokamakCore/Renderer.swift +++ b/Sources/TokamakCore/Renderer.swift @@ -66,4 +66,14 @@ public protocol Renderer: AnyObject { with host: MountedHost, completion: @escaping () -> () ) + + /** Returns a body of a given pritimive view, or `nil` if `view` is not a primitive view for + this renderer. + */ + func primitiveBody(for view: Any) -> AnyView? + + /** Returns `true` if a given view type is a primitive view that should be deferred to this + renderer. + */ + func isPrimitiveView(_ type: Any.Type) -> Bool } diff --git a/Sources/TokamakCore/Shapes/Shape.swift b/Sources/TokamakCore/Shapes/Shape.swift index 214efdf51..7b7bcb8e4 100644 --- a/Sources/TokamakCore/Shapes/Shape.swift +++ b/Sources/TokamakCore/Shapes/Shape.swift @@ -46,7 +46,7 @@ public struct FillStyle: Equatable, ShapeStyle { } } -public struct _ShapeView: PrimitiveView where Content: Shape, Style: ShapeStyle { +public struct _ShapeView: _PrimitiveView where Content: Shape, Style: ShapeStyle { @Environment(\.self) public var environment @Environment(\.foregroundColor) public var foregroundColor public var shape: Content diff --git a/Sources/TokamakCore/StackReconciler.swift b/Sources/TokamakCore/StackReconciler.swift index 6adb043f7..b14310443 100644 --- a/Sources/TokamakCore/StackReconciler.swift +++ b/Sources/TokamakCore/StackReconciler.swift @@ -18,7 +18,7 @@ import CombineShim /** 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 + all coming from `body` or `renderedBody` 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. @@ -52,7 +52,7 @@ public final class StackReconciler { /** 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? + private(set) unowned var renderer: R /** 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 @@ -73,7 +73,7 @@ public final class StackReconciler { self.scheduler = scheduler rootTarget = target - rootElement = AnyView(view).makeMountedView(target, environment, nil) + rootElement = AnyView(view).makeMountedView(renderer, target, environment, nil) performInitialMount() } @@ -201,45 +201,50 @@ public final class StackReconciler { }.store(in: &mountedApp.persistentSubscriptions) } - private func render( - compositeElement: MountedCompositeElement, - body bodyKeypath: ReferenceWritableKeyPath, Any>, - result: KeyPath, (Any) -> T> - ) -> T { + private func body( + of compositeElement: MountedCompositeElement, + keyPath: ReferenceWritableKeyPath, Any> + ) -> Any { compositeElement.updateEnvironment() if let info = typeInfo(of: compositeElement.type) { var stateIdx = 0 let dynamicProps = info.dynamicProperties( &compositeElement.environmentValues, - source: &compositeElement[keyPath: bodyKeypath] + source: &compositeElement[keyPath: keyPath] ) compositeElement.transientSubscriptions = [] for property in dynamicProps { // Setup state/subscriptions if property.type is ValueStorage.Type { - setupStorage(id: stateIdx, for: property, of: compositeElement, body: bodyKeypath) + setupStorage(id: stateIdx, for: property, of: compositeElement, body: keyPath) stateIdx += 1 } if property.type is ObservedProperty.Type { - setupTransientSubscription(for: property, of: compositeElement, body: bodyKeypath) + setupTransientSubscription(for: property, of: compositeElement, body: keyPath) } } } - return compositeElement[keyPath: result](compositeElement[keyPath: bodyKeypath]) + return compositeElement[keyPath: keyPath] } func render(compositeView: MountedCompositeView) -> AnyView { - render(compositeElement: compositeView, body: \.view.view, result: \.view.bodyClosure) + let view = body(of: compositeView, keyPath: \.view.view) + + guard let renderedBody = renderer.primitiveBody(for: view) else { + return compositeView.view.bodyClosure(view) + } + + return renderedBody } func render(mountedApp: MountedApp) -> _AnyScene { - render(compositeElement: mountedApp, body: \.app.app, result: \.app.bodyClosure) + mountedApp.app.bodyClosure(body(of: mountedApp, keyPath: \.app.app)) } func render(mountedScene: MountedScene) -> _AnyScene.BodyResult { - render(compositeElement: mountedScene, body: \.scene.scene, result: \.scene.bodyClosure) + mountedScene.scene.bodyClosure(body(of: mountedScene, keyPath: \.scene.scene)) } func reconcile( diff --git a/Sources/TokamakCore/State/TargetRef.swift b/Sources/TokamakCore/State/TargetRef.swift index f333b1139..de812cfe3 100644 --- a/Sources/TokamakCore/State/TargetRef.swift +++ b/Sources/TokamakCore/State/TargetRef.swift @@ -12,10 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +/// A helper protocol for erasing generic parameters of the `_TargetRef` type. protocol TargetRefType { var target: Target? { get set } } +/** Allows capturing target instance of aclosest descendant host view. The resulting instance + is written to a given `binding`. The actual assignment to this binding is done within the + `MountedCompositeView` implementation. */ public struct _TargetRef: View, TargetRefType { let binding: Binding @@ -31,8 +35,9 @@ public struct _TargetRef: View, TargetRefType { } public extension View { - /** Allows capturing target instance of aclosest descendant host view. The resulting instance - is written to a given `binding`. */ + /** A modifier that returns a `_TargetRef` value, which captures a target instance of a + closest descendant host view. + The resulting instance is written to a given `binding`. */ @_spi(TokamakCore) func _targetRef(_ binding: Binding) -> _TargetRef { .init(binding: binding, view: self) diff --git a/Sources/TokamakCore/Styles/ButtonStyle.swift b/Sources/TokamakCore/Styles/ButtonStyle.swift index d3826e9d8..074f1fe05 100644 --- a/Sources/TokamakCore/Styles/ButtonStyle.swift +++ b/Sources/TokamakCore/Styles/ButtonStyle.swift @@ -15,7 +15,7 @@ // Created by Gene Z. Ragan on 07/22/2020. public struct ButtonStyleConfiguration { - public struct Label: PrimitiveView { + public struct Label: _PrimitiveView { let content: AnyView } diff --git a/Sources/TokamakCore/Tokens/Color.swift b/Sources/TokamakCore/Tokens/Color.swift index f36dd689c..eb9227a51 100644 --- a/Sources/TokamakCore/Tokens/Color.swift +++ b/Sources/TokamakCore/Tokens/Color.swift @@ -299,7 +299,7 @@ public extension Color { static let secondary: Self = .init(systemColor: .secondary) static let accentColor: Self = .init(_EnvironmentDependentColorBox { - ($0.accentColor ?? Self.blue) + $0.accentColor ?? Self.blue }) init(_ color: UIColor) { diff --git a/Sources/TokamakCore/Tokens/TextAlignment.swift b/Sources/TokamakCore/Tokens/TextAlignment.swift index 7859b2820..d2ba4ea71 100644 --- a/Sources/TokamakCore/Tokens/TextAlignment.swift +++ b/Sources/TokamakCore/Tokens/TextAlignment.swift @@ -17,8 +17,8 @@ public enum TextAlignment: Hashable, CaseIterable { case leading, - center, - trailing + center, + trailing } extension EnvironmentValues { diff --git a/Sources/TokamakCore/Views/AnyView.swift b/Sources/TokamakCore/Views/AnyView.swift index 3d0cb6db3..47e0263a5 100644 --- a/Sources/TokamakCore/Views/AnyView.swift +++ b/Sources/TokamakCore/Views/AnyView.swift @@ -16,7 +16,7 @@ // /// A type-erased view. -public struct AnyView: PrimitiveView { +public struct AnyView: _PrimitiveView { /// The type of the underlying `view`. let type: Any.Type @@ -50,21 +50,8 @@ public struct AnyView: PrimitiveView { bodyType = V.Body.self self.view = view - if view is ViewDeferredToRenderer { - bodyClosure = { - let deferredView: Any - if let opt = $0 as? AnyOptional, let value = opt.value { - deferredView = value - } else { - deferredView = $0 - } - // swiftlint:disable:next force_cast - return (deferredView as! ViewDeferredToRenderer).deferredBody - } - } else { - // swiftlint:disable:next force_cast - bodyClosure = { AnyView(($0 as! V).body) } - } + // swiftlint:disable:next force_cast + bodyClosure = { AnyView(($0 as! V).body) } } } } diff --git a/Sources/TokamakCore/Views/Buttons/Button.swift b/Sources/TokamakCore/Views/Buttons/Button.swift index f5800731a..d00d46219 100644 --- a/Sources/TokamakCore/Views/Buttons/Button.swift +++ b/Sources/TokamakCore/Views/Buttons/Button.swift @@ -48,7 +48,7 @@ public struct Button