Passing Data from List to SheetView

Hello,
I have a search view that displays a list from a .json file, when an object is tapped a detailed sheet view is meant to appear. The problem is that my

.sheet(isPresented: $showSheet) {
            ProductDetail(product: product)
                .environmentObject(DataModel())
        }

block isn’t accepting product as a valid argument.
Any help would be appreciated,
Josh

This is my Search View:

import SwiftUI

struct SearchView: View {
    @StateObject var model = DataModel()
    @State private var searchText = ""
    @State private var showSheet = false
    
    var body: some View {
        NavigationStack {
            List {
                ForEach(filteredProducts, id: \.self) { product in
                    NavigationLink {
                        Text(product.name)
                    } label: {
                        PageView(pages: model.features.map { FeatureCard(product: $0) })
                            .aspectRatio(3 / 2, contentMode: .fit)
                            .listRowInsets(EdgeInsets())
                            .onTapGesture {
                                showSheet = true
                            }
                    }
                }
            }
            .navigationTitle("Search")
            .navigationBarTitleDisplayMode(.inline)
        }
        .searchable(text: $searchText, prompt: "Tap to Search for Light Products")
        .sheet(isPresented: $showSheet) {
            ProductDetail(product: product)
                .environmentObject(DataModel())
        }
    }
    var filteredProducts: [Product] {
        if searchText.isEmpty {
            return model.products
        } else {
            return model.products.filter {
                $0.name.lowercased().contains(searchText.lowercased()) ||
                $0.flavour.lowercased().contains(searchText.lowercased())
            }
        }
    }
}
struct SheetView: View {
    let product: Product
    
    var body: some View {
        VStack {
            Text(product.name)
        }
    }
}

struct SearchView_Previews: PreviewProvider {
    static var previews: some View {
        SearchView()
            .environmentObject(DataModel())
    }
}

My ProductDetail() View:

import SwiftUI

struct ProductDetail: View {
    @StateObject var model = DataModel()
    @EnvironmentObject var dataModel: DataModel
    
    var product: Product
    var productIndex: Int {
        dataModel.products.firstIndex(where: { $0.id == product.id })!
    }
    
    var body: some View {
        ScrollView {
            
            VStack(alignment: .leading) {
                
                PageView(pages: model.features.map { FeatureCard(product: $0) })
                    .aspectRatio(3 / 2, contentMode: .fit)
                    .listRowInsets(EdgeInsets())
                       
                Text(product.name)
                    .font(.title)
                
                Text(product.flavour)
                    .font(.subheadline)
                    .foregroundColor(.secondary)
                
                
                NavigationLink(destination: BagView())
                {Text("$7.99")}
                    .buttonStyle(.borderedProminent)
                    .padding()

                Divider()
                
                Text(product.description)
            }
            .padding(.bottom)
        }
        .navigationTitle(product.name)
        .navigationBarTitleDisplayMode(.inline)
    }
}

struct ProductDetail_Previews: PreviewProvider {
    static let dataModel = DataModel()
    
    static var previews: some View {
        ProductDetail(product: dataModel.products[0])
            .environmentObject(dataModel)
    }
}

My DataModel:

import Foundation
import Combine

class DataModel: ObservableObject {
    @Published var products = [Product]()

    init() {
        products = load("ProductData.json")
    }
    var features: [Product] {
        products.filter { $0.isFeatured }
    }
    
    func load<T: Decodable>(_ filename: String) -> T {
        let data: Data
        guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
        else {
            fatalError("Couldn't find \(filename) in main bundle.")
        }
        do {
            data = try Data(contentsOf: file)
        } catch {
            fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
        }
        do {
            let decoder = JSONDecoder()
            return try decoder.decode(T.self, from: data)
        } catch {
            fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
        }
    }
}

My Product definition file:

import Foundation
import SwiftUI

struct Product: Hashable, Codable, Identifiable {
    var id: Int
    var name: String
    var flavour: String
    var description: String
    var isFeatured: Bool
    
    var category: Category
    enum Category: String, CaseIterable, Codable {
        case drink = "Drink"
        case yoghurt = "Yoghurt"
    }
    
    var image: String
    private var imageName: Image {
        Image(image)
    }
    var image2: String
    private var image2Name: Image {
        Image(image2)
    }
    var image3: String
    private var image3Name: Image {
        Image(image3)
    }
    var featureImage: Image? {
            isFeatured ? Image(image) : nil
        }
}

Here is a sample of what my .JSON file looks like:

{
        "id": 1001,
        "name": "Lemon Zest Energy",
        "category": "Drink",
        "flavour": "Lemon",
        "description": "Revitalize your senses and conquer your day with the zesty and refreshing taste of Lemon Zest Energy. Packed with essential vitamins and energy-boosting ingredients, this lemon-flavored energy drink will give you the boost you need to power through your day. Perfect for those who need a quick pick-me-up or for those who want to add a little extra zest to their daily routine.",
        "image": "Lemon 1",
        "image2": "Lemon 2",
        "image3": "Lemon 3"
    }

product is defined within the ForEach but you are attempting to use it outside the ForEach. If you want to use product in the sheet modifier, you will need to move sheet within the scope where product is defined.

Also, question: Why are you getting a DataModel from the environment, but then also creating an entirely new DataModel in your ProductDetail? That seems like not such a good idea.

Whoops, thanks for pointing that out.
I followed your suggestion and am now receiving a Cannot convert value of type 'Product.Type' to expected argument type 'Product' error on both the ProductDetail(product: Product) and the SearchView(product: Product) lines.
I just simply added a var product: Product variable.

This is my updated code:

import SwiftUI

struct SearchView: View {
    @StateObject var model = DataModel()
    @State private var searchText = ""
    @State private var showSheet = false
    var product: Product
    
    var body: some View {
        NavigationStack {
            List {
                ForEach(filteredProducts, id: \.self) { product in
                    NavigationLink {
                        Text(product.name)
                    } label: {
                        PageView(pages: model.features.map { FeatureCard(product: $0) })
                            .aspectRatio(3 / 2, contentMode: .fit)
                            .listRowInsets(EdgeInsets())
                            .onTapGesture {
                                showSheet = true
                            }
                    }
                }
            }
            .navigationTitle("Search")
            .navigationBarTitleDisplayMode(.inline)
        }
        .searchable(text: $searchText, prompt: "Tap to Search for Light Products")
        .sheet(isPresented: $showSheet) {
            ProductDetail(product: Product)
                .environmentObject(DataModel())
        }
    }
    var filteredProducts: [Product] {
        if searchText.isEmpty {
            return model.products
        } else {
            return model.products.filter {
                $0.name.lowercased().contains(searchText.lowercased()) ||
                $0.flavour.lowercased().contains(searchText.lowercased())
            }
        }
    }
}

struct SearchView_Previews: PreviewProvider {
    static var previews: some View {
        SearchView(product: Product)
            .environmentObject(DataModel())
    }
}

@The-Wolf

In this section of code:

        .sheet(isPresented: $showSheet) {
            ProductDetail(product: Product)
                .environmentObject(DataModel())
        }

change the call to ProductDetail() from:
ProductDetail(product: Product)
to
ProductDetail(product: product) Note that the parameter you are passing is lower case and is the one that you are passing in to SearchView.

Also, why are you injecting .environmentObject(DataModel()) at that point when you have DataModel() defined as a @StateObject at the top of your SearchView?

I tried that but am now receiving a Instance member 'product' cannot be used on type 'SearchView_Previews' error on the SearchView(product: product) in the SearchView_Previews: struct.
This is my updated code:

import SwiftUI

struct SearchView: View {
    @StateObject var model = DataModel()
    @State private var searchText = ""
    @State private var showSheet = false
    var product: Product
    
    var body: some View {
        NavigationStack {
            List {
                ForEach(filteredProducts, id: \.self) { product in
                    NavigationLink {
                        Text(product.name)
                    } label: {
                        PageView(pages: model.features.map { FeatureCard(product: $0) })
                            .aspectRatio(3 / 2, contentMode: .fit)
                            .listRowInsets(EdgeInsets())
                            .onTapGesture {
                                showSheet = true
                            }
                    }
                }
            }
            .navigationTitle("Search")
            .navigationBarTitleDisplayMode(.inline)
        }
        .searchable(text: $searchText, prompt: "Tap to Search for Light Products")
        .sheet(isPresented: $showSheet) {
            ProductDetail(product: product)
                .environmentObject(DataModel())
        }
    }
    var filteredProducts: [Product] {
        if searchText.isEmpty {
            return model.products
        } else {
            return model.products.filter {
                $0.name.lowercased().contains(searchText.lowercased()) ||
                $0.flavour.lowercased().contains(searchText.lowercased())
            }
        }
    }
}


struct SearchView_Previews:
    PreviewProvider {
    var product: Product
    static var previews: some View {
        SearchView(product: product)
            .environmentObject(DataModel())
    }
}

@The-Wolf

OK I think I see the issue here. To be honest this is hard to debug when we don’t have access to the entire project and how each view sits in context.

Modify your tapGesture closure to this:

.onTapGesture {
    product = $0
    showSheet = true
} 

I tried and am receiving a Cannot assign to value: 'product' is a 'let' constant and a Cannot assign value of type 'CGPoint' to type 'Product' on the product = $0 line.
Here’s the project Light - Google Drive

@The-Wolf

It quickly became obvious that the PageViewController, PageView and PageControl can be replaced with a SwiftUI TabView operating in page view style.

So that’s what I did in SearchView. Having done that there were no images being displayed and that was because the isFeatured property of your Product model is Optional and therefore the property is nil when the json code is loaded. I added the isFeatured property to all the json code entires and set it to true. Now we are getting somewhere.

One of the issues I was facing is that the assets catalogue had three sets of images for each drink and inside each of those sets was three sizes so the total images for each drink was 9. This was causing major issues with the App in terms of lagginess.

I edited all of that and left 1 image per drink and the performance vastly improved. That single image in each case is plenty big enough to cover every Apple device you are likely to encounter.

The bares bones of the App looks fine so far so having got the search to work you can now move onto the next step.

Here is a copy of your updated project.

Save it to a completely different location than you currently have (to avoid over writing your exisiting project) and see what you think.

If there is anything you don’t understand, just give me a yell and I’ll do my best to clear it up for you.