skip to Main Content

I’m working on a project using mealDB, and this is my first time working with a json DB and none of the data is populating in my app. I have a function:

func fetchDesserts() async throws -> [Meal] {
        let urlString = "https://themealdb.com/api/json/v1/1/filter.php?c=Dessert"
        guard let url = URL(string: urlString) else {
            throw URLError(.badURL)
        }

        let (data, _) = try await URLSession.shared.data(from: url)

        let mealResponse = try JSONDecoder().decode(MealResponse.self, from: data)
        var meals: [Meal] = []

        for mealData in mealResponse.meals {
            guard let imageUrl = URL(string: mealData.strMealThumb) else {
                continue
            }

            let (imageData, _) = try await URLSession.shared.data(from: imageUrl)
            if let image = UIImage(data: imageData) {
                let meal = Meal(name: mealData.strMeal, image: image, id: Int(mealData.idMeal) ?? 0)
                meals.append(meal)
            }
        }

        return meals
    }

That I call here:

struct ContentView: View {
    @StateObject var model = MealViewModel()
    
    var body: some View {
        NavigationStack {
            VStack {
            // Dessert Button
              NavigationLink(destination: {
                  DessertView(desserts: model.desserts)
                      .onAppear(perform: {
                          Task {
                              do {
                                  model.desserts = try await fetchDesserts()
                              } catch {
                                  print(error)
                              }
                          }
                      })
              }, label: {
                  ZStack {
                      RoundedRectangle(cornerRadius: 25.0)
                          .frame(width: 200, height: 100)
                      Text("Desserts")
                          .foregroundStyle(.white)
                  }
              })
            }
            .padding()
        }
    }
}

I don’t know if I improperly structured my query or am calling it incorrectly. On the mealDB website they state to use the API key "1" for development but I don’t know where I would specify the API key. If someone can provide some feedback it would be much appreciated.

EDIT
I have updated the implementation of the task and modeled the query after workingDogs query from the attached repo yet I still receive the error nw_connection_copy_connected_local_endpoint_block_invoke [C1] Connection has no local endpoint.

Here is my updated Code:

struct DessertView: View {
    // for procesing fetch request from json URL
    @State private var processing: Bool = false
    // desserts collectd
    @State private var desserts = [Meal]()
    
    var body: some View {
        VStack {
            if processing {
                ProgressView(label: {
                    Text("Loading Desserts...")
                })
            }
            else {
                // Standard View
            }
        }
        .task { // Query from mealsDB
            processing = true
            let response: ApiResponse? = await fetchMeals()
            processing = false
        }
    }
    
    // Fetch Meals Func
    func fetchMeals<T: Decodable>() async -> T? {
        // for testing
        
        // desserts
        let url = URL(string: "https://themealdb.com/api/json/v1/1/filter.php?c=Dessert")!
        
        
        let request = URLRequest(url: url)
        do {
            let (data, response) = try await URLSession.shared.data(for: request)
            guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
                // throw URLError(.badServerResponse)   //  todo
                print(URLError(.badServerResponse))
                return nil
            }
            return try JSONDecoder().decode(T.self, from: data)
        }
        catch {
            return nil
        }
    }
}
struct Meal: Decodable, Identifiable {
    var id: String
    var name: String?
    var imgURL: String? // String that represents a url of an image of the meal
    
    enum CodingKeys: String, CodingKey {
        case id = "idMeal"
        case name = "strMeal"
        case imgURL = "strMealThumb"
    }
    
    init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try container.decode(String.self, forKey: .id)
        self.name = try container.decodeIfPresent(String.self, forKey: .name)
        self.imgURL = try container.decodeIfPresent(String.self, forKey: .imgURL)
    }
}

struct ApiResponse: Decodable {
    var meals: [Meal]?
}

2

Answers


  1. In SwiftUI it’s .task, not Task and it removes the need for the old @StateObject, so you can simply do:

    struct DessertView: View {
        @Environment(.mealsAPI) var mealsAPI
        @State var desserts: [Meal]?
    
        var body: some View {
            if let desserts {
                ForEach(desserts) {
                    ...
                } 
            }
            else {
                Text("Loading...")
                .task {
                     do {
                         desserts = try await mealsAPI.fetchDesserts()
                     } catch {
                        print(error)
                     }
                }
            }
        }
    

    Best to use Environment for your async funcs controller struct so it can be mocked in Previews.

    Login or Signup to reply.
  2. Try this example code based on your original code and my fetchMeals, works well for me.

    struct ContentView: View {
        var body: some View {
            NavigationStack {
                DessertView()
            }
        }
    }
    
    struct DessertView: View {
        // for procesing fetch request from json URL
        @State private var processing: Bool = false
        // desserts collectd
        @State private var desserts = [Meal]()
        
        var body: some View {
            VStack {
                if processing {
                    ProgressView(label: {
                        Text("Loading Desserts...")
                    })
                }
                else {
                    List(desserts) { meal in
                        NavigationLink(meal.name ?? "", destination: DetailView(meal: meal))
                    }
                }
            }
            .task { // Query from mealsDB
                processing = true
                let response: ApiResponse? = await fetchMeals()
                if let meals = response?.meals {  
                    desserts = meals   // <--- here
                }
                processing = false
            }
        }
        
        // Fetch Meals Func
        func fetchMeals<T: Decodable>() async -> T? {
            // desserts
            let url = URL(string: "https://themealdb.com/api/json/v1/1/filter.php?c=Dessert")!
            
            let request = URLRequest(url: url)
            do {
                let (data, response) = try await URLSession.shared.data(for: request)
       //         print("n----> data: (String(data: data, encoding: .utf8) as AnyObject) n")
                
                guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
                    // throw URLError(.badServerResponse)   //  todo
                    print(URLError(.badServerResponse))
                    return nil
                }
                return try JSONDecoder().decode(T.self, from: data)
            }
            catch {
                print("----> error: (error)") // <--- important
                return nil
            }
        }
    }
    
    struct DetailView: View {
        var meal: Meal
        
        var body: some View {
            VStack (spacing: 40) {
                if let imgurl = meal.imgURL {
                    AsyncImage(url: URL(string: imgurl)) { image in
                        image.resizable()
                    } placeholder: {
                        Image(systemName: "photo.circle.fill").resizable()
                    }
                    .frame(width: 333, height: 333)
                    .padding(40)
                } else {
                    Text("NO IMAGE THUMB").foregroundStyle(.blue)
                }
            }.padding(20)
        }
        
    }
    
    struct Meal: Decodable, Identifiable {
        var id: String
        var name: String?
        var imgURL: String? // String that represents a url of an image of the meal
        
        enum CodingKeys: String, CodingKey {
            case id = "idMeal"
            case name = "strMeal"
            case imgURL = "strMealThumb"
        }
    }
    
    struct ApiResponse: Decodable {
        var meals: [Meal]?
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search