Это продолжение. Начало находится в посте «Использование 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
дляperPage
IndicatorResponseMetadata
использует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 Model
CountryViewModel
, должна обрабатывать только бизнес-логику, выборку данных и управление состоянием.
Рассматривая структуру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 Mode
l. Возможно, они должны быть 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
структурами внутриView
Model
, поскольку они используются только там.
Аналогично,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
, которые декодируютJSON
API
в Модели.
Наконец, методfetchAllData
View 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.»