1
0

HotReloadableView.swift 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. import Foundation
  2. /// A view which attempts to persist the state of its view subtree even
  3. /// when the subtree's structure changes. Uses state serialization (via
  4. /// view graph snapshotting) to persist view state even when a child
  5. /// view's implementation gets swapped out with an implementation from
  6. /// a newly-loaded dylib (this is what makes this useful for hot reloading).
  7. ///
  8. /// Only expected to be used directly by SwiftCrossUI itself or third
  9. /// party libraries extending SwiftCrossUI's hot reloading capabilities.
  10. public struct HotReloadableView: TypeSafeView {
  11. typealias Children = HotReloadableViewChildren
  12. public var body = EmptyView()
  13. var child: any View
  14. public init(_ child: any View) {
  15. self.child = child
  16. }
  17. public init(@ViewBuilder _ child: () -> some View) {
  18. self.child = child()
  19. }
  20. func children<Backend: BaseAppBackend>(
  21. backend: Backend,
  22. snapshots: [ViewGraphSnapshotter.NodeSnapshot]?,
  23. environment: EnvironmentValues
  24. ) -> HotReloadableViewChildren {
  25. let snapshot = snapshots?.count == 1 ? snapshots?.first : nil
  26. return HotReloadableViewChildren(
  27. from: self,
  28. backend: backend,
  29. snapshot: snapshot,
  30. environment: environment
  31. )
  32. }
  33. func asWidget<Backend: BaseAppBackend>(
  34. _ children: HotReloadableViewChildren,
  35. backend: Backend
  36. ) -> Backend.Widget {
  37. backend.createContainer()
  38. }
  39. /// Attempts to update the child. If the initial update succeeds then the child's concrete type
  40. /// hasn't changed and the ViewGraph has handled state persistence on our behalf. Otherwise,
  41. /// we must recreate the child node and swap out our current child widget with the new view's
  42. /// widget. Before displaying the child, we also attempt to transfer a snapshot of the old
  43. /// view graph sub tree's state onto the new view graph sub tree. This is not possible to do
  44. /// perfectly by definition, so if we can't successfully transfer the state of the sub tree
  45. /// we just fall back on the failing view's default state.
  46. func computeLayout<Backend: BaseAppBackend>(
  47. _ widget: Backend.Widget,
  48. children: HotReloadableViewChildren,
  49. proposedSize: ProposedViewSize,
  50. environment: EnvironmentValues,
  51. backend: Backend
  52. ) -> ViewLayoutResult {
  53. var (viewTypeMatched, result) = children.node.computeLayoutWithNewView(
  54. child,
  55. proposedSize,
  56. environment
  57. )
  58. if !viewTypeMatched {
  59. let snapshotter = ViewGraphSnapshotter()
  60. let snapshot = children.node.transform(with: snapshotter)
  61. children.node = ErasedViewGraphNode(
  62. for: child,
  63. backend: backend,
  64. snapshot: snapshot,
  65. environment: environment
  66. )
  67. // We can assume that the view types match since we just recreated the view
  68. // on the line above.
  69. let (_, newResult) = children.node.computeLayoutWithNewView(
  70. child,
  71. proposedSize,
  72. environment
  73. )
  74. result = newResult
  75. children.hasChangedChild = true
  76. }
  77. return result
  78. }
  79. func commit<Backend: BaseAppBackend>(
  80. _ widget: Backend.Widget,
  81. children: HotReloadableViewChildren,
  82. layout: ViewLayoutResult,
  83. environment: EnvironmentValues,
  84. backend: Backend
  85. ) {
  86. if children.hasChangedChild {
  87. backend.removeAllChildren(of: widget)
  88. backend.insert(children.node.getWidget().into(), into: widget, at: 0)
  89. backend.setPosition(ofChildAt: 0, in: widget, to: .zero)
  90. children.hasChangedChild = false
  91. }
  92. _ = children.node.commit()
  93. backend.setSize(of: widget, to: layout.size.vector)
  94. }
  95. }
  96. class HotReloadableViewChildren: ViewGraphNodeChildren {
  97. /// The erased underlying node.
  98. var node: ErasedViewGraphNode
  99. var widgets: [AnyWidget] {
  100. [node.getWidget()]
  101. }
  102. var erasedNodes: [ErasedViewGraphNode] {
  103. [node]
  104. }
  105. var hasChangedChild = true
  106. /// Creates the erased child node and wraps the child's widget in a single-child container.
  107. init<Backend: BaseAppBackend>(
  108. from view: HotReloadableView,
  109. backend: Backend,
  110. snapshot: ViewGraphSnapshotter.NodeSnapshot?,
  111. environment: EnvironmentValues
  112. ) {
  113. node = ErasedViewGraphNode(
  114. for: view.child,
  115. backend: backend,
  116. snapshot: snapshot,
  117. environment: environment
  118. )
  119. }
  120. }