Skip to content

Commit

Permalink
Unify code of MountedApp/MountedCompositeView (#219)
Browse files Browse the repository at this point in the history
We currently have the reconciler code duplicated in these types. I also have a draft `MountedScene` implementation, which most probably would rely on the same reconcilliation algorithm. In this PR it's made generic and can be shared across these types of mounted elements.
  • Loading branch information
MaxDesiatov authored Jul 28, 2020
1 parent 6624844 commit f5af009
Show file tree
Hide file tree
Showing 8 changed files with 83 additions and 99 deletions.
4 changes: 2 additions & 2 deletions Sources/TokamakCore/App/_AnyApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<A: App>(_ 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
Expand Down
4 changes: 2 additions & 2 deletions Sources/TokamakCore/App/_AnyScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@

public struct _AnyScene: Scene {
let scene: Any
let sceneType: Any.Type
let type: Any.Type

init<S: Scene>(_ scene: S) {
self.scene = scene
sceneType = S.self
type = S.self
}

public var body: Never {
Expand Down
54 changes: 11 additions & 43 deletions Sources/TokamakCore/MountedViews/MountedApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,57 +34,25 @@ final class MountedApp<R: Renderer>: MountedCompositeElement<R> {
mountedChildren.forEach { $0.unmount(with: reconciler) }
}

func mountChild<S: Scene>(_ childBody: S) -> MountedElement<R> {
private func mountChild<S: Scene>(_ childBody: S) -> MountedElement<R> {
let mountedScene: MountedScene<R> = 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<R>) {
// 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<R> = 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<R> = 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) }
)
}
}

Expand All @@ -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)
Expand Down
14 changes: 8 additions & 6 deletions Sources/TokamakCore/MountedViews/MountedCompositeElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,19 @@ class MountedCompositeElement<R: Renderer>: MountedElement<R>, 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)
Expand Down
48 changes: 9 additions & 39 deletions Sources/TokamakCore/MountedViews/MountedCompositeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
appearanceAction.appear?()
}

let child: MountedElement<R> = childBody.makeMountedView(parentTarget,
environmentValues)
let child: MountedElement<R> = childBody.makeMountedView(parentTarget, environmentValues)
mountedChildren = [child]
child.mount(with: reconciler)
}
Expand All @@ -41,42 +40,13 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
}

override func update(with reconciler: StackReconciler<R>) {
// 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<R> = 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<R> = 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) }
)
}
}
8 changes: 4 additions & 4 deletions Sources/TokamakCore/MountedViews/MountedElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ enum MountedElementKind {
}

public class MountedElement<R: Renderer> {
var element: MountedElementKind
private var element: MountedElementKind

public internal(set) var app: _AnyApp {
get {
Expand Down Expand Up @@ -67,8 +67,8 @@ public class MountedElement<R: Renderer> {

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
}
}
Expand Down Expand Up @@ -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.")
}
}
}
44 changes: 44 additions & 0 deletions Sources/TokamakCore/StackReconciler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,48 @@ public final class StackReconciler<R: Renderer> {
func render(mountedApp: MountedApp<R>) -> some Scene {
render(compositeElement: mountedApp, body: \.app.app, result: \.app.bodyClosure)
}

func reconcile<Element>(
_ mountedElement: MountedCompositeElement<R>,
with element: Element,
getElementType: (Element) -> Any.Type,
updateChild: (MountedElement<R>) -> (),
mountChild: (Element) -> MountedElement<R>
) {
// 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<R> = 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<R> = mountChild(childBody)
mountedElement.mountedChildren = [newMountedChild]
newMountedChild.mount(with: self)
}
}
}
}
6 changes: 3 additions & 3 deletions Sources/TokamakDOM/DOMRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]()
Expand Down

0 comments on commit f5af009

Please sign in to comment.