Использование DeepSeek при создании iOS приложения Countries Страны Мира. Часть 3. Swift 6 concurrency и Map.

Это продолжение. Начало находится на «Использование 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 многопоточности:

  1. Строгая изоляция акторов Actor
  • Слой сервиса WorldBankService, обернутый в ТИП актора actor
  • Изоляция @MainActor для View и ViewModel
  • ключевое слово nonisolated для безопасности синхронных методов
  1. Соответствие Sendable
  • Все ТИПы моделей помечены как Sendable
  • ТИПы ошибок сделаны Sendable
  • Правильная Value семантика для передачи данных
  1. Современная структурная многопоточность
  • async let для многопоточных запросов
  • Поддержка обновляемых модификаторов
  • Отмена задачи Task через .task
  1. Управление безопасностью ТИПов
  • Выделенный уровень сервиса для работы в сети
  • Четкое разделение этапов обработки данных
  • Правильное распространение ошибок через async/await
  1. Оптимизированный поток данных
  • @Published свойства только для @MainActor
  • Производные вычисляемые свойства для использования в Views
  • Эффективная группировка и фильтрация данных
  1. Улучшенная обработка ошибок
  • Локализованные описания ошибок
  • Унифицированный поток обработки ошибок
  • Удобное для пользователя сообщение о коде ошибки 
  1. Оптимизация производительности
  • Многопоточная выборка данных
  • Неблокирующие обновления UI
  • Эффективное управление памятью

Чтобы использовать эту реализацию:

  1. Обеспечьте Xcode 16+ для поддержки  Swift 6
  2. Добавьте NSAppTransportSecurity при необходимости
  3. Рассмотрите возможность добавления локального кэширования для лучшей поддержки в автономном режиме
  4. Реализуйте правильные состояния загрузки и восстановление после ошибок
  5. Добавьте accessibility метки для элементов UI

Эта версия использует возможности Swift 6 многопоточности для создания:

  • Полностью потокобезопасного потока данных
  • Оптимального использования ресурсов
  • Плавного UI взаимодействия 
  • Масштабируемой архитектуры
  • Перспективной основы для усовершенствований

Запускаем приложение и имеем тот же UI, но поддерживающий многопоточность Swift 6 :

Страна на карте Map

Покажи страну country на карте в CountryDetailView

Мы получаем рассуждения, которые длятся 50 секунд и вот их перевод:

Хорошо, мне нужно добавить карту для отображения страны в CountryDetail. Начну с напоминания существующей структуры кода. Основное View — CountryListView, в котором перечислены страны, сгруппированные по регионам. Каждая строка страны переходит в CountryDetailView.
Сначала мне нужно создать CountryDetailView. Это View должно отображать название страны, флаг, столицу, население, ВВП и карту, показывающую местоположение страны.

Чтобы отобразить карту, я буду использовать MapKit. В SwiftUI Map 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. 1. Интеграция карты:
  • Использует Map View MapKit с координатами region
  • Показывает маркер MapMarker на карте в местоположении страны
  • Фиксированный уровень масштабирования zoom с отключенным взаимодействием пользователя с картой
  1. 2. Геокодирование:
  • Использует CLGeocoder для преобразования названия страны в координаты
  • Изящно обрабатывает ошибки геокодирования
  • Показывает состояние загрузки во время геокодирования
  1. 3. Макет (Layout) детального View:
  • Большое отображение флага
  • Код ISO и столица
  • Форматированное население и ВВП
  • Отзывчивый дизайн для всех размеров экрана
  1. 4. Обработка ошибок:
  • Оповещение об ошибках геокодирования
  • Показ загрузки во время сетевых операций
  • Безопасное развертывание необязательных Optional значений
  1. 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()». Давайте взглянем на предоставленный нам код.

В mapSection CountryDetailView есть MapAnnotation с MapMarker. Текущий код использует:
MapAnnotation(coordinate: region.center) {
    MapMarker()
  }
Подождите, в MapKit SwiftUI инициализатору MapMarker требуется параметр coordinate. Ошибка возникает из-за того, что coordinate не передается. Код пользователя пытается создать MapMarker без указания координаты coordinate, что является обязательным.

Поэтому исправлением будет передача координаты в MapMarker. Правильное использование должно быть:
MapMarker(coordinate:ordinate)
В MapAnnotation координата coordinateregion.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, но в их текущей настройке его нет. Координата хранится в region viewModel.

Это указывает на проблему дизайна. Регион 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))
            }
        }
    }

Внесены основные исправления:

  1. Добавлен параметр coordinate: для MapMarker
  2. Упрощенная аннотация: удален ненужный массив annotationItems, поскольку мы показываем только один маркер (А вот это зря !!)
  3. Прямой доступ к центру региона: используется 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+:

  1. Новый инициализатор карт:

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)
            }
        }
    }
}

Ключевые улучшения для фоновой обработки:

  1. Организация многопоточности:
  • nonisolated для функции геокодирования, чтобы вырваться из MainActor
  • withCheckedThrowingContinuation для моста completion handler к async/await
  • Разделение логики геокодирования и управления состоянием
  1. Управление потоками:
  • Геокодирование происходит в фоновом потоке автоматически
  • Явные MainActor-обновления  для свойств, связанных с UI
  • Выделенный экземпляр геокодера geocoder ддя запроса (безопасный для потоков)
  1. Обработка ошибок:
  • Правильное распространение ошибок с использованием ошибок Swift
  • Четкое разделение геокодирования и управления состоянием
  • Отложенная очистка состояния загрузки
  1. Управление приоритетами:
  • Приоритет задачи, инициированной пользователем, для отзывчивого 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.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *