Unify code of `MountedApp`/`MountedCompositeView` (#219)

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.
This commit is contained in:
Max Desiatov 2020-07-28 18:01:29 +01:00 committed by GitHub
parent 66248448ab
commit f5af009db2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 83 additions and 99 deletions

View File

@ -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

View File

@ -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 {

View File

@ -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) }
)
}
}
@ -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)

View File

@ -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)

View File

@ -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)
}
@ -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) }
)
}
}

View File

@ -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 {
@ -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
}
}
@ -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.")
}
}
}

View File

@ -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)
}
}
}
}

View File

@ -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]()