sjlearmonth
sjlearmonth

Reputation: 61

How do I initialise my ViewModel in my SwiftUI app

I have a SwiftUI App which uses a public API to download cocktail data by name but I am not very familiar with SwiftUI and I cannot see a way of initialising my view model in my DetailsView file.

here is my swift file if cocktail data structs;

struct Drinks: Decodable {
    var cocktails: [Cocktail]
}

struct Cocktail: Decodable, Identifiable {
    var id: String {
        return idDrink
    }
    let idDrink: String
    let strDrink: String
    let strDrinkThumb: String
    let strAlcoholic: String
    let strGlass: String
    let strInstructions: String
    let strIngredient1: String?
    let strIngredient2: String?
    let strIngredient3: String?
    let strIngredient4: String?
    let strIngredient5: String?
    let strIngredient6: String?
    let strIngredient7: String?
    let strIngredient8: String?
    let strIngredient9: String?
    let strIngredient10: String?
    let strIngredient11: String?
    let strIngredient12: String?
}

here is my NetworkManager class;

class NetworkManager {
        
    func fetchData(_ urlString: String, completion: @escaping (Drinks, Bool) -> Void) {
        guard let url = URL(string: urlString) else { return }
        var drinks: Drinks?
        let session = URLSession(configuration: .default)
        let task = session.dataTask(with: url) { data, response, error in
            if error == nil {
                let decoder = JSONDecoder()
                guard let safeData = data else { return }
                do {
                    drinks = try decoder.decode(Drinks.self, from: safeData)
                        completion(drinks!, false)
                } catch let error {
                    print(error.localizedDescription)
                    if error.localizedDescription == "The data couldn’t be read because it is missing." {
                            completion(drinks ?? Drinks(cocktails: [Cocktail]()), true)
                    } else {
                        print(error.localizedDescription)
                    }
                }
            }
        }
        task.resume()
    }
}

Here is my ViewModel class;

class ViewModel: ObservableObject {
    
    let networkManager = NetworkManager()
    var urlString: String
    @Published var drinks: Drinks = Drinks(cocktails: [Cocktail]())
    @Published var dataIsFound: Bool = true
    
    init(urlString: String) {
        self.urlString = urlString
        FetchData()
    }
    
    func FetchData() {
    
        networkManager.fetchData(urlString) { results, error in
            DispatchQueue.main.async {
            self.drinks = results
            self.dataIsFound = !error
            }
        }
    }

and here is my DetailsView struct;

struct DetailsView: View {
    
    @StateObject var viewModel = ViewModel()
        
    var body: some View {
        
        List(viewModel.drinks.cocktails) { cocktail in

            VStack(alignment: .center) {
                HStack(alignment: .center) {
                    Text(cocktail.strDrink + "  -")
                        .navigationTitle("Cocktail by first letter")
                        .frame(alignment: .center)
                    Text(cocktail.strAlcoholic)
                        .frame(alignment: .center)
                }
                
                WebImage(url: URL(string: cocktail.strDrinkThumb))
                    .resizable()
                    .frame(width: UIScreen.main.bounds.width - 20.0, height: UIScreen.main.bounds.width - 20.0, alignment: .center)
                    
                
                Text("~ Ingredients List ~\n").frame(alignment: .center)
                
                ForEach(viewModel.buildIngredients(cocktail), id: \.self) { ingredient in
                    Text(ingredient)
                }
                
                Text("\n~ Recipe Instructions ~\n\n")
                
                Text(cocktail.strInstructions + "\n").fixedSize(horizontal: false, vertical: true)
            }
        }        
    }
}

Any help would be appreciated. Thanks.

Upvotes: 4

Views: 9096

Answers (1)

jnpdx
jnpdx

Reputation: 52347

Here is one common pattern, using ViewModel() to initialize and then calling fetchData on onAppear:

class ViewModel: ObservableObject {
    
    let networkManager = NetworkManager()
    @Published var drinks: Drinks = Drinks(cocktails: [Cocktail]())
    @Published var dataIsFound: Bool = true
    
    
    func fetchData(urlString: String) {
        //call fetchData on network manager
    }
}

struct DetailsView: View {
    
    var urlString : String
    @StateObject private var viewModel = ViewModel()
        
    var body: some View {
        
        List(viewModel.drinks.cocktails) { cocktail in
            //list content
        }
        .onAppear {
            viewModel.fetchData(urlString: urlString)
        }
    }
}

Another option is to use your View's init. In this case, the @StateObject's init is called with the urlString passed into the View. Because StateObject's wrappedValue parameter uses and autoclosure and only gets run if the view is added to the hierarchy, you don't have to worry that the view model will be re-initialized on every init of the View.

class ViewModel: ObservableObject {
    
    let networkManager = NetworkManager()
    @Published var drinks: Drinks = Drinks(cocktails: [Cocktail]())
    @Published var dataIsFound: Bool = true
    
    init(urlString: String) {
        fetchData(urlString: urlString)
    }
    
    func fetchData(urlString: String) {
        //call fetchData on network manager
    }
}

struct DetailsView: View {
    
    @StateObject private var viewModel : ViewModel
    
    init(urlString: String) {
        _viewModel = StateObject(wrappedValue: ViewModel(urlString: urlString))
    }
    
    var body: some View {
        //body content
    }
}

Upvotes: 17

Related Questions