Я решила создать довольно простое тестовое iOS приложение Countries, которое показывает все страны Мира по регионам (Европа, Азия, Латинская Америка и т.д.) и для каждой страны её название и флаг, a также численность населения population и размер ВВП (валового внутреннего продукта) gdp:

Код находится на Github.
Создание iOS приложения Countries для отображения стран Мира
Давайте попросим DeepSeek создать полноценного iOS приложения Countries для отображения стран Мира по категориям с использованием SwiftUI и данных Всемирного Банка.
Создай iOS приложение, которое показывает страны Мира по категориям. Используй информацию Всемирного Банка. |
Мы использовали режимы “DeepThinkingn (R1)” и «Search», который обещает применять логические выводы для получения результата:

Рассуждения 1
В результате мы получаем рассуждения, которые длятся 184 секунд (более 3 минут):
И вот их перевод:
Хорошо, мне нужно создать iOS приложение с помощью SwiftUI
, которое отображает страны мира, классифицированные с помощью API
Всемирного банка. Позвольте мне разбить мои действия на шаги.
Сначала я должен выяснить, какие endpoints
API Всемирного банка использовать. Пользователь упомянул категории, так что, может быть, API индикаторов indicator
? Позвольте мне проверить. У Всемирного банка есть API стран countries
, которое может получать список стран с информацией о регионах region
, в которых они находятся. O, верно, регионы region
могут быть категориями. Например, страны могут быть сгруппированы по регионам, таким как Европа, Азия и т. д.
Итак, план такой: получить список стран countries
, у каждой страны country
есть регион region
, а затем сгруппировать их по регионам. Затем отобразить эти группы в приложении.
Далее мне нужно создать Модель данные.API
Всемирного банка возвращает страныcountry
с такими свойствами, как идентификаторid
, имяname
, регионregion
и т. д. Я создам структуруstruct Country
для страны, которая включает эти поля. Затем, возможно, структуруstruct WorldBankResponse
дляAPI
ответаresponse
, который содержит массив стран[Country]
.
Подождите, структураstruct WorldBankResponse
API
ответаresponse
, может иметь другой формат. Позвольте мне напомнить:API
Всемирного банка обычно “оборачивает” данные в массивJSON
, где каждый элемент является объектом. Например,endpoints
для стран — это что-то вродеhttp://api.worldbank.org/v2/country?format=json
. Давайте проверим структуру. Ответresponse
может содержать информацию о страницахpage
в первом элементе и фактические данные во втором. Поэтому в коде мне нужно будет это обработать.
Итак, структураJSON
— это массив, где первый элемент — это метаданныеmetadata
(например, страницаpage
, страницыpages
и т. д.), а второй элемент — массив объектов стран[Country]
. У каждой страныCountry
есть такие свойства, как «id
», «iso2Code
», «name
», «region
» (последний также является другим объектом с «id
», «value
») и т. д.
Следовательно, структураstruct C
ountry
должна иметьid
,name
,iso2code
иregion
. Структураstruct Region
будет иметьid
иvalue
. Затем при декодировании мне нужно правильно распарсить регион region:Region
.
Теперь, что касается сетевой части. Я создамObservableObject
классclass
, возможно, с именемCountryModel
, который выбирает данные с помощьюAPI
. Он будет использоватьURLSession
для выполнения запроса.URL-адрес "http://api.worldbank.org/v2/country?format=json&per_page=300"
для получения всех стран в одном запросе(per_page=300
, чтобы избежать проблем с разбиением на страницы).
Но подождите,API
Всемирного банка допускаетper_page=300
? Мне следует проверить. Возможно, максимум —1000
, так что300
должно быть в порядке. Но, возможно, стран больше. Давайте посмотрим, общее количество стран составляет около200
с чем-то, так чтоper_page=300
безопасно.
ВCountryModel
у меня будет@Published
свойствоcountries
для стран, сгруппированных по регионамregion
. Возможно, словарьDictionary
, где ключомkey
будет название регионаregion.value
, а значением — массив стран[Country]
. Затем в представленииSwiftUI
View
я могу отобразитьSections
для каждого регионаregion
.
Таким образом,CountryModel
выбирает данныеdata
, парсит (декодирует) их, а затем группирует страныcountries
по значению value их регионаregion
. Группировка может быть выполнена с помощью инициализатора словаряDictionary
, напримерDictionary(grouping:countries, by: { $0.region.value }).
Но сначала нужно получить страныcountries
. Модель также должна обрабатывать ошибки. Поэтому, возможно, нужно иметь перечислениеenum
для состояния загрузки loadState: загрузкаloading
, загруженоloaded
, ошибкаerror
.
Собирая всё это вместе: модель запускается в состоянииloading
, получает данныеdata
, декодирует (decodes
) их, группирует по регионамregion
и обновляет состояние доloaded
. Если есть ошибка, она устанавливает состояние вerror
.
ДляSwiftUI
View
: основнымView
будет списокList
с разделамиSection
для каждого регионаregion
. Заголовокheader
каждого разделаSection
— это название регионаregion
, а строками его — страныcountries
в этом регионе.
Таким образом,ContentView
будет иметь@StateObject CountryModel
. В зависимости от состояния он показывает индикатор загрузки, список или сообщение об ошибке.
Но как обрабатывать сгруппированные страны? Модель может иметь словарь Dictionary [String: [Country]], и в View будем итерировать по отсортированным ключам keys словаря Dictionary для создания разделов Section.
Подождите, но порядок регионов может быть случайным. Можно, отсортировать ключиkeys
в алфавитном порядке для согласованности.
Кроме того, некоторые страныCountry
могут иметь регионregion
с пустым или нулевымnull
значением. Нужно отфильтровать их или обработать. Например, Всемирный банк может иметь такие регионыregion
, как"Aggregates"
, которые не являются фактическими странами. Поэтому в модели после выборки отфильтруйте страныcountries
, гдеregion.id
— это «NA
» или что-то в этом роде, или где значениеvalue
регионаregion
является"Aggregates"
.
Если посмотреть на API Всемирного банка, регионыregion
включают такие агрегаты, как "Arab World
" («Арабский мир»), «Еврозона» ("Euro area
") и т. д. Это не фактические регионы, а группировки. Поэтому, возможно, стоит отфильтровать страныcountries
, где значениеvalue
регионаregion
не является агрегатом"Aggregates"
.
Как? Объект региона region
для страны может иметь такой идентификатор id
, как «EAS» для Восточной Азии и Тихоокеанского региона (East Asia & Pacific
), в то время как агрегаты имеют другие идентификаторы id
. В качестве альтернативы проверьте идентификатор региона region.id
и исключите определенные коды. В качестве альтернативы ответ response
API
для региона region
страны country
может иметь код, начинающийся с чего-то другого. Например, фактические регионы имеют коды типа «LCN» для Латинской Америки и Карибского бассейна (Latin America & Caribbean
) , в то время как агрегаты имеют коды типа «ARB» для Арабского мира (Arab World
). Поэтому, возможно, следует отфильтровать страны countries
, в которых идентификатор региона region.id
отсутствует в списке регионов-агрегатов.
В качестве альтернативы country endpoint
API
Всемирного банка включает параметр для исключения агрегатов. Позвольте мне проверить: добавление ?incomeLevel=LIC&lendingType=IBD
может отфильтровать, но, возможно, проще отфильтровать в коде. Поэтому после извлечения всех стран отфильтруйте те, где region.id
находится в списке регионов-агрегатов. Например, идентификатор региона для таких агрегатов, как "ARB", "CEB" и т. д. Поэтому, возможно, в структуре struct Country
проверьте, является ли идентификатор региона region.id
одним из агрегатов, и исключите эти страны.
Но, возможно, для простоты пользователь хочет все страны, включая агрегаты. Подождите, пользователь сказал "Страны мира", что обычно относится к суверенным государствам. Поэтому, возможно, отфильтруйте агрегаты. Поэтому в модели после извлечения отфильтруйте страны, чтобы исключить те, где идентификатор регионаregion.id
находится в списке регионов-агрегатов.
Но как узнать, какие регионы являются агрегатами? Позвольте мне проверить документацию Всемирного банка. Объекты регионов для стран могут иметь код, а агрегаты имеют разные коды. Например, регион для страны может иметь такие коды, как «EAS
», «ECS
» и т. д., в то время как агрегаты имеют «ARB
», «CEB
» и т. д. Поэтому, возможно, в коде после извлечения отфильтровать страны, где идентификатор регионаregion.id
находится в предопределенном наборе кодов агрегатов.
В качестве альтернативы проверьте, содержит ли значениеvalue
регионаregion
слово «aggregate
» или уровень «income
». Не уверен. В качестве альтернативы найдите список кодов регионов, которые являются агрегатами. Например, согласно документацииAPI
Всемирного банка, агрегаты включают такие регионы, как «ARB
» (Арабский мир), «CEB
» (Центральная Европа и страны Балтии) и т. д. Поэтому, возможно, создайте набор таких кодов и исключите страны, где region.id находится в этом наборе.
Но для этого нужно знать конкретные коды. Поскольку пользователь не указал, возможно, пропустите этот шаг на данный момент и просто отобразите все страны, включая агрегаты, но это может быть нежелательно. Хм. Возможно, исходный код может включать все, а затем упомянуть, что фильтрация может быть добавлена позже.
В качестве альтернативы, продолжайте без фильтрации и смотрите. В качестве примера, возможно, продолжайте без фильтрации и отметьте это в комментариях к коду.
Итак, двигаемся дальше. CountryModel выбирает все страныcountries
, группирует их по регионамregion
и отобразит их.View
покажет разделыSection
для каждого региона region, отсортированные по имени.
Теперь обрабатываем ответresponse
API
. СтруктураJSON
представляет собой массив, где первый элемент — метаданные, второй — массив стран[Country]
. Поэтому модели необходимо декодировать эту структуру. Поэтому создайте структуру для ответа:struct WorldBankResponse: Decodable { let countries: [Country] }
, но подождите, фактическая структура — это массив из двух элементов. Так что, возможно, вSwift
используйте массив массивов. Затем данные извлекаются как[[AnyCodable]]
, но это неправильно. Подождите, первый элемент — это объект с информацией о страницахpage
, второй — массив объектов стран[Country]
.
Итак, возможно, создайте, например, такую структуру:
struct WBResponse: Decodable {
let page: Int
let pages: Int
let perPage: Int
let total: Int
}
Но поскольку первый элемент — это объект с этими свойствами, а второй — массив объектов Country
, возможно, в Swift мы можем смоделировать response
как двухэлементный массив, где первый — WBResponseMetadata
, а второй — [Country]
.
Но как декодировать это в Swift
? Поскольку массивы JSON
не имеют ключей, нам нужно использовать контейнер без ключей unkeyedContainer
.
Итак, создайте структуру, которая декодирует первый элемент как метаданные, а второй — как массив стран countries: [Country]
.
Дайте подумать. JSON
верхнего уровня — это массив. Первый элемент — это словарь с метаданными, а второй — массив словарей [Country]
.
Итак, в Swift
мы можем создать структуру, которая декодирует это с помощью контейнера без ключей unkeyedContainer
. Вот как это можно сделать:
struct WorldBankCountryResponse: Decodable {
let countries: [Country]
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
// Декодировать первый элемент (метаданные)
_ = try container.decode(ResponseMetadata.self)
// Декодировать второй элемент (массив стран)
let countries = try container.decode([Country].self)
self.countries = countries
}
}
struct ResponseMetadata: Decodable {
let page: Int
let pages: Int
let perPage: String // perPage -- это строка вроде "50"
let total: Int
}
Подождите, но в ответе response
API
Всемирного банка структура первого элемента имеет такие ключи, как "page
", "pages
", "per_page
", "total
". Итак, структура ResponseMetadata
должна иметь эти ключи, используя Swift
CodingKey
:
struct ResponseMetadata: Decodable {
let page: Int
let pages: Int
let perPage: String
let total: Int
enum CodingKeys: String, CodingKey {
case page, pages
case perPage = "per_page"
case total
}
}
Но значениеper_page
иногда может быть числом вAPI
? Или это всегда строкаString
? Позвольте мне проверить. Например, ответ может иметь"per_page": "50"
в качестве строкиString
. Таким образом, в структуреResponseMeta
свойствоperPage
— это строкаString
. Но вSwift
, если это число вJSON
, декодирование не удастся. Поэтому, возможно, используйте собственную стратегию декодирования или сделайтеperPage
какInt
. Но согласно документацииAPI
Всемирного банка параметрper_page
возвращается как строкаString
. Хм... Может быть, стоит посмотреть пример ответаresponse
?
Пример ответаresponse
API
Всемирного банка для стран:
[
{ "page": 1, "pages": 6, "per_page": "50", "total": 299},
[ { "id": "ABW", "iso2Code": "AW", "name": "Aruba",
"region": { "id": "LCN", "iso2code": "ZJ", "value": "Latin America & Caribbean "},
"adminregion": { "id": "", "iso2code": "", "value": ""},
... },
...
]
]
Итак, в первом элементе массива "per_page
" — это строкаString
, аperPage
вResponseMetadata
также должен бытьString
. Но при инициализации, возможно, мы можем преобразовать его вInt
для использования, но в метаданных, возможно, нам это не нужно. Поскольку модель извлекает все страны сper_page=300
, возможно, метаданные не критичны, но коду все равно нужно их декодировать.
Двигаемся дальше. Структура struct Country
будет иметь вид:
struct Country: Decodable, Identifiable {
let id: String
let iso2Code: String
let name: String
let region: Region
struct Region: Decodable {
let id: String
let value: String
}
}
СCodingKeys
для обработки ключейJSON
. Например,iso2Code
происходит от "iso2code
" вJSON
, что соответствует, ноregion
является объектом. Поэтому структуры правильно вложены.
ТеперьURL
для выборки —«http://api.worldbank.org/v2/country?format=json&per_page=300
». Но подождите,API
Всемирного банка требуетHTTPS
.
Позвольте мне проверить.
Да, все их endPoints API — HTTPS. ПоэтомуURL
должен быть«https://api.worldbank.org/v2/country?format=json&per_page=300»
.
ВCountryModel
функция выборкиfetchCountries()
создаст задачуURLSession.shared. data (from:url )
для выборки данных из этого URL, декодирует ответresponse
вWorldBankCountryResponse
, извлечет страныcountries
, сгруппирует их поregion.value
, а затем обновит@Published
свойства.
Но модель должна обрабатывать асинхроннуюasync
задачу. Использованиеasync / await
было бы лучше и если поддерживается iOS 15+, мы можем использоватьasync/await
. В противном случае используйте обработчики завершенияcompletionHandlers
.SwiftUI
совместим с iOS 13+, но пользователь не указал версию iOS и учитывая, что последняя версия Xcode — 15, аSwiftUI
претерпел изменения, возможно, стоит использоватьasync/await
с модификатором.task
вView
.
Итак, вCountryModel
:
@MainActor
class CountryModel: ObservableObject {
@Published var groupedCountries: [String: [Country]] = [:]
@Published var isLoading = false
@Published var errorMessage: String?
func fetchCountries() async {
isLoading = true
errorMessage = nil
do {
let url = URL(string:
"https://api.worldbank.org/v2/country?format=json&per_page=300")!
let (data, _) = try await URLSession.shared.data(from: url)
let response = try
JSONDecoder().decode(WorldBankCountryResponse.self, from: data)
// Отфильтруйте агрегаты, если это необходимо
let countries = response.countries
let grouped = Dictionary(grouping: countries, by:
{ $0.region.value })
groupedCountries = grouped
isLoading = false
} catch {
errorMessage = error.localizedDescription
isLoading = false
}
}
}
Подождите, ноWorldBankCountryResponse
— это структураstruct
, которая декодирует массив в страны count
ries. ЗатемgroupedCountries
— это словарь[String: [Country]]
.
Но вView
при отображении нам нужно отсортировать регионыregion
в алфавитном порядке и отобразить каждый в отдельном разделеSection
.
Итак, вContentView
:
struct ContentView: View {
@StateObject private var model = CountryModel()
var body: some View {
Group {
if model.isLoading {
ProgressView()
} else if let errorMessage = model.errorMessage {
Text("Ошибка: \(errorMessage)")
} else {
List {
ForEach(model.groupedCountries.keys.sorted(),
id: \.self) { region in
Section(header: Text(region)) {
ForEach(model.groupedCountries[region] ?? [])
{ country in
Text(country.name)
}
}
}
}
}
}
.task {
await model.fetchCountries()
}
}
}
Но для этого требуется, чтобы структураCountry
была идентифицируемойIdentifiable
. Идентификаторid
структурыstruct Country
— это строкаString
, которая является идентификаторомid
страныcountry
изAPI
, например, «ABW». Так что все в порядке.
Но подождите, я объявил структуруstruct Country
какIdentifiable
, а идентификаторid
— строкаString
, так что да.
А как насчет регионов с одинаковым названием? Ключиkeys
— этоregion.value
, которые могут иметь пробелы в конце. Например, в примере ответаresponse
«Latin America & Caribbean
» имеет пробел в конце. Поэтому нужно обрезать пробелы при группировке. Поэтому в замыкании группировки обрежьтеregion.value
.
Следовательно, вы будете группировать таким образом:
let grouped = Dictionary(grouping: countries, by:
{ $0.region.value.trimmingCharacters(in: .whitespaces) })
Поэтому регионы с “хвостовыми”пробелами будут сгруппированы правильно.
Кроме того,region.value
может необязательно иметь заглавную букву. Например, «latin america & caribbean
» против «Latin America & Caribbean
». НоAPI
, похоже, возвращает их правильно, так что, возможно, это не проблема. Но для надежности, возможно, следует преобразовать их в стандартный регистрlowercased()
, ноAPI
, скорее всего, вернет их с правильным использованием заглавных букв.
Еще один момент: некоторые регионы могут быть пустыми или неприемлемыми. Например, «adminregion
» в примере имеет в качестве идентификатораid
и значенияvalue
пустые строки“”
, но основной «region
» должен иметь действительные данные для реальных стран. Но агрегаты типа «Arab World» ("Арабский мир") могут иметь значениеvalue
"Arab World" для регионаregion
.
Поэтому, если мы хотим исключить агрегаты, нам нужно отфильтровать эти страныcountries
. Но опять же, если мы не знаем, какие регионыregion
являются агрегатами, это сложно. Ради этого примера давайте продолжим без фильтрации, но упомянем, что в реальном приложении фильтрация может потребоваться.
Теперь, тестирование кода. Давайте проверим, скомпилируется ли код.
Структураstruct Country
являетсяDecodable
иIdentifiable
.WorldBankCountryResponse
использует пользовательскийdecoder
для получения странcountries
из второго элемента массива. Модель используетasync/await
для выборки и декодирования.
Потенциальные проблемы:
- ИнициализаторInit(from:)
структурыWorldBankCountryResponse
может “выбрасывать”throws
ошибкуerror
, если структура не отвечает реальнымJSON
данным. Например, еслиAPI
возвращает сообщение об ошибке, декодирование завершится неудачей. Необходимо обработать ошибки, возможно, проверить код состояния ответаHTTP
перед декодированием.
Но в текущем коде модель перехватываетcatch
ошибкиerror
декодирования и показывает сообщение об ошибке. Так что, это решено.
Еще одна проблема: регионы сортируются в алфавитном порядке, что правильно.
ВView
List
используетForEach
для отсортированных ключейkeys.sorted()
словаряgroupedCountries
. Каждый разделSection
использует название региона в качестве заголовка header и перечисляет страны в этом регионе.
Теперь давайте напишем полный код.
Но подождите,WorldBankCountryResponse
— это структураstruct
, которая декодирует первый элемент как метаданные, а второй — как страныcountries
. Но вSwift
при декодировании массива осуществляется не ключевым контейнером unkeyedContainer, который должен обрабатывать элементы массива по порядку. Код в инициализатореinit(from:) WorldBankCountryResponse
делает это: сначала декодирует метаданные (которые отбрасываются), затем декодирует массив стран countries.
Структураstruct ResponseMetadata
является частью первого элемента. Но в WorldBankCountryResponse нам не нужны метаданные, поэтому код их отбрасывает. Сохраняются только страны countries.
Итак, код должен работать.
Теперь соберем все вместе.
Возможные улучшения:
- Сообщения об ошибках отAPI
(например, если ответresponse
является error объектом) не обрабатываются.API
Всемирного банка может возвращать ошибку в другом формате. Но для простоты код предполагает, что ответ правильный.
- UI является базовым. Возможно, стоит добавить больше стилей, но пользователь не указал.
- Ячейки стран могут отображать больше информации, например кодISO
.
Но пользователь попросил отображать страны по категориям, что код и делает, группируя по регионам.
Версия 1
Таким образом, окончательный код будет выглядеть так.
Вот SwiftUI код для iOS приложения, которое отображает страны Всемирного банка, сгруппированные по регионам, с использованием API Всемирного банка:
import SwiftUI
struct ContentView: 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.fetchCountries()
}
}
}
struct CountryRow: View {
let country: Country
var body: some View {
HStack {
Text(country.name)
Spacer()
Text(country.iso2Code)
.foregroundColor(.secondary)
}
}
}
struct Country: Decodable, Identifiable {
let id: String
let iso2Code: String
let name: String
let region: Region
struct Region: Decodable {
let id: String
let value: String
}
enum CodingKeys: String, CodingKey {
case id, name, region
case iso2Code = "iso2code"
}
}
class CountryViewModel: ObservableObject {
enum LoadingState {
case loading, loaded, error(String)
}
@Published var loadingState: LoadingState = .loading
@Published var groupedCountries: [String: [Country]] = [:]
private let apiURL =
"https://api.worldbank.org/v2/country?format=json&per_page=300"
func fetchCountries() async {
guard let url = URL(string: apiURL) else {
loadingState = .error("Invalid URL")
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
let countries = try parseResponse(data)
// Group countries by region and filter out aggregates
let filteredCountries = countries.filter { $0.region.id != "NA" }
let grouped = Dictionary(grouping: filteredCountries) {
$0.region.value.trimmingCharacters(in: .whitespaces)
}
await MainActor.run {
groupedCountries = grouped
loadingState = .loaded
}
} catch {
await MainActor.run {
loadingState = .error(error.localizedDescription)
}
}
}
private func parseResponse(_ data: Data) throws -> [Country] {
struct WorldBankResponse: Decodable {
let country: [Country]
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
_ = try container.decode(ResponseMetadata.self) // Skip metadata
country = try container.decode([Country].self)
}
}
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"
}
}
return try JSONDecoder().decode(WorldBankResponse.self,
from: data).country
}
}
#Preview {
ContentView()
}
Основные особенности этой реализации:
- Выборка данных:
- Использует endpoint country API Всемирного банка
- Выбирает до 300 стран (охватывает все доступные страны)
- Использует
async/await
для сетевых запросов
2. Обработка данных:
- Анализирует
JSON
ответresponse
с помощью пользовательского декодирования - Фильтрует агрегированные регионы (например, «Арабский мир») с помощью проверок идентификаторов
id
регионовregion
- Группирует страны
countries
по их регионам с помощью словаря
3. Компоненты UI:
NavigationView
с состояниями загрузки- Список
List
с разделамиSection
для каждого регионаregion
- Пользовательский
CountryRow
, отображающий название страны и код ISO - Обработка ошибок
4. Архитектура:
- Шаблон
MVVM
с выделеннойViewM odel
@Published
свойства для управления состоянием- Отделение логики доступа к сети от компонентов UI
Чтобы использовать это приложение:
- Создайте новый проект SwiftUI в Xcode
- Замените
ContentView
этим кодом - Убедитесь, что у проекта есть разрешения на доступ в Интернет (добавьте NSAppTransportSecurity в Info.plist (если необходимо)
При запуске приложение автоматически выберет и отобразит страны countries
, сгруппированные по регионам. В каждом разделе Section
показаны страны, принадлежащие определенному региону, с названием страны и кодом ISO
, отображаемыми в каждой строке.
Примечание: API Всемирного банка может иногда быть недоступен или иметь ограничения по скорости. Для использования в производстве следует добавить надлежащую обработку ошибок и потенциальные механизмы кэширования.
Если мы запустим приложение, то получим сообщение об ошибке:
“ The data couldn’t be read because it is missing.”
(«Не удалось прочитать данные, поскольку они отсутствуют»)
Корректировка декодирования структуры struct Country
Проблема, по-видимому, в декодировании структуры struct Country
, поэтому попросим DeepSeek исправить эту ошибку:
Данные не могут быть прочитаны, потому что они пропущены. Обратите внимание на имена полей в объекте Country |
В результате мы получаем рассуждения, которые длятся 51 секунду (чуть менее 1 минуты):
Вот их перевод:
Хорошо, пользователь упомянул, что данные не могут быть прочитаны, потому что они отсутствуют, и они хотят, чтобы я проверил имена полейJSON
в объектеCountry
. Давайте вернемся к исходному коду, который я предоставил, и посмотрим, в чем может быть проблема.
Во-первых, я помню, чтоAPI
Всемирного банка возвращает массивJSON
, где первый элемент — метаданные, а второй — фактический список стран. Исходный код использует структуруWorldBankResponse
, которая пытается это проанализировать. Но, возможно, есть проблема с тем, как структурировано декодирование.
Если посмотреть на структуруCountry
, ключи кодирования — этоid
,name
,region
иiso2Code
.JSON
из Всемирного банка может использовать другие ключи. Давайте проверим фактический ответAPI
. Например, запись о стране может выглядеть так:
{"id": "BRA","iso2Code": "BR","name": "Brazil",
"region": {"id": "LCN","value": "Latin America & Caribbean "},
...
}
Подождите, в структуреCountry
идентификаторid
— это строкаString
, но вJSON
идентификатор страны — это "BR", что являетсяiso2Code
. Но есть еще одно поле с именем "iso2Code
". Это может быть проблемой. СтруктураCountry
исходного кода использует идентификатор в качестве идентификатора страны, но на самом деле идентификатор страны вJSON
— этоiso2Code
. Так что, возможно, идентификатор структурыCountry
должен соответствовать полю "id
" изJSON
, которое на самом деле являетсяiso2Code
. Но есть отдельное поле "iso2Code
". Подождите, это не имеет смысла. Давайте проверим еще раз.
В ответеJSON
response
у каждой страны есть поле "id
", которое является двухбуквенным кодом ISO, а также есть поле "iso2code
" (строчная буква "c"), которое может быть тем же самым. Подождите, может быть,API
использует "iso2Code
" со строчной буквой "c" вJSON
, но код используетCodingKeys
для сопоставленияiso2Code
с "iso2code
". Давайте проверим.
В исходном коде структураCountry
имеет:
case iso2Code = "iso2code"
Но вJSON
поле называется "iso2Code
" или "iso2code
"? ЕслиJSON
использует "iso2Code
" с заглавной буквой "C", то ключ кодирования неверен. Например, если ключJSON
- "iso2code
" (все строчные буквы, кроме "2"), то ключ кодирования правильный. Но, возможно, фактический ключ другой. Позвольте мне проверить документациюAPI
Всемирного банка или пример ответа.
При поискеendpoint country API
Всемирного банка пример ответа показывает:
[
{ "page": 1, "pages": 6, "per_page": "50", "total": 299},
[
{ "id": "ABW", "iso2Code": "AW", "name": "Aruba",
"region": {"id": "LCN","value": "Latin America & Caribbean"},
...
},
...
]
]
А, здесь у страны есть "id
": "ABW
" (это трехбуквенный код) и "iso2Code
": "AW
" (двухбуквенный код). Итак, вJSON
ключами являются "id
", "iso2Code
", "name
", "region
" и т. д. Но в структуреstruct Country
исходного кода свойствоCodingKeys
дляiso2Code
установлено на "iso2code
" (строчная буква "c"), что неверно. Правильный ключJSON
— "iso2Code
" с заглавной буквой "C". Поэтому ключ кодирования должен быть:
case iso2Code = "iso2Code"
Но вSwift
ключиCodingKeys
чувствительны к регистру. Поэтому в исходном коде было:
case iso2Code = "iso2code"
Что неверно, поскольку JSON использует "iso2Code
" с заглавной буквой "C". Это ошибка. Следовательно, свойствоiso2Code
структурыCountry
не декодируется правильно, поскольку ключ кодирования неверен, что приводит к потере данных.
Кроме того, идентификатор структурыCountry
сопоставляется с полем "id
"JSON
, которое представляет собой трехбуквенный код (например, "ABW
"), в то время какiso2Code
представляет собой двухбуквенный код. Таким образом, идентификаторid
структурыstruct Countr
y исходного кода представляет собой трехбуквенный код страны, аiso2Code
представляет собой двухбуквенный код. Но вView
CountryRow
отображаетiso2Code
, что было бы правильным как двухбуквенный код.
Но главная проблема заключается в неправильном ключе кодирования дляiso2Code
.JSON
использует "iso2Code
", поэтому ключ кодирования должен быть чувствителен к регистру. Изменение ключа кодирования с "iso2code
" на "iso2Code
" должно устранить проблему декодирования.
Другая возможная проблема заключается в синтаксическом анализеWorldBankResponse
. Исходный код использует структуруWorldBankResponse
, которая пытается декодировать первый элемент как метаданные, а второй как список стран. Однако исходный массив может иметь больше элементов или структура может быть другой. Давайте проверим пример ответа еще раз.
Ответresponse
представляет собой массив, где первый элемент — объект с метаданными, а второй элемент — массив объектов стран. Поэтому декодирование вWorldBankResponse
верно: используется не ключевой контейнерunkeyedContainer
, декодирование первого элемента какResponseMetadata
, а затем декодирование следующего элемента как[Country]
.
Но, возможно, структура метаданных отличается. Давайте проверим структуруResponseMetadata
. В ней естьpage
,pages
,perPage
(сопоставлено с "per_page
") иtotal
.JSON
для метаданных имеет "per_page
" в качестве строкиString
, которая соответствуетCodingKey
. Так что все должно быть в порядке.
Другая возможная проблема заключается в том, что регион структурыCountry
является необязательным. Если регион отсутствует в некоторых странах, декодирование не удастся. НоAPI
Всемирного банка, вероятно, включает регион для всех стран. Однако в исходном коде при группировке они отфильтровывают регионы, гдеregion.id == "NA"
, что может быть агрегированным. Но если регион отсутствует, декодирование не удастся. Но согласно ответуAPI
, у каждой страны есть регион.
Поэтому основное исправление — это исправление ключа кодированияiso2Code
с "iso2code
" на "iso2Code
".
Еще одна вещь, которую нужно проверить: в структуреCountry
идентификаторid
— это трехбуквенный код, но в исходном коде при отображенииCountryRow
используетсяcountry.iso2Code
, который должен быть двухбуквенным кодом. Это верно.
Поэтому ошибка пользователя, скорее всего, связана с тем, чтоiso2Code
не декодируется, что приводит к отсутствию некоторых данных. Но поскольку исходный код использовал "iso2code
" в качестве ключа кодирования вместо "iso2Code
", свойствоiso2Code
не устанавливалось, что приводило к отсутствию данных при попытке отобразить его или использовать в другом месте.
Исправленная структураCountry
должна иметь:
case iso2Code = "iso2Code"
Это ключевое исправление. Другие возможные проблемы могут быть связаны с декодированием региона, но пользователь специально упомянул имена полейJSON
в объектеCountry
, поэтому сосредоточился наiso2Code
.
Это ответ пользователю:
Вы абсолютно правы — была ошибка в отображении поля JSON. API Всемирного банка использует iso2Code
(с заглавной буквой C) в своем ответе JSON, но наш код использовал iso2code
(строчная c) в CodingKeys
. Вот исправленная реализация:
struct Country: Decodable, Identifiable {
let id: String
let iso2Code: String
let name: String
let region: Region
struct Region: Decodable {
let id: String
let value: String
}
enum CodingKeys: String, CodingKey {
case id, name, region
case iso2Code = "iso2Code" // Fixed the case here
}
}
Ключевое исправление находится в перечислении enum CodingKeys
, где мы теперь правильно отображается:
case iso2Code = "iso2Code"
Версия 2
Вот полностью исправленная View Model
с правильной обработкой JSON
:
class CountryViewModel: ObservableObject {
enum LoadingState {
case loading, loaded, error(String)
}
@Published var loadingState: LoadingState = .loading
@Published var groupedCountries: [String: [Country]] = [:]
private let apiURL =
"https://api.worldbank.org/v2/country?format=json&per_page=300"
func fetchCountries() async {
guard let url = URL(string: apiURL) else {
loadingState = .error("Invalid URL")
return
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
let countries = try parseResponse(data)
// Filter out regional aggregates and group by region
let filteredCountries = countries.filter {
!$0.region.value.lowercased().contains("aggregate") &&
$0.region.id != "NA"
}
let grouped = Dictionary(grouping: filteredCountries) { country in
country.region.value.trimmingCharacters(in: .whitespacesAndNewlines)
}
await MainActor.run {
groupedCountries = grouped
loadingState = .loaded
}
} catch {
await MainActor.run {
loadingState = .error("Failed to load countries:
\(error.localizedDescription)")
}
}
}
private func parseResponse(_ data: Data) throws -> [Country] {
struct WorldBankResponse: Decodable {
let countries: [Country]
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
_ = try container.decode(ResponseMetadata.self)
countries = try container.decode([Country].self)
}
}
struct ResponseMetadata: Decodable {
let page: Int
let pages: Int
let perPage: String
let total: Int
}
return try JSONDecoder().decode(WorldBankResponse.self,
from: data).countries
}
}
Ключевые улучшения в этой версии:
- Исправленное сопоставление
iso2Code
JSON - Лучшая фильтрация агрегированных регионов
- Более надежная очистка имен регионов
- Улучшенные сообщения об ошибках
- Правильная обработка структуры не ключевого контейнера
unkeyedContainer
Структура ответа response API Всемирного банка:
[
{ // Metadata object
"page": 1,
"pages": 5,
"per_page": "50",
"total": 299
},
[ // Actual countries array
{
"id": "ABW",
"iso2Code": "AW",
"name": "Aruba",
"region": {
"id": "LCN",
"value": "Latin America & Caribbean "
},
// ... other fields
}
]
]
Это решение теперь правильно:
- Обрабатывает структуру массива из двух элементов
- Сопоставляет все обязательные поля с правильным регистром
- Более надежно отфильтровывает агрегированные регионы
- Удаляет пробелы из имен регионов
- Использует правильную многопоточность Swift с
async/awa
it
Ошибка, которую вы видели («Не удалось прочитать данные, поскольку они отсутствуют»), была вызвана именно неправильным сопоставлением iso2code
/ iso2Code
, которое теперь устранено.
Да, эта ошибка устранена, но в представленном выше коде появилась новая ошибка:
Теперь struct ResponseMetadata
выглядит немного по-другому:
struct ResponseMetadata: Decodable {
let page: Int
let pages: Int
let perPage: String
let total: Int
}
A раньше это было так:
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"
}
}
И это было правильно.
Поэтому, если вы в предыдущем коде только изменили строку
case iso2Code = "iso2code"
на
case iso2Code = "iso2Code"
, оставив:
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"
}
}
… то приложение будет работать правильно.
A вот если вы полностью заменили функцию func parseResponse (_ data: Data) throws -> [Country]
на новую, то опять получите ту же самую ошибку.
Вывод: DeepSeek не держит контекст, может самопроизвольно изменить правильный код. Будьте осторожны!!!
Запускаем правильное приложение с заменой одной буквы в названии свойства и получаем результат:
Что прекрасно в этой реализации iOS приложения, предоставленной DeepSeek в отличие от версии, предоставленной Claude 3.5 Sonnet :
- Используется современный многопоточный механизм
async / await
, a неCGD
- Прекрасно настроен пользовательский
init
с помощьюunkeyedContainer
для DecodeJSON
данных, представляющих собой массив неоднородных элементов, что обычно не является стандартным форматомJSON
.
Добавляем численность населения population и ВВП gdp
Теперь мы хотим добавить численность населения population
и ВВП gdp
для каждой страны:
добавь population , gdp , флаг flag и столицу capitalCity для каждой страны. Возьмите capitalCity из JSON для country и сконструируйте флаг из эмодзи |
В результате мы получаем рассуждения, которые длятся 53 секунду (чуть менее 1 минуты):
Вот их перевод:
Хорошо, давайте рассмотрим запрос пользователя на добавление населенияpopulation
, ВВПgdp
, столицы и флагов для каждой страны в приложении SwiftUI. Сначала мне нужно выяснить, где получить эти данные изAPI
Всемирного банка. Предыдущая реализация использовала базовуюcountry endpoin
t, которая могла не давать всех нужных нам полей.
Подождите, пользователь упомянул добавление населенияpopulation
и ВВПgdp
.API
Всемирного банка имеет отдельные индикаторыindicator
для них:SP.POP.TOTL
для населенияpopulation
иNY.GDP.MKTP.CD
для ВВПgdp
. Поэтому мне нужно сделать дополнительныеAPI
вызовы, чтобы получить эти показатели для каждой страны. Но обработка нескольких вызововAPI
может усложнить ситуацию, особенно потому, что нам нужно получать данные для каждой страны по отдельности. Это может быть медленно или иметь ограничения по лимиту на запросы. Хм, может быть, есть способ запросить сразу пакет данных по населенияpopulation
и ВВПgdp
или использовать другуюendpoint
, которая включает больше данных.
Кроме того, пользователь хочет столицу. Я помню, чтоcountry endpoint
Всемирного банка предоставляет некоторую базовую информацию, включая столицу. Позвольте мне проверить документациюAPI
. Да,country endpoint
возвращает полеcapitalCity
, то есть имя столицы. Отлично, теперь мы можем извлечь его напрямую, без другого вызоваAPI
.
Флаг пользователь хочет создать из эмодзи. Я помню, что коды стран можно преобразовать в эмодзи флагов с помощью региональных индикаторных символовUnicode
. Каждая буква кода ISO 3166-1 alpha-2 соответствует региональному индикаторному символу. Например, «US
» становится 🇺🇸. Поэтому мне нужно написать функцию, которая берет кодISO2
и генерирует эмодзи флага.
Теперь интегрируем их в существующий код. Структуреstruct Country
нужны новые свойства:population
,gdp
,capitalCity
иflag
. Подождите, пользователь может ожидать данных в реальном времени, но выборка ВВПgdp
и населенияpopulation
для каждой страны при запуске приложения может быть слишком медленным. Может быть, нам следует добавить их как необязательныеOptional
свойства и выбирать их в фоновом (background
) режиме, обновляяUI
по мере их поступления. Однако это усложняет View Model. Или, возможно, пользователю нужны статические данные, но данные Всемирного банка являются ежегодно обновляемыми. Нужно уточнить, но поскольку пользователь не указал, может быть, мы можем включить последние доступные значения.
Подождите, первоначальная реализация использовалаcountry endpoint
для списка стран, которая может не включать населениеpopulation
и ВВПgdp
. Поэтому, чтобы получить их, нам нужно запросить индикаторы через ихindicator endpoint
. Но каждый индикатор требует отдельного вызова для каждой страны. Это слишком много вызовов. Возможно, пользователь не знает об этом и ожидает все данные из однойendpoint
. Позвольте мне еще раз проверитьAPI
. Точкаcountry endpoint
(v2/country) возвращает некоторую базовую информацию, включаяcapitalCity
, но не населениеpopulation
или ВВПgdp
. Так что да, для этого требуются отдельныеAPI
вызовы.
Но делать сотни вызовов API нецелесообразно. Может быть, есть способ получить население и ВВП всех стран за один вызов? Если посмотреть на документациюAPI
, то форматendpoint
будет примерно таким: /v2/country/all/indicator/SP.POP.TOTL?format=json&date=2022 для населенияpopulation
. Аналогично для ВВПgdp
. Таким образом, мы можем получить данные о населении всех стран за один вызов, и то же самое для ВВП. Затем сопоставить эти значения с каждой страной.
Это лучший подход. Итак, план такой:
1. Получить список стран, как и раньше.
2. Получить последние данные о населении для всех стран за один вызов.
3. Получить последние данные о ВВП для всех стран за один вызов.
4. Объединить все эти данные в моделеstruct Country
.
Но для этого требуется обработка трех отдельныхAPI
вызовов и объединение данных. Необходимо структурироватьView Model
для асинхронной обработки, возможно, используяasync let
для многопоточных запросов.
Кроме того, данные о населении и ВВП могут быть доступны не для всех стран, поэтому они должны быть необязательными Optional
в Модели.
Для эмодзи флага функция возьметiso2Code
страны и преобразует каждый символ в соответствующий региональный индикатор. Например, «US
» станет «U
» и «S
», которые являются значениямиUnicode
, начинающимися с 0x1F1E6 (A — это 0x1F1E6). Таким образом, вычитание «A
» из каждого символа дает смещение. Но нужно обрабатывать строчные буквы и гарантировать, что код состоит из двух символов.
Теперь обновим структуруstruct Country
:
1. ДобавимcapitalCity
из существующейcountry endpoint
.
2. Добавимpopulation
иgdp
в качестве необязательныхOptional
свойств.
3. Добавим вычисляемое свойство дляflag
.
ВView Model
после выборки стран выбираем данные о населенииpopulation
и ВВПgdp
, затем “привязываем” их к соответствующим странам.
Но как структурироватьAPI
вызовы дляpopulation
иGDP
? Ответresponse
дляindicator endpoint
немного отличается от ответаresponse
дляcountry endpoint
. Это массив записей, где каждая запись имеетcountryiso2code
, значениеvalue
и датуdate
. Таким образом, мы можем создать словарь, сопоставляющий коды стран с их значениями населенияpopulation
и ВВПgdp
.
Итак, шаги в функцииfunc fetchCountries ViewModel
:
1. Выбрать страныcountries
.
2. Выбрать данные о населенииpopulation
.
3. Выбрать данные о ВВПgdp
.
4. Объединить все данные в модели страныstruct Country
.
5. Группировать по регионамregion
.
Необходимо обрабатывать ошибки для каждогоAPI
вызова. Возможно, использоватьasync let
для их многопоточного запуска, а затем ожидатьawait
все три результата.
Но обработка ошибок может оказаться сложной. В качестве альтернативы выбирайте их последовательно, но это будет медленнее.
После того, как данные будут выбраны, создайте словариDictionary
для населенияpopulation
и ВВПgdp
, затем обновите каждую странуCountry
соответствующими значениями. Для флага вычисляемое свойство в структуреCountry
сгенерирует эмодзи.
ВUI
обновитеCountryRow
для отображенияcapitalCity
,population
,GDP
и флага. Возможно, с помощьюNavigationLink
создание более подробногоView
, но пользователь не указал, поэтому, просто расширяем существующую строку.
Тестирование пограничных случаев: страны без кодовISO2
, отсутствующие данные и т. д. Также убедитесь, что функцияflag
обрабатывает случаи, когда код не состоит из двух букв.
Теперь обновим код:
В структуреstruct Country
:
- Добавьте свойстваcapitalCity
,population
,gdp
.
- Добавьте вычисляемое свойствоflag
.
ВView Model
:
- Измените функциюfunc fetchCountries
, чтобы выбирать также данные о населении и ВВП.
- Проанализируйте эти ответыresponse
и сопоставьте их со странами.
ВCountryRow
:
- Отображение флага, столицы, населения, ВВП.
Потенциальные проблемы: ограничения скоростиAPI
, отсутствующие данные, неверные форматы дат, обработка необязательныхOptional
значений вUI
.
Кроме того,API
Всемирного банка может возвращать данные о населении и ВВП с разными кодами стран (например, некоторые агрегаты “Aggregates
”), поэтому их нужно отфильтровывать, как и раньше.
Реализация функции эмодзи флага как расширения extension для строкиString
.
Собираем все вместе, проверяем каждую часть.
Вот улучшенная реализация с эмодзи населения, ВВП, столицы и флага:
import SwiftUI
struct ContentView: 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)
}
}
}
}
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"
}
}
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
// Merge economic data
finalCountries = finalCountries.map { country in
var modified = country
modified.population = Int(populationDict[country.iso2Code] ?? 0)
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(ResponseMetadata.self)
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(ResponseMetadata.self)
entries = try container.decode([Entry].self)
}
struct Entry: Decodable {
let countryiso2code: String
let value: Double?
enum CodingKeys: String, CodingKey {
case countryiso2code = "countryiso2code"
case value = "value"
}
}
}
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 }
let code = String(entry.countryiso2code.suffix(2))
dict[code] = value
}
}
}
struct ResponseMetadata: Decodable {
let page: Int
let pages: Int
let perPage: String
let total: Int
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Ключевые улучшения:
Новые поля данных:
- Столица
capitalCity
(из базовогоAPI country
) - Население
population
(из индикатораSP.POP.TOTL
) - ВВП
gdp
(из индикатораNY.GDP.MKTP.CD
) - Флаг эмодзи, сгенерированный из кода
ISO2
Улучшенный UI:
- Отображение флага эмодзи
- Метка
Label
для столицы - Отформатированные значения населения и ВВП
- Системные иконки для разных типов данных
- Лучшее размещение и иерархия
Обработка данных:
- Многопоточные вызовы API для данных о стране, населении и ВВП
- Объединение данных с использованием
async / await
- Обработка ошибок для нескольких источников данных
- Фильтрация недопустимых записей
Форматирование:
- Столица
capitalCity
(из API базовой страны) - Население
population
(из индикатораSP.POP.TOTL
) - ВВП
gdp
(из индикатораNY.GDP.MKTP.CD
) - Флаг эмодзи, сгенерированный из кода
ISO2
Примечание:
- Вызовы API используют жестко закодированные даты данных 2022 года — возможно, вы захотите сделать это динамически
- Некоторые страны могут не иметь последних экономических данных
- Система эмодзи флагов использует коды
ISO2
для генерации региональных индикаторных символов - Значения ВВП отображаются в необработанных суммах в долларах США (вы можете отформатировать как миллиарды/триллионы)
Чтобы использовать эту реализацию:
- Убедитесь, что у вашего приложения есть разрешения на доступ в Интернет
- Обработайте потенциальные ограничения скорости API (Всемирный банк разрешает 100 запросов в минуту)
- Добавьте состояния загрузки для данных отдельных стран
- Рассмотрите возможность добавления
refresh
функциональности - Реализуйте обработку ошибок для отдельных endpoints
Теперь приложение будет отображать:
- Эмодзи флага страны 🇺🇸 🇨🇳 🇮🇳 и т. д.
- Столицу
- Численность населения
- ВВП в долларах США
- Региональную группировку
- Код ISO2
Версия 3
Если мы запустим данный код, то получим уже привычное нам сообщение об ошибке:
“ The data couldn’t be read because it is missing.”
Это опять связано с декодированием данных.
Корректируем поля в Индикаторе
И опять в улучшенной реализации с эмодзи населения, ВВП, столицы и флага мы имеем НЕВЕРНЫЙ код для ResponseMetadata
Продолжение следует в посте «Использование DeepSeek при создании iOS приложения Countries Страны Мира. Часть 2. Численность населения, ВВП, флаг.»