Sheet inside ForEach doesn't loop over items SwiftUI
Asked Answered
F

3

20

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])
                    }
                }
            }
        }
    }
}
Feltonfelts answered 25/7, 2020 at 13:24 Comment(0)
B
34

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])
            }
        }
    }
}
Brilliancy answered 25/7, 2020 at 13:42 Comment(5)
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?Feltonfelts
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'tFeltonfelts
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.Brilliancy
Oh okay, I understand now. I'll try to adjust everything based on that. Thank youFeltonfelts
If you have a custom object instead of a String, take a look at this article. hackingwithswift.com/books/ios-swiftui/…Capon
C
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)
            })
        }
    }
}
Chlordane answered 27/9, 2020 at 21:58 Comment(0)
C
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")
            }

        }
    }
}
Circumambulate answered 12/5, 2021 at 15:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.