|
1 | 1 | import OrderedCollections |
2 | 2 |
|
3 | 3 | /// A view that displays a variable amount of children. |
4 | | -public struct ForEach<Items: Collection, ID: Hashable, Child> where Items.Index == Int { |
| 4 | +public struct ForEach<Items: Collection, ID: Hashable, Child> { |
5 | 5 | /// A variable-length collection of elements to display. |
6 | 6 | var elements: Items |
7 | 7 | /// A method to display the elements as views. |
8 | 8 | var child: (Items.Element) -> Child |
9 | 9 | /// The path to the property used as Identifier |
10 | | - var idKeyPath: KeyPath<Items.Element, ID> |
| 10 | + var idKeyPath: KeyPath<Items.Element, ID>? |
11 | 11 | } |
12 | 12 |
|
13 | 13 | extension ForEach: TypeSafeView, View where Child: View { |
@@ -83,6 +83,17 @@ extension ForEach: TypeSafeView, View where Child: View { |
83 | 83 | children.queuedChanges = [] |
84 | 84 | } |
85 | 85 |
|
| 86 | + guard let idKeyPath else { |
| 87 | + return deprecatedUpdate( |
| 88 | + widget, |
| 89 | + children: children, |
| 90 | + proposedSize: proposedSize, |
| 91 | + environment: environment, |
| 92 | + backend: backend, |
| 93 | + dryRun: dryRun |
| 94 | + ) |
| 95 | + } |
| 96 | + |
86 | 97 | var layoutableChildren: [LayoutSystem.LayoutableChild] = [] |
87 | 98 |
|
88 | 99 | let oldMap = children.nodeIdentifierMap |
@@ -177,6 +188,104 @@ extension ForEach: TypeSafeView, View where Child: View { |
177 | 188 | dryRun: dryRun |
178 | 189 | ) |
179 | 190 | } |
| 191 | + |
| 192 | + @MainActor |
| 193 | + func deprecatedUpdate<Backend: AppBackend>( |
| 194 | + _ widget: Backend.Widget, |
| 195 | + children: Children, |
| 196 | + proposedSize: SIMD2<Int>, |
| 197 | + environment: EnvironmentValues, |
| 198 | + backend: Backend, |
| 199 | + dryRun: Bool |
| 200 | + ) -> ViewUpdateResult { |
| 201 | + func addChild(_ child: Backend.Widget) { |
| 202 | + if dryRun { |
| 203 | + children.queuedChanges.append(.addChild(AnyWidget(child))) |
| 204 | + } else { |
| 205 | + backend.addChild(child, to: widget) |
| 206 | + } |
| 207 | + } |
| 208 | + |
| 209 | + func removeChild(_ child: Backend.Widget) { |
| 210 | + if dryRun { |
| 211 | + children.queuedChanges.append(.removeChild(AnyWidget(child))) |
| 212 | + } else { |
| 213 | + backend.removeChild(child, from: widget) |
| 214 | + } |
| 215 | + } |
| 216 | + |
| 217 | + // TODO: The way we're reusing nodes for technically different elements means that if |
| 218 | + // Child has state of its own then it could get pretty confused thinking that its state |
| 219 | + // changed whereas it was actually just moved to a new slot in the array. Probably not |
| 220 | + // a huge issue, but definitely something to keep an eye on. |
| 221 | + var layoutableChildren: [LayoutSystem.LayoutableChild] = [] |
| 222 | + for (i, node) in children.nodes.enumerated() { |
| 223 | + guard i < elements.count else { |
| 224 | + break |
| 225 | + } |
| 226 | + let index = elements.index(elements.startIndex, offsetBy: i) |
| 227 | + let childContent = child(elements[index]) |
| 228 | + if children.isFirstUpdate { |
| 229 | + addChild(node.widget.into()) |
| 230 | + } |
| 231 | + layoutableChildren.append( |
| 232 | + LayoutSystem.LayoutableChild( |
| 233 | + update: { proposedSize, environment, dryRun in |
| 234 | + node.update( |
| 235 | + with: childContent, |
| 236 | + proposedSize: proposedSize, |
| 237 | + environment: environment, |
| 238 | + dryRun: dryRun |
| 239 | + ) |
| 240 | + } |
| 241 | + ) |
| 242 | + ) |
| 243 | + } |
| 244 | + children.isFirstUpdate = false |
| 245 | + |
| 246 | + let nodeCount = children.nodes.count |
| 247 | + let remainingElementCount = elements.count - nodeCount |
| 248 | + if remainingElementCount > 0 { |
| 249 | + let startIndex = elements.index(elements.startIndex, offsetBy: nodeCount) |
| 250 | + for i in 0..<remainingElementCount { |
| 251 | + let childContent = child(elements[elements.index(startIndex, offsetBy: i)]) |
| 252 | + let node = AnyViewGraphNode( |
| 253 | + for: childContent, |
| 254 | + backend: backend, |
| 255 | + environment: environment |
| 256 | + ) |
| 257 | + children.nodes.append(node) |
| 258 | + addChild(node.widget.into()) |
| 259 | + layoutableChildren.append( |
| 260 | + LayoutSystem.LayoutableChild( |
| 261 | + update: { proposedSize, environment, dryRun in |
| 262 | + node.update( |
| 263 | + with: childContent, |
| 264 | + proposedSize: proposedSize, |
| 265 | + environment: environment, |
| 266 | + dryRun: dryRun |
| 267 | + ) |
| 268 | + } |
| 269 | + ) |
| 270 | + ) |
| 271 | + } |
| 272 | + } else if remainingElementCount < 0 { |
| 273 | + let unused = -remainingElementCount |
| 274 | + for i in (nodeCount - unused)..<nodeCount { |
| 275 | + removeChild(children.nodes[i].widget.into()) |
| 276 | + } |
| 277 | + children.nodes.removeLast(unused) |
| 278 | + } |
| 279 | + |
| 280 | + return LayoutSystem.updateStackLayout( |
| 281 | + container: widget, |
| 282 | + children: layoutableChildren, |
| 283 | + proposedSize: proposedSize, |
| 284 | + environment: environment, |
| 285 | + backend: backend, |
| 286 | + dryRun: dryRun |
| 287 | + ) |
| 288 | + } |
180 | 289 | } |
181 | 290 |
|
182 | 291 | /// Stores the child nodes of a ``ForEach`` view. |
@@ -226,10 +335,28 @@ class ForEachViewChildren< |
226 | 335 | init<Backend: AppBackend>( |
227 | 336 | from view: ForEach<Items, ID, Child>, |
228 | 337 | backend: Backend, |
229 | | - idKeyPath: KeyPath<Items.Element, ID>, |
| 338 | + idKeyPath: KeyPath<Items.Element, ID>?, |
230 | 339 | snapshots: [ViewGraphSnapshotter.NodeSnapshot]?, |
231 | 340 | environment: EnvironmentValues |
232 | 341 | ) { |
| 342 | + guard let idKeyPath else { |
| 343 | + nodes = view.elements |
| 344 | + .map(view.child) |
| 345 | + .enumerated() |
| 346 | + .map { (index, child) in |
| 347 | + let snapshot = index < snapshots?.count ?? 0 ? snapshots?[index] : nil |
| 348 | + return ViewGraphNode( |
| 349 | + for: child, |
| 350 | + backend: backend, |
| 351 | + snapshot: snapshot, |
| 352 | + environment: environment |
| 353 | + ) |
| 354 | + } |
| 355 | + .map(AnyViewGraphNode.init(_:)) |
| 356 | + identifiers = [] |
| 357 | + nodeIdentifierMap = [:] |
| 358 | + return |
| 359 | + } |
233 | 360 | var nodeIdentifierMap = [ID: AnyViewGraphNode<Child>]() |
234 | 361 | var identifiers = OrderedSet<ID>() |
235 | 362 | var viewNodes = [AnyViewGraphNode<Child>]() |
@@ -260,27 +387,38 @@ class ForEachViewChildren< |
260 | 387 |
|
261 | 388 | // MARK: - Alternative Initializers |
262 | 389 | extension ForEach where Items.Element: Hashable, ID == Items.Element { |
| 390 | + /// Creates a view that creates child views on demand based on a collection of data. |
| 391 | + @available( |
| 392 | + *, |
| 393 | + deprecated, |
| 394 | + message: "Use ForEach with id argument on non-Identifiable Elements instead." |
| 395 | + ) |
263 | 396 | @_disfavoredOverload |
264 | 397 | public init( |
265 | 398 | items elements: Items, |
266 | 399 | _ child: @escaping (Items.Element) -> Child |
267 | 400 | ) { |
268 | 401 | self.elements = elements |
269 | 402 | self.child = child |
270 | | - self.idKeyPath = \.self |
| 403 | + self.idKeyPath = nil |
271 | 404 | } |
272 | 405 | } |
273 | 406 |
|
274 | 407 | extension ForEach where Child == [MenuItem], Items.Element: Hashable, ID == Items.Element { |
275 | 408 | /// Creates a view that creates child views on demand based on a collection of data. |
| 409 | + @available( |
| 410 | + *, |
| 411 | + deprecated, |
| 412 | + message: "Use ForEach with id argument on non-Identifiable Elements instead." |
| 413 | + ) |
276 | 414 | @_disfavoredOverload |
277 | 415 | public init( |
278 | | - items elements: Items, |
| 416 | + _ elements: Items, |
279 | 417 | @MenuItemsBuilder _ child: @escaping (Items.Element) -> [MenuItem] |
280 | 418 | ) { |
281 | 419 | self.elements = elements |
282 | 420 | self.child = child |
283 | | - self.idKeyPath = \.self |
| 421 | + self.idKeyPath = nil |
284 | 422 | } |
285 | 423 | } |
286 | 424 |
|
|
0 commit comments