Как правильно получить данные JSON из URL-адреса в Swift

Я работаю над проектом с использованием foodDB, и это мой первый раз, когда я работаю с базой данных json, и никакие данные не заполняются в моем приложении. У меня есть функция:

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
    }

Что я называю здесь:

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()
        }
    }
}

Я не знаю, неправильно ли я структурировал свой запрос или неправильно его вызываю. На веб-сайте foodDB указано, что для разработки нужно использовать ключ API «1», но я не знаю, где указать ключ API. Если кто-то может дать отзыв, мы будем очень признательны.

РЕДАКТИРОВАТЬ Я обновил реализацию задачи и смоделировал запрос после запросаworkDogs из прикрепленного репозитория, но все равно получаю ошибку nw_connection_copy_connected_local_endpoint_block_invoke [C1] Connection has no local endpoint.

Вот мой обновленный код:

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

Ключ 1 — это компонент пути после v1. Посмотрите документацию, какие API поддерживают ключ.

vadian 07.08.2024 19:40

Я не совсем понимаю, в чем заключается ваш вопрос. Вы получаете ошибку? Потому что ссылка должна работать так, как она у вас есть. Кроме того, на самом деле это не проблема, специфичная для iOS, а скорее вопрос еды.

Fabio 08.08.2024 00:14

Взгляните на мой тестовый код на GitHub здесь: FreeMeal, чтобы узнать, как получать еду из API themealdb.

workingdog support Ukraine 08.08.2024 04:07

@workingdogsupportUkraine Я обновил свой код и смоделировал его по вашему репозиторию, но все еще получаю ошибки, не могли бы вы оставить отзыв?

JonGrimes20 08.08.2024 17:05

Вам не нужны эти настройкиinit(from decoder:...), поскольку вы не используете какие-либо пользовательские структуры, требующие специальной логики. Однако вы можете сделать свой imgURL типом URL-адреса, а затем правильно декодировать его в своей инициализации, чтобы вам не приходилось хранить строку. Но что касается вашей ошибки: не могли бы вы добавить, где вы запускаете этот код? Симулятор iOS? Мак? И можете ли вы запустить Curl с тем же URL-адресом и получить результат?

Fabio 08.08.2024 23:25

ваш код работает для меня (с небольшими исправлениями), смотрите мой ответ. Дайте мне знать, если этот код вам не подходит. Если вы получаете какие-либо ошибки, покажите полное сообщение об ошибке.

workingdog support Ukraine 09.08.2024 01:02
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
6
95
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

В SwiftUI это .task, а не Task, и это устраняет необходимость в старом @StateObject, поэтому вы можете просто сделать:

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)
                 }
            }
        }
    }

Лучше всего использовать Environment для структуры контроллера асинхронных функций, чтобы ее можно было имитировать в предварительном просмотре.

для параметра \.mealsAPI переменной Enviroment невозможно определить контекст ключевого пути. Заменить ли это URL-адресом, с которого я хочу выполнить запрос? Если вы можете предоставить дополнительный контекст для вашего решения.

JonGrimes20 08.08.2024 15:24

Конечно, вот Developer.apple.com/documentation/swiftui/environmentkey

malhal 08.08.2024 15:50
@StateObject на самом деле не старый. Однако в своем предыдущем коде он использовал модель представления, поэтому на самом деле правильным выбором является @StateObject, а не @State. Если вы просто хотите сохранить desserts, вам следует использовать @State, как вы написали. Кроме того, в вашем фрагменте отсутствует важная информация о том, что среду необходимо передать из родительского представления или установить значение по умолчанию.
Fabio 08.08.2024 23:30

@Fabio, это SwiftUI, структуры представления — это модель представления.

malhal 09.08.2024 00:24

@malhal, его viewModel назывался MealViewModel(), поэтому я подумал, что у него там также есть логика fetchDesserts, но я посмотрел на нее еще раз, и кажется, что это не так. Если у вас есть отдельная модель представления, чтобы лучше отделить вашу бизнес-логику от самого представления, вам следует использовать @StateObject. Чтобы прояснить это: я не говорил, что ваш путь неправильный.

Fabio 09.08.2024 00:45

@Fabio, он уже отдельный, у нас даже нет доступа к слою представления в SwiftUI, он автоматически управляется платформой, т. е. на основе структур модели представления он создает/обновляет объекты UIKit в зависимости от контекста и платформы.

malhal 09.08.2024 08:50
@StateObject предназначен для случаев, когда вам нужно время существования ссылочного типа в состоянии, например, конвейер объединения или реализация делегата.
malhal 09.08.2024 08:52

@malhal Думаю, мы неправильно понимаем друг друга, но это нормально

Fabio 09.08.2024 11:49
Ответ принят как подходящий

Попробуйте этот пример кода, основанный на исходном коде и моем fetchMeals, у меня он хорошо работает.

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

Другие вопросы по теме