I'm facing an issue where the displayed Context Menu
shows the wrong data, even though the List
underneath displays the correct one. The issue is that once triggering the action on the context menu of the first item, you'll see how the List
re-renders and shows the correct data but if you trigger the context menu again for the first item, it won't show the correct state. If you open the context menu for the second item, it will display the correct state, but if you now select "Two", and open the same context menu, the State
will be wrong (it'll display only 1 selected when it should show 1 & 2, like the List
displays it).
It feels like it's off by one (like presenting the previous state instead of the latest one) and I'm not sure if it's just a bug or I'm using it wrong.
Here's a snippet of code to reproduce the issue:
@main
struct ContextMenuBugApp: App {
let availableItems = ["One", "Two", "Three", "Four", "Five"]
@State var selectedItems: [String] = []
var body: some Scene {
WindowGroup {
List {
ForEach(availableItems, id: \.self) { item in
HStack {
let isAlreadySelected = selectedItems.contains(item)
Text("Row \(item), selected: \(isAlreadySelected ? "true" : "false")")
}.contextMenu {
ForEach(availableItems, id: \.self) { item in
let isAlreadySelected = selectedItems.contains(item)
Button {
isAlreadySelected ? selectedItems.removeAll(where: { $0 == item }) : selectedItems.append(item)
} label: {
Label(item, systemImage: isAlreadySelected ? "checkmark.circle.fill" : "")
}
}
}
}
}
}
}
}
Video demonstrating the issue: https://twitter.com/xmollv/status/1412397838319898637
Thanks!
It seems to be an iOS 15 regression (at least on Release Candidate), it works fine on iOS 14.6.
you can force the contextMenu to redraw with background view with arbitrary id. i.e.:
@main
struct ContextMenuBugApp: App {
let availableItems = ["One", "Two", "Three", "Four", "Five"]
@State var selectedItems: [String] = []
func isAlreadySelected(_ item: String) -> Bool {
selectedItems.contains(item)
}
var body: some Scene {
WindowGroup {
List {
ForEach(availableItems, id: \.self) { item in
HStack {
Text("Row \(item), selected: \(isAlreadySelected(item) ? "true" : "false")")
}
.background(
Color.clear
.contextMenu {
ForEach(availableItems, id: \.self) { item in
Button {
isAlreadySelected(item) ? selectedItems.removeAll(where: { $0 == item }) : selectedItems.append(item)
} label: {
Label(item, systemImage: isAlreadySelected(item) ? "checkmark.circle.fill" : "")
}
}
}.id(selectedItems.count)
)
}
}
}
}
}
If it doesn’t work, you can try just putting id to contextMenu without the background (this could be based on iOS version, it didn’t work before, so be careful and test prior iOS)