0

Let me preface this by saying that this is not a problem. This is a solution I stumbled upon that works really well. I need clarification as to why this works and whether this is a feature because I haven't found a lot of info that describes this. I have a large app that employs this throughout so I need to make sure this solution will stick around in the long term.

I needed a way to pass in many parameters to a new unique View, and to use those parameters to create a network request that was called only once. An example would be a ProductsView which consists of a List of products. When an individual product is tapped within the List a NavigationLink segues to a ProductView. The parameters are passed to create a network request within the ProductViewModel which is then used to update the ProductView.

Some things I tried include...

Creating a @StateObject var productViewModel = ProductViewModel within ProductsView. That didn't work because each ProductView is unique and needs it's own ProductViewModel.

Using @Binding vars within ProductView and then passing them into ProductViewModel within .onAppear to make a network request. That didn't work because the @Binding vars didn't work with nested views within ProductView.

Using regular vars such as var productID: Int or @State var productID: Int. This worked but proved to be way too messy and the network request being called within .onAppear was unreliable being called multiple times in some cases.

https://stackoverflow.com/a/62636048/6724254 way to hacky for me.

The solution I found that works the best is to create ProductViewModels ProductView(productViewModel: ProductViewModel(productID: product.productID))) within the NavigationLink destination call. A very simple example...

import SwiftUI

struct Product: Identifiable{
    var id = UUID()
    var productID = 0
    var name = ""
}

class ProductViewModel: ObservableObject {
    
    @Published var product = Product()
    
    init(productID: Int){
        
        print("init ProductViewModel ProductID: \(productID)")
        
        getFromNetwork(productID: productID)
    }
    
    func getFromNetwork(productID: Int){
        
        // network request made here
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            
            self.product = Product(productID: productID, name: "Product Name \(productID)")
            
        }
        
    }
   
}

// Each ProductView must be unique
struct ProductView: View {
    
    @StateObject var productViewModel: ProductViewModel
    
    var body: some View {
        if productViewModel.product.productID > 0{
            VStack{

                Text("Current Product: " + productViewModel.product.name)
                
                NavigationLink(destination: ProductView(productViewModel: ProductViewModel(productID: Int.random(in: 1..<100)))) {
                    Text("Recommended Product")
                }
            }
        }
    }
    
}

struct ProductsView: View {
    
    @State var products = [Product(productID: 1), Product(productID: 2), Product(productID: 3), Product(productID: 4), Product(productID: 5), Product(productID: 6)]
    
    var body: some View {
        NavigationView {
            
            List(products) { product in
                NavigationLink(destination: ProductView(productViewModel: ProductViewModel(productID: product.productID))) {
                    Text("Product ID: \(product.productID)")
                }
            }
            .navigationTitle("Products")
        }
        
    }
}

So my question is whether this solution is reliable in ways that I haven't foreseen, and will it stick around in the long term?

  • this should work, but why don't you load ALL products into an array for listing them and then pass the array down to other views? –or rephrased: do you really need to load single products each time? – ChrisR Feb 25 '22 at 20:15
  • @ChrisR Not a bad idea, but not possible because each product page loads a ton of data from the network like recommended products and other personalization features. – K E N N E R Feb 25 '22 at 20:18
  • I use the new `.task { }` modifier in my view, available for iOS 15 on. It's a very convenient way to download the content directly from the view, no need to create the view model in that case. – HunterLion Feb 25 '22 at 20:27
  • but you need to get a hold of all products for listing them in the first place, right? – ChrisR Feb 25 '22 at 20:31
  • @HunterLion was actually just looking into that this morning along with await/async. Very promising! – K E N N E R Feb 25 '22 at 20:58

1 Answers1

0

In any case, if it helps – this would be the classic way to load all products into an array, and pass that down:

struct Product: Identifiable{
    var id = UUID()
    var productID = 0
    var name = ""
}

class ProductViewModel: ObservableObject {
    
    @Published var products: [Product]
    
    init(){
        products = []
        for productID in 1...20 {
            getFromNetwork(productID: productID)
        }
    }
    
    func getFromNetwork(productID: Int){
        // network request made here
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            let new = Product(productID: productID, name: "Product Name \(productID)")
            self.products.append(new)
        }
    }
}

// Each ProductView must be unique
struct ProductView: View {
    
    var product: Product
    
    @EnvironmentObject var model: ProductViewModel
    
    var body: some View {
        if product.productID > 0 {
            VStack{
                
                Text("Current Product: " + product.name)
                
                NavigationLink(destination: ProductView(product: model.products[Int.random(in: 0..<20)])) {
                    Text("Recommended Product")
                }
            }
        }
    }
    
}

struct ProductsView: View {
    
    @StateObject var model = ProductViewModel()
    
    var body: some View {
        NavigationView {
            
            List(model.products) { product in
                NavigationLink(destination: ProductView(product: product)) {
                    Text("Product ID: \(product.productID)")
                }
            }
            .navigationTitle("Products")
        }
        .environmentObject(model)
    }
}
ChrisR
  • 9,523
  • 1
  • 8
  • 26
  • Thanks for your response Chris, but yeah I wish I could do this but the `ProductView` is way too complicated. It'd be like if Amazon loaded every single product fully in their product's feed. There are reviews, product recommendations and ton of other things that have to be loaded. It would wreck my server if I fully loaded each product in the`ProductsView`. So I'm pretty much stuck with just showing a preview of the product in the `ProductsView`. But I will be using your solution for much smaller feeds like when displaying a users orders. – K E N N E R Feb 25 '22 at 20:40
  • 1
    yes, I expected that from your comment. As I said, I'm quite positive that your current approach works as well. And @HunterLion 's comment might help too. – ChrisR Feb 25 '22 at 20:55