ViewGraphSnapshotter.swift 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. import Foundation
  2. public struct ViewGraphSnapshotter: ErasedViewGraphNodeTransformer {
  3. public struct NodeSnapshot: CustomDebugStringConvertible, Equatable {
  4. var viewTypeName: String
  5. /// Property names mapped to encoded JSON objects
  6. var state: [String: Data]
  7. var children: [NodeSnapshot]
  8. public var debugDescription: String {
  9. var description = "\(viewTypeName)"
  10. if !state.isEmpty {
  11. description += "\n| state: {"
  12. for (propertyName, data) in state {
  13. let encodedState = "<encoded state>"
  14. description += "\n| \(propertyName): \(encodedState),"
  15. }
  16. description += "\n| }"
  17. }
  18. if !children.isEmpty {
  19. var childDescriptions: [String] = []
  20. for (i, child) in children.enumerated() {
  21. let linePrefix: String
  22. if i == children.count - 1 {
  23. linePrefix = " "
  24. } else {
  25. linePrefix = "| "
  26. }
  27. let childDescription = child.debugDescription
  28. .split(separator: "\n")
  29. .joined(separator: "\n\(linePrefix)")
  30. childDescriptions.append("|-> \(childDescription)")
  31. }
  32. description += "\n"
  33. description += childDescriptions.joined(separator: "\n")
  34. }
  35. return description
  36. }
  37. public func isValid<V: View>(for viewType: V.Type) -> Bool {
  38. name(of: V.self) == viewTypeName
  39. }
  40. public func restore<V: View>(to view: V) {
  41. guard isValid(for: V.self) else {
  42. return
  43. }
  44. Self.updateState(of: view, withSnapshot: state)
  45. }
  46. private static func updateState<V: View>(of view: V, withSnapshot state: [String: Data]) {
  47. forEachField(of: view) { name, _, fieldValue in
  48. guard
  49. let stateProperty = fieldValue as? any SnapshottableProperty,
  50. let propertyName = name,
  51. let encodedState = state[propertyName]
  52. else {
  53. return // i.e. continue
  54. }
  55. stateProperty.tryRestoreFromSnapshot(encodedState)
  56. }
  57. }
  58. }
  59. public init() {}
  60. public func transform<U: View, Backend: BaseAppBackend>(
  61. node: ViewGraphNode<U, Backend>
  62. ) -> NodeSnapshot {
  63. Self.snapshot(of: AnyViewGraphNode(node))
  64. }
  65. public static func snapshot<V: View>(of node: AnyViewGraphNode<V>) -> NodeSnapshot {
  66. var stateSnapshot: [String: Data] = [:]
  67. forEachField(of: node.getView()) { name, _, fieldValue in
  68. guard
  69. let stateProperty = fieldValue as? any SnapshottableProperty,
  70. let propertyName = name,
  71. let encodedState = try? stateProperty.snapshot()
  72. else {
  73. return // i.e. continue
  74. }
  75. stateSnapshot[propertyName] = encodedState
  76. }
  77. let nodeChildren = node.getChildren().erasedNodes
  78. let snapshotter = ViewGraphSnapshotter()
  79. let childSnapshots = nodeChildren.map { child in
  80. child.transform(with: snapshotter)
  81. }
  82. return NodeSnapshot(
  83. viewTypeName: name(of: V.self),
  84. state: stateSnapshot,
  85. children: childSnapshots
  86. )
  87. }
  88. public static nonisolated func name<V: View>(of viewType: V.Type) -> String {
  89. String(String(describing: V.self).split(separator: "<")[0])
  90. }
  91. /// Attempts to match a list of snapshots to a list of views. Uses assumptions about
  92. /// a few common types of changes which occur when using hot reloading (e.g. adding/removing
  93. /// single-child modifier views, adding an extra view between two siblings, etc). At
  94. /// the end of the day, this task is impossible to do in general (by definition), so
  95. /// this function is expected to just slowly improve over time to suit the majority of
  96. /// use-cases.
  97. static func match(
  98. _ snapshots: [NodeSnapshot],
  99. to viewTypeNames: [String]
  100. ) -> [NodeSnapshot?] {
  101. var sortedSnapshots: [NodeSnapshot?] = Array(repeating: nil, count: viewTypeNames.count)
  102. var skippedSnapshots: [NodeSnapshot] = []
  103. var usedIndices: Set<Int> = []
  104. for snapshot in snapshots {
  105. var foundView = false
  106. for (i, viewTypeName) in viewTypeNames.enumerated() where !usedIndices.contains(i) {
  107. if snapshot.viewTypeName == viewTypeName {
  108. sortedSnapshots[i] = snapshot
  109. foundView = true
  110. usedIndices.insert(i)
  111. break
  112. }
  113. }
  114. if !foundView {
  115. skippedSnapshots.append(snapshot)
  116. }
  117. }
  118. if sortedSnapshots == [nil] {
  119. let viewTypeName = viewTypeNames[0]
  120. var children = snapshots
  121. while children.count == 1 {
  122. let child = children[0]
  123. if child.viewTypeName == viewTypeName {
  124. return [child]
  125. } else {
  126. children = child.children
  127. }
  128. }
  129. if snapshots.count == 1 {
  130. return snapshots
  131. }
  132. }
  133. return sortedSnapshots
  134. }
  135. }