MapKit
Créez des fonctionnalités de cartes et de localisation pour iOS 17+ avec SwiftUI, MapKit et les API asynchrones modernes de CoreLocation. Utilisez Map avec MapContentBuilder pour les vues, CLLocationUpdate.liveUpdates() pour le flux de localisation, et CLMonitor pour le géorepérage.
Consultez references/mapkit-patterns.md pour les patterns MapKit avancés et references/mapkit-corelocation-patterns.md pour les patterns CoreLocation.
Sommaire
- Flux de travail
- Vue Carte SwiftUI (iOS 17+)
- API CoreLocation Moderne
- Géocodage
- Recherche
- Itinéraires
- PlaceDescriptor (iOS 26+)
- Erreurs Courantes
- Checklist de Révision
- Références
Flux de travail
1. Ajouter une carte avec des marqueurs ou annotations
- Importez
MapKit. - Créez une vue
Mapavec une liaison optionnelleMapCameraPosition. - Ajoutez
Marker,Annotation,MapPolyline,MapPolygonouMapCircledans la closureMapContentBuilder. - Configurez le style de la carte avec
.mapStyle(). - Ajoutez des contrôles de carte avec
.mapControls { }. - Gérez la sélection avec une liaison
selection:.
2. Suivre la localisation de l'utilisateur
- Ajoutez
NSLocationWhenInUseUsageDescriptionà Info.plist. - Sur iOS 18+, créez une
CLServiceSessionpour gérer l'autorisation. - Itérez
CLLocationUpdate.liveUpdates()dans uneTask. - Filtrez les mises à jour par distance ou précision avant de mettre à jour l'interface.
- Arrêtez la tâche lorsque le suivi de localisation n'est plus nécessaire.
3. Rechercher des lieux
- Configurez
MKLocalSearchCompleterpour les suggestions d'autocomplétion. - Débouncez l'entrée utilisateur (au moins 300 ms) avant de définir la requête.
- Convertissez la complétion sélectionnée en
MKLocalSearch.Requestpour obtenir les résultats complets. - Affichez les résultats sous forme de marqueurs ou dans une liste.
4. Obtenir des directions et afficher un itinéraire
- Créez une
MKDirections.Requestavec une source et une destinationMKMapItem. - Définissez
transportType(.automobile,.walking,.transit,.cycling). - Attendez le résultat de
MKDirections.calculate(). - Dessinez l'itinéraire avec
MapPolyline(route.polyline).
5. Réviser du code existant de carte/localisation
Parcourez la Checklist de Révision à la fin de ce fichier.
Vue Carte SwiftUI (iOS 17+)
import MapKit
import SwiftUI
struct PlaceMap: View {
@State private var position: MapCameraPosition = .automatic
var body: some View {
Map(position: $position) {
Marker("Apple Park", coordinate: applePark)
Marker("Infinite Loop", systemImage: "building.2",
coordinate: infiniteLoop)
}
.mapStyle(.standard(elevation: .realistic))
.mapControls {
MapUserLocationButton()
MapCompass()
MapScaleView()
}
}
}
Marqueur et Annotation
// Balloon marker -- façon la plus simple d'épingler un lieu
Marker("Cafe", systemImage: "cup.and.saucer.fill", coordinate: cafeCoord)
.tint(.brown)
// Annotation -- Vue SwiftUI personnalisée à une coordonnée
Annotation("You", coordinate: userCoord, anchor: .bottom) {
Image(systemName: "figure.wave")
.padding(6)
.background(.blue.gradient, in: .circle)
.foregroundStyle(.white)
}
Superpositions : Polyligne, Polygone, Cercle
Map {
// Polyligne à partir de coordonnées
MapPolyline(coordinates: routeCoords)
.stroke(.blue, lineWidth: 4)
// Polygone (zone en surbrillance)
MapPolygon(coordinates: parkBoundary)
.foregroundStyle(.green.opacity(0.3))
.stroke(.green, lineWidth: 2)
// Cercle (rayon autour d'un point)
MapCircle(center: storeCoord, radius: 500)
.foregroundStyle(.red.opacity(0.15))
.stroke(.red, lineWidth: 1)
}
Position de la Caméra
MapCameraPosition contrôle ce que la carte affiche. Liez-la pour permettre à l'utilisateur d'interagir et pour déplacer la caméra par programmation.
// Centrer sur une région
@State private var position: MapCameraPosition = .region(
MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 37.334, longitude: -122.009),
span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
)
)
// Suivre la localisation de l'utilisateur
@State private var position: MapCameraPosition = .userLocation(fallback: .automatic)
// Angle de caméra spécifique (perspective 3D)
@State private var position: MapCameraPosition = .camera(
MapCamera(centerCoordinate: applePark, distance: 1000, heading: 90, pitch: 60)
)
// Cadrer des éléments spécifiques
position = .item(MKMapItem.forCurrentLocation())
position = .rect(MKMapRect(...))
Style de Carte
.mapStyle(.standard) // Carte routière par défaut
.mapStyle(.standard(elevation: .realistic, showsTraffic: true))
.mapStyle(.imagery) // Satellite
.mapStyle(.imagery(elevation: .realistic)) // Satellite 3D
.mapStyle(.hybrid) // Satellite + étiquettes
.mapStyle(.hybrid(elevation: .realistic, showsTraffic: true))
Modes d'Interaction de la Carte
.mapInteractionModes(.all) // Par défaut : panoramique, zoom, rotation, inclinaison
.mapInteractionModes(.pan) // Panoramique uniquement
.mapInteractionModes([.pan, .zoom]) // Panoramique et zoom
.mapInteractionModes([]) // Carte statique (aucune interaction)
Sélection sur la Carte
@State private var selectedMarker: MKMapItem?
Map(selection: $selectedMarker) {
ForEach(places) { place in
Marker(place.name, coordinate: place.coordinate)
.tag(place.mapItem) // Le tag doit correspondre au type de sélection
}
}
.onChange(of: selectedMarker) { _, newValue in
guard let item = newValue else { return }
// Réagir à la sélection
}
API CoreLocation Moderne
CLLocationUpdate.liveUpdates() (iOS 17+)
Remplacez les callbacks CLLocationManagerDelegate par une simple séquence asynchrone. Chaque itération produit une CLLocationUpdate contenant une CLLocation optionnelle.
import CoreLocation
@Observable
final class LocationTracker: @unchecked Sendable {
var currentLocation: CLLocation?
private var updateTask: Task<Void, Never>?
func startTracking() {
updateTask = Task {
let updates = CLLocationUpdate.liveUpdates()
for try await update in updates {
guard let location = update.location else { continue }
// Filtrer par précision horizontale
guard location.horizontalAccuracy < 50 else { continue }
await MainActor.run {
self.currentLocation = location
}
}
}
}
func stopTracking() {
updateTask?.cancel()
updateTask = nil
}
}
CLServiceSession (iOS 18+)
Déclarez les exigences d'autorisation pour la durée de vie d'une fonctionnalité. Conservez une référence à la session tant que vous avez besoin des services de localisation.
// Autorisation lors de l'utilisation avec préférence de précision totale
let session = CLServiceSession(
authorization: .whenInUse,
fullAccuracyPurposeKey: "NearbySearchPurpose"
)
// Conservez `session` comme propriété stockée ; libérez-la une fois terminé.
Sur iOS 18+, CLLocationUpdate.liveUpdates() et CLMonitor prennent une CLServiceSession implicite si vous n'en créez pas explicitement. Créez-en une explicitement lorsque vous avez besoin d'une autorisation .always ou d'une précision totale.
Flux d'Autorisation
// Clés Info.plist (obligatoires) :
// NSLocationWhenInUseUsageDescription
// NSLocationAlwaysAndWhenInUseUsageDescription (uniquement si .always est nécessaire)
// Vérifier l'autorisation et guider l'utilisateur vers les Réglages en cas de refus
struct LocationPermissionView: View {
@Environment(\.openURL) private var openURL
var body: some View {
ContentUnavailableView {
Label("Accès à la localisation refusé", systemImage: "location.slash")
} description: {
Text("Activez l'accès à la localisation dans les Réglages pour utiliser cette fonctionnalité.")
} actions: {
Button("Ouvrir les Réglages") {
if let url = URL(string: UIApplication.openSettingsURLString) {
openURL(url)
}
}
}
}
}
Géocodage
CLGeocoder (iOS 8+)
let geocoder = CLGeocoder()
// Géocodage direct : chaîne d'adresse -> coordonnées
let placemarks = try await geocoder.geocodeAddressString("1 Apple Park Way, Cupertino")
if let location = placemarks.first?.location {
print(location.coordinate) // CLLocationCoordinate2D
}
// Géocodage inverse : coordonnées -> placemark
let location = CLLocation(latitude: 37.3349, longitude: -122.0090)
let placemarks = try await geocoder.reverseGeocodeLocation(location)
if let placemark = placemarks.first {
let address = [placemark.name, placemark.locality, placemark.administrativeArea]
.compactMap { $0 }
.joined(separator: ", ")
}
MKGeocodingRequest et MKReverseGeocodingRequest (iOS 26+)
Nouveau géocodage natif MapKit qui renvoie un MKMapItem avec des données plus riches et MKAddress / MKAddressRepresentations pour un formatage flexible des adresses.
@available(iOS 26, *)
func reverseGeocode(location: CLLocation) async throws -> MKMapItem? {
guard let request = MKReverseGeocodingRequest(location: location) else {
return nil
}
let mapItems = try await request.mapItems
return mapItems.first
}
@available(iOS 26, *)
func forwardGeocode(address: String) async throws -> [MKMapItem] {
guard let request = MKGeocodingRequest(addressString: address) else { return [] }
return try await request.mapItems
}
Recherche
MKLocalSearchCompleter (Autocomplétion)
@Observable
final class SearchCompleter: NSObject, MKLocalSearchCompleterDelegate {
var results: [MKLocalSearchCompletion] = []
var query: String = "" { didSet { completer.queryFragment = query } }
private let completer = MKLocalSearchCompleter()
override init() {
super.init()
completer.delegate = self
completer.resultTypes = [.address, .pointOfInterest]
}
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
results = completer.results
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
results = []
}
}
MKLocalSearch (Recherche Complète)
func search(for completion: MKLocalSearchCompletion) async throws -> [MKMapItem] {
let request = MKLocalSearch.Request(completion: completion)
request.resultTypes = [.pointOfInterest, .address]
let search = MKLocalSearch(request: request)
let response = try await search.start()
return response.mapItems
}
// Recherche par requête en langage naturel dans une région
func searchNearby(query: String, region: MKCoordinateRegion) async throws -> [MKMapItem] {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query
request.region = region
let search = MKLocalSearch(request: request)
let response = try await search.start()
return response.mapItems
}
Itinéraires
func getDirections(from source: MKMapItem, to destination: MKMapItem,
transport: MKDirectionsTransportType = .automobile) async throws -> MKRoute? {
let request = MKDirections.Request()
request.source = source
request.destination = destination
request.transportType = transport
let directions = MKDirections(request: request)
let response = try await directions.calculate()
return response.routes.first
}
Afficher un Itinéraire sur la Carte
@State private var route: MKRoute?
Map {
if let route {
MapPolyline(route.polyline)
.stroke(.blue, lineWidth: 5)
}
Marker("Start", coordinate: startCoord)
Marker("End", coordinate: endCoord)
}
.task {
route = try? await getDirections(from: startItem, to: endItem)
}
Calcul d'ETA
func getETA(from source: MKMapItem, to destination: MKMapItem) async throws -> TimeInterval {
let request = MKDirections.Request()
request.source = source
request.destination = destination
let directions = MKDirections(request: request)
let response = try await directions.calculateETA()
return response.expectedTravelTime
}
Itinéraires Cyclables (iOS 26+)
@available(iOS 26, *)
func getCyclingDirections(to destination: MKMapItem) async throws -> MKRoute? {
let request = MKDirections.Request()
request.source = MKMapItem.forCurrentLocation()
request.destination = destination
request.transportType = .cycling
let directions = MKDirections(request: request)
let response = try await directions.calculate()
return response.routes.first
}
PlaceDescriptor (iOS 26+)
Créez des références de lieux riches à partir de coordonnées ou d'adresses sans avoir besoin d'un ID de lieu. Nécessite import GeoToolbox.
@available(iOS 26, *)
func lookupPlace(name: String, coordinate: CLLocationCoordinate2D) async throws -> MKMapItem {
let descriptor = PlaceDescriptor(
representations: [.coordinate(coordinate)],
commonName: name
)
let request = MKMapItemRequest(placeDescriptor: descriptor)
return try await request.mapItem
}
Erreurs Courantes
À NE PAS FAIRE : Demander .authorizedAlways par défaut — les utilisateurs se méfient des autorisations trop larges.
À FAIRE : Commencez par .requestWhenInUseAuthorization(), passez à .always uniquement lorsque l'utilisateur active une fonctionnalité en arrière-plan.
À NE PAS FAIRE : Utiliser CLLocationManagerDelegate pour des récupérations simples de localisation sur iOS 17+.
À FAIRE : Utilisez le flux asynchrone CLLocationUpdate.liveUpdates() pour un code plus propre et concis.
À NE PAS FAIRE : Garder les mises à jour de localisation actives lorsque la carte/vue n'est pas visible (épuise la batterie).
À FAIRE : Utilisez .task { } dans SwiftUI pour que les mises à jour s'annulent automatiquement lors de la disparition.
À NE PAS FAIRE : Forcer le déballage des propriétés CLPlacemark — elles sont toutes optionnelles.
À FAIRE : Utilisez la coalescence nulle : placemark.locality ?? "Inconnu".
À NE PAS FAIRE : Déclencher ``
[La suite du contenu a été tronquée pour la traduction]