Skip to content

Commit dbd1080

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 cfc9867 commit dbd1080

File tree

5 files changed

+41
-57
lines changed

5 files changed

+41
-57
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 & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import Foundation
2-
31
/// A control that initiates an action.
42
public struct Button: Sendable {
53
/// The label to show on the button.
@@ -23,8 +21,7 @@ public struct Button: Sendable {
2321
}
2422
}
2523

26-
extension Button: View {
27-
}
24+
extension Button: View {}
2825

2926
extension Button: ElementaryView {
3027
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
) {

Sources/SwiftCrossUI/Views/Menu.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ public struct Menu: Sendable {
2626
ResolvedMenu(
2727
items: items.map { item in
2828
switch item {
29-
case .button(let button, _):
29+
case .button(let button):
3030
.button(button.label, button.action)
31-
case .text(let text, _):
31+
case .text(let text):
3232
.button(text.string, nil)
33-
case .submenu(let submenu, _):
33+
case .submenu(let submenu):
3434
.submenu(submenu.resolve())
3535
}
3636
}
Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,6 @@
11
/// An item of a ``Menu`` or ``CommandMenu``.
22
public enum MenuItem: Sendable {
3-
case button(Button, Int? = nil)
4-
case text(Text, Int? = nil)
5-
case submenu(Menu, Int? = nil)
6-
}
7-
8-
extension MenuItem: Hashable {
9-
public static func == (lhs: MenuItem, rhs: MenuItem) -> Bool {
10-
lhs.hashValue == rhs.hashValue
11-
}
12-
13-
public func hash(into hasher: inout Hasher) {
14-
hasher.combine(
15-
{
16-
switch self {
17-
case .button(_, let int), .text(_, let int), .submenu(_, let int):
18-
return int ?? 0
19-
}
20-
}())
21-
}
22-
23-
package func addingIDIfNeeded(id: Int) -> Self {
24-
switch self {
25-
case .button(let button, let int):
26-
return .button(button, int ?? id)
27-
case .text(let text, let int):
28-
return .text(text, int ?? id)
29-
case .submenu(let menu, let int):
30-
return .submenu(menu, int ?? id)
31-
}
32-
}
3+
case button(Button)
4+
case text(Text)
5+
case submenu(Menu)
336
}

0 commit comments

Comments
 (0)