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:
parent
66248448ab
commit
f5af009db2
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]()
|
||||
|
|
Loading…
Reference in New Issue