1
0

ViewGraph.swift 3.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. import Foundation
  2. /// The root of the view graph which shadows a root view's structure with extra metadata,
  3. /// cross-update state persistence, and behind the scenes backend widget handling.
  4. ///
  5. /// This is where state updates are propagated through the view hierarchy, and also where view
  6. /// bodies get recomputed. The root node is type-erased because otherwise the selected backend
  7. /// would have to get propagated through the entire scene graph which would leak it into
  8. /// ``Scene`` implementations (exposing users to unnecessary internal details).
  9. @MainActor
  10. public class ViewGraph<Root: View> {
  11. /// The view graph's
  12. public typealias RootNode = AnyViewGraphNode<Root>
  13. /// The root node storing the node for the root view's body.
  14. public var rootNode: RootNode
  15. /// A cancellable handle to observation of the view's state.
  16. private var cancellable: Cancellable?
  17. /// The root view being managed by this view graph.
  18. private var view: Root
  19. /// The latest size proposal.
  20. private var latestProposal: ProposedViewSize
  21. /// The latest proposal as of the last commit (used when updated the root
  22. /// view due to a state change as opposed to a window resizing event).
  23. private var committedProposal: ProposedViewSize
  24. /// The current size of the root view.
  25. private var currentRootViewResult: ViewLayoutResult
  26. /// The environment most recently provided by this node's parent scene.
  27. private var parentEnvironment: EnvironmentValues
  28. private var isFirstUpdate = true
  29. private var setIncomingURLHandler: ((@escaping (URL) -> Void) -> Void)?
  30. /// Creates a view graph for a root view with a specific backend.
  31. ///
  32. /// - Parameters:
  33. /// - view: The root view to create a graph for.
  34. /// - backend: The app's backend.
  35. /// - environment: The current environment.
  36. public init<Backend: BaseAppBackend>(
  37. for view: Root,
  38. backend: Backend,
  39. environment: EnvironmentValues
  40. ) {
  41. rootNode = AnyViewGraphNode(for: view, backend: backend, environment: environment)
  42. self.view = view
  43. latestProposal = .zero
  44. committedProposal = .zero
  45. parentEnvironment = environment
  46. currentRootViewResult = ViewLayoutResult.leafView(size: .zero)
  47. setIncomingURLHandler =
  48. (backend as? any BackendFeatures.IncomingURLs)?.setIncomingURLHandler(to:)
  49. }
  50. /// Recomputes the entire UI (e.g. due to the root view's state updating).
  51. ///
  52. /// If the update is due to the parent scene getting updated then the view
  53. /// is recomputed and passed as `newView`.
  54. public func computeLayout(
  55. with newView: Root? = nil,
  56. proposedSize: ProposedViewSize,
  57. environment: EnvironmentValues
  58. ) -> ViewLayoutResult {
  59. parentEnvironment = environment
  60. latestProposal = proposedSize
  61. let result = rootNode.computeLayout(
  62. with: newView ?? view,
  63. proposedSize: proposedSize,
  64. environment: parentEnvironment
  65. )
  66. self.currentRootViewResult = result
  67. if let newView {
  68. self.view = newView
  69. }
  70. return result
  71. }
  72. /// Commits the result of the last computeLayout call to the underlying
  73. /// widget hierarchy.
  74. public func commit() {
  75. committedProposal = latestProposal
  76. self.currentRootViewResult = rootNode.commit()
  77. if isFirstUpdate {
  78. setIncomingURLHandler? { url in
  79. self.currentRootViewResult.preferences.onOpenURL?(url)
  80. }
  81. isFirstUpdate = false
  82. }
  83. }
  84. public func snapshot() -> ViewGraphSnapshotter.NodeSnapshot {
  85. ViewGraphSnapshotter.snapshot(of: rootNode)
  86. }
  87. }