Skip to content

Commit 466c259

Browse files
committed
added fallback to old code where no keypath is specified
1 parent 116b72c commit 466c259

File tree

2 files changed

+145
-8
lines changed

2 files changed

+145
-8
lines changed

Examples/Sources/StressTestExample/StressTestApp.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,11 @@ struct StressTestApp: App {
4343
for _ in 0..<1000 {
4444
values.append(Self.options.randomElement()!)
4545
}
46-
4746
self.values[tab!] = values
4847
}
4948
if let values = values[tab!] {
5049
ScrollView {
51-
ForEach(values) { value in
50+
ForEach(items: values) { value in
5251
Text(value)
5352
}
5453
}.frame(minWidth: 300)

Sources/SwiftCrossUI/Views/ForEach.swift

Lines changed: 144 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import OrderedCollections
22

33
/// 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> {
55
/// A variable-length collection of elements to display.
66
var elements: Items
77
/// A method to display the elements as views.
88
var child: (Items.Element) -> Child
99
/// The path to the property used as Identifier
10-
var idKeyPath: KeyPath<Items.Element, ID>
10+
var idKeyPath: KeyPath<Items.Element, ID>?
1111
}
1212

1313
extension ForEach: TypeSafeView, View where Child: View {
@@ -83,6 +83,17 @@ extension ForEach: TypeSafeView, View where Child: View {
8383
children.queuedChanges = []
8484
}
8585

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+
8697
var layoutableChildren: [LayoutSystem.LayoutableChild] = []
8798

8899
let oldMap = children.nodeIdentifierMap
@@ -177,6 +188,104 @@ extension ForEach: TypeSafeView, View where Child: View {
177188
dryRun: dryRun
178189
)
179190
}
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+
}
180289
}
181290

182291
/// Stores the child nodes of a ``ForEach`` view.
@@ -226,10 +335,28 @@ class ForEachViewChildren<
226335
init<Backend: AppBackend>(
227336
from view: ForEach<Items, ID, Child>,
228337
backend: Backend,
229-
idKeyPath: KeyPath<Items.Element, ID>,
338+
idKeyPath: KeyPath<Items.Element, ID>?,
230339
snapshots: [ViewGraphSnapshotter.NodeSnapshot]?,
231340
environment: EnvironmentValues
232341
) {
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+
}
233360
var nodeIdentifierMap = [ID: AnyViewGraphNode<Child>]()
234361
var identifiers = OrderedSet<ID>()
235362
var viewNodes = [AnyViewGraphNode<Child>]()
@@ -260,27 +387,38 @@ class ForEachViewChildren<
260387

261388
// MARK: - Alternative Initializers
262389
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+
)
263396
@_disfavoredOverload
264397
public init(
265398
items elements: Items,
266399
_ child: @escaping (Items.Element) -> Child
267400
) {
268401
self.elements = elements
269402
self.child = child
270-
self.idKeyPath = \.self
403+
self.idKeyPath = nil
271404
}
272405
}
273406

274407
extension ForEach where Child == [MenuItem], Items.Element: Hashable, ID == Items.Element {
275408
/// 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+
)
276414
@_disfavoredOverload
277415
public init(
278-
items elements: Items,
416+
_ elements: Items,
279417
@MenuItemsBuilder _ child: @escaping (Items.Element) -> [MenuItem]
280418
) {
281419
self.elements = elements
282420
self.child = child
283-
self.idKeyPath = \.self
421+
self.idKeyPath = nil
284422
}
285423
}
286424

0 commit comments

Comments
 (0)