Я работаю над проектом с использованием 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]?
}
Я не совсем понимаю, в чем заключается ваш вопрос. Вы получаете ошибку? Потому что ссылка должна работать так, как она у вас есть. Кроме того, на самом деле это не проблема, специфичная для iOS, а скорее вопрос еды.
Взгляните на мой тестовый код на GitHub здесь: FreeMeal, чтобы узнать, как получать еду из API themealdb.
@workingdogsupportUkraine Я обновил свой код и смоделировал его по вашему репозиторию, но все еще получаю ошибки, не могли бы вы оставить отзыв?
Вам не нужны эти настройкиinit(from decoder:...)
, поскольку вы не используете какие-либо пользовательские структуры, требующие специальной логики. Однако вы можете сделать свой imgURL типом URL-адреса, а затем правильно декодировать его в своей инициализации, чтобы вам не приходилось хранить строку. Но что касается вашей ошибки: не могли бы вы добавить, где вы запускаете этот код? Симулятор iOS? Мак? И можете ли вы запустить Curl с тем же URL-адресом и получить результат?
ваш код работает для меня (с небольшими исправлениями), смотрите мой ответ. Дайте мне знать, если этот код вам не подходит. Если вы получаете какие-либо ошибки, покажите полное сообщение об ошибке.
В 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-адресом, с которого я хочу выполнить запрос? Если вы можете предоставить дополнительный контекст для вашего решения.
Конечно, вот Developer.apple.com/documentation/swiftui/environmentkey
@StateObject
на самом деле не старый. Однако в своем предыдущем коде он использовал модель представления, поэтому на самом деле правильным выбором является @StateObject
, а не @State
. Если вы просто хотите сохранить desserts
, вам следует использовать @State
, как вы написали. Кроме того, в вашем фрагменте отсутствует важная информация о том, что среду необходимо передать из родительского представления или установить значение по умолчанию.
@Fabio, это SwiftUI, структуры представления — это модель представления.
@malhal, его viewModel назывался MealViewModel()
, поэтому я подумал, что у него там также есть логика fetchDesserts, но я посмотрел на нее еще раз, и кажется, что это не так. Если у вас есть отдельная модель представления, чтобы лучше отделить вашу бизнес-логику от самого представления, вам следует использовать @StateObject
. Чтобы прояснить это: я не говорил, что ваш путь неправильный.
@Fabio, он уже отдельный, у нас даже нет доступа к слою представления в SwiftUI, он автоматически управляется платформой, т. е. на основе структур модели представления он создает/обновляет объекты UIKit в зависимости от контекста и платформы.
@StateObject
предназначен для случаев, когда вам нужно время существования ссылочного типа в состоянии, например, конвейер объединения или реализация делегата.
@malhal Думаю, мы неправильно понимаем друг друга, но это нормально
Попробуйте этот пример кода, основанный на исходном коде и моем 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]?
}
Ключ
1
— это компонент пути послеv1
. Посмотрите документацию, какие API поддерживают ключ.