swiftuiswiftui-listswiftui-state

Why does Context Menu display the old state even though the List has correctly been updated?


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!

Edit:

It seems to be an iOS 15 regression (at least on Release Candidate), it works fine on iOS 14.6.


Solution

  • 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)