SwiftUI: Major unannounced change in iOS18.4 beta1

Hi, I have noticed a major change to a SwiftUI API behavior in iOS18.4beta1 which breaks my app's functionality, and I've started hearing from users running the new beta that the app doesn't correctly work for them anymore.

The problem is with views that contain a List with multiple-selection, and the contextMenu API applied with the ‘primaryAction’ callback that is triggered when the user taps on a row. Previously, if the user tapped on a row, this callback was triggered with the 'selectedItems' showing the tapped item. With iOS18.4beta, the same callback is triggered with ‘selectedItems’ being empty.

I have the code to demonstrate the problem:

struct ListSelectionTestView: View {
    
    @State private var items: [TimedItem] = [
        TimedItem(number: 1, timestamp: "2024-11-20 10:00"),
        TimedItem(number: 2, timestamp: "2024-11-20 11:00"),
        TimedItem(number: 3, timestamp: "2024-11-20 12:00")
    ]
    
    @State var selectedItems = Set<TimedItem.ID>()
    
    var body: some View {
        NavigationStack {
            List(selection: $selectedItems) {
                
                ForEach(items) { item in
                    Text("Item \(item.number) - \(item.timestamp)")
                }
            }
            .contextMenu(forSelectionType: TimedItem.ID.self, menu: {_ in
                Button(action: {
                    print("button called - count = \(selectedItems.count)")
                }) {
                    Label("Add Item", systemImage: "square.and.pencil")
                }
                
            }, primaryAction: {_ in
                print("primaryAction called - count = \(selectedItems.count)")
            })
        }
    }
}

struct TimedItem: Identifiable {
    let id = UUID()
    let number: Int
    let timestamp: String
}

#Preview {
    ListSelectionTestView()
}

Running the same code on iOS18.2, and tapping on a row will print this to the console:

primaryAction called - count = 1

Running the same code on iOS18.4 beta1, and tapping on a row will print this to the console:

primaryAction called - count = 0

So users who were previously selecting an item from the row, and then seeing expected behavior with the selected item, will now suddenly tap on a row and see nothing. My app's functionality relies on the user selecting an item from a list to see another detailed view with the selected item's contents, and it doesn't work anymore.

This is a major regression issue. Please confirm and let me know. I have filed a feedback: FB16593120

This is an expected behavior. At the time the callback is triggered there is nothing selected.

To enable selection put the list into edit mode by either modifying the editMode value, or adding an EditButton to your app’s interface. When you put the list into edit mode, the list shows a circle next to each list item. Once an item has selected, selectedItems.count would have the appropriate value.

Hi, Why is it 'expected behavior' in iOS18.4 if it's behaved differently all this time? It's been working this way since iOS16 I believe. Was that just a bug?

This API was the most convenient way to handle both row selection AND provide a context menu to the user. The problem I run into is that if I add the contextMenu(forSelectionType: menu: primaryAction:) as a view modifier, then it eats the onChange(of: selectedItems) change handler. Both don't seem to exist together. Is there another way to track which item the user is selecting?

So now I'm stuck ... either I don't provide a context menu for the items in the list, or I can't handle the row selection at all (without completely breaking the UI by putting the list in Edit mode).

Please consider keeping the existing pre-iOS18.4 behavior. If it's been working fine in production since iOS16, then I don't see what the eagerness to change it is.

So I realized that the primaryAction closure has a closure parameter, and using that gets the row item that the user tapped on.

List(selection: $selectedItems) {
                
    ForEach(items) { item in
        Text("Item \(item.number) - \(item.timestamp)")
    }
}
.contextMenu(forSelectionType: TimedItem.ID.self, menu: {_ in
    Button(action: {
        print("button called - count = \(selectedItems.count)")
    }) {
        Label("Add Item", systemImage: "square.and.pencil")
    }
    
}, primaryAction: {items in
    print("primaryAction called - count = \(items.count)") // not selectedItems.count
})

Hopefully this is 'expected behavior' and isn't expected to change any time soon

SwiftUI: Major unannounced change in iOS18.4 beta1
 
 
Q