Search code examples
iosjsonswiftswiftui

How to properly retrieve json data from URL in swift


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]?
}

Solution

  • Try this example code based on your original code and my fetchMeals, works well for me.

    struct ContentView: View {
        var body: some View {
            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 {
            NavigationStack {  // <--- here
                VStack {
                    if processing {
                        ProgressView("Loading Desserts...")
                    }
                    else {
                        // --- here
                        List(desserts) { meal in
                            NavigationLink(meal.name ?? "", destination: DetailView(meal: meal))
                        }
                    }
                }
            }
            // outside the NavigationStack
            .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]?
    }