Разработка
Рисуем карты с помощью 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
.
xxxxxxxxxx
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 рекомендуют подготавливать все данные перед построением графиков, чтобы избежать проблем с производительностью.
xxxxxxxxxx
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
. С помощью небольшого расширения мы можем извлечь все координаты и определить центр области:
xxxxxxxxxx
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
, чтобы получить название и население региона. Теперь мы можем построить график с картой.
Построение графика
Давайте подготовим представление для контента:
xxxxxxxxxx
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
, чтобы разложить логику отрисовки на составляющие. Начнем с цветных областей:
xxxxxxxxxx
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
для населения. Добавим эти представления на график:
xxxxxxxxxx
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
:
xxxxxxxxxx
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
предотвращает соединение линий для разных областей. Добавим это представление на график:
xxxxxxxxxx
Chart {
ForEach(features) { featureData in
FeatureAreaPlot(featureData: featureData)
}
ForEach(features) { featureData in
FeatureLinePlot(featureData: featureData)
}
}
Мы разделили вывод на две части, чтобы нарисовать линии над областями. Начиная с iOS 17 вы можете использовать модификатор zIndex
для каждой метки, чтобы управлять порядком отрисовки. Однако в данном случае он не работает. Если вы знаете, как это исправить, не стесняйтесь поделиться своим решением в X.
Теперь у нас есть карта с цветными областями и белыми линиями:
Последняя часть — это отрисовка точек. Для этого мы можем использовать PointMark
:
xxxxxxxxxx
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 недели назад
Конец программирования в том виде, в котором мы его знаем
-
Видео и подкасты для разработчиков7 дней назад
Как устроена мобильная архитектура. Интервью с тех. лидером юнита «Mobile Architecture» из AvitoTech
-
Магазины приложений3 недели назад
Магазин игр Aptoide запустился на iOS в Европе
-
Новости3 недели назад
Видео и подкасты о мобильной разработке 2025.8