AnyView.swift 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. import Foundation
  2. /// A view which erases the type of its child.
  3. ///
  4. /// Useful in dynamic use-cases such as hot reloading, but not recommended if
  5. /// there are alternate strongly-typed solutions to your problem since
  6. /// ``AnyView`` has significantly more overhead than strongly typed views.
  7. public struct AnyView: TypeSafeView {
  8. typealias Children = AnyViewChildren
  9. public var body = EmptyView()
  10. var child: any View
  11. public init(_ child: any View) {
  12. self.child = child
  13. }
  14. func children<Backend: BaseAppBackend>(
  15. backend: Backend,
  16. snapshots: [ViewGraphSnapshotter.NodeSnapshot]?,
  17. environment: EnvironmentValues
  18. ) -> AnyViewChildren {
  19. let snapshot = snapshots?.count == 1 ? snapshots?.first : nil
  20. return AnyViewChildren(
  21. from: self,
  22. backend: backend,
  23. snapshot: snapshot,
  24. environment: environment
  25. )
  26. }
  27. func layoutableChildren<Backend: BaseAppBackend>(
  28. backend: Backend,
  29. children: AnyViewChildren
  30. ) -> [LayoutSystem.LayoutableChild] {
  31. // TODO: Figure out a convention for views like this where ``layoutableChildren`` will
  32. // never get used unless something has already gone pretty wrong.
  33. body.layoutableChildren(backend: backend, children: children)
  34. }
  35. func asWidget<Backend: BaseAppBackend>(
  36. _ children: AnyViewChildren,
  37. backend: Backend
  38. ) -> Backend.Widget {
  39. let container = backend.createContainer()
  40. backend.insert(children.node.getWidget().into(), into: container, at: 0)
  41. backend.setPosition(ofChildAt: 0, in: container, to: .zero)
  42. return container
  43. }
  44. /// Attempts to update the child. If the initial update fails then it means that the child's
  45. /// concrete type has changed and we must recreate the child node and swap out our current
  46. /// child widget with the new view's widget.
  47. func computeLayout<Backend: BaseAppBackend>(
  48. _ widget: Backend.Widget,
  49. children: AnyViewChildren,
  50. proposedSize: ProposedViewSize,
  51. environment: EnvironmentValues,
  52. backend: Backend
  53. ) -> ViewLayoutResult {
  54. var (viewTypesMatched, result) = children.node.computeLayoutWithNewView(
  55. child,
  56. proposedSize,
  57. environment
  58. )
  59. // If the new view's type doesn't match the old view's type then we need to create a new
  60. // view graph node for the new view.
  61. if !viewTypesMatched {
  62. children.widgetNeedsReinsertion = true
  63. children.node = ErasedViewGraphNode(
  64. for: child,
  65. backend: backend,
  66. environment: environment
  67. )
  68. // We can just assume that the update succeeded because we just created the node
  69. // a few lines earlier (so it's guaranteed that the view types match).
  70. let (_, newResult) = children.node.computeLayoutWithNewView(
  71. child,
  72. proposedSize,
  73. environment
  74. )
  75. result = newResult
  76. }
  77. return result
  78. }
  79. func commit<Backend: BaseAppBackend>(
  80. _ widget: Backend.Widget,
  81. children: AnyViewChildren,
  82. layout: ViewLayoutResult,
  83. environment: EnvironmentValues,
  84. backend: Backend
  85. ) {
  86. if children.widgetNeedsReinsertion {
  87. backend.remove(childAt: 0, from: widget)
  88. backend.insert(children.node.getWidget().into(), into: widget, at: 0)
  89. backend.setPosition(ofChildAt: 0, in: widget, to: .zero)
  90. children.widgetNeedsReinsertion = false
  91. }
  92. _ = children.node.commit()
  93. backend.setSize(of: widget, to: layout.size.vector)
  94. }
  95. }
  96. class AnyViewChildren: ViewGraphNodeChildren {
  97. /// The erased underlying node.
  98. var node: ErasedViewGraphNode
  99. /// Stores whether or not the displayed view changed during computeLayout.
  100. var widgetNeedsReinsertion = false
  101. var widgets: [AnyWidget] {
  102. return [node.getWidget()]
  103. }
  104. var erasedNodes: [ErasedViewGraphNode] {
  105. [node]
  106. }
  107. /// Creates the erased child node and wraps the child's widget in a single-child container.
  108. init<Backend: BaseAppBackend>(
  109. from view: AnyView,
  110. backend: Backend,
  111. snapshot: ViewGraphSnapshotter.NodeSnapshot?,
  112. environment: EnvironmentValues
  113. ) {
  114. node = ErasedViewGraphNode(
  115. for: view.child,
  116. backend: backend,
  117. snapshot: snapshot,
  118. environment: environment
  119. )
  120. }
  121. }