Інтеграція Apple MapKit в iOS-програму
MapKit — вбудований фреймворк Apple, ніяких ключів та біллінга. Це головна перевага над Google Maps SDK при розробці під iOS. Але за останні три мажорні версії iOS MapKit істотно змінився: SwiftUI-нативна Map view з'явилась у iOS 14 та отримала повноцінний API анотацій лишень у iOS 17. Якщо підтримуєте iOS 15+, потрібно балансувати між новим та старим API.
MKMapView vs SwiftUI Map — що вибрати
MKMapView — зрілий UIKit-компонент з повним контролем через делегати. Map з SwiftUI простіше в базових сценаріях, але до iOS 17 не підтримував кастомні анотації в декларативному стилі — доводилось обертати MKMapView через UIViewRepresentable.
Для iOS 17+ SwiftUI Map з Annotation та MapPolygon покриває більшість кейсів:
import MapKit
struct ContentView: View {
@State private var position: MapCameraPosition = .region(
MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 55.7558, longitude: 37.6173),
latitudinalMeters: 5000,
longitudinalMeters: 5000
)
)
var body: some View {
Map(position: $position) {
Annotation("Офіс", coordinate: CLLocationCoordinate2D(latitude: 55.7558, longitude: 37.6173)) {
Image(systemName: "building.2.fill")
.foregroundStyle(.blue)
.padding(8)
.background(.white)
.clipShape(Circle())
}
UserAnnotation()
}
.mapStyle(.standard(elevation: .realistic))
.mapControls {
MapUserLocationButton()
MapCompass()
MapScaleView()
}
}
}
Для iOS 15-16 кастомні анотації — тільки через MKMapView + UIViewRepresentable.
MKMapView: анотації та делегат
class MapViewController: UIViewController, MKMapViewDelegate {
private let mapView = MKMapView()
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self
mapView.frame = view.bounds
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(mapView)
let annotation = MKPointAnnotation()
annotation.coordinate = CLLocationCoordinate2D(latitude: 55.7558, longitude: 37.6173)
annotation.title = "Точка А"
mapView.addAnnotation(annotation)
}
// Кастомний вид анотації
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
guard !(annotation is MKUserLocation) else { return nil }
let identifier = "CustomPin"
var view = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
as? MKMarkerAnnotationView
if view == nil {
view = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: identifier)
view?.canShowCallout = true
view?.glyphImage = UIImage(systemName: "car.fill")
view?.markerTintColor = .systemBlue
} else {
view?.annotation = annotation
}
return view
}
}
MKMarkerAnnotationView — стандартний вид з підтримкою callout, glyphs з SF Symbols та кластеризацією через clusteringIdentifier. Для повністю кастомного виду використовуйте MKAnnotationView з власним UIView всередині.
Маршрути: MKDirections
MapKit будує маршрути через MKDirections.Request без додаткової оплати. Режими: .automobile, .walking, .transit (тільки у підтримуваних регіонах).
func buildRoute(from: CLLocationCoordinate2D, to: CLLocationCoordinate2D) {
let request = MKDirections.Request()
request.source = MKMapItem(placemark: MKPlacemark(coordinate: from))
request.destination = MKMapItem(placemark: MKPlacemark(coordinate: to))
request.transportType = .automobile
MKDirections(request: request).calculate { [weak self] response, error in
guard let route = response?.routes.first else { return }
self?.mapView.addOverlay(route.polyline, level: .aboveRoads)
self?.mapView.setVisibleMapRect(
route.polyline.boundingMapRect,
edgePadding: UIEdgeInsets(top: 50, left: 50, bottom: 50, right: 50),
animated: true
)
}
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let polyline = overlay as? MKPolyline {
let renderer = MKPolylineRenderer(polyline: polyline)
renderer.strokeColor = .systemBlue
renderer.lineWidth = 4
return renderer
}
return MKOverlayRenderer(overlay: overlay)
}
Геокодування через CLGeocoder
Без звернення до Google або Yandex — CLGeocoder та MKLocalSearch працюють на серверах Apple:
MKLocalSearch(request: {
let req = MKLocalSearch.Request()
req.naturalLanguageQuery = "Червона площа, Москва"
req.region = mapView.region
return req
}()).start { response, _ in
guard let item = response?.mapItems.first else { return }
print(item.placemark.coordinate)
}
Важливі обмеження
MapKit не підтримує кастомні тайлові шари з сторонніх джерел так гнучко, як Google Maps (немає прямого аналога TileOverlayProvider для довільних XYZ-тайлів без серверної проксування). Якщо потрібні офлайн-карти або нестандартні тайли — дивіться в сторону MapLibre Native або Mapbox.
Часові рамки
1–3 дні. Базова карта з анотаціями — 1 день. Маршрути, кластеризація, пошук — 2–3 дні. Вартість розраховується індивідуально після аналізу вимог.







