Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace ViewDeferredToRenderer, fix renderer tests #408

Merged
merged 11 commits into from
Jun 7, 2021
13 changes: 4 additions & 9 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
),
]
)
13 changes: 9 additions & 4 deletions Sources/TokamakCore/MountedViews/MountedApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ final class MountedApp<R: Renderer>: MountedCompositeElement<R> {
// They also have no parents, so the `parent` argument is discarded as well.
let childBody = reconciler.render(mountedApp: self)

let child: MountedElement<R> = mountChild(childBody)
let child: MountedElement<R> = mountChild(reconciler.renderer, childBody)
Copy link
Collaborator Author

@MaxDesiatov MaxDesiatov Jun 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renderer instances now need to be explicitly passed through in a lot more places. This allows mounted elements to call back to the renderer to determine whether a given view is primitive for this renderer.

mountedChildren = [child]
child.mount(before: nil, on: self, with: reconciler)
}
Expand All @@ -39,9 +39,14 @@ final class MountedApp<R: Renderer>: MountedCompositeElement<R> {
mountedChildren.forEach { $0.unmount(with: reconciler) }
}

private func mountChild(_ childBody: _AnyScene) -> MountedElement<R> {
/// 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<R> {
let mountedScene: MountedScene<R> = 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)
Expand All @@ -59,7 +64,7 @@ final class MountedApp<R: Renderer>: MountedCompositeElement<R> {
$0.environmentValues = environmentValues
$0.scene = _AnyScene(element)
},
mountChild: { mountChild($0) }
mountChild: { mountChild(reconciler.renderer, $0) }
)
}
}
10 changes: 7 additions & 3 deletions Sources/TokamakCore/MountedViews/MountedCompositeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
let childBody = reconciler.render(compositeView: self)

let child: MountedElement<R> = childBody.makeMountedView(
reconciler.renderer,
parentTarget,
environmentValues,
self
)
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<R>` instance.
Expand All @@ -51,7 +53,7 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
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 {
Expand Down Expand Up @@ -89,7 +91,9 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
$0.environmentValues = environmentValues
$0.view = AnyView(element)
},
mountChild: { $0.makeMountedView(parentTarget, environmentValues, self) }
mountChild: {
$0.makeMountedView(reconciler.renderer, parentTarget, environmentValues, self)
}
)
}
}
3 changes: 2 additions & 1 deletion Sources/TokamakCore/MountedViews/MountedElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -222,13 +222,14 @@ extension TypeInfo {

extension AnyView {
func makeMountedView<R: Renderer>(
_ renderer: R,
_ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues,
_ parent: MountedElement<R>?
) -> MountedElement<R> {
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)
Expand Down
21 changes: 14 additions & 7 deletions Sources/TokamakCore/MountedViews/MountedHostView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
on parent: MountedElement<R>? = nil,
with reconciler: StackReconciler<R>
) {
guard let target = reconciler.renderer?.mountTarget(
guard let target = reconciler.renderer.mountTarget(
before: sibling,
to: parentTarget,
with: self
Expand All @@ -57,7 +57,7 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
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
Expand All @@ -74,7 +74,7 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
override func unmount(with reconciler: StackReconciler<R>) {
guard let target = target else { return }

reconciler.renderer?.unmount(
reconciler.renderer.unmount(
target: target,
from: parentTarget,
with: self
Expand All @@ -88,7 +88,7 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {

updateEnvironment()
target.view = view
reconciler.renderer?.update(target: target, with: self)
reconciler.renderer.update(target: target, with: self)

var childrenViews = view.children

Expand All @@ -101,7 +101,9 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {

// 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
Expand All @@ -124,7 +126,12 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
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)
}
Expand All @@ -144,7 +151,7 @@ public final class MountedHostView<R: Renderer>: MountedElement<R> {
// mount remaining views
for firstChild in childrenViews {
let newChild: MountedElement<R> =
firstChild.makeMountedView(target, environmentValues, self)
firstChild.makeMountedView(reconciler.renderer, target, environmentValues, self)
newChild.mount(on: self, with: reconciler)
newChildren.append(newChild)
}
Expand Down
21 changes: 15 additions & 6 deletions Sources/TokamakCore/MountedViews/MountedScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ final class MountedScene<R: Renderer>: MountedCompositeElement<R> {
let childBody = reconciler.render(mountedScene: self)

let child: MountedElement<R> = childBody
.makeMountedElement(parentTarget, environmentValues, self)
.makeMountedElement(reconciler.renderer, parentTarget, environmentValues, self)
mountedChildren = [child]
child.mount(before: sibling, on: self, with: reconciler)
}
Expand All @@ -60,7 +60,9 @@ final class MountedScene<R: Renderer>: MountedCompositeElement<R> {
$0.view = AnyView(view)
}
},
mountChild: { $0.makeMountedElement(parentTarget, environmentValues, self) }
mountChild: {
$0.makeMountedElement(reconciler.renderer, parentTarget, environmentValues, self)
}
)
}
}
Expand All @@ -76,21 +78,23 @@ extension _AnyScene.BodyResult {
}

func makeMountedElement<R: Renderer>(
_ renderer: R,
_ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues,
_ parent: MountedElement<R>?
) -> MountedElement<R> {
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<R: Renderer>(
_ renderer: R,
_ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues,
_ parent: MountedElement<R>?
Expand All @@ -104,11 +108,16 @@ extension _AnyScene {
let children: [MountedElement<R>]
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 = []
Expand Down
10 changes: 10 additions & 0 deletions Sources/TokamakCore/Renderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion Sources/TokamakCore/Shapes/Shape.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public struct FillStyle: Equatable, ShapeStyle {
}
}

public struct _ShapeView<Content, Style>: PrimitiveView where Content: Shape, Style: ShapeStyle {
public struct _ShapeView<Content, Style>: _PrimitiveView where Content: Shape, Style: ShapeStyle {
@Environment(\.self) public var environment
@Environment(\.foregroundColor) public var foregroundColor
public var shape: Content
Expand Down
35 changes: 20 additions & 15 deletions Sources/TokamakCore/StackReconciler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -52,7 +52,7 @@ public final class StackReconciler<R: Renderer> {
/** 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
Expand All @@ -73,7 +73,7 @@ public final class StackReconciler<R: Renderer> {
self.scheduler = scheduler
rootTarget = target

rootElement = AnyView(view).makeMountedView(target, environment, nil)
rootElement = AnyView(view).makeMountedView(renderer, target, environment, nil)

performInitialMount()
}
Expand Down Expand Up @@ -201,45 +201,50 @@ public final class StackReconciler<R: Renderer> {
}.store(in: &mountedApp.persistentSubscriptions)
}

private func render<T>(
compositeElement: MountedCompositeElement<R>,
body bodyKeypath: ReferenceWritableKeyPath<MountedCompositeElement<R>, Any>,
result: KeyPath<MountedCompositeElement<R>, (Any) -> T>
) -> T {
private func body(
of compositeElement: MountedCompositeElement<R>,
keyPath: ReferenceWritableKeyPath<MountedCompositeElement<R>, 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<R>) -> 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<R>) -> _AnyScene {
render(compositeElement: mountedApp, body: \.app.app, result: \.app.bodyClosure)
mountedApp.app.bodyClosure(body(of: mountedApp, keyPath: \.app.app))
}

func render(mountedScene: MountedScene<R>) -> _AnyScene.BodyResult {
render(compositeElement: mountedScene, body: \.scene.scene, result: \.scene.bodyClosure)
mountedScene.scene.bodyClosure(body(of: mountedScene, keyPath: \.scene.scene))
}

func reconcile<Element>(
Expand Down
9 changes: 7 additions & 2 deletions Sources/TokamakCore/State/TargetRef.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<V: View, T>: View, TargetRefType {
let binding: Binding<T?>

Expand All @@ -31,8 +35,9 @@ public struct _TargetRef<V: View, T>: 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<T: Target>(_ binding: Binding<T?>) -> _TargetRef<Self, T> {
.init(binding: binding, view: self)
Expand Down
2 changes: 1 addition & 1 deletion Sources/TokamakCore/Styles/ButtonStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Loading