ForEach.swift 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. import Foundation
  2. /// A view that displays a variable amount of children.
  3. public struct ForEach<Items: Collection, ID: Hashable, Child> {
  4. /// A variable-length collection of elements to display.
  5. var elements: Items
  6. /// A method to display the elements as views.
  7. var child: (Items.Element) -> Child
  8. /// The path to the property used as Identifier
  9. var idKeyPath: KeyPath<Items.Element, ID>?
  10. }
  11. extension ForEach: TypeSafeView, View where Child: View {
  12. typealias Children = ForEachViewChildren<Items, ID, Child>
  13. /// Creates a view that creates child views on demand based on a collection
  14. /// of data.
  15. ///
  16. /// One instance of `child` will be rendered for every element in
  17. /// `elements`.
  18. ///
  19. /// - Parameters:
  20. /// - elements: The collection to build an array of views from.
  21. /// - keyPath: A key path to the element type's ID.
  22. /// - child: A view builder that returns an appropriate view for
  23. /// each element of `elements`.
  24. public init(
  25. _ elements: Items,
  26. id keyPath: KeyPath<Items.Element, ID>,
  27. @ViewBuilder _ child: @escaping (Items.Element) -> Child
  28. ) {
  29. self.elements = elements
  30. self.child = child
  31. self.idKeyPath = keyPath
  32. }
  33. public var body: EmptyView {
  34. return EmptyView()
  35. }
  36. public var _asMenuItems: [MenuItem] {
  37. elements.map(child).flatMap(\._asMenuItems)
  38. }
  39. func children<Backend: BaseAppBackend>(
  40. backend: Backend,
  41. snapshots: [ViewGraphSnapshotter.NodeSnapshot]?,
  42. environment: EnvironmentValues
  43. ) -> Children {
  44. return Children(
  45. from: self,
  46. backend: backend,
  47. idKeyPath: idKeyPath,
  48. snapshots: snapshots,
  49. environment: environment
  50. )
  51. }
  52. func asWidget<Backend: BaseAppBackend>(
  53. _ children: Children,
  54. backend: Backend
  55. ) -> Backend.Widget {
  56. return backend.createContainer()
  57. }
  58. func computeLayout<Backend: BaseAppBackend>(
  59. _ widget: Backend.Widget,
  60. children: Children,
  61. proposedSize: ProposedViewSize,
  62. environment: EnvironmentValues,
  63. backend: Backend
  64. ) -> ViewLayoutResult {
  65. func insertChild(_ child: Backend.Widget, atIndex index: Int) {
  66. children.queuedChanges.append(.insertChild(AnyWidget(child), index))
  67. }
  68. func removeChild(atIndex index: Int) {
  69. children.queuedChanges.append(.removeChild(index))
  70. }
  71. func swap(childAt firstIndex: Int, withChildAt secondIndex: Int) {
  72. children.queuedChanges.append(.swapChildren(firstIndex, secondIndex))
  73. }
  74. // Use the previous update Method when no keyPath is set on a
  75. // [Hashable] Collection to optionally keep the old behaviour.
  76. guard let idKeyPath else {
  77. return deprecatedUpdate(
  78. widget,
  79. children: children,
  80. proposedSize: proposedSize,
  81. environment: environment,
  82. backend: backend
  83. )
  84. }
  85. var oldIdentifiers = children.identifiers
  86. let newIdentifiers = elements.map { $0[keyPath: idKeyPath] }
  87. // If the identifiers of our elements have changed, then we must rearrange
  88. // our nodes and widgets so that child view states remain with their
  89. // corresponding identifiers.
  90. if oldIdentifiers != newIdentifiers {
  91. var oldIdentifierMap = children.identifierMap
  92. var oldNodes = children.nodes
  93. var seenIdentifiers = Set<ID>()
  94. var oldNodesReused = 0
  95. children.nodes = []
  96. children.identifierMap = [:]
  97. children.identifiers = []
  98. children.layoutableChildren = []
  99. var offset = 0
  100. var duplicateCount = 0
  101. for (index, element) in elements.enumerated() {
  102. let identifier = newIdentifiers[index]
  103. let childContent = child(element)
  104. let node: AnyViewGraphNode<Child>
  105. if !seenIdentifiers.insert(identifier).inserted {
  106. // We cannot keep view state attached to the correct ForEach element
  107. // when there are duplicate identifiers. Any elements with unique
  108. // identifiers are guaranteed to keep functioning correctly. Elements
  109. // with non-unique identifiers will get their corresponding view graph
  110. // nodes recreated each time the identifiers of our elements change,
  111. // unless they are the first element with the shared identifier, in which
  112. // case they will inherit the view graph node of the previous first element
  113. // with that same identifier.
  114. logger.warning(
  115. "duplicate identifier in ForEach; view state may not act as you would expect",
  116. metadata: ["identifier": "\(identifier)"]
  117. )
  118. duplicateCount += 1
  119. }
  120. if let oldIndex = oldIdentifierMap.removeValue(forKey: identifier) {
  121. // If the identifier already has a corresponding node, reuse it.
  122. node = oldNodes[oldIndex]
  123. oldNodesReused += 1
  124. // If the node's corresponding widget isn't already at the correct
  125. // position (accounting for insertions), then swap it with the widget
  126. // at the target position and update our accounting accordinly.
  127. if index != offset + oldIndex {
  128. // When talking about current widget indices, we add `offset` to oldIndex.
  129. // When talking about old element indices, we subtract `offset` from index.
  130. swap(childAt: offset + oldIndex, withChildAt: index)
  131. oldNodes.swapAt(oldIndex, index - offset)
  132. oldIdentifierMap[oldIdentifiers[index - offset]] = oldIndex
  133. oldIdentifiers.swapAt(oldIndex, index - offset)
  134. }
  135. } else {
  136. // If the identifier is new, create a node for it and insert its
  137. // widget at the correct position.
  138. node = AnyViewGraphNode(
  139. for: childContent,
  140. backend: backend,
  141. environment: environment
  142. )
  143. insertChild(node.widget.into(), atIndex: index)
  144. // `offset` tracks how many elements have been inserted, which we
  145. // use to adjust old indices. All nodes before the one we just
  146. // inserted are already at their final position, so we never have
  147. // to adjust old indices that point to before our latest insertion, otherwise
  148. // such a simple adjustment wouldn't be possible.
  149. offset += 1
  150. }
  151. children.nodes.append(node)
  152. children.identifierMap[identifier] = index
  153. children.identifiers.append(identifier)
  154. children.layoutableChildren.append(
  155. LayoutSystem.LayoutableChild(node) { child(element) }
  156. )
  157. }
  158. // TODO: We should be able to reuse unused widgets in newly created nodes.
  159. // Remove unused widgets, starting from the end of the container for
  160. // cheaper removals.
  161. let removalCount = oldNodes.count - oldNodesReused
  162. if removalCount > 0 {
  163. for i in (0..<removalCount).reversed() {
  164. removeChild(atIndex: children.nodes.count + i)
  165. }
  166. }
  167. }
  168. // Recompute layoutable children if the last commit cleared them
  169. if children.layoutableChildren.isEmpty && !children.nodes.isEmpty {
  170. children.layoutableChildren = zip(children.nodes, elements).map { (node, element) in
  171. LayoutSystem.LayoutableChild(node) { child(element) }
  172. }
  173. }
  174. return LayoutSystem.computeStackLayout(
  175. container: widget,
  176. children: children.layoutableChildren,
  177. cache: &children.stackLayoutCache,
  178. proposedSize: proposedSize,
  179. environment: environment,
  180. backend: backend
  181. )
  182. }
  183. @MainActor
  184. func deprecatedUpdate<Backend: BaseAppBackend>(
  185. _ widget: Backend.Widget,
  186. children: Children,
  187. proposedSize: ProposedViewSize,
  188. environment: EnvironmentValues,
  189. backend: Backend
  190. ) -> ViewLayoutResult {
  191. @inline(__always)
  192. func insertChild(_ child: Backend.Widget, atIndex index: Int) {
  193. children.queuedChanges.append(.insertChild(AnyWidget(child), index))
  194. }
  195. @inline(__always)
  196. func removeChild(atIndex index: Int) {
  197. children.queuedChanges.append(.removeChild(index))
  198. }
  199. let elementsStartIndex = elements.startIndex
  200. var layoutableChildren: [LayoutSystem.LayoutableChild] = []
  201. for (i, node) in children.nodes.enumerated() {
  202. guard i < elements.count else {
  203. break
  204. }
  205. let index = elements.index(elementsStartIndex, offsetBy: i)
  206. if children.isFirstUpdate {
  207. insertChild(node.widget.into(), atIndex: i)
  208. }
  209. let layoutableChild = LayoutSystem.LayoutableChild(node) { child(elements[index]) }
  210. layoutableChildren.append(layoutableChild)
  211. }
  212. children.isFirstUpdate = false
  213. let nodeCount = children.nodes.count
  214. let remainingElementCount = elements.count - nodeCount
  215. if remainingElementCount > 0 {
  216. let startIndex = elements.index(elementsStartIndex, offsetBy: nodeCount)
  217. for i in 0..<remainingElementCount {
  218. let element = elements[elements.index(startIndex, offsetBy: i)]
  219. let node = AnyViewGraphNode(
  220. for: child(element),
  221. backend: backend,
  222. environment: environment
  223. )
  224. insertChild(node.widget.into(), atIndex: children.nodes.count)
  225. children.nodes.append(node)
  226. let layoutableChild = LayoutSystem.LayoutableChild(node) { child(element) }
  227. layoutableChildren.append(layoutableChild)
  228. }
  229. } else if remainingElementCount < 0 {
  230. let unusedCount = -remainingElementCount
  231. for i in 0..<unusedCount {
  232. removeChild(atIndex: nodeCount - i - 1)
  233. }
  234. children.nodes.removeLast(unusedCount)
  235. }
  236. children.layoutableChildren = layoutableChildren
  237. return LayoutSystem.computeStackLayout(
  238. container: widget,
  239. children: layoutableChildren,
  240. cache: &children.stackLayoutCache,
  241. proposedSize: proposedSize,
  242. environment: environment,
  243. backend: backend
  244. )
  245. }
  246. func commit<Backend: BaseAppBackend>(
  247. _ widget: Backend.Widget,
  248. children: Children,
  249. layout: ViewLayoutResult,
  250. environment: EnvironmentValues,
  251. backend: Backend
  252. ) {
  253. for change in children.queuedChanges {
  254. switch change {
  255. case .insertChild(let child, let index):
  256. backend.insert(child.into(), into: widget, at: index)
  257. case .removeChild(let index):
  258. backend.remove(childAt: index, from: widget)
  259. case .swapChildren(let firstIndex, let secondIndex):
  260. backend.swap(childAt: firstIndex, withChildAt: secondIndex, in: widget)
  261. }
  262. }
  263. children.queuedChanges = []
  264. LayoutSystem.commitStackLayout(
  265. container: widget,
  266. children: children.layoutableChildren,
  267. cache: &children.stackLayoutCache,
  268. layout: layout,
  269. environment: environment,
  270. backend: backend
  271. )
  272. // Reset layoutable children cache so that we recompute them during the
  273. // next update cycle. This is important at the moment because the `child`
  274. // closure and `elements` array may have changed. In future we'll separate
  275. // view body recomputation from the computeLayout step, which should simplify
  276. // things.
  277. children.layoutableChildren = []
  278. }
  279. }
  280. /// Stores the child nodes of a ``ForEach`` view.
  281. ///
  282. /// Also handles the ``ForEach`` view's widget unlike most ``ViewGraphNodeChildren``
  283. /// implementations. This logic could mostly be moved into ``ForEach`` but it would still
  284. /// be accessing ``ForEachViewChildren/storage`` so it'd just introduce an extra layer of
  285. /// property accesses. It also means that the complexity is in a single type instead of
  286. /// split across two.
  287. ///
  288. /// Most of the complexity comes from resizing the list widget and moving around elements
  289. /// when elements are added/removed.
  290. class ForEachViewChildren<
  291. Items: Collection,
  292. ID: Hashable,
  293. Child: View
  294. >: ViewGraphNodeChildren {
  295. /// The nodes for all current children of the ``ForEach`` view.
  296. var nodes: [AnyViewGraphNode<Child>] = []
  297. /// A map from element identifier to node index.
  298. var identifierMap: [ID: Int]
  299. /// The identifiers corresponding to ``nodes``.
  300. var identifiers: [ID]
  301. /// Changes queued during computeLayout.
  302. var queuedChanges: [Change] = []
  303. /// A queued widget operation to perform during `ForEach.commit`.
  304. enum Change: CustomStringConvertible {
  305. case insertChild(AnyWidget, Int)
  306. case removeChild(Int)
  307. case swapChildren(Int, Int)
  308. var description: String {
  309. switch self {
  310. case .insertChild(let widget, let index):
  311. "Insert widget \(ObjectIdentifier(widget.widget as AnyObject)) at \(index)"
  312. case .removeChild(let index):
  313. "Remove widget at \(index)"
  314. case .swapChildren(let firstIndex, let secondIndex):
  315. "Swap widgets at \(firstIndex) and \(secondIndex)"
  316. }
  317. }
  318. }
  319. /// Only used by ``ForEach/deprecatedUpdate(_:children:proposedSize:environment:backend:)``.
  320. var isFirstUpdate = true
  321. /// A cache of the view's children, used when the ForEach's element
  322. /// identifiers haven't changed since the previous layout computation.
  323. var layoutableChildren: [LayoutSystem.LayoutableChild] = []
  324. var widgets: [AnyWidget] {
  325. nodes.map(\.widget)
  326. }
  327. // TODO: This pattern of erasing by wrapping in a temporary class seems
  328. // inefficient. Could ErasedViewGraphNode maybe be a struct instead?
  329. var erasedNodes: [ErasedViewGraphNode] {
  330. nodes.map(ErasedViewGraphNode.init(wrapping:))
  331. }
  332. var stackLayoutCache = StackLayoutCache.initial
  333. init<Backend: BaseAppBackend>(
  334. from view: ForEach<Items, ID, Child>,
  335. backend: Backend,
  336. idKeyPath: KeyPath<Items.Element, ID>?,
  337. snapshots: [ViewGraphSnapshotter.NodeSnapshot]?,
  338. environment: EnvironmentValues
  339. ) {
  340. identifierMap = [:]
  341. identifiers = []
  342. if idKeyPath == nil {
  343. // Deprecated code path. I'm not touching this anymore cause it's
  344. // gonna get deleted before any proper release.
  345. nodes = view.elements
  346. .map(view.child)
  347. .enumerated()
  348. .map { (index, child) in
  349. let snapshot = index < snapshots?.count ?? 0 ? snapshots?[index] : nil
  350. return ViewGraphNode(
  351. for: child,
  352. backend: backend,
  353. snapshot: snapshot,
  354. environment: environment
  355. )
  356. }
  357. .map(AnyViewGraphNode.init(_:))
  358. } else {
  359. nodes = []
  360. }
  361. }
  362. }
  363. extension ForEach where ID == Int {
  364. /// Creates a view that creates child views on demand based on a collection of data.
  365. @available(
  366. *,
  367. deprecated,
  368. renamed: "init(_:id:_:)",
  369. message: """
  370. ForEach requires an explicit 'id' parameter for non-Identifiable \
  371. elements to correctly persist state across view updates
  372. """
  373. )
  374. @_disfavoredOverload
  375. public init(
  376. _ elements: Items,
  377. @ViewBuilder _ child: @escaping (Items.Element) -> Child
  378. ) {
  379. self.elements = elements
  380. self.child = child
  381. self.idKeyPath = nil
  382. }
  383. }
  384. extension ForEach where Items.Element: Identifiable, ID == Items.Element.ID {
  385. /// Creates a view that creates child views on demand based on a collection of identifiable data.
  386. public init(
  387. _ elements: Items,
  388. @ViewBuilder _ child: @escaping (Items.Element) -> Child
  389. ) {
  390. self.elements = elements
  391. self.child = child
  392. self.idKeyPath = \.id
  393. }
  394. }
  395. // MARK: Deprecated MenuItem-based inits
  396. extension ForEach where ID == Int {
  397. /// Creates a view that creates child views on demand based on a collection of data.
  398. @available(
  399. *,
  400. deprecated,
  401. message: """
  402. ForEach requires an explicit 'id' parameter for non-Identifiable \
  403. elements to correctly persist state across view updates
  404. """
  405. )
  406. @_disfavoredOverload
  407. public init(
  408. menuItems elements: Items,
  409. @ViewBuilder _ child: @escaping (Items.Element) -> Child
  410. ) {
  411. self.elements = elements
  412. self.child = child
  413. self.idKeyPath = nil
  414. }
  415. }
  416. extension ForEach {
  417. /// Creates a view that creates child views on demand based on a collection of data.
  418. @available(
  419. *,
  420. deprecated,
  421. renamed: "init(_:id:_:)",
  422. message: """
  423. Special treatment of menu item ForEach blocks is no longer necessary. \
  424. Remove the menuItems parameter label.
  425. """
  426. )
  427. public init(
  428. menuItems elements: Items,
  429. id keyPath: KeyPath<Items.Element, ID>,
  430. @ViewBuilder _ child: @escaping (Items.Element) -> Child
  431. ) {
  432. self.elements = elements
  433. self.child = child
  434. self.idKeyPath = keyPath
  435. }
  436. }
  437. extension ForEach where Items.Element: Identifiable, ID == Items.Element.ID {
  438. /// Creates a view that creates child views on demand based on a collection of data.
  439. @available(
  440. *,
  441. deprecated,
  442. renamed: "init(_:_:)",
  443. message: """
  444. Special treatment of menu item ForEach blocks is no longer necessary. \
  445. Remove the menuItems parameter label.
  446. """
  447. )
  448. public init(
  449. menuItems elements: Items,
  450. @ViewBuilder _ child: @escaping (Items.Element) -> Child
  451. ) {
  452. self.elements = elements
  453. self.child = child
  454. self.idKeyPath = \.id
  455. }
  456. }