Reputation: 61
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
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