EitherView.swift 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. /// A view used by ``ViewBuilder`` to support if/else conditional statements.
  2. public struct EitherView<A: View, B: View> {
  3. typealias NodeChildren = EitherViewChildren<A, B>
  4. public var body = EmptyView()
  5. /// Stores one of two possible view types.
  6. enum Storage {
  7. case a(A)
  8. case b(B)
  9. }
  10. var storage: Storage
  11. /// Creates an either view with its first case visible initially.
  12. init(_ a: A) {
  13. storage = .a(a)
  14. }
  15. /// Creates an either view with its second case visible initially.
  16. init(_ b: B) {
  17. storage = .b(b)
  18. }
  19. }
  20. extension EitherView: View {
  21. public var _asMenuItems: [MenuItem] {
  22. switch storage {
  23. case .a(let a): a._asMenuItems
  24. case .b(let b): b._asMenuItems
  25. }
  26. }
  27. }
  28. extension EitherView: TypeSafeView {
  29. func children<Backend: BaseAppBackend>(
  30. backend: Backend,
  31. snapshots: [ViewGraphSnapshotter.NodeSnapshot]?,
  32. environment: EnvironmentValues
  33. ) -> NodeChildren {
  34. return EitherViewChildren(
  35. from: self,
  36. backend: backend,
  37. snapshots: snapshots,
  38. environment: environment
  39. )
  40. }
  41. func asWidget<Backend: BaseAppBackend>(
  42. _ children: EitherViewChildren<A, B>,
  43. backend: Backend
  44. ) -> Backend.Widget {
  45. return backend.createContainer()
  46. }
  47. func computeLayout<Backend: BaseAppBackend>(
  48. _ widget: Backend.Widget,
  49. children: EitherViewChildren<A, B>,
  50. proposedSize: ProposedViewSize,
  51. environment: EnvironmentValues,
  52. backend: Backend
  53. ) -> ViewLayoutResult {
  54. let result: ViewLayoutResult
  55. let hasSwitchedCase: Bool
  56. switch storage {
  57. case .a(let a):
  58. switch children.node {
  59. case .a(let nodeA):
  60. result = nodeA.computeLayout(
  61. with: a,
  62. proposedSize: proposedSize,
  63. environment: environment
  64. )
  65. hasSwitchedCase = false
  66. case .b:
  67. let nodeA = AnyViewGraphNode(
  68. for: a,
  69. backend: backend,
  70. environment: environment
  71. )
  72. children.node = .a(nodeA)
  73. result = nodeA.computeLayout(
  74. with: a,
  75. proposedSize: proposedSize,
  76. environment: environment
  77. )
  78. hasSwitchedCase = true
  79. }
  80. case .b(let b):
  81. switch children.node {
  82. case .b(let nodeB):
  83. result = nodeB.computeLayout(
  84. with: b,
  85. proposedSize: proposedSize,
  86. environment: environment
  87. )
  88. hasSwitchedCase = false
  89. case .a:
  90. let nodeB = AnyViewGraphNode(
  91. for: b,
  92. backend: backend,
  93. environment: environment
  94. )
  95. children.node = .b(nodeB)
  96. result = nodeB.computeLayout(
  97. with: b,
  98. proposedSize: proposedSize,
  99. environment: environment
  100. )
  101. hasSwitchedCase = true
  102. }
  103. }
  104. children.hasSwitchedCase = children.hasSwitchedCase || hasSwitchedCase
  105. return result
  106. }
  107. func commit<Backend: BaseAppBackend>(
  108. _ widget: Backend.Widget,
  109. children: EitherViewChildren<A, B>,
  110. layout: ViewLayoutResult,
  111. environment: EnvironmentValues,
  112. backend: Backend
  113. ) {
  114. if children.hasSwitchedCase {
  115. backend.removeAllChildren(of: widget)
  116. backend.insert(children.node.widget.into(), into: widget, at: 0)
  117. backend.setPosition(ofChildAt: 0, in: widget, to: .zero)
  118. children.hasSwitchedCase = false
  119. }
  120. _ = children.node.erasedNode.commit()
  121. backend.setSize(of: widget, to: layout.size.vector)
  122. }
  123. }
  124. /// Uses an `enum` to store a view graph node for one of two possible child view types.
  125. class EitherViewChildren<A: View, B: View>: ViewGraphNodeChildren {
  126. /// A view graph node that wraps one of two possible child view types.
  127. @MainActor
  128. enum EitherNode {
  129. case a(AnyViewGraphNode<A>)
  130. case b(AnyViewGraphNode<B>)
  131. /// The widget corresponding to the currently displayed child view.
  132. var widget: AnyWidget {
  133. switch self {
  134. case .a(let node):
  135. return node.widget
  136. case .b(let node):
  137. return node.widget
  138. }
  139. }
  140. var erasedNode: ErasedViewGraphNode {
  141. switch self {
  142. case .a(let node):
  143. return ErasedViewGraphNode(wrapping: node)
  144. case .b(let node):
  145. return ErasedViewGraphNode(wrapping: node)
  146. }
  147. }
  148. }
  149. /// The view graph node for the currently displayed child.
  150. var node: EitherNode
  151. /// Tracks whether the view has switched cases since the last non-dryrun update.
  152. /// Initially `true`.
  153. var hasSwitchedCase = true
  154. var widgets: [AnyWidget] {
  155. return [node.widget]
  156. }
  157. var erasedNodes: [ErasedViewGraphNode] {
  158. [node.erasedNode]
  159. }
  160. /// Creates storage for an either view's current child (which can change at any time).
  161. init<Backend: BaseAppBackend>(
  162. from view: EitherView<A, B>,
  163. backend: Backend,
  164. snapshots: [ViewGraphSnapshotter.NodeSnapshot]?,
  165. environment: EnvironmentValues
  166. ) {
  167. // TODO: Ensure that this is valid in all circumstances. It should be, given that
  168. // we're assuming that the parent view's state was restored from the same snapshot
  169. // which should mean that the same EitherView case will be selected (if we assume
  170. // that views are pure, which we have to).
  171. let snapshot = snapshots?.first
  172. switch view.storage {
  173. case .a(let a):
  174. node = .a(
  175. AnyViewGraphNode(
  176. for: a,
  177. backend: backend,
  178. snapshot: snapshot,
  179. environment: environment
  180. )
  181. )
  182. case .b(let b):
  183. node = .b(
  184. AnyViewGraphNode(
  185. for: b,
  186. backend: backend,
  187. snapshot: snapshot,
  188. environment: environment
  189. )
  190. )
  191. }
  192. }
  193. }