1+ import OrderedCollections
2+
13/// A view that displays a variable amount of children.
24public struct ForEach < Items: Collection , Child> where Items. Index == Int {
35 /// A variable-length collection of elements to display.
@@ -40,7 +42,7 @@ extension ForEach where Items == [Int] {
4042 }
4143}
4244
43- extension ForEach : TypeSafeView , View where Child: View {
45+ extension ForEach : TypeSafeView , View where Child: View , Items . Element : Identifiable {
4446 typealias Children = ForEachViewChildren < Items , Child >
4547
4648 public var body : EmptyView {
@@ -112,20 +114,69 @@ extension ForEach: TypeSafeView, View where Child: View {
112114 children. queuedChanges = [ ]
113115 }
114116
115- // TODO: The way we're reusing nodes for technically different elements means that if
116- // Child has state of its own then it could get pretty confused thinking that its state
117- // changed whereas it was actually just moved to a new slot in the array. Probably not
118- // a huge issue, but definitely something to keep an eye on.
119117 var layoutableChildren : [ LayoutSystem . LayoutableChild ] = [ ]
120- for (i, node) in children. nodes. enumerated ( ) {
121- guard i < elements. count else {
122- break
118+
119+ let oldNodes = children. nodes
120+ let oldMap = children. nodeIdentifierMap
121+ let oldIdentifiers = children. identifiers
122+ let identifiersStart = oldIdentifiers. startIndex
123+
124+ children. nodes = [ ]
125+ children. nodeIdentifierMap = [ : ]
126+ children. identifiers = [ ]
127+
128+ // Once this is true, every node that existed in the previous update and
129+ // still exists in the new one is reinserted to ensure that items are
130+ // rendered in the correct order.
131+ var requiresOngoingReinsertion = false
132+
133+ for (i, element) in elements. enumerated ( ) {
134+ let childContent = child ( element)
135+ let node : AnyViewGraphNode < Child >
136+
137+ if let oldNode = oldMap [ element. id] {
138+ node = oldNode
139+
140+ // Checks if there is a preceding item that was not preceding in
141+ // the previous update. If such an item exists, it means that
142+ // the order of the collection has changed or that an item was
143+ // inserted somewhere in the middle, rather than simply appended.
144+ requiresOngoingReinsertion =
145+ requiresOngoingReinsertion
146+ || {
147+ guard
148+ let ownOldIndex = oldIdentifiers. firstIndex ( of: element. id)
149+ else { return false }
150+
151+ let subset = oldIdentifiers [ identifiersStart..< ownOldIndex]
152+ return !children. identifiers. subtracting ( subset) . isEmpty
153+ } ( )
154+
155+ if requiresOngoingReinsertion {
156+ removeChild ( oldNode. widget. into ( ) )
157+ addChild ( oldNode. widget. into ( ) )
158+ }
159+ } else {
160+ // New Items need ongoing reinsertion to get
161+ // displayed at the correct location.
162+ requiresOngoingReinsertion = true
163+ node = AnyViewGraphNode (
164+ for: childContent,
165+ backend: backend,
166+ environment: environment
167+ )
168+ addChild ( node. widget. into ( ) )
123169 }
124- let index = elements. startIndex. advanced ( by: i)
125- let childContent = child ( elements [ index] )
170+
171+ children. nodeIdentifierMap [ element. id] = node
172+ children. identifiers. append ( element. id)
173+
174+ children. nodes. append ( node)
175+
126176 if children. isFirstUpdate {
127177 addChild ( node. widget. into ( ) )
128178 }
179+
129180 layoutableChildren. append (
130181 LayoutSystem . LayoutableChild (
131182 update: { proposedSize, environment, dryRun in
@@ -139,40 +190,13 @@ extension ForEach: TypeSafeView, View where Child: View {
139190 )
140191 )
141192 }
193+
142194 children. isFirstUpdate = false
143195
144- let nodeCount = children. nodes. count
145- let remainingElementCount = elements. count - nodeCount
146- if remainingElementCount > 0 {
147- let startIndex = elements. startIndex. advanced ( by: nodeCount)
148- for i in 0 ..< remainingElementCount {
149- let childContent = child ( elements [ startIndex. advanced ( by: i) ] )
150- let node = AnyViewGraphNode (
151- for: childContent,
152- backend: backend,
153- environment: environment
154- )
155- children. nodes. append ( node)
156- addChild ( node. widget. into ( ) )
157- layoutableChildren. append (
158- LayoutSystem . LayoutableChild (
159- update: { proposedSize, environment, dryRun in
160- node. update (
161- with: childContent,
162- proposedSize: proposedSize,
163- environment: environment,
164- dryRun: dryRun
165- )
166- }
167- )
168- )
169- }
170- } else if remainingElementCount < 0 {
171- let unused = - remainingElementCount
172- for i in ( nodeCount - unused) ..< nodeCount {
173- removeChild ( children. nodes [ i] . widget. into ( ) )
174- }
175- children. nodes. removeLast ( unused)
196+ for removed in oldMap. filter ( {
197+ !children. identifiers. contains ( $0. key)
198+ } ) . values {
199+ removeChild ( removed. widget. into ( ) )
176200 }
177201
178202 return LayoutSystem . updateStackLayout (
@@ -199,9 +223,17 @@ extension ForEach: TypeSafeView, View where Child: View {
199223class ForEachViewChildren <
200224 Items: Collection ,
201225 Child: View
202- > : ViewGraphNodeChildren where Items. Index == Int {
226+ > : ViewGraphNodeChildren where Items. Index == Int , Items . Element : Identifiable {
203227 /// The nodes for all current children of the ``ForEach`` view.
204228 var nodes : [ AnyViewGraphNode < Child > ] = [ ]
229+
230+ /// The nodes for all current children of the ``ForEach`` view, queriable by their identifier.
231+ var nodeIdentifierMap : [ Items . Element . ID : AnyViewGraphNode < Child > ]
232+
233+ /// The identifiers of all current children ``ForEach`` view in the order they are displayed.
234+ /// Can be used for checking if an element was moved or an element was inserted in front of it.
235+ var identifiers : OrderedSet < Items . Element . ID >
236+
205237 /// Changes queued during `dryRun` updates.
206238 var queuedChanges : [ Change ] = [ ]
207239
@@ -227,18 +259,30 @@ class ForEachViewChildren<
227259 snapshots: [ ViewGraphSnapshotter . NodeSnapshot ] ? ,
228260 environment: EnvironmentValues
229261 ) {
230- nodes = view. elements
231- . map ( view. child)
232- . enumerated ( )
233- . map { ( index, child) in
262+ var nodeIdentifierMap = [ Items . Element . ID : AnyViewGraphNode < Child > ] ( )
263+ var identifiers = OrderedSet < Items . Element . ID > ( )
264+ var viewNodes = [ AnyViewGraphNode < Child > ] ( )
265+
266+ for (index, element) in view. elements. enumerated ( ) {
267+ let child = view. child ( element)
268+ let viewGraphNode = {
234269 let snapshot = index < snapshots? . count ?? 0 ? snapshots ? [ index] : nil
235270 return ViewGraphNode (
236271 for: child,
237272 backend: backend,
238273 snapshot: snapshot,
239274 environment: environment
240275 )
241- }
242- . map ( AnyViewGraphNode . init ( _: ) )
276+ } ( )
277+
278+ let anyViewGraphNode = AnyViewGraphNode ( viewGraphNode)
279+ viewNodes. append ( anyViewGraphNode)
280+
281+ identifiers. append ( element. id)
282+ nodeIdentifierMap [ element. id] = anyViewGraphNode
283+ }
284+ nodes = viewNodes
285+ self . identifiers = identifiers
286+ self . nodeIdentifierMap = nodeIdentifierMap
243287 }
244288}
0 commit comments