Это продолжение. Начало находится в посте «Использование DeepSeek при создании iOS приложения Countries Страны Мира. Часть 1. Список стран.».
Код находится на Github.
Корректируем поля в Индикаторе
И опять в улучшенной реализации с населением, ВВП, столицы и эмодзи флага мы имеем НЕВЕРНЫЙ код для ResponseMetadata
struct ResponseMetadata: Decodable {
let page: Int
let pages: Int
let perPage: String
let total: Int
}вместо ПРАВИЛЬНОГО:
struct ResponseMetadata: Decodable {
let page: Int
let pages: Int
let perPage: String
let total: Int
enum CodingKeys: String, CodingKey {
case page, pages, total
case perPage = "per_page"
}
}Но даже если мы будем использовать правильный код, мы все равно получим ту же самую ошибку: “ The data couldn’t be read because it is missing.”
Небольшая отладка и просмотр JSON данных для численности населения:
[
{
"page": 1,
"pages": 9,
"per_page": 30,
"total": 266,
"sourceid": "2",
"lastupdated": "2025-01-28"
},
[
{
"indicator": {
"id": "NY.GDP.MKTP.CD",
"value": "GDP (current US$)"
},
"country": {
"id": "ZH",
"value": "Africa Eastern and Southern"
},
"countryiso3code": "AFE",
"date": "2022",
"value": 1191423176242.96,
"unit": "",
"obs_status": "",
"decimal": 0
},
....
]
]… позволит определить, что поля countryiso2code, указанного в Модели struct Entry, НЕТ в JSON данных, а есть поле countryiso3code, которое является 3-х буквенным IOS кодом страны, но это не то, что нам нужно. Нам нужен 2-х буквенный IOS код страны, который находится в id в объекте country индикатора, так что DeepSeek немного запутался. И не зря, он не рискнул взять id в объекте country response indicator, так как id в объекте Country response country — это 3-х буквенный IOS кодом страны. Нелогичность конструирования JSON данных в API World Bank приводит DeepSeek в замешательство.
Давайте сообщим ему об этом:
В JSON indicator данных нет поля countryios2code. Используй id из объекта country в JSON indicator данных. |
В результате мы получаем рассуждения, которые длятся 18 секунд:
Вот перевод этих рассуждений:
Хорошо, давайте посмотрим. Пользователь упомянул, что вJSONданных индикатораindicatorнет поляcountryiso2code, и что вместо него мы должны использоватьidв объектеcountry. Хм, мне нужно снова
Во-первых, я припоминаю, чтоJSONданные индикатораindicatorизAPIВсемирного банка имеют другую структуру. Основным идентификатором страныcountryтам является НЕcountryiso2code, а объектcountryс идентификаторомid, который по сути являетсяISO2кодом. Позвольте мне это проверить.
Рассматривая пример ответаresponseотindicator endpoint, например, для численности населенииpopulation, мы убеждаемся, что каждая запись, вероятно, имеет объектcountry, содержащийidиvalue. Поэтому вместоcountryiso2codeмы должны получить доступ кcountry.id, который и будетISO2кодом.
В предыдущем коде структураstruct EntryдляIndicatorResponseуказанаcountryiso2code, но это неверно. Правильный путь —country.id. Поэтому мне нужно настроить ключи декодирования и структуру, чтобы отразить это.
Позвольте мне изменить структуруEntry.CodingKeysдляEntryдолжны декодироватьcountryкак вложенный объект, а затем извлечь егоid. Поэтому структураstruct Entryдолжна иметь свойствоcountry, которое является вложенной структуройstructсо свойствомidиvalue.
Подождите, структураJSONданных для индикатораindicator, вероятно, выглядит так:
{
"country": {"id": "US", "value": "United States"},
"value": 331002651
}Поэтомуstruct Entryдолжна иметь структуруstruct Countryсid, а неcountryiso2code. Поэтому в структуреstruct Entryу меня должно быть:
struct Entry: Decodable {
let country: Country
let value: Double?
struct Country: Decodable {
let id: String
}
}Затем при обработкеEntryиспользуйтеcountry.idвместоcountryiso2code.
Таким образом, в функцииfunc fetchIndicatorDataпри заполнении словаряdictionaryключом становитсяentry.country.id.
Подождите, но в предыдущем коде пользователь использовалcountryiso2code, которого не существует. Изменение этого на использование вложенногоcountry.idдолжно исправить проблему.
Позвольте мне соответствующим образом скорректировать структуруstruct EntryвIndicatorResponse. Также при необходимости обновитеCodingKeys. Поскольку ключамиJSONданных являютсяcountryиvalue, нет необходимости в пользовательскихCodingKeys, так как имена свойств не отличаются.
Как только это будет исправлено, код должен правильно сопоставлятьISO2коды стран из поляcountry.idв данных индикатораindicator.
Тестирование этого изменения гарантирует, что данные о населенииpopulationи ВВПgdpбудут правильно ассоциироваться с правильными странамиcountryс использованием ихISO2кодов из поляidв объектеcountryкаждой записи индикатораindicator.
Для пользователя выдается:
Вы абсолютно правы — я допустил ошибку в обработке структуры данных индикатора indicator. Давайте исправим это, правильно получив доступ к идентификатору страны id из вложенного объекта country в ответах response индикатора indicator. Вот исправленная реализация:
class CountryViewModel: ObservableObject {
// ... keep previous properties and other methods unchanged ...
private func fetchIndicatorData(url: String) async throws -> [String: Double] {
struct IndicatorResponse: Decodable {
let entries: [Entry]
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
_ = try container.decode(ResponseMetadata.self)
entries = try container.decode([Entry].self)
}
struct Entry: Decodable {
let country: Country
let value: Double?
struct Country: Decodable {
let id: String // This is the ISO2 code
}
}
}
let (data, _) = try await URLSession.shared.data(from:
URL(string: url)!)
let response = try JSONDecoder().decode(IndicatorResponse.self,
from: data)
return response.entries.reduce(into: [:]) { dict, entry in
guard let value = entry.value else { return }
dict[entry.country.id] = value
}
}
}Ключевые изменения произошли в структуре IndicatorResponse.Entry, где теперь мы правильно декодируем вложенный объект country:
struct Entry: Decodable {
let country: Country
let value: Double?
struct Country: Decodable {
let id: String // ISO2 code from indicator data
}
}Это соответствует фактической структуре ответа response API индикатора indicator Всемирного банка, которая выглядит следующим образом (позже мы увидим, что ошибочно указано, что per_page в метаданных indicator является String, a на самом деле это Int ):
[
{
"page": 1,
"pages": 5,
"per_page": "50",
"total": 299
},
[
{
"indicator": {"id": "SP.POP.TOTL", "value": "Population, total"},
"country": {"id": "US", "value": "United States"},
"value": 331002651,
"decimal": "0",
"date": "2021"
},
// ... other entries
]
]Остальная часть кода остается прежней, но позвольте мне выделить важные связи:
1. Идентификация страны:
- Базовые данные страны Country используют поле iso2Code
- Данные индикатора indicator используют поле country.id
- Оба представляют ISO2 коды и используются для взаимосвязи
2. Объединение данных:
finalCountries = finalCountries.map { country in
var modified = country
modified.population = populationDict[country.iso2Code]
modified.gdp = gdpDict[country.iso2Code]
return modified
}3. Обработка ошибок
guard let value = entry.value //пропускает записи без данных- Метод
reduceбезопасно создает словарь - В окончательном списке стран
filteredфильтруются страны с пустыми столицами
Это исправление гарантирует, что мы правильно извлекаем ISO2 коды стран из обоих источников данных:
- Из основной
country endpoint:iso2Code - Из
indicator endpoint:country.id
Остальная часть UI и модели данных остаются неизменной, но теперь экономические данные будут правильно сопоставляться со странами на основе их ISO2 кодов.
Запускаем приложение и мы все равно получим ту же самую ошибку: “ The data couldn’t be read because it is missing.”
Небольшая отладка и просмотр “живых” JSON данных для численности населения:
[
{
"page": 1,
"pages": 9,
"per_page": 30,
"total": 266,
"sourceid": "2",
"lastupdated": "2025-01-28"
},
[
{
"indicator": {"id": "SP.POP.TOTL", "value": "Population, total"},
"country": {"id": "US", "value": "United States"},
"value": 331002651,
"decimal": "0",
"date": "2021"
},
// ... other entries
]
]… позволяют заметить, что поле «per_page» является целым числом Int в метаданных indicator, в то время как в метаданных country это строка String. Следовательно, для декодирования структура IndicatorResponseMetadata должна отличаться от ResponseMetadata, которая сейчас используется в метаданных indicator. Еще раз убеждаемся, что ИИ плохо работают с плохо спроектированными человеком структурами.
Давайте сообщим об этом DeepSeek:
IndicatorResponseMetadata отличается от ResponseMetadata: поле per_page в ResponseMetadata является строкой String, a поле per_page в IndicatorResponseMetadata является целым числом Int |
В результате мы получаем рассуждения, которые длятся 19 секунд:
Вот перевод этих рассуждений:
Хорошо, давайте займемся этой проблемой. Пользователь указал на несоответствие между полями `per_page` в двух разных структурах метаданных ответа. В исходном `ResponseMetadata` `per_page` — этоString, но в `IndicatorResponseMetadata` — этоInt. Это вызывает ошибку декодирования, поскольку строгая система типовSwiftожидает, что ТИПы должны точно совпадать.
Во-первых, мне нужно проверить фактические структурыAPIответовresponseВсемирного банка. Позвольте мне напомнить, что основнаяcountry endpointвозвращает `per_page` какString, в то время какindicator endpointвозвращают `per_page` какInt. Это несоответствие в дизайнеAPIявляется основной причиной проблемы.
Чтобы решить эту проблему, я не могу использовать одну и ту же структуру `ResponseMetadata` для обоих случаев. Вместо этого мне нужно создать отдельные структуры метаданных для каждого типа ответа. Для основного списка стран метаданные имеют `per_page` как строкуString, а для данных индикатора — этоInt.
Итак, я определяю две отдельные структуры: `CountryResponseMetadata` и `IndicatorResponseMetadata`. Каждая будет иметь правильный ТИП для `per_page`. Затем в процессе декодирования для каждогоAPIвызова я буду использовать соответствующую структуру метаданных.
В методе `fetchCountries` при декодировании данных о странах я буду использовать `CountryResponseMetadata`. В методе `fetchIndicatorData` я буду использовать `IndicatorResponseMetadata` для правильного анализа метаданных индикатора. Таким образом, каждый ответresponseдекодируется с правильными ТИПами, предотвращая любые несоответствия.
Кроме того, мне нужно убедиться, что все ссылки на `ResponseMetadata` в коде заменены правильной структурой. Например, в структуре `WorldBankResponse` не ключевой контейнерunkeyedContainerдолжен декодировать `CountryResponseMetadata`, a в `IndicatorResponse` - это `IndicatorResponseMetadata` вместо прежнего `ResponseMetadata`.
Тестирование этого изменения имеет решающее значение. Мне нужно будет смоделировать обаAPIответа, чтобы подтвердить, что декодирование работает без ошибок. Если поле `per_page` правильно анализируется какStringилиIntв зависимости отendpoint, проблема должна быть решена.
Еще одно соображение — обработка ошибок. Если вAPIответахresponseесть какие-либо другие несоответствия, приложение должно корректно их обработать. Добавление более описательных сообщений об ошибках может помочь в отладке подобных проблем в будущем.
Наконец, я обновлю фрагменты кода, предоставляемые пользователю, обязательно выделив изменения в определениях структур и их использовании в процессе декодирования. Это четкое разделение гарантирует, что каждая частьAPIответаresponseбудет правильно проанализирована в соответствии с ее структурой.
Кратко для пользователя:
Вы абсолютно правы — разные endpoints API Всемирного банка используют разные ТИПы данных для поля per_page. Давайте правильно справимся с этим, создав отдельные структуры метаданных для каждого типа ответа response.
Вот исправленная реализация:
// Separate metadata structures for different endpoints
struct CountryResponseMetadata: Decodable {
let page: Int
let pages: Int
let perPage: String // String in country endpoint
let total: Int
enum CodingKeys: String, CodingKey {
case page, pages, total
case perPage = "per_page"
}
}
struct IndicatorResponseMetadata: Decodable {
let page: Int
let pages: Int
let perPage: Int // Int in indicator endpoints
let total: Int
enum CodingKeys: String, CodingKey {
case page, pages, total
case perPage = "per_page"
}
}
class CountryViewModel: ObservableObject {
// ... rest of the class remains the same ...
private func fetchCountries() async throws -> [Country] {
struct WorldBankResponse: Decodable {
let countries: [Country]
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
_ = try container.decode(CountryResponseMetadata.self) // Use correct metadata
countries = try container.decode([Country].self)
}
}
let (data, _) = try await URLSession.shared.data(from: URL(string: countryURL)!)
return try JSONDecoder().decode(WorldBankResponse.self, from: data).countries
}
private func fetchIndicatorData(url: String) async throws -> [String: Double] {
struct IndicatorResponse: Decodable {
let entries: [Entry]
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
_ = try container.decode(IndicatorResponseMetadata.self) // Use correct metadata
entries = try container.decode([Entry].self)
}
struct Entry: Decodable {
let country: Country
let value: Double?
struct Country: Decodable {
let id: String
}
}
}
let (data, _) = try await URLSession.shared.data(from:
URL(string: url)!)
let response = try JSONDecoder().decode(IndicatorResponse.self,
from: data)
return response.entries.reduce(into: [:]) { dict, entry in
guard let value = entry.value else { return }
dict[entry.country.id] = value
}
}
}Ключевые изменения:
- Созданы отдельные типы
CountryResponseMetadataиIndicatorResponseMetadata CountryResponseMetadataиспользуетStringдляperPageIndicatorResponseMetadataиспользуетIntдляperPage- Обновлено декодирование в fetchCountries и fetchIndicatorData для использования соответствующих ТИПов метаданных
Это соответствует фактической структуре API Всемирного банка:
- Country endpoint response:
{
"page": 1,
"pages": 6,
"per_page": "50",
"total": 299
}- Indicator endpoint response:
{
"page": 1,
"pages": 10,
"per_page": 50,
"total": 500
}Остальной код может оставаться неизменным. Это решение правильно обрабатывает:
- Разные ТИПы данных для одного и того же поля на разных endpoints
- Вложенную структуру
country.idв ответахresponseиндикатораindicator - Безопасное декодирование JSON со Swift протоколом
Codable - Правильную обработку ошибок при несоответствиях декодирования
Это должно устранить любые ошибки декодирования, связанные с несоответствиями ТИПов в метаданных, при этом сохраняя всю предыдущую функциональность с населением, ВВП, столицами и эмодзи флагов.
Запускаем приложение — получаем результат:
Разделение Model и ViewModel
Давайте попросим выделить Model из файла CountryViewModel:
Раздели Model и View Model в нашем коде CountryViewModel |
В результате мы получаем рассуждения, на которые затрачено 64 секунд (чуть более 1 минуты):
Вот перевод этих рассуждений:
Хорошо, пользователь хочет, чтобы я разделилModelиView Modelв существующем кодеSwiftUI. Начну с того, что напомню текущую структуру. В предыдущем коде была структураstruct Countryи классclass CountryViewModel, но все они находились в одном файле. Чтобы следовать передовой практике, мне следует разбить их на отдельные файлы или, по крайней мере, на отдельные разделы в коде.
Во-первых, мне нужно определить все Модели данных. Структураstruct Countryуже присутствует, но есть также вложенные структуры, такие какstruct Regionи метаданные для ответовAPI response. Их следует переместить в отдельную группу Моделей.View ModelCountryViewModel, должна обрабатывать только бизнес-логику, выборку данных и управление состоянием.
Рассматривая структуруstruct Country, у нее есть такие свойства, какid,iso2Code,name,capitalCity,region,populationиgdp. Структураstruct Regionявляется частьюstruct Country, поэтому, возможно, ее следует оставить в МоделиCountry, если она не используется где-то еще. Такие структуры метаданных, какCountryResponseMetadataиIndicatorResponseMetadata, используются при декодировании ответовAPI, поэтому они также относятся к разделу Модели.
Далее,View Model CountryViewModelв настоящее время имеет@PublishedсвойстваloadStateиgroupedCountries. Она также имеетfetchметоды для выборки данных. Мне нужно убедиться, чтоView Modelне содержит никаких определений Моделей. РольView Modelзаключается в управлении данными и состоянием, поэтому она должен использовать Модели, но НЕ определять их.
Мне следует создать отдельную группу дляModels, которая включаетCountry,Regionи структуры метаданных. ЗатемView Modelимпортирует модели и использует их. Кроме того,IndicatorResponseи его вложенные структурыEntryиCountryявляются частью логики выборки данных, поэтому их следует поместить в область действия View Model, возможно, какprivateструктурыstructвнутри методов, где они используются.
Подождите,IndicatorResponseиспользуется в методеfetchIndicatorData. Поскольку это зависит от того, какView Modelобрабатывает данные, возможно, лучше хранить эти структуры ответовresponseвнутриView Modelкакprivateвложенные структуры. Таким образом, Модели остаются чистыми и представляют только данные приложения, а не структурыAPIответовresponse.
Еще одно соображение — перечислениеenum LoadingState. Это часть управления состояниемView Model, поэтому оно должно оставаться внутриView Model. Модели не должны иметь никаких перечисленийenum, связанных с состояниемView Model.
Мне также нужно проверить, нет ли других вложенных структур данных.ResponseMetadataранее была отдельной структурой, но после последних изменений она разделена наCountryResponseMetadataиIndicatorResponseMetadata. Они должны быть частью Моделей, поскольку они связаны со структуройAPIответаresponse.
Подождите, на самом деле, эти структуры метаданных используются только во время процесса декодирования вView Model. Возможно, они должны быть private в методахView Model, где они используются. Потому что если они являются частью Модели, но не используются где-либо еще, лучше оставить их внутриView Model, чтобы не раскрывать ненужные типы.
Итак, план такой:
1. Создайте группуModels, содержащуюCountry,Regionи любые другие сущности данных.
2. Переместите структуры метаданных (CountryResponseMetadata,IndicatorResponseMetadata) воView Modelкакprivateструктуры, поскольку они используются только во время декодирования.
3. Убедитесь, что методыView Modelиспользуют эти Модели и управляют выборкой и обработкой данных.
4. ВыделитеView Modelв отдельный файл или раздел, сохранив его отдельно от Моделей.
Если посмотреть на существующий код, структураCountryопределена в том же файле, что иView Model. Чтобы разделить их, я могу реорганизовать код в два раздела: один для Моделей и один дляView Model. ВSwiftэто можно сделать с помощью расширений или просто упорядочив код. Однако в реальном проекте они могут находиться в отдельных файлах.
Еще один момент, на который следует обратить внимание, — этоstruct IndicatorResponseи его вложенные структуры. Поскольку они предназначены для анализа данных индикатора вView Model, они должны оставаться вView Modelкакprivateструктуры. Это позволяет Моделям сосредоточиться на данных приложения, а не на структурахAPIответовresponse.
Мне также нужно проверитьCodingKeysдля МоделиCountry. Ранее в ней былиCodingKeysдляiso2CodeиcapitalCity. Это верно, так какAPIиспользует разные имена ключей. Соответствие протоколуCodableдолжно оставаться в Модели.
Генерация эмодзи флага — это вычисляемоеflagсвойство в МоделиCountry. Это нормально, потому что оно получено изiso2Code, который является частью модели. Это логикаView, но она достаточно проста, чтобы оставаться в Модели.
Теперь соберем все вместе:
1. ГруппаModelsбудет иметь:
-CountryсRegionиCodingKeys.
- Возможно, структуры метаданных, если они используются где-то еще, но, скорее всего, они остаются вView Model.
2. ГруппаView Modelбудет иметь:
-CountryViewModelсloadingState, c@Publishedсвойствами и методами.
- Структуры метаданных как private вложенные структуры внутри методовView Model, где они используются.
Подождите, вSwiftу вас не могут быть структуры внутри методов, но вы можете иметь их внутри классаclass View Modelкакprivateструктуры. Таким образом,CountryResponseMetadataиIndicatorResponseMetadataмогут бытьprivateструктурами внутриViewModel, поскольку они используются только там.
Аналогично,IndicatorResponseи его структурыstruct Entryиstruct Countryдолжны бытьprivateв области действия методаfetchIndicatorDataвоView Model. Но посколькуSwiftне допускает структуры внутри методов, их необходимо вложить в классView Modelкакprivateструктуры.
Это спорное утверждение.
1. Модели:
-Country:id, iso2Code, имя, capitalCity, region, population, gdp
- СтруктураRegion
- перечислениеenum CodingKeys
- вычисляемое свойствоflag
2. View Model:
-CountryViewModel
- перечислениеenum LoadingState
-@Publishedсвойства
-URL-адреса API
- Методы выборки данных
-privateструктуры для ответовAPI(CountryResponseMetadata,IndicatorResponseMetadata,IndicatorResponseи т. д.)
Это разделение позволяет Моделям сосредоточиться на представлении данных, а View Model — на обработке данных и бизнес-логике.
Тестирование этого, при разделении на разные разделы или файлы,View ModelимпортируетFoundationиSwiftUI, если необходимо, но поскольку этоObservableObject, ему нуженSwiftUI. Модели нуждаются только вFoundationдляCodable.
Мне также нужно убедиться, что вся логика декодирования вView Modelиспользует правильныеprivateструктуры дляAPIответов, сохраняя Модели чистыми.
Еще одна проверка:CodingKeysструктурыCountryвключаютiso2CodeиcapitalCity, которые соответствуют ключамAPI. Это верно. Методы анализаView Modelиспользуют структуры ответаresponse, которые декодируютJSONAPIв Модели.
Наконец, методfetchAllDataView Modelобъединяет данные из несколькихendpointsи объединяет их в МоделиCountry, что верно.
Итак, окончательный ответ должен представлять код, разделенный наModelsиView Model, с соответствующими структурамиstructи перечислениямиenumв каждой из них, гарантируя, чтоView Modelобрабатывает данные, а Модели представляют структуру данных.
Вот правильно разделенная структура кода:
1. Models/Country.swift (Data Model Layer)
import Foundation
struct Country: Decodable, Identifiable {
let id: String
let iso2Code: String
let name: String
let capitalCity: String
let region: Region
var population: Int?
var gdp: Double?
var flag: String {
iso2Code.unicodeScalars
.map { 127397 + $0.value }
.compactMap(UnicodeScalar.init)
.map(String.init)
.joined()
}
struct Region: Decodable {
let id: String
let value: String
}
enum CodingKeys: String, CodingKey {
case id, name, region
case iso2Code = "iso2Code"
case capitalCity = "capitalCity"
}
}2. Models/WorldBankResponse.swift (API Response Models)
import Foundation
struct CountryResponseMetadata: Decodable {
let page: Int
let pages: Int
let perPage: String
let total: Int
enum CodingKeys: String, CodingKey {
case page, pages, total
case perPage = "per_page"
}
}
struct IndicatorResponseMetadata: Decodable {
let page: Int
let pages: Int
let perPage: Int
let total: Int
enum CodingKeys: String, CodingKey {
case page, pages, total
case perPage = "per_page"
}
}3. ViewModels/CountryViewModel.swift (Business Logic Layer)
import SwiftUI
import Combine
class CountryViewModel: ObservableObject {
enum LoadingState {
case loading, loaded, error(String)
}
@Published var loadingState: LoadingState = .loading
@Published var groupedCountries: [String: [Country]] = [:]
private let countryURL = "https://api.worldbank.org/v2/country?format=json&per_page=300"
private let populationURL = "https://api.worldbank.org/v2/country/all/indicator/SP.POP.TOTL?format=json&date=2022&per_page=300"
private let gdpURL = "https://api.worldbank.org/v2/country/all/indicator/NY.GDP.MKTP.CD?format=json&date=2022&per_page=300"
func fetchAllData() async {
do {
async let countries = fetchCountries()
async let populationData = fetchIndicatorData(url: populationURL)
async let gdpData = fetchIndicatorData(url: gdpURL)
var finalCountries = try await countries
let populationDict = try await populationData
let gdpDict = try await gdpData
finalCountries = finalCountries.map { country in
var modified = country
modified.population = populationDict[country.iso2Code]
modified.gdp = gdpDict[country.iso2Code]
return modified
}
let filtered = finalCountries.filter {
!$0.region.value.lowercased().contains("aggregate") &&
$0.region.id != "NA" &&
$0.capitalCity != ""
}
let grouped = Dictionary(grouping: filtered) {
$0.region.value.trimmingCharacters(in: .whitespacesAndNewlines)
}
await MainActor.run {
groupedCountries = grouped
loadingState = .loaded
}
} catch {
await MainActor.run {
loadingState = .error("Failed to load data: \(error.localizedDescription)")
}
}
}
private func fetchCountries() async throws -> [Country] {
struct WorldBankResponse: Decodable {
let countries: [Country]
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
_ = try container.decode(CountryResponseMetadata.self)
countries = try container.decode([Country].self)
}
}
guard let url = URL(string: countryURL) else {
throw URLError(.badURL)
}
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(WorldBankResponse.self, from: data).countries
}
private func fetchIndicatorData(url: String) async throws -> [String: Double] {
struct IndicatorResponse: Decodable {
let entries: [Entry]
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
_ = try container.decode(IndicatorResponseMetadata.self)
entries = try container.decode([Entry].self)
}
struct Entry: Decodable {
let country: Country
let value: Double?
struct Country: Decodable {
let id: String
}
}
}
guard let url = URL(string: url) else {
throw URLError(.badURL)
}
let (data, _) = try await URLSession.shared.data(from: url)
let response = try JSONDecoder().decode(IndicatorResponse.self, from: data)
return response.entries.reduce(into: [:]) { dict, entry in
guard let value = entry.value else { return }
dict[entry.country.id] = value
}
}
}4. Views/CountryListView.swift (UI Layer)
import SwiftUI
struct CountryListView: View {
@StateObject private var viewModel = CountryViewModel()
var body: some View {
NavigationView {
Group {
switch viewModel.loadingState {
case .loading:
ProgressView("Loading countries...")
case .loaded:
List {
ForEach(viewModel.groupedCountries.keys.sorted(), id: \.self) { region in
Section(header: Text(region)) {
ForEach(viewModel.groupedCountries[region] ?? []) { country in
CountryRow(country: country)
}
}
}
}
case .error(let message):
Text(message)
.foregroundColor(.red)
}
}
.navigationTitle("World Countries")
}
.task {
await viewModel.fetchAllData()
}
}
}
struct CountryRow: View {
let country: Country
var body: some View {
HStack(spacing: 12) {
Text(country.flag)
.font(.system(size: 32))
VStack(alignment: .leading, spacing: 4) {
Text(country.name)
.font(.headline)
HStack(spacing: 16) {
Label(country.capitalCity, systemImage: "building.2")
Text(country.iso2Code)
.monospaced()
}
.font(.caption)
HStack(spacing: 16) {
if let population = country.population {
Label(population.formatted() + " people", systemImage: "person.2")
}
if let gdp = country.gdp {
Label("$" + gdp.formatted(), systemImage: "dollarsign.circle")
}
}
.font(.caption)
.foregroundColor(.secondary)
}
}
}
}Это разделение обеспечивает:
- Четкую архитектуру: следует шаблону MVVM с отдельными слоями
- Тестируемость:
View Modelможно тестировать независимо - Удобство обслуживания: каждый компонент несет единственную ответственность
- Повторное использование: модели можно использовать в разных представлениях
- Масштабируемость: легко добавлять новые функции или изменять существующие
Ключевые улучшения:
- Модели содержат только структуры данных и
Codableлогику View Modelобрабатывает данных и всю бизнес-логикуViewsсосредоточены исключительно на отображении- Модели
APIответовresponseотделены от моделей предметной области - Централизованная обработка ошибок в
View Model - Сетевой уровень безопасности типов с правильным распространением ошибок
Запускаем приложение — получаем результат, который не изменился, так как никаких функциональных добавлений мы не внесли:
Обработка ошибок
Теперь давайте улучшим обработку ошибок с помощью do { } catch { } блока.
| Добавьте do { } catch { } блоки в функции выборки fetch |
В результате мы получаем рассуждения, которые заняли 27 секунд:
Вот перевод этих рассуждений:
Хорошо, давайте займемся добавлением соответствующих блоковdo-catchк функциямfetchвCountryViewModel. Пользователь хочет, чтобы обработка ошибок была более явной и локализованной. Сначала мне нужно просмотреть существующий код, чтобы увидеть, где могут возникнуть исключения, и убедиться, что все возможные ошибки перехватываются и обрабатываются соответствующим образом.
Рассматривая функции `fetchCountries()` и `fetchIndicatorData(url:)`, я замечаю, что в настоящее время они используют один блок `do-catch` на уровне `fetchAllData()`. Хотя это работает, лучше иметь обработку ошибок ближе к тому месту, где могут возникнуть ошибки, для более детального контроля. Таким образом, каждый сетевой запрос может обрабатывать свои собственные исключения, аView Modelможет предоставлять более конкретные сообщения об ошибках.
В `fetchCountries()`основными точками сбоя являются:
1. СозданиеURLиз строки.
2. Выборка данных из сети.
3. ДекодированиеJSONответаresponse.
Аналогично, в `fetchIndicatorData(url:)` применяются те же пункты, плюс обработкаentriesи их значенийvalue.
Я начну с того, что оберну каждую потенциально генерирующую операцию в свой собственный блок `do-catch`. Для созданияURL, еслиURLнедействителен, мы должны выдать `URLError.badURL`. При выборке данных могут возникнуть сетевые ошибки, такие как отсутствие подключения к Интернету или проблемы с сервером. Ошибки декодирования могут возникнуть, если структураJSONне соответствует нашим моделям.
Для каждой обнаруженной ошибки я запишу описательное сообщение и повторно “выброшу”throwsболее понятную пользователю ошибку. Это поможет в отладке и гарантирует, что пользователь получит содержательную обратную связь. В `fetchAllData()`вView Modelя обновлю обработку ошибок, чтобы отловить эти конкретные ошибки и обновить состояние загрузкиloadingStateсоответствующим образом.
Мне также нужно убедиться, что все ошибки правильно передаются вView Model, чтобыUIмог отражать правильное состояние. Это означает преобразование ошибок более низкого уровня в сообщения, которые могут быть отображены пользователю.
Тестирование каждого сценария имеет решающее значение. Например, что произойдет, еслиAPIвернет непредвиденную структуруJSON? Декодирование должно завершиться неудачей, и ошибка должна быть перехвачена и обработана. Аналогично, если сетевой запрос истекает по времени, блок catch должен перехватить это и обновитьUI.
Еще одно соображение — сообщения об ошибках. Вместо отображения необработанных описаний ошибок они должны быть удобными для пользователя. Например, замените "The Internet connection appears to be offline." («Подключение к Интернету, похоже, отключено») на "No internet connection. Please check your network settings." («Нет подключения к Интернету. Проверьте настройки сети»).
Наконец, я проверю, что все изменения сохраняют существующую функциональность, например, группировку стран по регионам и правильное отображение их данных. Важно убедиться, что добавление блоковdo-catchне приводит к появлению новых ошибок или прерыванию потока данных.
Пользователю:
Вот обновленная View Model с правильной обработкой ошибок в каждой функции выборки:
class CountryViewModel: ObservableObject {
// ... existing properties and enums ...
func fetchAllData() async {
do {
async let countries = try fetchCountries()
async let populationData =
try fetchIndicatorData(url: populationURL)
async let gdpData = try fetchIndicatorData(url: gdpURL)
let (finalCountries, populationDict, gdpDict) =
try await (countries, populationData, gdpData)
let mergedCountries = mergeData(
countries: finalCountries,
population: populationDict,
gdp: gdpDict
)
await MainActor.run {
groupedCountries = groupedCountries
loadingState = .loaded
}
} catch {
await handleError(error)
}
}
private func fetchCountries() async throws -> [Country] {
do {
guard let url = URL(string: countryURL) else {
throw APIError.invalidURL
}
let (data, response) =
try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw APIError.invalidResponse
}
return try decodeCountries(from: data)
} catch {
throw handleFetchError(error, context: "countries")
}
}
private func fetchIndicatorData(url: String) async throws -> [String: Double] {
do {
guard let url = URL(string: url) else {
throw APIError.invalidURL
}
let (data, response) =
try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw APIError.invalidResponse
}
return try decodeIndicatorData(from: data)
} catch {
throw handleFetchError(error, context: "indicator data")
}
}
// MARK: - Decoding Helpers
private func decodeCountries(from data: Data) throws -> [Country] {
do {
struct WorldBankResponse: Decodable {
let countries: [Country]
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
_ = try container.decode(CountryResponseMetadata.self)
countries = try container.decode([Country].self)
}
}
return try JSONDecoder().decode(WorldBankResponse.self,
from: data).countries
} catch {
throw APIError.decodingFailed(
message: "Countries decoding failed: \(error.localizedDescription)"
)
}
}
private func decodeIndicatorData(from data: Data) throws -> [String: Double] {
do {
struct IndicatorResponse: Decodable {
let entries: [Entry]
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
_ = try container.decode(IndicatorResponseMetadata.self)
entries = try container.decode([Entry].self)
}
struct Entry: Decodable {
let country: CountryEntry
let value: Double?
struct CountryEntry: Decodable {
let id: String
}
}
}
let response =
try JSONDecoder().decode(IndicatorResponse.self, from: data)
return response.entries.reduce(into: [:]) { dict, entry in
guard let value = entry.value else { return }
dict[entry.country.id] = value
}
} catch {
throw APIError.decodingFailed(
message: "Indicator data decoding failed: \(error.localizedDescription)"
)
}
}
// MARK: - Error Handling
private func handleError(_ error: Error) async {
let message: String
switch error {
case let apiError as APIError:
message = apiError.localizedDescription
case let urlError as URLError:
message = handleUrlError(urlError)
default:
message = "Unknown error: \(error.localizedDescription)"
}
await MainActor.run {
loadingState = .error(message)
}
}
private func handleUrlError(_ error: URLError) -> String {
switch error.code {
case .notConnectedToInternet:
return "No internet connection. Please check your network settings."
case .timedOut:
return "Request timed out. Please try again later."
case .networkConnectionLost:
return "Network connection lost. Please check your connection."
default:
return "Network error: \(error.localizedDescription)"
}
}
private func handleFetchError(_ error: Error, context: String) -> Error {
print("Error fetching \(context): \(error)")
if let apiError = error as? APIError {
return apiError
}
return APIError.requestFailed(
message: "Failed to fetch \(context): \(error.localizedDescription)"
)
}
// MARK: - Data Processing
private func mergeData(
countries: [Country],
population: [String: Double],
gdp: [String: Double]
) -> [String: [Country]] {
let filtered = countries
.map { country in
var modified = country
modified.population =
population[country.iso2Code].flatMap(Int.init)
modified.gdp = gdp[country.iso2Code]
return modified
}
.filter {
!$0.region.value.lowercased().contains("aggregate") &&
$0.region.id != "NA" &&
!$0.capitalCity.isEmpty
}
return Dictionary(grouping: filtered) {
$0.region.value.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
}
// Error Types
enum APIError: LocalizedError {
case invalidURL
case invalidResponse
case decodingFailed(message: String)
case requestFailed(message: String)
var errorDescription: String? {
switch self {
case .invalidURL:
return "Invalid API endpoint URL"
case .invalidResponse:
return "Received invalid response from server"
case .decodingFailed(let message):
return "Data parsing failed: \(message)"
case .requestFailed(let message):
return "Network request failed: \(message)"
}
}
}Ключевые улучшения в обработке ошибок:
1, Структурированная обработка ошибок:
- Пользовательское перечисление
enum APIErrorс локализованными описаниями - Отдельные типы ошибок для разных сценариев сбоев
- Обработка ошибок URL-адреса
2. Гранулярные блоки try / сatch:
- Отдельная обработка ошибок для каждой операции выборки
- Явная проверка ошибок для кодов состояния HTTP
- Подробная обработка ошибок декодирования
3. Распространение ошибок:
- Ошибки всплывают с сообщениями, зависящими от контекста
- Центральная обработка ошибок в методе
handleError - Удобные для пользователя сообщения об ошибках
4. Особенности сетевых ошибок:
- Обрабатывает распространенные сетевые ошибки (тайм-аут, отсутствие соединения)
- Проверяет коды состояния ответа HTTP
- Правильная проверка URL-адреса перед запросами
5. Безопасность декодирования:
- Отдельные методы декодирования с обработкой ошибок
- Тип-безопасный анализ JSON
- Сообщения об ошибках включают контекст декодирования
6. Регистрация ошибок:
- Консольная регистрация для отладки
- Пользовательские сообщения без технических подробности
- Контекстные сообщения об ошибках (какой тип данных не сработал)
Эта реализация обеспечивает:
- Лучшую видимость ошибок для разработчиков
- Более полезные сообщения об ошибках для пользователей
- Более безопасные сетевые операции
- Правильное разделение проблем
- Подробный контекст ошибки для отладки
Запускаем приложение и имеем тот же UI, но уже с обработкой ошибок:
Продолжение следует в посте «Использование DeepSeek при создании iOS приложения Countries Страны Мира. Часть 3. Swift 6 concurrency и Map.»