Skip to content

Commit 2dba16b

Browse files
committed
Fixed ForEach [MenuItem] compatibility, documentation improvement & cleanup
sadly an argument name seems to be required on menuitem forEach initializer, the compiler is apparently unable to infer the right child from the context.
1 parent 76968ca commit 2dba16b

File tree

3 files changed

+35
-22
lines changed

3 files changed

+35
-22
lines changed

Sources/SwiftCrossUI/Builders/MenuItemsBuilder.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ public struct MenuItemsBuilder {
1717
first.items
1818
}
1919

20-
public static func buildPartialBlock<Items: Collection>(
21-
first: ForEach<Items, MenuItem, [MenuItem]>
20+
public static func buildPartialBlock<Items: Collection, ID: Hashable>(
21+
first: ForEach<Items, ID, [MenuItem]>
2222
) -> [MenuItem] {
2323
first.elements.map(first.child).flatMap { $0 }
2424
}
@@ -51,9 +51,9 @@ public struct MenuItemsBuilder {
5151
accumulated + buildPartialBlock(first: next)
5252
}
5353

54-
public static func buildPartialBlock<Items: Collection>(
54+
public static func buildPartialBlock<Items: Collection, ID: Hashable>(
5555
accumulated: [MenuItem],
56-
next: ForEach<Items, MenuItem, [MenuItem]>
56+
next: ForEach<Items, ID, [MenuItem]>
5757
) -> [MenuItem] {
5858
accumulated + buildPartialBlock(first: next)
5959
}

Sources/SwiftCrossUI/Views/Button.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ public struct Button: Sendable {
2121
}
2222
}
2323

24-
extension Button: View {
25-
}
24+
extension Button: View {}
2625

2726
extension Button: ElementaryView {
2827
public func asWidget<Backend: AppBackend>(backend: Backend) -> Backend.Widget {

Sources/SwiftCrossUI/Views/ForEach.swift

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ extension ForEach: TypeSafeView, View where Child: View {
8383
children.queuedChanges = []
8484
}
8585

86+
// Use the previous update Method when no keyPath is set on a
87+
// [Hashable] Collection to optionally keep the old behaviour.
8688
guard let idKeyPath else {
8789
return deprecatedUpdate(
8890
widget,
@@ -109,24 +111,32 @@ extension ForEach: TypeSafeView, View where Child: View {
109111
// still exists in the new one is reinserted to ensure that items are
110112
// rendered in the correct order.
111113
var requiresOngoingReinsertion = false
114+
115+
// Forces node recreation when enabled (expensive on large Collections).
116+
// Use only when idKeyPath yields non-unique values. Prefer Identifiable
117+
// or guaranteed unique, constant identifiers for optimal performance.
118+
// Node caching and diffing require unique, stable IDs.
112119
var ongoingNodeReusingDisabled = false
120+
121+
// Avoid reallocation
113122
var inserted = false
114123

115124
for element in elements {
116125
let childContent = child(element)
117126
let node: AnyViewGraphNode<Child>
118127

128+
// Track duplicates: inserted=false if ID exists.
129+
// Disables node reuse if any duplicate gets found.
119130
(inserted, _) = children.identifiers.append(element[keyPath: idKeyPath])
120131
ongoingNodeReusingDisabled = ongoingNodeReusingDisabled || !inserted
121132

122133
if !ongoingNodeReusingDisabled {
123134
if let oldNode = oldMap[element[keyPath: idKeyPath]] {
124135
node = oldNode
125136

126-
// Checks if there is a preceding item that was not preceding in
127-
// the previous update. If such an item exists, it means that
128-
// the order of the collection has changed or that an item was
129-
// inserted somewhere in the middle, rather than simply appended.
137+
// Detects reordering or mid-collection insertion:
138+
// Checks if there is a preceding item that was not
139+
// preceding in the previous update.
130140
requiresOngoingReinsertion =
131141
requiresOngoingReinsertion
132142
|| {
@@ -139,20 +149,24 @@ extension ForEach: TypeSafeView, View where Child: View {
139149
return !children.identifiers.subtracting(subset).isEmpty
140150
}()
141151

142-
if requiresOngoingReinsertion {
152+
// Removes node from its previous position and
153+
// re-adds it at the new correct one.
154+
if requiresOngoingReinsertion, !children.isFirstUpdate {
143155
removeChild(oldNode.widget.into())
144156
addChild(oldNode.widget.into())
145157
}
146158
} else {
147159
// New Items need ongoing reinsertion to get
148-
// displayed at the correct locat ion.
160+
// displayed at the correct location.
149161
requiresOngoingReinsertion = true
150162
node = AnyViewGraphNode(
151163
for: childContent,
152164
backend: backend,
153165
environment: environment
154166
)
155-
addChild(node.widget.into())
167+
if !children.isFirstUpdate {
168+
addChild(node.widget.into())
169+
}
156170
}
157171
children.nodeIdentifierMap[element[keyPath: idKeyPath]] = node
158172
} else {
@@ -165,10 +179,6 @@ extension ForEach: TypeSafeView, View where Child: View {
165179

166180
children.nodes.append(node)
167181

168-
if children.isFirstUpdate, !ongoingNodeReusingDisabled {
169-
addChild(node.widget.into())
170-
}
171-
172182
layoutableChildren.append(
173183
LayoutSystem.LayoutableChild(
174184
update: { proposedSize, environment, dryRun in
@@ -183,9 +193,11 @@ extension ForEach: TypeSafeView, View where Child: View {
183193
)
184194
}
185195

186-
children.isFirstUpdate = false
187-
188-
if !ongoingNodeReusingDisabled {
196+
if children.isFirstUpdate {
197+
for nodeToAdd in children.nodes {
198+
addChild(nodeToAdd.widget.into())
199+
}
200+
} else if !ongoingNodeReusingDisabled {
189201
for removed in oldMap.filter({
190202
!children.identifiers.contains($0.key)
191203
}).values {
@@ -200,6 +212,8 @@ extension ForEach: TypeSafeView, View where Child: View {
200212
}
201213
}
202214

215+
children.isFirstUpdate = false
216+
203217
return LayoutSystem.updateStackLayout(
204218
container: widget,
205219
children: layoutableChildren,
@@ -434,7 +448,7 @@ extension ForEach where Child == [MenuItem], Items.Element: Hashable, ID == Item
434448
)
435449
@_disfavoredOverload
436450
public init(
437-
_ elements: Items,
451+
menuItems elements: Items,
438452
@MenuItemsBuilder _ child: @escaping (Items.Element) -> [MenuItem]
439453
) {
440454
self.elements = elements
@@ -447,7 +461,7 @@ extension ForEach where Child == [MenuItem] {
447461
/// Creates a view that creates child views on demand based on a collection of data.
448462
@_disfavoredOverload
449463
public init(
450-
_ elements: Items,
464+
menuItems elements: Items,
451465
id keyPath: KeyPath<Items.Element, ID>,
452466
@MenuItemsBuilder _ child: @escaping (Items.Element) -> [MenuItem]
453467
) {

0 commit comments

Comments
 (0)