Skip to content
New issue

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

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

Already on GitHub? # to your account

Add View Traits and transitions #426

Merged
merged 22 commits into from
Jul 28, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7e1296a
Add view traits and transitions
carson-katri Jul 10, 2021
6cba0f2
Merge branch 'main' of https://github.com/swiftwasm/Tokamak into tran…
carson-katri Jul 13, 2021
ad6b8a5
Working animated transitions
carson-katri Jul 14, 2021
ebdc5cc
Working insertion/removal transitions and trait keys
carson-katri Jul 17, 2021
ec557c0
Add license and cleanup main.swift
carson-katri Jul 17, 2021
6f614b3
Cleanup
carson-katri Jul 18, 2021
b84d127
Refactor
carson-katri Jul 18, 2021
b054b2b
Keep track of TransitionPhase to only transition root mounts/unmounts
carson-katri Jul 19, 2021
2b0482a
Fix function lengths
carson-katri Jul 19, 2021
e0607e9
Prevent duplicate elements on quick unmount/mount
carson-katri Jul 20, 2021
afc8caf
Fix GTK build
carson-katri Jul 23, 2021
f22fada
Fix unowned crash
carson-katri Jul 23, 2021
e3dda95
Fix CanTransitionTraitKey being true when no animation is linked to t…
carson-katri Jul 24, 2021
7b9424a
Fix slide-in
carson-katri Jul 24, 2021
ed7aa63
Remove recursion from UnmountTask
carson-katri Jul 24, 2021
da1aef8
Fix stack buffer overflow
carson-katri Jul 24, 2021
f859f57
Revise transition behavior
carson-katri Jul 24, 2021
3ffadfb
Revise transition behavior
carson-katri Jul 24, 2021
6fc10d8
Fix Package.swift
carson-katri Jul 24, 2021
0613990
Merge branch 'main' of https://github.com/TokamakUI/Tokamak into tran…
carson-katri Jul 25, 2021
2d5c80b
Allow root animation transitions to run when transaction.animation ==…
carson-katri Jul 26, 2021
c1e7f25
Attempt resolving linter warnings
carson-katri Jul 28, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions NativeDemo/TokamakDemo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
/* Begin PBXBuildFile section */
207C05702610E16E00BBBE54 /* DatePickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 207C056F2610E16E00BBBE54 /* DatePickerDemo.swift */; };
207C05712610E16E00BBBE54 /* DatePickerDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 207C056F2610E16E00BBBE54 /* DatePickerDemo.swift */; };
26136823269E8EB5006F372E /* TransitionDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26136822269E8EB5006F372E /* TransitionDemo.swift */; };
26136824269E8EB5006F372E /* TransitionDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26136822269E8EB5006F372E /* TransitionDemo.swift */; };
262DA7B32695D99500CABEAE /* ShapeStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 262DA7B22695D99500CABEAE /* ShapeStyleDemo.swift */; };
262DA7B42695D99500CABEAE /* ShapeStyleDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 262DA7B22695D99500CABEAE /* ShapeStyleDemo.swift */; };
26A3BFB0269BD18A0004DA16 /* AnimationDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26A3BFAF269BD18A0004DA16 /* AnimationDemo.swift */; };
Expand Down Expand Up @@ -101,6 +103,7 @@

/* Begin PBXFileReference section */
207C056F2610E16E00BBBE54 /* DatePickerDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DatePickerDemo.swift; sourceTree = "<group>"; };
26136822269E8EB5006F372E /* TransitionDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransitionDemo.swift; sourceTree = "<group>"; };
262DA7B22695D99500CABEAE /* ShapeStyleDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShapeStyleDemo.swift; sourceTree = "<group>"; };
26A3BFAF269BD18A0004DA16 /* AnimationDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimationDemo.swift; sourceTree = "<group>"; };
3DCDE44324CA6AD400910F17 /* SidebarDemo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SidebarDemo.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -197,6 +200,7 @@
85ED189924AD425E0085DFA0 /* TokamakDemo */ = {
isa = PBXGroup;
children = (
26136822269E8EB5006F372E /* TransitionDemo.swift */,
26A3BFAF269BD18A0004DA16 /* AnimationDemo.swift */,
262DA7B22695D99500CABEAE /* ShapeStyleDemo.swift */,
D120FDDA257E7145008FFBAD /* TextEditorDemo.swift */,
Expand Down Expand Up @@ -375,6 +379,7 @@
B5DBA22B24D509B4003D3347 /* RedactDemo.swift in Sources */,
B56F22E024BC89FD001738DF /* ColorDemo.swift in Sources */,
26A3BFB0269BD18A0004DA16 /* AnimationDemo.swift in Sources */,
26136823269E8EB5006F372E /* TransitionDemo.swift in Sources */,
B51F215024B920B400CF2583 /* PathDemo.swift in Sources */,
85ED18AF24AD425E0085DFA0 /* EnvironmentDemo.swift in Sources */,
85ED18A324AD425E0085DFA0 /* SpacerDemo.swift in Sources */,
Expand Down Expand Up @@ -410,6 +415,7 @@
D1D6B62424D817350041E1D9 /* GeometryReaderDemo.swift in Sources */,
B5DBA22C24D509B4003D3347 /* RedactDemo.swift in Sources */,
26A3BFB1269BD18A0004DA16 /* AnimationDemo.swift in Sources */,
26136824269E8EB5006F372E /* TransitionDemo.swift in Sources */,
B56F22E124BC89FD001738DF /* ColorDemo.swift in Sources */,
B51F215124B920B400CF2583 /* PathDemo.swift in Sources */,
85ED18A424AD425E0085DFA0 /* SpacerDemo.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion Sources/TokamakCore/Animation/Transaction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public struct Transaction {

public init(animation: Animation?) {
self.animation = animation
disablesAnimations = true
disablesAnimations = false
}
}

Expand Down
6 changes: 4 additions & 2 deletions Sources/TokamakCore/Modifiers/ViewModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ public protocol ViewModifier {
func body(content: Content) -> Self.Body
}

public struct _ViewModifier_Content<Modifier>: View where Modifier: ViewModifier {
public struct _ViewModifier_Content<Modifier>: View
where Modifier: ViewModifier
{
public let modifier: Modifier
public let view: AnyView

Expand All @@ -27,7 +29,7 @@ public struct _ViewModifier_Content<Modifier>: View where Modifier: ViewModifier
self.view = view
}

public var body: AnyView {
public var body: some View {
view
}
}
Expand Down
24 changes: 18 additions & 6 deletions Sources/TokamakCore/MountedViews/MountedApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,33 @@ import OpenCombineShim
// `View`s
final class MountedApp<R: Renderer>: MountedCompositeElement<R> {
override func mount(
before _: R.TargetType? = nil,
on _: MountedElement<R>? = nil,
with reconciler: StackReconciler<R>
before sibling: R.TargetType? = nil,
on parent: MountedElement<R>? = nil,
in reconciler: StackReconciler<R>,
with transaction: Transaction
) {
super.prepareForMount()

// `App` elements have no siblings, hence the `before` argument is discarded.
// They also have no parents, so the `parent` argument is discarded as well.
let childBody = reconciler.render(mountedApp: self)

let child: MountedElement<R> = mountChild(reconciler.renderer, childBody)
mountedChildren = [child]
child.mount(before: nil, on: self, with: reconciler)
child.transaction = transaction
child.mount(before: nil, on: self, in: reconciler, with: transaction)

super.mount(before: sibling, on: parent, in: reconciler, with: transaction)
}

override func unmount(with reconciler: StackReconciler<R>) {
mountedChildren.forEach { $0.unmount(with: reconciler) }
override func unmount(
in reconciler: StackReconciler<R>,
with transaction: Transaction,
parentTask: UnmountTask<R>?
) {
super.unmount(in: reconciler, with: transaction, parentTask: parentTask)
mountedChildren
.forEach { $0.unmount(in: reconciler, with: transaction, parentTask: parentTask) }
}

/// Mounts a child scene within the app.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,11 @@ class MountedCompositeElement<R: Renderer>: MountedElement<R> {
_ view: AnyView,
_ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues,
_ viewTraits: _ViewTraitStore,
_ parent: MountedElement<R>?
) {
self.parentTarget = parentTarget
super.init(view, environmentValues, parent)
super.init(view, environmentValues, viewTraits, parent)
}
}

Expand Down
44 changes: 39 additions & 5 deletions Sources/TokamakCore/MountedViews/MountedCompositeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,32 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
override func mount(
before sibling: R.TargetType? = nil,
on parent: MountedElement<R>? = nil,
with reconciler: StackReconciler<R>
in reconciler: StackReconciler<R>,
with transaction: Transaction
) {
super.prepareForMount()

var transaction = transaction
(view.view as? _TransactionModifierProtocol)?.modifyTransaction(&transaction)
// Disable animations on mount so `animation(_:)` doesn't try to animate
// until the transition finishes.
transaction.disablesAnimations = true
self.transaction = transaction

let childBody = reconciler.render(compositeView: self)

if let traitModifier = view.view as? _TraitWritingModifierProtocol {
traitModifier.modifyViewTraitStore(&viewTraits)
}
let child: MountedElement<R> = childBody.makeMountedView(
reconciler.renderer,
parentTarget,
environmentValues,
viewTraits,
self
)
mountedChildren = [child]
child.mount(before: sibling, on: self, with: reconciler)
child.mount(before: sibling, on: self, in: reconciler, with: transaction)

// `_TargetRef` (and `TargetRefType` generic eraser protocol it conforms to) is a composite
// view, so it's enough check for it only here.
Expand Down Expand Up @@ -73,10 +85,25 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
preferenceReader.preferenceStore(self.preferenceStore)
}
})

super.mount(before: sibling, on: parent, in: reconciler, with: transaction)
}

override func unmount(with reconciler: StackReconciler<R>) {
mountedChildren.forEach { $0.unmount(with: reconciler) }
override func unmount(
in reconciler: StackReconciler<R>,
with transaction: Transaction,
parentTask: UnmountTask<R>?
) {
super.unmount(in: reconciler, with: transaction, parentTask: parentTask)

var transaction = transaction
transaction.disablesAnimations = false
(view.view as? _TransactionModifierProtocol)?.modifyTransaction(&transaction)

mountedChildren.forEach {
$0.viewTraits = self.viewTraits
$0.unmount(in: reconciler, with: transaction, parentTask: parentTask)
}

if let appearanceAction = view.view as? AppearanceActionType {
appearanceAction.disappear?()
Expand All @@ -85,6 +112,7 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {

override func update(in reconciler: StackReconciler<R>, with transaction: Transaction) {
var transaction = transaction
transaction.disablesAnimations = false
(view.view as? _TransactionModifierProtocol)?.modifyTransaction(&transaction)
let element = reconciler.render(compositeView: self)
reconciler.reconcile(
Expand All @@ -98,7 +126,13 @@ final class MountedCompositeView<R: Renderer>: MountedCompositeElement<R> {
$0.transaction = transaction
},
mountChild: {
$0.makeMountedView(reconciler.renderer, parentTarget, environmentValues, self)
$0.makeMountedView(
reconciler.renderer,
parentTarget,
environmentValues,
viewTraits,
self
)
}
)
}
Expand Down
62 changes: 52 additions & 10 deletions Sources/TokamakCore/MountedViews/MountedElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,13 @@ public class MountedElement<R: Renderer> {

var mountedChildren = [MountedElement<R>]()

public internal(set) var transaction: Transaction = .init(animation: nil)
public var transaction: Transaction = .init(animation: nil)
/// Where this element is the process of mounting/unmounting.
var transitionPhase = TransitionPhase.willMount
/// The current `UnmountTask` of this element.
var unmountTask: UnmountTask<R>?

var environmentValues: EnvironmentValues
public internal(set) var environmentValues: EnvironmentValues

unowned var parent: MountedElement<R>?
/// `didSet` on this field propagates the preference changes up the view tree.
Expand All @@ -98,24 +102,34 @@ public class MountedElement<R: Renderer> {
}
}

public internal(set) var viewTraits: _ViewTraitStore
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are these traits somehow different from SwiftUI accessibility traits? I can't find anything else about SwiftUI traits, just wondering what's used as a reference here.

Copy link
Member Author

@carson-katri carson-katri Jul 17, 2021

Choose a reason for hiding this comment

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

SwiftUI has _ViewTraitKey internally, and uses them for transition, onDelete, onMove and some other modifiers (I think drag & drop?). From what I can tell, a trait is data attached to a single view, and in many cases seems to be a way to get the trait out of SwiftUI and into the host platform (for instance, for UITableView swipe actions or NSItemProvider-based drag & drop).

Although I'm really just speculating as to how SwiftUI uses them by looking at the swiftinterface. In Tokamak they are meant to be passed to the nearest host element so renderers can access them.

Copy link
Contributor

Choose a reason for hiding this comment

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

Traits are also used for zIndex(_:) and layoutPriority(_:). I haven't look into whether Tokamak has implemented these modifiers. If they're not implemented, traits would be really helpful.


init(_ app: _AnyApp, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
element = .app(app)
self.parent = parent
self.environmentValues = environmentValues
viewTraits = .init()
updateEnvironment()
}

init(_ scene: _AnyScene, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
element = .scene(scene)
self.parent = parent
self.environmentValues = environmentValues
viewTraits = .init()
updateEnvironment()
}

init(_ view: AnyView, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
init(
_ view: AnyView,
_ environmentValues: EnvironmentValues,
_ viewTraits: _ViewTraitStore,
_ parent: MountedElement<R>?
) {
element = .view(view)
self.parent = parent
self.environmentValues = environmentValues
self.viewTraits = viewTraits
updateEnvironment()
}

Expand All @@ -131,16 +145,43 @@ public class MountedElement<R: Renderer> {
}
}

/// You must call `super.prepareForMount` before all other mounting work.
func prepareForMount() {
// Allow the root of a mount to transition
// (if their parent isn't mounting, then they are the root of the mount).
if parent?.transitionPhase == .normal {
viewTraits.insert(true, forKey: CanTransitionTraitKey.self)
}
}

/// You must call `super.mount` after all other mounting work.
func mount(
before sibling: R.TargetType? = nil,
on parent: MountedElement<R>? = nil,
with reconciler: StackReconciler<R>
in reconciler: StackReconciler<R>,
with transaction: Transaction
) {
fatalError("implement \(#function) in subclass")
// Set the phase to `normal` after finished mounting.
transitionPhase = .normal
}

func unmount(with reconciler: StackReconciler<R>) {
fatalError("implement \(#function) in subclass")
/// You must call `super.unmount` before all other unmounting work.
func unmount(
in reconciler: StackReconciler<R>,
with transaction: Transaction,
parentTask: UnmountTask<R>?
) {
if !(self is MountedHostView<R>) {
unmountTask = parentTask?.appendChild()
}

// Set the phase to `willUnmount` before unmounting.
transitionPhase = .willUnmount
// Allow the root of an unmount to transition
// (if their parent isn't unmounting, then they are the root of the unmount).
if parent?.transitionPhase == .normal {
viewTraits.insert(true, forKey: CanTransitionTraitKey.self)
}
}

func update(in reconciler: StackReconciler<R>, with transaction: Transaction) {
Expand Down Expand Up @@ -228,14 +269,15 @@ extension AnyView {
_ renderer: R,
_ parentTarget: R.TargetType,
_ environmentValues: EnvironmentValues,
_ viewTraits: _ViewTraitStore,
_ parent: MountedElement<R>?
) -> MountedElement<R> {
if type == EmptyView.self {
return MountedEmptyView(self, environmentValues, parent)
return MountedEmptyView(self, environmentValues, viewTraits, parent)
} else if bodyType == Never.self && !renderer.isPrimitiveView(type) {
return MountedHostView(self, parentTarget, environmentValues, parent)
return MountedHostView(self, parentTarget, environmentValues, viewTraits, parent)
} else {
return MountedCompositeView(self, parentTarget, environmentValues, parent)
return MountedCompositeView(self, parentTarget, environmentValues, viewTraits, parent)
}
}
}
16 changes: 13 additions & 3 deletions Sources/TokamakCore/MountedViews/MountedEmptyView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,20 @@ final class MountedEmptyView<R: Renderer>: MountedElement<R> {
override func mount(
before sibling: R.TargetType? = nil,
on parent: MountedElement<R>? = nil,
with reconciler: StackReconciler<R>
) {}
in reconciler: StackReconciler<R>,
with transaction: Transaction
) {
super.prepareForMount()
super.mount(before: sibling, on: parent, in: reconciler, with: transaction)
}

override func unmount(with reconciler: StackReconciler<R>) {}
override func unmount(
in reconciler: StackReconciler<R>,
with transaction: Transaction,
parentTask: UnmountTask<R>?
) {
super.unmount(in: reconciler, with: transaction, parentTask: parentTask)
}

override func update(in reconciler: StackReconciler<R>, with transaction: Transaction?) {}
}
Loading