Tokamak/Sources/TokamakCore/MountedViews/MountedHostView.swift

212 lines
7.3 KiB
Swift

// Copyright 2018-2021 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Max Desiatov on 03/12/2018.
//
/* A representation of a `View`, which has a `body` of type `Never`, stored in the tree of mounted
views by `StackReconciler`.
*/
public final class MountedHostView<R: Renderer>: MountedElement<R> {
/** Target of a closest ancestor host view. As a parent of this view
might not be a host view, but a composite view, we need to pass
around the target of a host view to its closests descendant host
views. Thus, a parent target is not always the same as a target of
a parent `View`. */
private let parentTarget: R.TargetType
/// Target of this host view supplied by a renderer after mounting has completed.
private(set) var target: R.TargetType?
init(
_ view: AnyView,
_ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues,
_ viewTraits: _ViewTraitStore,
_ parent: MountedElement<R>?
) {
self.parentTarget = parentTarget
super.init(view, environmentValues, viewTraits, parent)
}
override func mount(
before sibling: R.TargetType? = nil,
on parent: MountedElement<R>? = nil,
in reconciler: StackReconciler<R>,
with transaction: Transaction
) {
super.prepareForMount(with: transaction)
self.transaction = transaction
guard let target = reconciler.renderer.mountTarget(
before: sibling,
to: parentTarget,
with: self
)
else { return }
self.target = target
guard !view.children.isEmpty else { return }
let isGroupView = view.type is GroupView.Type
// Don't allow children to transition their mounting since they aren't individually
// appearing (unless its a `GroupView`, which is flattened).
mountedChildren = view.children.map {
$0.makeMountedView(
reconciler.renderer,
target,
environmentValues,
isGroupView ? self.viewTraits : .init(),
self
)
}
/* Remember that `GroupView`s are always "flattened", their `target` instances are targets of
their parent elements. We need the insertion "cursor" `sibling` to be preserved when children
are mounted in that case. Thus pass the `sibling` target to the children if `view` is a
`GroupView`.
*/
mountedChildren.forEach {
$0.mount(before: isGroupView ? sibling : nil, on: self, in: reconciler, with: transaction)
}
super.mount(before: sibling, on: parent, in: reconciler, with: transaction)
}
private var parentUnmountTask = UnmountTask<R>()
override func unmount(
in reconciler: StackReconciler<R>,
with transaction: Transaction,
parentTask: UnmountTask<R>?
) {
super.unmount(in: reconciler, with: transaction, parentTask: parentTask)
guard let target = target else { return }
let task = UnmountHostTask(self, in: reconciler) {
self.mountedChildren.forEach {
$0.unmount(in: reconciler, with: transaction, parentTask: self.unmountTask)
}
}
task.isCancelled = parentTask?.isCancelled ?? false
unmountTask = task
parentTask?.childTasks.append(task)
reconciler.renderer.unmount(
target: target,
from: parentTarget,
with: task
)
}
/// Stop any unfinished unmounts and complete them without transitions.
private func invalidateUnmount() {
parentUnmountTask.cancel()
parentUnmountTask.completeImmediately()
parentUnmountTask = .init()
}
override func update(in reconciler: StackReconciler<R>, with transaction: Transaction) {
guard let target = target else { return }
invalidateUnmount()
updateEnvironment()
target.view = view
reconciler.renderer.update(target: target, with: self)
var childrenViews = view.children
let traits = view.type is GroupView.Type ? viewTraits : .init()
switch (mountedChildren.isEmpty, childrenViews.isEmpty) {
// if existing children present and new children array is empty
// then unmount all existing children
case (false, true):
mountedChildren.forEach {
$0.unmount(in: reconciler, with: transaction, parentTask: self.parentUnmountTask)
}
mountedChildren = []
// if no existing children then mount all new children
case (true, false):
mountedChildren = childrenViews.map {
$0.makeMountedView(reconciler.renderer, target, environmentValues, traits, self)
}
mountedChildren.forEach { $0.mount(on: self, in: reconciler, with: transaction) }
// if both arrays have items then reconcile by types and keys
case (false, false):
var newChildren = [MountedElement<R>]()
// iterate through every `mountedChildren` element and compare with
// a corresponding `childrenViews` element, remount if type differs, otherwise
// run simple update
while let mountedChild = mountedChildren.first, let childView = childrenViews.first {
let newChild: MountedElement<R>
if childView.typeConstructorName == mountedChildren[0].view.typeConstructorName {
mountedChild.environmentValues = environmentValues
mountedChild.view = childView
mountedChild.updateEnvironment()
mountedChild.update(in: reconciler, with: transaction)
newChild = mountedChild
} else {
/* note the order of operations here: we mount the new child first, use the mounted child
as a "cursor" sibling when mounting. Only then we can dispose of the old mounted child
by unmounting it.
*/
newChild = childView.makeMountedView(
reconciler.renderer,
target,
environmentValues,
traits,
self
)
newChild.mount(
before: mountedChild.firstDescendantTarget, on: self, in: reconciler, with: transaction
)
mountedChild.unmount(in: reconciler, with: transaction, parentTask: parentUnmountTask)
}
newChildren.append(newChild)
mountedChildren.removeFirst()
childrenViews.removeFirst()
}
// more mounted views left than views were to be rendered:
// unmount remaining `mountedChildren`
if !mountedChildren.isEmpty {
for child in mountedChildren {
child.unmount(in: reconciler, with: transaction, parentTask: parentUnmountTask)
}
} else {
// more views left than children were mounted,
// mount remaining views
for firstChild in childrenViews {
let newChild: MountedElement<R> =
firstChild.makeMountedView(reconciler.renderer, target, environmentValues, traits, self)
newChild.mount(on: self, in: reconciler, with: transaction)
newChildren.append(newChild)
}
}
mountedChildren = newChildren
// both arrays are empty, nothing to reconcile
case (true, true): ()
}
}
}