Это продолжение. Начало находится на «Использование DeepSeek при создании iOS приложения Countries Страны Мира. Часть 1« и «Использование DeepSeek при создании iOS приложения Countries Страны Мира. Часть 2″.
Код находится на Github.
Swift 6 concurrency
| Преобразуй это iOS приложение в Swift 6 concurrency версию |
В результате мы получаем рассуждения, которые заняли 57 секунд:
Вот перевод этих рассуждений:
Хорошо, мне нужно преобразовать существующее приложениеSwiftUI iOSдля использования возможностей многопоточностиSwift 6. Позвольте мне начать с того, чтоSwift 6предлагает в плане многопоточности. Я знаю, чтоSwift 5.5представилasync / await, акторыactorи структурную многопоточность, но ожидается, чтоSwift 6улучшит их более строгими проверками многопоточности, полной изоляцией акторовactorи, возможно, некоторыми новыми возможностями.
Во-первых, если посмотреть на текущий код,View Modelиспользует аннотацииasync/awaitи@MainActor. ВSwift 6модель многопоточности становится более строгой, поэтому мне нужно убедиться, что весь асинхронный код правильно помечен и что гонки данных (data races) предотвращены. Существующий код использует View Model на основе классов со свойствами@Published. ВSwift 6, возможно, было бы лучше использовать акторыactorдляView Modelдля обеспечения безопасности потоков.
Подождите, ноView ModelвSwiftUIчасто является@MainActor, потому что он обновляетUI, что должно происходить в основном потоке (main thread). Так что, возможно,CountryViewModelследует пометить с помощью@MainActor, чтобы гарантировать, что все обновленияUIпроисходят в основном потоке (main thread). Текущий код использует@MainActor.runвнутриView Model, но вSwift 6создание всего класса@MainActorможет быть более уместным.
Далее, функции выборки данныхfetch. Текущий код использует асинхронныеasyncфункции иTaskв модификаторе.task. ВSwift 6мне следует проверить, есть ли какие-либо новые модификаторы или изменения в том, как обрабатываются Task. Кроме того, обработка ошибок использует блокиdo-catch;Swift 6может ввести более структурированную обработку ошибок или потребовать более явного распространения ошибок.
В кодеCountryViewModelданные выбираются асинхронно. Чтобы сделать его совместимым соSwift 6, мне следует убедиться, что все изменяемые состояния должным образом изолированы. ПосколькуView Modelявляется классом, использование@MainActorпоможет, но если есть фоновые (background) операции, возможно, следует использовать ключевое словоnonisolatedдля функций, которые не затрагивают состояние.
Если посмотреть на функцииfetchCountriesиfetchIndicatorData, то они выполняют сетевые запросы. ВSwift 6, возможно, было бы полезно использоватьasync letи группы задачTask Groupболее широко. Также следует гарантировать, что любое общее изменяемое состояние защищено акторамиactorили@MainActor.
Еще один момент — использование[String: [Country]]вgroupedCountries. Поскольку это@Publishedи изменяется вView Model, крайне важно, чтобы все обновления происходили в основном потоке (main thread). ПометкаView Modelкак@MainActorдолжна обеспечить это.
А как насчетURLSession data tasks? Текущий код используетURLSession.shared.data(from: url), который является асинхронным и должен быть совместимым. ВSwift 6, возможно, есть новыеAPIили изменения, но я думаю, что существующий код подойдет.
Обработка ошибок: текущий код имеет пользовательское перечислениеenum APIError.Swift 6может потребовать более явной обработки ошибок, но существующих блоковdo-catchдолжно быть достаточно. Однако мне следует проверить, влияют ли какие-либо новые функции многопоточности на распространение ошибок.
ФункцияmergeDataизменяет страныcountries, добавляя населениеpopulationи ВВПgdp. Поскольку это делается вView Modelна@MainActor, это безопасно. Но вSwift 6, возможно, можно было бы рассмотреть использованиеAsyncSequenceили других инструментов многопоточности, но текущий подход, скорее всего, все еще актуален.
Тестирование: убедитесь, что все асинхронныеasyncфункции правильно помечены какasync, и что любые замыкания, которые могут вызвать проблемы многопоточности, замененыasyncальтернативами. Также проверьте наличие любых специфичных дляSwift 6предупреждений или ошибок, связанных с проверками многопоточности.
Итак, шаги по преобразованию:
1. ПометьтеCountryViewModelкак@MainActor, чтобы убедиться, что все обновленияUIнаходятся в основном потоке.
2. Проверьте все асинхронные функции и убедитесь, что они правильно помечены какasyncиthrows.
3. Замените все оставшиеся обработчики завершения или объедините код с помощьюasync/await.
4. Используйтеnonisolatedдля функций вView Model, которые не обращаются к изменяемому состоянию.
5. Убедитесь, что весь доступ к свойствам@Publishedпроисходит в пределах@MainActor.
6. При необходимости обновите использованиеTaskв модификаторе.task.
7. Проверьте наличие новых функцийSwift 6многопоточности и примените их, например,AsyncSequenceили функциюcancel()для отмены задачTask.
8. Убедитесь, что обработка ошибок совместима с более строгой модельюSwift 6многопоточности.
9. Протестируйте приложение, чтобы убедиться в отсутствии гонок данных (Data races) или проблем многопоточности.
Подождите, существующий код пользователя уже используетasync/await, поэтому основные изменения будут включать принятие более строгих проверок многопоточности, использование@MainActorдляView Modelи обеспечение изоляции всех изменений состояния. Кроме того, замените любые принудительные развертки (force unwraps)Optionalзначений на правильную обработку ошибок, посколькуSwift 6может применять более строгие правила дляOptionalпараметров.
В представленном коде естьguard let url= URL(string: countryURL) else { throw APIError.invalidURL }, что хорошо. Но другим частям кода могут потребоваться дополнительные проверки.
Кроме того, вSwift 6разрешено использованиеasync let, которое используется в текущем коде. Поэтому я думаю, что главная задача — аннотироватьView Modelс помощью@MainActorи гарантировать, что все мутации состояния находятся в основном потоке (main thread).
Итак, окончательные изменения кода:
- Добавьте @MainActor в класс CountryViewModel.
- Убедитесь, что все функции, изменяющие свойства @Published, находятся в основном актере @MainActor.
- Проверьте наличие оставшегося не асинхронного кода, который может вызвать гонки данных (data races).
- Обновите обработку ошибок, чтобы использовать улучшенные функции Swift 6 многопоточности, если применимо.
После внесения этих изменений приложение должно быть совместимо с моделью Swift 6 многопоточности, обеспечивая безопасность потоков и правильное использование async/await.
Для пользователя:
Вот оптимизированная для Swift 6 многопоточности версия со строгой изоляцией aкторов actor и современными шаблонами многопоточности :
import SwiftUI
// MARK: - Main View
struct CountryListView: View {
@State private var viewModel = CountryViewModel()
var body: some View {
NavigationStack {
Group {
switch viewModel.loadingState {
case .loading:
ProgressView("Loading countries...")
case .loaded:
List {
ForEach(viewModel.sortedRegions, id: \.self) { region in
Section(header: Text(region)) {
ForEach(viewModel.countries(in: region)) { country in
CountryRow(country: country)
}
}
}
}
case .error(let message):
ContentUnavailableView("Loading Failed",
systemImage: "globe", description: Text(message))
}
}
.navigationTitle("World Countries")
.refreshable { await viewModel.load() }
}
.task { await viewModel.load() }
}
}
// MARK: - View Components
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(.currency(code: "USD")),
systemImage: "dollarsign.circle")
}
}
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
}
// MARK: - ViewModel
@MainActor
final class CountryViewModel: ObservableObject {
enum LoadingState {
case loading, loaded, error(String)
}
@Published private(set) var loadingState: LoadingState = .loading
private var countriesByRegion: [String: [Country]] = [:]
private let service = WorldBankService()
var sortedRegions: [String] {
countriesByRegion.keys.sorted()
}
func countries(in region: String) -> [Country] {
countriesByRegion[region] ?? []
}
func load() async {
loadingState = .loading
do {
try await service.fetchAllData()
countriesByRegion = service.groupedCountries
loadingState = .loaded
} catch {
loadingState = .error(error.localizedDescription)
}
}
}
// MARK: - Service Layer
actor WorldBankService {
private(set) 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 throws {
async let countries = fetchCountries()
async let populationData = fetchIndicatorData(url: populationURL)
async let gdpData = fetchIndicatorData(url: gdpURL)
let (baseCountries, population, gdp) = try await (countries, populationData, gdpData)
let merged = merge(
countries: baseCountries,
population: population,
gdp: gdp
)
groupedCountries = groupCountries(merged)
}
private func fetchCountries() async throws -> [Country] {
let (data, _) = try await fetchResource(from: countryURL)
return try decodeCountries(from: data)
}
private func fetchIndicatorData(url: String) async throws -> [String: Double] {
let (data, _) = try await fetchResource(from: url)
return try decodeIndicatorData(from: data)
}
private nonisolated func fetchResource(from urlString: String) async throws -> (Data, URLResponse) {
guard let url = URL(string: urlString) else {
throw APIError.invalidURL
}
return try await URLSession.shared.data(from: url)
}
// MARK: - Data Processing
private func merge(
countries: [Country],
population: [String: Double],
gdp: [String: Double]
) -> [Country] {
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
}
}
private func groupCountries(_ countries: [Country]) -> [String: [Country]] {
Dictionary(grouping: countries) {
$0.region.value.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
// MARK: - Decoding
private nonisolated func decodeCountries(from data: Data) 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)
}
}
return try JSONDecoder().decode(WorldBankResponse.self, from: data).countries
}
private nonisolated func decodeIndicatorData(from data: Data) 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: 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
}
}
}
// MARK: - Models & Error Handling
struct Country: Decodable, Identifiable, Sendable {
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, Sendable {
let id: String
let value: String
}
enum CodingKeys: String, CodingKey {
case id, name, region
case iso2Code = "iso2Code"
case capitalCity = "capitalCity"
}
}
enum APIError: LocalizedError, Sendable {
case invalidURL
case invalidResponse
case decodingFailed(message: String)
var errorDescription: String? {
switch self {
case .invalidURL: "Invalid API endpoint URL"
case .invalidResponse: "Received invalid response from server"
case .decodingFailed(let message): "Data parsing failed: \(message)"
}
}
}
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"
}
}Ключевые реализованные возможности Swift 6 многопоточности:
- Строгая изоляция акторов Actor
- Слой сервиса
WorldBankService, обернутый в ТИП актораactor - Изоляция
@MainActor дляViewиViewModel - ключевое слово
nonisolatedдля безопасности синхронных методов
- Соответствие Sendable
- Все ТИПы моделей помечены как
Sendable - ТИПы ошибок сделаны
Sendable - Правильная
Valueсемантика для передачи данных
- Современная структурная многопоточность
- a
sync letдля многопоточных запросов - Поддержка обновляемых модификаторов
- Отмена задачи
Taskчерез.task
- Управление безопасностью ТИПов
- Выделенный уровень сервиса для работы в сети
- Четкое разделение этапов обработки данных
- Правильное распространение ошибок через async/await
- Оптимизированный поток данных
@Published свойства только для@MainActor- Производные вычисляемые свойства для использования в
Views - Эффективная группировка и фильтрация данных
- Улучшенная обработка ошибок
- Локализованные описания ошибок
- Унифицированный поток обработки ошибок
- Удобное для пользователя сообщение о коде ошибки
- Оптимизация производительности
- Многопоточная выборка данных
- Неблокирующие обновления UI
- Эффективное управление памятью
Чтобы использовать эту реализацию:
- Обеспечьте Xcode 16+ для поддержки Swift 6
- Добавьте
NSAppTransportSecurityпри необходимости - Рассмотрите возможность добавления локального кэширования для лучшей поддержки в автономном режиме
- Реализуйте правильные состояния загрузки и восстановление после ошибок
- Добавьте accessibility метки для элементов UI
Эта версия использует возможности Swift 6 многопоточности для создания:
- Полностью потокобезопасного потока данных
- Оптимального использования ресурсов
- Плавного UI взаимодействия
- Масштабируемой архитектуры
- Перспективной основы для усовершенствований
Запускаем приложение и имеем тот же UI, но поддерживающий многопоточность Swift 6 :
Страна на карте Map
| Покажи страну country на карте в CountryDetailView |
Мы получаем рассуждения, которые длятся 50 секунд и вот их перевод:
Хорошо, мне нужно добавить карту для отображения страны вCountryDetail. Начну с напоминания существующей структуры кода. Основное View —CountryListView, в котором перечислены страны, сгруппированные по регионам. Каждая строка страны переходит вCountryDetailView.
Сначала мне нужно создатьCountryDetailView. Это View должно отображать название страны, флаг, столицу, население, ВВП и карту, показывающую местоположение страны.
Чтобы отобразить карту, я буду использоватьMapKit. ВSwiftUIMap Viewтребует координатcoordinates. Поэтому мне нужно получить широтуlatitudeи долготуlongitudeстраныcountry.APIВсемирного банка может не предоставлять этого, поэтому мне понадобится другой источник данных. Возможно,API RestCountriesили другой сервис геокодирования. Или используйте CoreLocation для геокодирования названия страны или столицы.
Подождите, геокодирование может достичь ограничений по скорости или потребовать ключAPI. Может быть, используйте кодISOстраны для получения координат. Или включите статический список координат страны. Но это не масштабируется. Хм.
В качестве альтернативы используйте геокодирующийCLGeocoderвMapKitдля преобразования названияnameстраны или столицыcapitalCityв координатыcoordinate. Это может сработать, но может быть медленным или ненадежным. Позвольте мне проверить.CLGeocoderможет геокодировать названияnameстран. Например, передача "Germany" должна вернуть ее координатыcoordinate.
Поэтому вCountryDetailView, при его появлении, инициируйте запрос геокодирования для названия страныcountry.name. После получения координатcoordinateотобразите их на карте. Также обработайте возможные ошибки, если геокодирование не удалось.
Теперь интегрируем это в существующий код. МодельCountryне имеет координат, поэтому мне нужно добавить@Publishedсвойство вView Modelдля регионаregion. Может быть, создать дляCountryDetailViewModel, который обрабатывает геокодирование.
Подождите, пользователь хочет картуMapвCountryDetailView. Позвольте мне описать шаги:
1. СоздайтеCountryDetailView, который принимает объектCountry.
2. ВCountryDetailViewиспользуйтеMapKitдля отображения местоположения страны.
3. Геокодируйте названиеnameстраны, чтобы получить координатыcoordinates.
4. Отображайте аннотациюAnnotationна карте.
5. Обрабатывайте состояния загрузки и ошибки.
Возможные проблемы: геокодирование асинхронно и может не всегда возвращать правильные координаты. Кроме того, некоторые страны могут иметь несколько результатов (например, «Georgia» — страна, а не штат США). Необходимо убедиться, что мы получаем страну, а не регион с одинаковым названием.
Чтобы смягчить ситуацию, можно геокодировать, используя как название name страны, так и столицуcapitalCity. Или использовать кодISO, если это возможно. В качестве альтернативы используйте сторонний API, который предоставляет координаты страны на основе кодов ISO.
Но чтобы упростить задачу, используйтеCLGeocoderс названием страны и обработайте первый результат. Теперь, изменив модель Country: никаких изменений не требуется, так как координатыcoordinatesне являются частью исходных данных. Вместо этогоViewModelдля подробногоViewбудет обрабатывать геокодирование. Создайте@StateObjectвCountryDetailViewдляViewModel, который содержит регионregionи обрабатывает геокодирование. ВViewModel, когда задана странаcountry, запустите процесс геокодирования. После получения координатcoordinatesобновите регионregion, что вызовет обновление картыMap. Также добавьте обработку ошибок в случае сбоя геокодирования. Реализация картыMap: используйтеMapс региономregion, который является@Stateпеременнойvar. После завершения геокодирования установите регионregionв соответствии с координатамиcoordinates. Добавьте маркерMarkerаннотацииAnnotation. Тестирование пограничных случаев: страныcountryс названиямиcountry.name, которые могут быть неправильно геокодированы. Возможно, добавьте резервный вариант для столицы, если название страны не работает.
Собираем все вместе: -CountryDetailViewимеетMap View. - При появлении.onAppearгеокодируйте название страны, чтобы получить координатыcoordinates. - Отображается состояние загрузки во время геокодирования. - Отображается картаMap, как только будут доступны координатыcoordinates. - Обработка ошибок, например, отображение сообщения при сбое геокодирования. Возможные проблемы с производительностью: геокодирование каждый раз при появленииCountryDetailView. Возможно, кэширование координатcoordinates, но для простоты геокодирование каждый раз.
Теперь пишем код: 1. ИмпортируемMapKitв файлSwift. 2. Создаем@Stateдля регионаregionвView. 3. ИспользуемCLGeocoderвViewModelдля геокодирования названия страны. 4. Обновляем регион region при получении координатcoordinates. 5. ДобавляемMap Viewс региономregionи аннотацией. Также рассмотрите возможность использованияMKCoordinateRegionдля начального положения и диапазона карты. Тестирование с некоторой странойcountryдля проверки работоспособности. Обработка возможныхnilзначений и ошибок.
Вот реализация отображения страны на карте в CountryDetailView с использованием MapKit и геокодирования:
import SwiftUI
import MapKit
struct CountryDetailView: View {
let country: Country
@StateObject private var viewModel: CountryDetailViewModel
init(country: Country) {
self.country = country
self._viewModel = StateObject(wrappedValue: CountryDetailViewModel(country: country))
}
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
headerSection
mapSection
detailsSection
}
.padding()
}
.navigationTitle(country.name)
.navigationBarTitleDisplayMode(.inline)
.overlay {
if viewModel.isLoading {
ProgressView("Locating country...")
}
}
.alert("Location Error", isPresented: $viewModel.showGeocodingError) {
Button("OK") { }
} message: {
Text("Could not find location for \(country.name)")
}
}
private var headerSection: some View {
HStack(alignment: .top) {
VStack(alignment: .leading) {
Text(country.flag)
.font(.system(size: 48))
Text(country.iso2Code)
.font(.title2)
.bold()
}
Spacer()
VStack(alignment: .trailing) {
Text(country.capitalCity)
.font(.title3)
.bold()
Text("Capital City")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
private var mapSection: some View {
Group {
if let region = viewModel.region {
Map(coordinateRegion: .constant(region),
interactionModes: .zoom,
annotationItems: [country]) { country in
MapAnnotation(coordinate: region.center) {
MapMarker()
}
}
.frame(height: 300)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
}
private var detailsSection: some View {
VStack(alignment: .leading, spacing: 15) {
if let population = country.population {
DetailRow(label: "Population",
value: population.formatted(),
systemImage: "person.2")
}
if let gdp = country.gdp {
DetailRow(label: "GDP",
value: gdp.formatted(.currency(code: "USD")),
systemImage: "dollarsign.circle")
}
}
}
}
@MainActor
class CountryDetailViewModel: ObservableObject {
let country: Country
@Published var region: MKCoordinateRegion?
@Published var isLoading = false
@Published var showGeocodingError = false
private let geocoder = CLGeocoder()
init(country: Country) {
self.country = country
Task { await geocodeCountry() }
}
func geocodeCountry() async {
isLoading = true
defer { isLoading = false }
do {
let placemarks = try await
geocoder.geocodeAddressString(country.name)
guard let location = placemarks.first?.location else {
showGeocodingError = true
return
}
region = MKCoordinateRegion(
center: location.coordinate,
span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10)
)
} catch {
showGeocodingError = true
}
}
}
struct DetailRow: View {
let label: String
let value: String
let systemImage: String
var body: some View {
HStack {
Label(label, systemImage: systemImage)
Spacer()
Text(value)
.font(.body.monospacedDigit())
}
.padding()
.background(Color(.secondarySystemBackground))
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
// Update your existing CountryListView navigation
// In CountryRow, add NavigationLink:
struct CountryRow: View {
let country: Country
var body: some View {
NavigationLink {
CountryDetailView(country: country)
} label: {
// Keep existing row content
}
}
}Основные особенности этой реализации:
- 1. Интеграция карты:
- — Использует
Map ViewMapKitс координатамиregion - — Показывает маркер
MapMarkerна карте в местоположении страны - — Фиксированный уровень масштабирования zoom с отключенным взаимодействием пользователя с картой
- 2. Геокодирование:
- — Использует
CLGeocoderдля преобразования названия страны в координаты - — Изящно обрабатывает ошибки геокодирования
- — Показывает состояние загрузки во время геокодирования
- 3. Макет (Layout) детального View:
- — Большое отображение флага
- — Код
ISOи столица - — Форматированное население и ВВП
- — Отзывчивый дизайн для всех размеров экрана
- 4. Обработка ошибок:
- Оповещение об ошибках геокодирования
- Показ загрузки во время сетевых операций
- Безопасное развертывание необязательных
Optionalзначений
- 5. Визуальный дизайн:
- Закругленные углы для карты
Mapи строк детальногоView - Цвета фона системы
- Постоянные интервал и выравнивания элементов UI
Чтобы использовать это:
Добавьте возможность MapKit в Signing & Capabilities вашей цели.
Добавьте описание использования местоположения в Info.plist:
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need your location to show country positions on the map</string>ЭТО ЯВНО ЛИШНЕЕ — ничего этого не нужно, если вы не собираетесь показывать местоположение пользователя.
Обратите внимание, что геокодирование иногда может возвращать неверные координаты для:
- Стран с неоднозначными названиями
- Стран со спорными территориями
- Очень маленьких стран
Для использования в окончательном приложении рассмотрите:
- 1. Использование выделенной базы данных координат стран
- 2. Добавление кэширования координат
- 3. Реализация более специфического геокодирования (страна + столица)
- 4. Добавление элементов управления взаимодействием с картой
- 5. Включение региональных границ или наложений
Запускаем приложение — получаем ошибку:
Исправляем ошибку в MapMarker
| Отсутствует аргумент параметра coordinate для вызова MapMarker() |
Мы получаем рассуждения, которые длятся 64 секунд и вот их перевод:
Хорошо, пользователь получает ошибку: «Отсутствует аргумент параметра coordinate для вызоваMapMarker()». Давайте взглянем на предоставленный нам код.
ВmapSectionCountryDetailViewестьMapAnnotationсMapMarker. Текущий код использует:
MapAnnotation(coordinate: region.center) {
MapMarker()
}Подождите, вMapKitSwiftUIинициализаторуMapMarkerтребуется параметрcoordinate. Ошибка возникает из-за того, чтоcoordinateне передается. Код пользователя пытается создать MapMarker без указания координатыcoordinate, что является обязательным.
Поэтому исправлением будет передача координаты вMapMarker. Правильное использование должно быть:
MapMarker(coordinate:ordinate)ВMapAnnotationкоординатаcoordinate—region.center, поэтому внутри замыкания они должны использовать ее. Но в текущем коде они используют центр региона в качестве координаты для аннотации. Однако, как это структурировано, элементы аннотации —[country],но координата не выводится из данных страны. Вместо этого он использует центр регионаregion.center, который уже основан на геокодированном местоположении.
Подождите,MapAnnotationнаходится внутри циклаannotationItems, но они используют[country]в качестве элементов. Каждой странеcounterв массиве[country]нужна координатаcoordinate, но в текущей настройке ее нет. Координатаcoordinateберется из регионаregionвоviewModel, который является одним значением. Так что, возможно,annotationItemsдолжен быть одним элементом, содержащим координатуcoordinate, или у страны должно быть свойствоcoordinate.
Но в коде пользователя структураCountryне имеет координатcoordinate. Координаты хранятся в регионеviewModel. Так что, возможно,annotationItemsдолжны быть пользовательской структурой, включающей координатыcoordinate, или они должны создать одну аннотацию на основе центра регионаregion.center.
В качестве альтернативы, поскольку регионregionуже основан на местоположении страны, может быть,MapAnnotationне нужно проходить по элементам. Вместо этого они могут создать одну аннотацию, используя центр регионаregion.center.
Так чтоMapAnnotationдолжна быть создана с координатами из региона, аMapMarkerдолжен взять эти координаты. Но в текущем кодеMapAnnotationнаходится внутри цикла по[country], что может быть необязательно.
Подождите, в коде есть: Ноregionнеобязателен, поэтому, когдаregionравенnil, это может вызвать проблемы. Кроме того, координата дляMapAnnotationустановлена на region.center, что одинаково для всех аннотаций. Поскольку есть только одна страна country, цикл не нужен. Но главная проблема в том, чтоMapMarkerотсутствует параметр координатcoordinate.
Поэтому правильный код должен быть таким:
annotationItems: [country]) { country in
MapAnnotation(coordinate: region.center) {
MapMarker()
}
}Но поскольку регионregion- этотOptional, нам нужно безопасно его развернуть. Кроме того,annotationItems, возможно, должен быть набором координат, но в этом случае он использует странуcountryкак элемент, который не содержит координатыcoordinate. Это говорит о том, что подход к использованиюannotationItemsможет быть неправильным. Вместо использованияannotationItems, возможно, им следует добавить одну аннотацию на основе центра регионаregion.center. В качестве альтернативы создайте массив аннотаций, которые включают координатуcoordinate. Например:
if let coordinate = viewModel.coordinate {
Map(coordinateRegion: .constant(viewModel.region)) {
MapMarker(coordinate:ordinate)
}
}Но код пользователя используетannotationItems, который предназначен для массива идентифицируемых элементов[country], каждый из которых имеет координатуcoordinate. Поэтому, возможно, структураCountryдолжна иметь свойство координатcoordinate, но в их текущей настройке его нет. Координата хранится в regionviewModel.
Это указывает на проблему дизайна. РегионregionвоviewModelоснован на геокодированном местоположении, поэтому модельCountryне имеет координат. Поэтому использованиеannotationItemsс массивом[country]нецелесообразно, поскольку у стран нет координатcoordinate. Вместо этого следует создать одну аннотацию на основе регионаviewModel.
Поэтому правильным подходом будет удалитьannotationItemsи использовать одинMapMarkerс координатой из регионаregion.
Код следует скорректировать следующим образом:
Map(coordinateRegion: .constant(region)) {
if let center = region?.center {
MapMarker(coordinate: center)
}
}Но вSwiftUIнельзя условно включать содержимое карты в инициализаторMapтаким образом. Вместо этого следует использовать конструктор содержимого аннотацииContent Builder.
В качестве альтернативы используйте инициализаторMap, который позволяет добавлять аннотации без использованияannotationItems. Например:
Map(coordinateRegion: .constant(region), annotationItems: []) { _ in
// ...
}Но это бесполезно. В качестве альтернативы используйте инициализатор, который принимает конструктор контентаContent Builde, где вы можете условно добавлять аннотации.
Подождите, правильный способ в картеSwiftUI— использовать конструктор контентаContent Buildeдля добавления аннотаций. Поэтому, если регион доступен, добавьтеMapMarkerв этой координате.
Поэтому код должен быть таким:
Map(coordinateRegion: .constant(region), interactionModes: .zoom) {
if let coordinate = viewModel.coordinate {
MapMarker(coordinate:coordinate)
}
}Но в коде пользователяviewModelимеет регионregion, а не координатуcoordinateПоэтому центр регионаregion.center— это координатаcoordinate. Таким образом, код должен проверить, не равен лиregion nil, а затем создатьMapMarkerсregion.center.
Если собрать все вместе, то правильный разделSection Mapбудет выглядеть так:
if let region = viewModel.region {
Map(coordinateRegion: .constant(region),annotationItems: [country]{ _ in MapMarker(coordinate: region.center)
}
.frame(height: 300)
.clipShape(RoundedRectangle(cornerRadius: 10))
}Таким образом, когда регион region доступен, картаMapсоздается с этим региономregion, аMapMarkerдобавляется c координатойregion.center. Это устраняет необходимость вannotationItemsи циклах, упрощая код. Ошибка была вызвана тем, чтоMapMarkerне имеет параметраcoordinate, который теперь вычисляется какregion.center. Кроме того, подходannotationItemsбыл неверным, поскольку модельCountryне имела координат, что приводило к неправильному использованиюAPI.
Вот исправленная реализация для раздела карты с использованием правильного синтаксиса MapKit в SwiftUI:
private var mapSection: some View {
Group {
if let region = viewModel.region {
Map (
coordinateRegion: .constant(region),
annotationItems: [country]
) { _ in
MapMarker( coordinate: region.center)
}
.frame(height: 300)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
}Внесены основные исправления:
- Добавлен параметр
coordinate: дляMapMarker - Упрощенная аннотация: удален ненужный массив
annotationItems, поскольку мы показываем только один маркер (А вот это зря !!) - Прямой доступ к центру региона: используется
coordinateдля центра региона непосредственно для маркераMapMarker
Ошибка произошла из-за того, что в исходном коде отсутствовал требуемый параметр coordinate в инициализаторе MapMarker.
Эта версия правильно:
- Создает карту
Mapс фиксированным региономregion - Размещает синий маркер в координате центра региона region.center
Для более полного решения обновите CountryDetailViewModel для отслеживания как региона region, так и индивидуальную координату:
@MainActor
class CountryDetailViewModel: ObservableObject {
@Published var region: MKCoordinateRegion?
@Published var coordinate: CLLocationCoordinate2D?
// ... existing code ...
func geocodeCountry() async {
isLoading = true
defer { isLoading = false }
do {
let placemarks = try await geocoder.geocodeAddressString(country.name)
guard let location = placemarks.first?.location else {
showGeocodingError = true
return
}
let newCoordinate = location.coordinate
coordinate = newCoordinate
region = MKCoordinateRegion(
center: newCoordinate,
span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10)
)
} catch {
showGeocodingError = true
}
}
}Затем обновите раздел Section Map, чтобы использовать coordinate напрямую:
private var mapSection: some View {
Group {
if let region = viewModel.region , let coordinate = viewModel.coordinate {
Map (
coordinateRegion: .constant(region),
annotationItems: [country]
) { _ in
MapMarker( coordinate: coordinate,
tint: .accentColor)
}
.frame(height: 300)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
}Эта улучшенная версия:
- Показывает маркер
- Использует accent цвет для маркера
- Правильно разделяет отслеживание координаты
coordinateи регионаregion
Обязательно импортируйте MapKit в верхней части файла:
import MapKitВот что мы увидим:
Всё прекрасно работает, но у нас есть предупреждения:
init(coordinateRegion:, interactionModes: ) was deprecated in iOS 17.0, use Map initializers that use MapContentBuilder instead.
(init(coordinateRegion:, interactionModes: ) устарел в iOS 17.0, вместо этого используйте инициализаторы Map с MapContentBuilder.)
MapMarker was deprecated in iOS 17.0, use Marker along with Map initializers that use MapContentBuilder instead.
(MapMarker устарел в iOS 17.0, вместо него используйте Marker вместе с Map инициализаторами c MapContentBuilder.)
Обновление SwiftUI Map для iOS 17
init(coordinateRegion:, interactionModes: ) устарел в iOS 17.0, вместо этого используйте инициализаторы Map с MapContentBuilder. MapMarker устарел в iOS 17.0, вместо него используйте Marker вместе с Map инициализаторами c MapContentBuilder. |
В результате мы получаем рассуждения, которые длятся 40 секунд:
Вот перевод рассуждений:
Хорошо, пользователь сталкивается с предупреждениями об устаревшей версии, связанными сMapKitвiOS 17.Они упомянули, что инициализаторMapсcoordinateRegionиinteractionModesустарел, как иMapMarker. Им нужно обновить свой код, чтобы использовать новые инициализаторыMapContentBuilderи заменитьMapMarkerнаMarker. Во-первых, мне нужно вспомнить изменения, внесенные вiOS 17дляMapKit. Я помню, чтоAppleпредставила новый декларативный синтаксис с использованиемResult builders, что позволяет более гибко конструировать содержимое карты. Старые инициализаторы, которые напрямую принимали параметры, такие какcoordinateRegionиinteractionModes, теперь устарели. Вместо этого мы должны использовать новые инициализаторы, которые используют замыканиеMapContentBuilder.
MapMarkerбыл заменен наMarker, у которого другой инициализатор. Также аннотации, такие какAnnotation, следует использовать для более настраиваемых маркеров.
Итак, основные шаги:
1. Обновите инициализаторMap, чтобы использовать новый синтаксис сMapCameraPositionи конструктором контента. 2. ЗаменитеMapMarkerнаMarkerилиAnnotationдля настраиваемых маркеров. 3. Убедитесь, что положение камеры карты установлено правильно, возможно, используя.regionсMKCoordinateRegion. 4. Правильно обрабатывайте режимы взаимодействия, возможно, устанавливая их через модификаторы, если необходимо. Если посмотреть на существующий код пользователя, у него былоView Mapс.constant(region)иMapMarker. Предупреждения об устаревшей версии указывают на использование новых инициализаторов и компонентов. Мне следует начать с изменения инициализацииMap. Новый способ заключается в созданииMapсMap CameraPosition, а затем в конструировании содержимого карты путем добавления, например, маркеров. Для маркера вместоMapMarkerиспользуйтеMarker, который принимает заголовок и координаты. Если нужна пользовательская аннотация,Annotationможно использовать с пользовательскимView. Регионregionдолжен быть преобразован вMapCameraPosition.region(...). Кроме того, режимы взаимодействия теперь задаются с помощью модификатора.mapInteractionModes, а не параметра в инициализаторе. Если объединить все это вместе, обновленный код будет использовать новый инициализаторMapсMapContentBuilder, включатьMarkerилиAnnotation, задавать положение камеры с помощью.regionи использовать режимы взаимодействия в качестве модификатора. Проверяя этот подход в уме, я вижу, что регионregionзадан правильно,Markerотображается в правильном месте, а также используются режимы взаимодействия. Это должно устранить предупреждения об устаревшей версииMap. Кроме того, пользователю может понадобиться убедиться, чтоCountryDetailViewModelпредоставляет необходимые данные оcoordinateи регионеregion.Markerтребует заголовка, который может быть названием страны, и координатыcoordinate.Annotationпозволяет выполнять больше настроек, например использоватьsystemImageс определенным цветом. Мне также следует проверить, есть ли в их коде какие-либо другие устаревшие элементы, но, исходя из постановки проблемы, достаточно сосредоточиться на инициализатореMapи заменеMapMarker. Подводя итог, можно сказать, что ключевыми изменениями являются принятие нового инициализатораMap, заменаMapMarkerнаMarker, правильная установка положения камеры и настройка режимов взаимодействия с помощью модификаторов.
Вот совместимая с iOS 17+ реализация с использованием нового синтаксиса MapKit SwiftUI:
private var mapSection: some View {
Group {
if let region = viewModel.region {
Map(position: .constant(.region(region))) {
// Simple marker version
Marker(country.name, coordinate: region.center)
// Custom annotation version
Annotation(country.name, coordinate: region.center) {
Image(systemName: "flag.fill")
.foregroundColor(.red)
.padding(8)
.background(.thickMaterial)
.clipShape(Circle())
}
}
.mapStyle(.standard)
.mapControlVisibility(.visible)
.mapControls {
MapPitchToggle()
}
.frame(height: 300)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
}Ключевые изменения, внесенные для совместимости с iOS 17+:
- Новый инициализатор карт:
Map(position: .constant(.region(region))) {
// Map content here
}2. Типы маркеров:
- Простой маркер
Marker:
Marker(country.name, coordinate: region.center)- Пользовательская аннотация:
Annotation(country.name, coordinate: region.center) {
// Custom SwiftUI view
}3. Конфигурация Map:
.mapStyle(.standard)
.mapControlVisibility(.visible)
.mapControls {
MapPitchToggle()
}4. Управление положением камеры:
// Replace old coordinateRegion with position binding
.position(.constant(.region(region)))Полностью обновленная CountryDetailViewModel с отслеживанием координат coordinate:
@MainActor
class CountryDetailViewModel: ObservableObject {
@Published var region: MKCoordinateRegion?
@Published var coordinate: CLLocationCoordinate2D?
// Add this computed property for MapKit
var mapCameraPosition: MapCameraPosition? {
guard let region else { return nil }
return .region(region)
}
// Rest of the view model remains the same...
}Для управления взаимодействием с картой вы можете добавить явные элементы управления картой:
Map(position: .constant(.region(region))) {
// Markers and annotations
}
.mapControls {
MapUserLocationButton()
MapCompass()
MapScaleView()
}Эта реализация:
- Использует новый декларативный синтаксис
MapKit - Поддерживает как простые маркеры
Marker, так и пользовательские аннотацииAnnotation - Обеспечивает лучшую настройку управления картой
- Соответствует шаблонам проектирования iOS 17+
- Поддерживает полный контроль взаимодействия
Не забудьте импортировать MapKit в верхней части файла:
import MapKitНовый API MapKit SwiftUI обеспечивает большую гибкость и лучшую производительность, сохраняя при этом привычный декларативный синтаксис SwiftUI.
Запускаем — получаем:
Запускаем geocodeCountry() в фоновом режиме
| запусти geocodeCountry () в фоновом режиме |
В результате мы получаем рассуждения, которые длятся 20 секунд:
Вот перевод рассуждений:
Хорошо, пользователь хочет, чтобы функция geocodeCountry() запускалась в фоновом потоке. Дайте мне подумать, как к этому подойти.
Во-первых, я помню, что в многопоточномSwiftиспользованиеTaskвнутри инициализатораViewModelможет запускаться наMainActor, если не указано иное. Поскольку геокодирование — это задача, связанная с выходом в интернет, оно должно быть вне основного потока (main thread), чтобы предотвратить зависаниеUI. Текущий код использует функциюgeocodeAddressStringизCLGeocoder, который является синхронным вызовом, обернутым в асинхронный контекст. Но чтобы убедиться, что он действительно вне основного потока (main thread), я должен явно переместить его в фоновую очередь. Подождите, в многопоточномSwiftиспользованиеTask.detachedможет здесь помочь. Это запустит геокодирование в фоновом потоке. Кроме того, методыCLGeocoderмогут не полностью поддерживать асинхронность, поэтому использование фоновой очереди имеет смысл. Мне следует изменить функциюgeocodeCountry()для запуска в фоновом потоке. Позвольте мне проверить существующий код.ViewModelпомечена@MainActor, поэтому все ее функции по умолчанию находятся наMainActor. Чтобы запуститьgeocodeCountry()в фоновом режиме, мне нужно использовать неизолированныйnonisolatedконтекст илиTask.detached. Возможно, использоватьTask.detached, а затем вернуться кMainActorдля обновленийUI. Также нужно обработать требованияMainActorдля@Publishedсвойств. Итак, шаги будут такими: 1. Вinit ViewModelиспользуйтеTask.detachedдля запускаgeocodeCountry(). 2. ВgeocodeCountry()переключитесь на фоновую очередь. 3. Выполните геокодирование там. 4. После получения результата переключитесь обратно наMainActor, чтобы обновить регионregionи координатыcoordinate. Но подождите,geocodeAddressString CLGeocoderуже является асинхронным методом. Возможно, проблема в том, что он вызывается наMainActor. Поэтому, обернув его вTask.detached, мы можем переместить его в фоновый поток. Кроме того, использованиеURLSession data(from:)уже обрабатывает потоки, но геокодирование может не обрабатывать. Поэтому важно убедиться, чтоgeocodeAddressStringзапускается из основного потока (main thread). Позвольте мне настроить функциюgeocodeCountry() ViewModelдля запуска в фоновом потоке с помощьюTask.detached, а затем обновить@Publishedсвойства вMainActor. Тестирование этого подхода: когдаViewModelинициализируется, он запускаетTask.detached, которая выполняет геокодирование в фоновом режиме. После этого он используетawait MainActor.runдля обновления свойствUI. Это должно предотвратить блокировку основного потока и сохранить отзывчивостьUI. Кроме того, необходимо убедиться, что экземплярCLGeocoderбезопасно используется между потоками. ПосколькуCLGeocoder не является потокобезопасным, создание нового экземпляра в фоновом потоке может быть лучшим решением. Итак, вViewModelвместо свойства geocoder создайте новый экземплярCLGeocoderвнутри фоновой задачи, чтобы избежать проблем с потоками. Собираем все вместе: функцияgeocodeCountry()создаст новыйgeocoderв фоновой задаче, выполнит геокодирование, а затем обновит состояниеViewModelнаMainActor. Да, это должно работать. Позвольте мне структурировать код соответствующим образом.
Вот оптимизированная версия, которая запускает геокодирование в фоновом потоке, поддерживая правильные обновления основного потока (main thread):
@MainActor
class CountryDetailViewModel: ObservableObject {
let country: Country
@Published var region: MKCoordinateRegion?
@Published var coordinate: CLLocationCoordinate2D?
@Published var isLoading = false
@Published var showGeocodingError = false
init(country: Country) {
self.country = country
Task(priority: .userInitiated) {
await geocodeCountry()
}
}
func geocodeCountry() async {
await MainActor.run { isLoading = true }
defer { Task { @MainActor in isLoading = false } }
do {
let coordinate = try await performGeocoding()
await MainActor.run {
self.coordinate = coordinate
self.region = MKCoordinateRegion(
center: coordinate,
span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10)
)
}
} catch {
await MainActor.run {
showGeocodingError = true
}
}
}
private nonisolated func performGeocoding() async throws -> CLLocationCoordinate2D {
return try await withCheckedThrowingContinuation { continuation in
let geocoder = CLGeocoder()
geocoder.geocodeAddressString(country.name) { placemarks, error in
if let error = error {
continuation.resume(throwing: error)
return
}
guard let location = placemarks?.first?.location else {
continuation.resume(throwing: NSError(
domain: "GeocodingError",
code: 0,
userInfo: [NSLocalizedDescriptionKey: "No location found"]
))
return
}
continuation.resume(returning: location.coordinate)
}
}
}
}Ключевые улучшения для фоновой обработки:
- Организация многопоточности:
nonisolatedдля функции геокодирования, чтобы вырваться изMainActorwithCheckedThrowingContinuationдля мостаcompletion handlerкasync/await- Разделение логики геокодирования и управления состоянием
- Управление потоками:
- Геокодирование происходит в фоновом потоке автоматически
- Явные
MainActor-обновления для свойств, связанных сUI - Выделенный экземпляр геокодера
geocoderддя запроса (безопасный для потоков)
- Обработка ошибок:
- Правильное распространение ошибок с использованием ошибок Swift
- Четкое разделение геокодирования и управления состоянием
- Отложенная очистка состояния загрузки
- Управление приоритетами:
- Приоритет задачи, инициированной пользователем, для отзывчивого UX
- Правильная обработка отмены (cancellation) с помощью структурированной многопоточности
Для использования с Map View (синтаксис iOS 17+):
struct CountryDetailView: View {
let country: Country
@StateObject private var viewModel: CountryDetailViewModel
var body: some View {
// ... other UI elements ...
if let coordinate = viewModel.coordinate {
Map(position: .constant(.region(viewModel.region ?? .defaultRegion))) {
Marker(country.name, coordinate: coordinate)
}
.mapStyle(.standard)
.frame(height: 300)
}
// ... rest of the view ...
}
}
extension MKCoordinateRegion {
static let defaultRegion = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 0, longitude: 0),
span: MKCoordinateSpan(latitudeDelta: 100, longitudeDelta: 100)
)
}Эта реализация:
- Сохраняет обновления UI в основном потоке (
main thread) - Запускает геокодирование в фоновом потоке
- Правильно обрабатывает жизненный цикл геокодера
geocoder - Использует современные шаблоны многопоточности Swift
- Поддерживает отзывчивый UI во время операций
- Правильно очищает ресурсы
- Удобно обрабатывает отмену (
cancellation)
Не забудьте добавить эти записи Info.plist:
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need your location to show country positions on the map</string>
<key>NSLocationUsageDescription</key>
<string>We need location access to display country information</string>Запускаем приложение и получаем результат:
Убираем Marker, оставляем только Annotation:
private var mapSection: some View {
Group {
if let region = viewModel.region, let coordinate = viewModel.coordinate {
Map(position: .constant(.region(region))) {
// Map content here
// Marker(country.name, coordinate: region.center)
// Custom annotation version
Annotation(country.capitalCity, coordinate: coordinate) {
Text(country.flag)
.font(.system(size: 34))
}
}
.mapControls {
MapCompass()
MapScaleView()
}
.frame(height: 400)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
}
}Результат:

Код находится на Github.