Sheet Presented From List Showing Incorrect Data

I have a simple SwiftUI app with a list of 2 items: Item 1 and item 2. Tapping on one of the items should show the corresponding page:

ContentView.swift

struct ContentView: View {

    @State var isPresented:Bool = false

    @State var items = [
        "item 1",
        "item 2",
    ]

    var body: some View {
        List {
            ForEach(items, id:\.self) { item in
                Button {
                    isPresented = true
                } label: {
                    Text(item)
                }
                .sheet(isPresented: $isPresented, content: {
                    CustomView(text: item)
                })
            }
            .onDelete { indexSet in
                items.remove(atOffsets: indexSet)
            }
        }
    }
}

CustomView.swift

struct CustomView: View {

    var text:String

    var body: some View {
        Text("Text: \(text)")
            .onAppear {
                print("text: \(text)")
            }
    }
}

The expected behavior with this code is tapping that Item 1 or Item 2 presents a sheet with a label reading "item 1" or "Item 2", respectively.

However, that is not what happens. Tapping on any of the items always shows item 2's page. Even after deleting item 2 and tapping item 1 again, the page always shows item 2.

This issue persisted from iOS 15 to iOS 16 (I didn't test it before iOS 15). Running on various simulators and physical devices, including iPhone 11 Pro, and iPhone 14 Pro. Xcode version 14.0, macOS 12.6.

Is this a bug? Or is my code wrong? Any help would be appreciated.

Answered by BabyJ in 728415022

You need to move the sheet modifier outside of the ForEach, as it's currently creating a sheet for every item bound to the same isPresented variable. When you set isPresented to true, you are essentially showing each item's sheet, but you only see the last one.


You can instead use the sheet(item:onDismiss:content:) modifier.

Something like this will work:

// the item parameter requires the data to be Identifiable
// you can make a custom struct to hold this, or manually conform String to Identifiable
struct Item: Identifiable {
    let id = UUID()
    let name: String
}
// you can remove the isPresented variable

@State selectedItem: Item?

var body: some View {
    ...

    ForEach(...) { item in
        Button {
            selectedItem = Item(name: item)
        } label: {
            ...
        }
    }
    // move outside loop so there is only one sheet that shows a view dependant on the selectedItem
    .sheet(item: $selectedItem) { item in
        CustomView(text: item)
    }

    ...
}

Hello,

To pass parameters to the sheet view, use sheet(item:onDismiss:content:).

ContentView:

struct ContentView: View {

//    @State var isPresented: Bool = false

    @State var items = ["item1", "item2"]

    @State var sheetParameter: MyItem? = nil // 'MyItem' have to conform to 'Identifiable'

    var body: some View {

        List{

            // Using array's index for id.

            ForEach(Array(items.enumerated()), id: \.element){index, item in

                Button(action: {

                    self.sheetParameter = MyItem(id: index, text: item)

                }, label: {

                    Text(item)

                })

                .sheet(item: $sheetParameter) {parameter in

                    CustomView(text: parameter.text)

                }

            }

            .onDelete{ indexSet in

                items.remove(atOffsets: indexSet)

            }

        }

    }

}

MyItem -- structure that conforms 'Identifiable':

struct MyItem: Identifiable {

    var id: Int

    var text: String

}

CustomView doesn't have to change.

The first parameter of sheet(item:onDismiss:content:) requires argument its type conform to 'Identifiable'. In my example, using array's index for id.

However, if your 'item' has other unique elements, you can use it for id.

Accepted Answer

You need to move the sheet modifier outside of the ForEach, as it's currently creating a sheet for every item bound to the same isPresented variable. When you set isPresented to true, you are essentially showing each item's sheet, but you only see the last one.


You can instead use the sheet(item:onDismiss:content:) modifier.

Something like this will work:

// the item parameter requires the data to be Identifiable
// you can make a custom struct to hold this, or manually conform String to Identifiable
struct Item: Identifiable {
    let id = UUID()
    let name: String
}
// you can remove the isPresented variable

@State selectedItem: Item?

var body: some View {
    ...

    ForEach(...) { item in
        Button {
            selectedItem = Item(name: item)
        } label: {
            ...
        }
    }
    // move outside loop so there is only one sheet that shows a view dependant on the selectedItem
    .sheet(item: $selectedItem) { item in
        CustomView(text: item)
    }

    ...
}
Sheet Presented From List Showing Incorrect Data
 
 
Q