19

I have an issue using a sheet inside a ForEach. Basically I have a List that shows many items in my array and an image that trigger the sheet. The problem is that when my sheet is presented it only shows the first item of my array which is "Harry Potter" in this case.

Here's the code

struct ContentView: View {
    @State private var showingSheet = false
    
    var movies = ["Harry potter", "Mad Max", "Oblivion", "Memento"]
    var body: some View {
        NavigationView {
            List {
                ForEach(0 ..< movies.count) { movie in
                    HStack {
                        Text(self.movies[movie])
                        Image(systemName: "heart")
                    }
                        .onTapGesture {
                            self.showingSheet = true
                    }
                    .sheet(isPresented: self.$showingSheet) {
                        Text(self.movies[movie])
                    }
                }
            }
        }
    }
}
xmetal
  • 681
  • 6
  • 16

3 Answers3

30

There should be only one sheet, so here is possible approach - use another sheet modifier and activate it by selection

Tested with Xcode 12 / iOS 14 (iOS 13 compatible)

extension Int: Identifiable {
    public var id: Int { self }
}

struct ContentView: View {
    @State private var selectedMovie: Int? = nil

    var movies = ["Harry potter", "Mad Max", "Oblivion", "Memento"]
    var body: some View {
        NavigationView {
            List {
                ForEach(0 ..< movies.count) { movie in
                    HStack {
                        Text(self.movies[movie])
                        Image(systemName: "heart")
                    }
                        .onTapGesture {
                            self.selectedMovie = movie
                    }
                }
            }
            .sheet(item: self.$selectedMovie) {
                Text(self.movies[$0])
            }
        }
    }
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • This example that I've shown is pretty simple and it works how you changed it but I have a more complex situation where I need to keep track of the position of the item in my array and I also need to dismiss the sheet when I click a Button. Is there any other way to fix this problem without using a @State property to call? – xmetal Jul 25 '20 at 14:35
  • I just don't understand why when I use Sheet inside the ForEach it doesn't keep track of the "movie" position while looping. That self.movies[movie] should change each time but it doesn't – xmetal Jul 25 '20 at 14:41
  • 2
    When you add .sheet modifier within ForEach you add many sheets (one per row) joined to one this state, so toggling one state you activate *all* sheets, which result in unpredictable behaviour. You need one sheet and one selected item, either by index or by item directly - it is by preference. – Asperi Jul 25 '20 at 15:07
  • Oh okay, I understand now. I'll try to adjust everything based on that. Thank you – xmetal Jul 25 '20 at 15:13
  • If you have a custom object instead of a String, take a look at this article. https://www.hackingwithswift.com/books/ios-swiftui/working-with-identifiable-items-in-swiftui – Nick N Apr 27 '21 at 01:43
7

I changed your code to have only one sheet and have the selected movie in one variable.

extension String: Identifiable {
    public var id: String { self }
}

struct ContentView: View {
    @State private var selectedMovie: String? = nil
    
    var movies = ["Harry potter", "Mad Max", "Oblivion", "Memento"]
    var body: some View {
        NavigationView {
            List {
                ForEach(movies) { movie in
                    HStack {
                        Text(movie)
                        Image(systemName: "heart")
                    }
                    .onTapGesture {
                        self.selectedMovie = movie
                    }
                }
            }
            .sheet(item: self.$selectedMovie, content: { selectedMovie in
                Text(selectedMovie)
            })
        }
    }
}
Dharman
  • 30,962
  • 25
  • 85
  • 135
2

Wanted to give my 2 cents on the matter. I was encountering the same problem and Asperi's solution worked for me. BUT - I also wanted to have a button on the sheet that dismisses the modal.

When you call a sheet with isPresented you pass a binding Bool and so you change it to false in order to dismiss. What I did in the item case is I passed the item as a Binding. And in the sheet, I change that binding item to nil and that dismissed the sheet.

So for example in this case the code would be:

var movies = ["Harry potter", "Mad Max", "Oblivion", "Memento"]
var body: some View {
    NavigationView {
        List {
            ForEach(0 ..< movies.count) { movie in
                HStack {
                    Text(self.movies[movie])
                    Image(systemName: "heart")
                }
                    .onTapGesture {
                        self.selectedMovie = movie
                }
            }
        }
        .sheet(item: self.$selectedMovie) {
            Text(self.movies[$0])

            // My addition here: a "Done" button that dismisses the sheet

            Button {
                selectedMovie = nil
            } label: {
                Text("Done")
            }

        }
    }
}
Rutang
  • 21
  • 1