Разработка
Рисуем карты с помощью Swift Charts
В этом посте мы узнали, как рисовать карты с помощью Swift Charts. Конечный результат не идеален, но я обнаружил много интересных и неочевидных моментов, которые могут быть полезны в будущем.
Артем Новичков, iOS-разработчик в Welltory, поделился интересной функцией Swift Charts — оказывается, с их помощью можно рисовать карты.
Swift Charts — это мощный фреймворк, позволяющий визуализировать данные различными способами. На сессии WWDC «Swift Charts: векторные и функциональные графики» я наткнулся на интересный пример: отрисовку карт.
Я вдохновился этим примером и решил нарисовать другую карту. Я живу в Казахстане, поэтому давайте визуализируем данные о населении казахстанских регионов. Вот справочная диаграмма с официального сайта статистики:
Источник: gov.kz
Получение данных
Изначально нам нужно получить информацию о координатах, названиях и населении региона. Лучше всего для этого подходит формат GeoJSON — открытый стандартный формат, предназначенный для представления простых географических объектов. Я использовал сервис Overpass для получения соответствующих координат и информации. Для получения данных можно использовать специальный язык запросов. Вот запрос для получения всех регионов Казахстана:
[out:json]; {{geocodeArea:Kazakhstan}}->.searchArea; relation["admin_level"="4"](area.searchArea); out geom;
В результате вы получите JSON-файл со всеми регионами Казахстана в специальном формате. Я немного изменил его, убрал города, чтобы упростить отрисовку, и обновил численность населения на актуальные значения.
Для обработки этого файла мы можем использовать MKGeoJSONDecoder из фреймворка MapKit. Он позволяет декодировать данные GeoJSON в коллекцию объектов MKGeoJSONFeature
.
import MapKit func loadFeatureData() throws -> [FeatureData] { let url = Bundle.main.url(forResource: "kazakhstan", withExtension: "geojson")! let kazakhstanData = try Data(contentsOf: url) let features = try MKGeoJSONDecoder() .decode(kazakhstanData) .compactMap { geoJSONObject in if let feature = geoJSONObject as? MKGeoJSONFeature { return FeatureData(feature: feature) } return nil } return features }
Кроме того, нам нужно создать структуру FeatureData
, чтобы получить информацию о каждом регионе. Инженеры Apple рекомендуют подготавливать все данные перед построением графиков, чтобы избежать проблем с производительностью.
import MapKit struct FeatureData: Identifiable { let id: String let coordinates: [CLLocationCoordinate2D] let center: CLLocationCoordinate2D let name: String? var population: Int = 0 init?(feature: MKGeoJSONFeature) { guard let properties = feature.properties, let polygon = feature.geometry.first as? MKPolygon else { return nil } id = feature.identifier ?? UUID().uuidString coordinates = polygon.coordinates center = polygon.center let propertiesData = try? JSONDecoder().decode([String: String].self, from: properties) name = propertiesData?["name:en"] if let rawPopulation = propertiesData?["population"], let population = Int(rawPopulation) { self.population = population } } }
В нашем случае все объекты содержат только один геометрический объект — MKPolygon
. С помощью небольшого расширения мы можем извлечь все координаты и определить центр области:
extension MKPolygon { var coordinates: [CLLocationCoordinate2D] { var coordinates = [CLLocationCoordinate2D](repeating: kCLLocationCoordinate2DInvalid, count: pointCount) getCoordinates(&coordinates, range: NSRange(location: 0, length: pointCount)) return coordinates } /// The center calculation algorithm is simple and may not work for regions with complex shapes. var center: CLLocationCoordinate2D { var minLat = CLLocationDegrees.greatestFiniteMagnitude var minLng = CLLocationDegrees.greatestFiniteMagnitude var maxLat = -CLLocationDegrees.greatestFiniteMagnitude var maxLng = -CLLocationDegrees.greatestFiniteMagnitude for coordinate in coordinates { minLat = min(minLat, coordinate.latitude) minLng = min(minLng, coordinate.longitude) maxLat = max(maxLat, coordinate.latitude) maxLng = max(maxLng, coordinate.longitude) } return CLLocationCoordinate2D(latitude: (minLat + maxLat) / 2, longitude: (minLng + maxLng) / 2) } }
Мы также используем feature.properties
, чтобы получить название и население региона. Теперь мы можем построить график с картой.
Построение графика
Давайте подготовим представление для контента:
import SwiftUI import Charts import MapKit struct ContentView: View { @State private var features: [FeatureData] = [] var body: some View { Chart { // Chart code goes here } .onAppear { do { features = try loadFeatureData() } catch { print("Error parsing GeoJSON: \(error)") } } } }
Чтобы имитировать оригинальную карту, мы будем рисовать различные компоненты:
- Цветные области с помощью
AreaPlot
; - Белые линии с помощью
LinePlot
; - Точки с помощью
PointMark
.
Для каждого компонента мы создаем отдельное представление ChartContent
, чтобы разложить логику отрисовки на составляющие. Начнем с цветных областей:
import Charts struct FeatureAreaPlot: ChartContent { let featureData: FeatureData var body: some ChartContent { AreaPlot(featureData.coordinates, x: .value("Longitude", \.longitude), y: .value("Latitude", \.latitude), stacking: .unstacked) .foregroundStyle(by: .value("Population", featureData.population)) } }
Мы задаем значения x и y как PlottableValue
с ключевыми путями из свойств CLLocationCoordinate2D
. Также мы установили параметр stacking
в значение unstacked
, чтобы рисовать каждый регион отдельно. Чтобы задать цвет, мы используем модификатор foregroundStyle(by:)
с PlottableValue
для населения. Добавим эти представления на график:
Chart { ForEach(features) { featureData in FeatureAreaPlot(featureData: featureData) } } .chartYScale(domain: 40...56) .chartXScale(domain: 40...95) .chartXAxis(.hidden) .chartYAxis(.hidden) .chartLegend(.hidden) .chartPlotStyle { $0.aspectRatio(2, contentMode: .fit) }
Кроме того, мы изменим масштаб, чтобы отцентрировать карту, скроем ненужные оси и легенду, а также установим фиксированное соотношение сторон. Вот результат:
Вы вероятно заметили странные артефакты на рисунке — две линии справа. Как я понял в процессе отладки, разные области каким-то образом влияют друг на друга. Я не нашел решения, как это исправить, но почти уверен, что это зависит от координат. Но в таком большом наборе данных трудно найти ошибку.
Следующая часть — нарисовать белые линии. Для этого мы можем использовать LinePlot
:
import Charts struct FeatureLinePlot: ChartContent { let featureData: FeatureData var body: some ChartContent { LinePlot(featureData.coordinates, x: .value("Longitude", \.longitude), y: .value("Latitude", \.latitude) ) .lineStyle(.init(lineWidth: 1, lineCap: .round, lineJoin: .round)) .foregroundStyle(by: .value("Feature", featureData.id)) } }
Код выглядит так же, как и предыдущий. Мы используем LinePlot
для отрисовки линии с заданным стилем. Стиль Foreground
предотвращает соединение линий для разных областей. Добавим это представление на график:
Chart { ForEach(features) { featureData in FeatureAreaPlot(featureData: featureData) } ForEach(features) { featureData in FeatureLinePlot(featureData: featureData) } }
Мы разделили вывод на две части, чтобы нарисовать линии над областями. Начиная с iOS 17 вы можете использовать модификатор zIndex
для каждой метки, чтобы управлять порядком отрисовки. Однако в данном случае он не работает. Если вы знаете, как это исправить, не стесняйтесь поделиться своим решением в X.
Теперь у нас есть карта с цветными областями и белыми линиями:
Последняя часть — это отрисовка точек. Для этого мы можем использовать PointMark
:
import SwiftUI import Charts struct FeaturePointMark: ChartContent { let featureData: FeatureData var body: some ChartContent { PointMark(x: .value("x", featureData.center.longitude), y: .value("y", featureData.center.latitude)) .symbol { Circle() .stroke(.white, lineWidth: 8) .fill(.blue) .frame(width: 20, height: 20) } .annotation(position: .top, alignment: .center, spacing: 4) { if let name = featureData.name { Text(name) .font(.caption2) } } .annotation(position: .bottom, alignment: .center, spacing: 4) { Text(featureData.population, format: .number) .font(.caption2) } } }
Здесь мы используем три модификатора:
symbol
для установки кастомного представления точки;annotation
для добавления верхнего текста с названием региона;annotation
для добавления нижнего текста с населением.
Добавление на график аналогично предыдущим шагам, поэтому давайте проверим результат:
Заключение
В этом посте мы узнали, как рисовать карты с помощью Swift Charts. Конечный результат не идеален, но я обнаружил много интересных и неочевидных моментов, которые могут быть полезны в будущем. У Swift Charts большой потенциал. Диаграммы могут быть интерактивными; мы можем усовершенствовать приведенный выше пример и добавить выбор региона. Более того, Swift Charts поддерживает доступность, и мы можем добавить VoiceOver, чтобы сделать карту более доступной.
Надеюсь, вам понравился этот пример, и вы попробуете нарисовать свои собственные карты. Как обычно, финальный проект вы можете найти на GitHub. Если у вас есть вопросы или предложения, не стесняйтесь делиться ими на X. Спасибо, что читаете!
-
Видео и подкасты для разработчиков4 недели назад
SwiftUI: алхимия приложений — превращаем идеи в реальность
-
Разработка4 недели назад
30 уроков от 30 лучших продуктовых лидеров
-
Новости4 недели назад
Видео и подкасты о мобильной разработке 2025.3
-
Магазины приложений1 неделя назад
Приложение Hot Tub появится на iOS в EC