# MapKit SwiftUI en entretien technique en 2026 : annotations, overlays et géolocalisation > Maîtrisez MapKit avec SwiftUI pour vos entretiens iOS : annotations personnalisées, overlays, géolocalisation, recherche de lieux et intégration Maps. - Published: 2026-03-10 - Updated: 2026-03-28 - Author: SharpSkill - Tags: mapkit, swiftui, ios, geolocalisation, entretien - Reading time: 14 min --- La cartographie constitue une fonctionnalité centrale dans de nombreuses applications iOS : livraison, fitness, immobilier, réseaux sociaux géolocalisés. Depuis iOS 17, MapKit offre une API SwiftUI native et moderne qui simplifie considérablement l'intégration de cartes. Les recruteurs évaluent régulièrement ces compétences lors des entretiens techniques. > **Structure de ce guide** > > Chaque question reproduit le format d'un entretien technique réel, avec une réponse détaillée et du code fonctionnel. Les concepts progressent du fondamental vers l'avancé. ## Les fondamentaux de Map en SwiftUI ### Question 1 : Comment afficher une carte basique avec SwiftUI ? Depuis iOS 17, SwiftUI propose le composant `Map` natif qui remplace l'approche `UIViewRepresentable` précédente. La vue `Map` accepte une position de caméra et du contenu à afficher. ```swift // BasicMapView.swift import SwiftUI import MapKit struct BasicMapView: View { // Position de la caméra contrôlant le centre et le zoom @State private var cameraPosition: MapCameraPosition = .automatic var body: some View { // Map basique sans contenu additionnel Map(position: $cameraPosition) } } // Map centrée sur une région spécifique struct CenteredMapView: View { // Définit une région centrée sur Paris @State private var cameraPosition: MapCameraPosition = .region( MKCoordinateRegion( center: CLLocationCoordinate2D(latitude: 48.8566, longitude: 2.3522), // Span définit le niveau de zoom (en degrés) span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05) ) ) var body: some View { Map(position: $cameraPosition) { // Contenu de la carte (markers, annotations, etc.) } .mapStyle(.standard) // Style de carte : standard, imagery, hybrid } } ``` La propriété `position` utilise `MapCameraPosition`, un enum permettant différents modes : `.automatic` (ajustement automatique), `.region` (région fixe), `.camera` (contrôle complet), ou `.userLocation` (centré sur l'utilisateur). ### Question 2 : Quels sont les différents styles de carte disponibles ? MapKit propose plusieurs styles de carte adaptés à différents cas d'usage. Le modificateur `.mapStyle()` configure l'apparence visuelle de la carte. ```swift // MapStyles.swift import SwiftUI import MapKit struct MapStylesDemo: View { @State private var position: MapCameraPosition = .automatic @State private var selectedStyle: MapStyleOption = .standard var body: some View { VStack { Map(position: $position) .mapStyle(selectedStyle.style) .frame(height: 400) // Sélecteur de style Picker("Style", selection: $selectedStyle) { ForEach(MapStyleOption.allCases) { option in Text(option.name).tag(option) } } .pickerStyle(.segmented) .padding() } } } enum MapStyleOption: String, CaseIterable, Identifiable { case standard case imagery case hybrid case standardElevated case imageryElevated var id: String { rawValue } var name: String { switch self { case .standard: return "Standard" case .imagery: return "Satellite" case .hybrid: return "Hybride" case .standardElevated: return "Relief" case .imageryElevated: return "Satellite 3D" } } var style: MapStyle { switch self { case .standard: // Carte routière classique return .standard case .imagery: // Vue satellite sans étiquettes return .imagery case .hybrid: // Satellite avec routes et noms return .hybrid case .standardElevated: // Standard avec relief 3D return .standard(elevation: .realistic) case .imageryElevated: // Satellite avec relief 3D return .imagery(elevation: .realistic) } } } ``` > **Points d'intérêt** > > Le style `.standard` accepte un paramètre `pointsOfInterest` pour filtrer les catégories affichées : `.including([.restaurant, .cafe])` ou `.excluding([.nightlife])`. ### Question 3 : Comment ajouter des markers et annotations sur la carte ? Les markers représentent des points d'intérêt sur la carte. SwiftUI MapKit offre plusieurs types : `Marker` (pin standard), `Annotation` (vue personnalisée) et `MapCircle`/`MapPolygon` (formes géométriques). ```swift // MarkersAndAnnotations.swift import SwiftUI import MapKit // Modèle de lieu conforme à Identifiable struct Place: Identifiable { let id = UUID() let name: String let coordinate: CLLocationCoordinate2D let category: PlaceCategory } enum PlaceCategory { case restaurant, hotel, attraction var icon: String { switch self { case .restaurant: return "fork.knife" case .hotel: return "bed.double" case .attraction: return "star" } } var tint: Color { switch self { case .restaurant: return .orange case .hotel: return .blue case .attraction: return .purple } } } struct MarkersMapView: View { @State private var position: MapCameraPosition = .region( MKCoordinateRegion( center: CLLocationCoordinate2D(latitude: 48.8566, longitude: 2.3522), span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1) ) ) let places: [Place] = [ Place(name: "Tour Eiffel", coordinate: CLLocationCoordinate2D(latitude: 48.8584, longitude: 2.2945), category: .attraction), Place(name: "Le Meurice", coordinate: CLLocationCoordinate2D(latitude: 48.8651, longitude: 2.3281), category: .hotel), Place(name: "Le Jules Verne", coordinate: CLLocationCoordinate2D(latitude: 48.8583, longitude: 2.2944), category: .restaurant) ] var body: some View { Map(position: $position) { // Markers avec icône système ForEach(places) { place in Marker(place.name, systemImage: place.category.icon, coordinate: place.coordinate) .tint(place.category.tint) } } } } // Annotations personnalisées avec vue SwiftUI struct CustomAnnotationsView: View { @State private var position: MapCameraPosition = .automatic @State private var selectedPlace: Place? let places: [Place] = [] // Données var body: some View { Map(position: $position, selection: $selectedPlace) { ForEach(places) { place in // Annotation avec vue personnalisée Annotation(place.name, coordinate: place.coordinate) { PlaceAnnotationView(place: place, isSelected: selectedPlace?.id == place.id) } } } } } // Vue personnalisée pour l'annotation struct PlaceAnnotationView: View { let place: Place let isSelected: Bool var body: some View { VStack(spacing: 4) { // Icône avec fond coloré Image(systemName: place.category.icon) .font(.system(size: isSelected ? 20 : 16)) .foregroundStyle(.white) .padding(8) .background(place.category.tint) .clipShape(Circle()) .shadow(radius: 4) // Nom affiché si sélectionné if isSelected { Text(place.name) .font(.caption) .fontWeight(.medium) .padding(.horizontal, 8) .padding(.vertical, 4) .background(.ultraThinMaterial) .clipShape(RoundedRectangle(cornerRadius: 8)) } } .animation(.spring(duration: 0.3), value: isSelected) } } ``` La différence clé : `Marker` utilise le rendu natif Apple Maps (pin standard), tandis que `Annotation` permet une personnalisation complète avec n'importe quelle vue SwiftUI. ## Géolocalisation et permissions ### Question 4 : Comment gérer les permissions de localisation et afficher la position utilisateur ? La géolocalisation nécessite des permissions explicites et une gestion appropriée des différents états d'autorisation. Le `CLLocationManager` gère les demandes de permission. ```swift // LocationManager.swift import SwiftUI import MapKit import CoreLocation @Observable class LocationManager: NSObject, CLLocationManagerDelegate { private let manager = CLLocationManager() // État de la localisation var location: CLLocation? var authorizationStatus: CLAuthorizationStatus = .notDetermined var isAuthorized: Bool { authorizationStatus == .authorizedWhenInUse || authorizationStatus == .authorizedAlways } override init() { super.init() manager.delegate = self manager.desiredAccuracy = kCLLocationAccuracyBest // Récupère le statut actuel authorizationStatus = manager.authorizationStatus } // Demande la permission de localisation func requestAuthorization() { manager.requestWhenInUseAuthorization() } // Démarre le suivi de position func startUpdatingLocation() { guard isAuthorized else { return } manager.startUpdatingLocation() } // Arrête le suivi pour économiser la batterie func stopUpdatingLocation() { manager.stopUpdatingLocation() } // MARK: - CLLocationManagerDelegate func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { authorizationStatus = manager.authorizationStatus if isAuthorized { startUpdatingLocation() } } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { // Prend la position la plus récente location = locations.last } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { print("Erreur de localisation: \(error.localizedDescription)") } } // Vue avec carte et position utilisateur struct UserLocationMapView: View { @State private var locationManager = LocationManager() @State private var cameraPosition: MapCameraPosition = .userLocation(fallback: .automatic) var body: some View { Map(position: $cameraPosition) { // Affiche le point bleu de position utilisateur UserAnnotation() } .mapControls { // Bouton pour recentrer sur la position MapUserLocationButton() // Boussole MapCompass() // Échelle MapScaleView() } .onAppear { locationManager.requestAuthorization() } .overlay(alignment: .top) { if !locationManager.isAuthorized { PermissionBanner() } } } } // Bannière demandant l'autorisation struct PermissionBanner: View { var body: some View { HStack { Image(systemName: "location.slash") Text("Activez la localisation dans les réglages") Spacer() Button("Ouvrir") { if let url = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(url) } } .buttonStyle(.bordered) } .padding() .background(.ultraThinMaterial) } } ``` > **Info.plist obligatoire** > > Les clés `NSLocationWhenInUseUsageDescription` et/ou `NSLocationAlwaysUsageDescription` doivent être définies dans Info.plist avec une description claire de l'usage de la localisation. ### Question 5 : Comment suivre la position en temps réel et tracer un parcours ? Le suivi en temps réel est essentiel pour les applications de fitness ou de navigation. Cette question teste la capacité à gérer un flux continu de données de localisation. ```swift // RouteTrackingView.swift import SwiftUI import MapKit import CoreLocation @Observable class RouteTracker: NSObject, CLLocationManagerDelegate { private let manager = CLLocationManager() // Historique des positions pour le tracé var routeCoordinates: [CLLocationCoordinate2D] = [] var currentLocation: CLLocation? var isTracking = false // Statistiques du parcours var totalDistance: CLLocationDistance = 0 private var lastLocation: CLLocation? override init() { super.init() manager.delegate = self manager.desiredAccuracy = kCLLocationAccuracyBest // Mise à jour même en arrière-plan manager.allowsBackgroundLocationUpdates = true manager.pausesLocationUpdatesAutomatically = false } func startTracking() { manager.requestWhenInUseAuthorization() manager.startUpdatingLocation() isTracking = true routeCoordinates = [] totalDistance = 0 lastLocation = nil } func stopTracking() { manager.stopUpdatingLocation() isTracking = false } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let newLocation = locations.last else { return } // Filtre les positions imprécises guard newLocation.horizontalAccuracy < 20 else { return } currentLocation = newLocation routeCoordinates.append(newLocation.coordinate) // Calcule la distance parcourue if let last = lastLocation { totalDistance += newLocation.distance(from: last) } lastLocation = newLocation } } struct RouteTrackingView: View { @State private var tracker = RouteTracker() @State private var cameraPosition: MapCameraPosition = .userLocation(fallback: .automatic) var body: some View { ZStack { Map(position: $cameraPosition) { // Position actuelle UserAnnotation() // Tracé du parcours en polyline if tracker.routeCoordinates.count > 1 { MapPolyline(coordinates: tracker.routeCoordinates) .stroke(.blue, lineWidth: 4) } } .mapStyle(.standard) // Interface de contrôle VStack { Spacer() // Statistiques HStack { VStack(alignment: .leading) { Text("Distance") .font(.caption) .foregroundStyle(.secondary) Text(formatDistance(tracker.totalDistance)) .font(.title2) .fontWeight(.bold) } Spacer() } .padding() .background(.ultraThinMaterial) .clipShape(RoundedRectangle(cornerRadius: 12)) .padding() // Bouton start/stop Button { if tracker.isTracking { tracker.stopTracking() } else { tracker.startTracking() } } label: { Label( tracker.isTracking ? "Arrêter" : "Démarrer", systemImage: tracker.isTracking ? "stop.fill" : "play.fill" ) .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .tint(tracker.isTracking ? .red : .green) .padding(.horizontal) .padding(.bottom) } } } private func formatDistance(_ meters: CLLocationDistance) -> String { if meters < 1000 { return String(format: "%.0f m", meters) } else { return String(format: "%.2f km", meters / 1000) } } } ``` ## Overlays et formes géométriques ### Question 6 : Comment dessiner des overlays personnalisés sur la carte ? Les overlays permettent d'afficher des zones, des parcours ou des régions sur la carte. MapKit SwiftUI offre `MapCircle`, `MapPolygon` et `MapPolyline`. ```swift // MapOverlays.swift import SwiftUI import MapKit struct DeliveryZoneMapView: View { @State private var position: MapCameraPosition = .region( MKCoordinateRegion( center: CLLocationCoordinate2D(latitude: 48.8566, longitude: 2.3522), span: MKCoordinateSpan(latitudeDelta: 0.15, longitudeDelta: 0.15) ) ) // Zones de livraison avec différents délais let deliveryZones: [DeliveryZone] = [ DeliveryZone( name: "Zone Express", center: CLLocationCoordinate2D(latitude: 48.8566, longitude: 2.3522), radius: 2000, deliveryTime: "15 min", color: .green ), DeliveryZone( name: "Zone Standard", center: CLLocationCoordinate2D(latitude: 48.8566, longitude: 2.3522), radius: 5000, deliveryTime: "30 min", color: .orange ) ] var body: some View { Map(position: $position) { // Cercles pour les zones de livraison ForEach(deliveryZones) { zone in MapCircle(center: zone.center, radius: zone.radius) .foregroundStyle(zone.color.opacity(0.2)) .stroke(zone.color, lineWidth: 2) } // Marker du restaurant Marker("Restaurant", systemImage: "storefront", coordinate: CLLocationCoordinate2D(latitude: 48.8566, longitude: 2.3522)) .tint(.red) } .overlay(alignment: .bottomLeading) { // Légende VStack(alignment: .leading, spacing: 8) { ForEach(deliveryZones) { zone in HStack { Circle() .fill(zone.color) .frame(width: 12, height: 12) Text("\(zone.name) - \(zone.deliveryTime)") .font(.caption) } } } .padding() .background(.ultraThinMaterial) .clipShape(RoundedRectangle(cornerRadius: 8)) .padding() } } } struct DeliveryZone: Identifiable { let id = UUID() let name: String let center: CLLocationCoordinate2D let radius: CLLocationDistance let deliveryTime: String let color: Color } // Polygone personnalisé (zone géographique complexe) struct PolygonOverlayView: View { @State private var position: MapCameraPosition = .automatic // Coordonnées d'un quartier let neighborhoodCoordinates: [CLLocationCoordinate2D] = [ CLLocationCoordinate2D(latitude: 48.853, longitude: 2.347), CLLocationCoordinate2D(latitude: 48.858, longitude: 2.352), CLLocationCoordinate2D(latitude: 48.862, longitude: 2.348), CLLocationCoordinate2D(latitude: 48.860, longitude: 2.340), CLLocationCoordinate2D(latitude: 48.855, longitude: 2.342) ] var body: some View { Map(position: $position) { // Polygone fermé automatiquement MapPolygon(coordinates: neighborhoodCoordinates) .foregroundStyle(.blue.opacity(0.3)) .stroke(.blue, lineWidth: 3) } } } ``` ### Question 7 : Comment afficher un itinéraire entre deux points ? Le calcul d'itinéraire utilise `MKDirections` pour obtenir les routes depuis les serveurs Apple. L'affichage se fait ensuite avec `MapPolyline`. ```swift // RouteDirections.swift import SwiftUI import MapKit @Observable class RouteCalculator { var route: MKRoute? var isCalculating = false var error: String? // Calcule l'itinéraire entre deux points func calculateRoute(from source: CLLocationCoordinate2D, to destination: CLLocationCoordinate2D) async { isCalculating = true error = nil // Crée les placemarks source et destination let sourcePlacemark = MKPlacemark(coordinate: source) let destinationPlacemark = MKPlacemark(coordinate: destination) // Configure la requête let request = MKDirections.Request() request.source = MKMapItem(placemark: sourcePlacemark) request.destination = MKMapItem(placemark: destinationPlacemark) request.transportType = .automobile // .walking, .transit disponibles request.requestsAlternateRoutes = false let directions = MKDirections(request: request) do { let response = try await directions.calculate() // Prend le premier itinéraire route = response.routes.first } catch { self.error = error.localizedDescription } isCalculating = false } } struct DirectionsMapView: View { @State private var calculator = RouteCalculator() @State private var position: MapCameraPosition = .automatic let startPoint = CLLocationCoordinate2D(latitude: 48.8738, longitude: 2.2950) // Arc de Triomphe let endPoint = CLLocationCoordinate2D(latitude: 48.8530, longitude: 2.3499) // Notre-Dame var body: some View { ZStack { Map(position: $position) { // Marqueurs départ et arrivée Marker("Départ", systemImage: "car.fill", coordinate: startPoint) .tint(.green) Marker("Arrivée", systemImage: "flag.fill", coordinate: endPoint) .tint(.red) // Tracé de l'itinéraire if let route = calculator.route { MapPolyline(route.polyline) .stroke(.blue, lineWidth: 5) } } // Informations sur l'itinéraire if let route = calculator.route { VStack { Spacer() RouteInfoCard(route: route) } } if calculator.isCalculating { ProgressView("Calcul de l'itinéraire...") .padding() .background(.ultraThinMaterial) .clipShape(RoundedRectangle(cornerRadius: 8)) } } .task { await calculator.calculateRoute(from: startPoint, to: endPoint) } } } struct RouteInfoCard: View { let route: MKRoute var body: some View { HStack(spacing: 20) { VStack(alignment: .leading) { Text("Distance") .font(.caption) .foregroundStyle(.secondary) Text(formatDistance(route.distance)) .font(.headline) } Divider() .frame(height: 30) VStack(alignment: .leading) { Text("Durée estimée") .font(.caption) .foregroundStyle(.secondary) Text(formatDuration(route.expectedTravelTime)) .font(.headline) } Spacer() } .padding() .background(.ultraThinMaterial) .clipShape(RoundedRectangle(cornerRadius: 12)) .padding() } private func formatDistance(_ meters: CLLocationDistance) -> String { let km = meters / 1000 return String(format: "%.1f km", km) } private func formatDuration(_ seconds: TimeInterval) -> String { let minutes = Int(seconds) / 60 if minutes < 60 { return "\(minutes) min" } else { let hours = minutes / 60 let remainingMinutes = minutes % 60 return "\(hours)h \(remainingMinutes)min" } } } ``` ## Recherche de lieux et géocodage ### Question 8 : Comment implémenter une recherche de lieux avec MKLocalSearch ? `MKLocalSearch` permet de rechercher des points d'intérêt, adresses ou commerces. Cette fonctionnalité est essentielle pour les applications avec barre de recherche géolocalisée. ```swift // PlaceSearch.swift import SwiftUI import MapKit @Observable class PlaceSearchManager { var searchResults: [MKMapItem] = [] var isSearching = false var searchText = "" private var searchTask: Task? // Recherche avec debounce func search(query: String, in region: MKCoordinateRegion) { searchTask?.cancel() guard !query.isEmpty else { searchResults = [] return } searchTask = Task { // Debounce de 300ms try? await Task.sleep(for: .milliseconds(300)) guard !Task.isCancelled else { return } isSearching = true let request = MKLocalSearch.Request() request.naturalLanguageQuery = query request.region = region request.resultTypes = [.pointOfInterest, .address] let search = MKLocalSearch(request: request) do { let response = try await search.start() if !Task.isCancelled { searchResults = response.mapItems } } catch { print("Erreur de recherche: \(error.localizedDescription)") searchResults = [] } isSearching = false } } func clearResults() { searchResults = [] searchText = "" } } struct SearchableMapView: View { @State private var searchManager = PlaceSearchManager() @State private var position: MapCameraPosition = .region( MKCoordinateRegion( center: CLLocationCoordinate2D(latitude: 48.8566, longitude: 2.3522), span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1) ) ) @State private var selectedItem: MKMapItem? @State private var visibleRegion: MKCoordinateRegion? var body: some View { NavigationStack { Map(position: $position, selection: $selectedItem) { // Affiche les résultats de recherche ForEach(searchManager.searchResults, id: \.self) { item in Marker(item: item) } } .onMapCameraChange { context in // Capture la région visible pour la recherche visibleRegion = context.region } .searchable(text: $searchManager.searchText, prompt: "Rechercher un lieu") .onChange(of: searchManager.searchText) { _, newValue in if let region = visibleRegion { searchManager.search(query: newValue, in: region) } } .overlay(alignment: .bottom) { if let item = selectedItem { PlaceDetailCard(item: item) } } .navigationTitle("Carte") .navigationBarTitleDisplayMode(.inline) } } } struct PlaceDetailCard: View { let item: MKMapItem var body: some View { VStack(alignment: .leading, spacing: 8) { Text(item.name ?? "Lieu inconnu") .font(.headline) if let address = item.placemark.title { Text(address) .font(.subheadline) .foregroundStyle(.secondary) } HStack { if let phone = item.phoneNumber { Button { if let url = URL(string: "tel:\(phone)") { UIApplication.shared.open(url) } } label: { Label("Appeler", systemImage: "phone") } .buttonStyle(.bordered) } Button { item.openInMaps() } label: { Label("Itinéraire", systemImage: "arrow.triangle.turn.up.right.diamond") } .buttonStyle(.borderedProminent) } } .padding() .frame(maxWidth: .infinity, alignment: .leading) .background(.ultraThickMaterial) .clipShape(RoundedRectangle(cornerRadius: 16)) .padding() } } ``` ### Question 9 : Comment convertir une adresse en coordonnées (géocodage) ? Le géocodage transforme une adresse textuelle en coordonnées GPS. Le géocodage inverse fait l'opération contraire. ```swift // Geocoding.swift import SwiftUI import MapKit import CoreLocation @Observable class GeocodingManager { private let geocoder = CLGeocoder() var coordinate: CLLocationCoordinate2D? var address: String? var isLoading = false var error: String? // Géocodage : adresse → coordonnées func geocode(address: String) async { isLoading = true error = nil do { let placemarks = try await geocoder.geocodeAddressString(address) if let placemark = placemarks.first, let location = placemark.location { coordinate = location.coordinate } else { error = "Adresse non trouvée" } } catch { self.error = error.localizedDescription } isLoading = false } // Géocodage inverse : coordonnées → adresse func reverseGeocode(coordinate: CLLocationCoordinate2D) async { isLoading = true error = nil let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) do { let placemarks = try await geocoder.reverseGeocodeLocation(location) if let placemark = placemarks.first { address = formatAddress(placemark) } else { error = "Adresse non trouvée" } } catch { self.error = error.localizedDescription } isLoading = false } // Formate l'adresse lisible private func formatAddress(_ placemark: CLPlacemark) -> String { var components: [String] = [] if let street = placemark.thoroughfare { if let number = placemark.subThoroughfare { components.append("\(number) \(street)") } else { components.append(street) } } if let city = placemark.locality { components.append(city) } if let country = placemark.country { components.append(country) } return components.joined(separator: ", ") } } struct GeocodingDemoView: View { @State private var manager = GeocodingManager() @State private var addressInput = "" @State private var position: MapCameraPosition = .automatic var body: some View { VStack { // Champ de saisie d'adresse HStack { TextField("Entrez une adresse", text: $addressInput) .textFieldStyle(.roundedBorder) Button("Rechercher") { Task { await manager.geocode(address: addressInput) if let coord = manager.coordinate { position = .region(MKCoordinateRegion( center: coord, span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01) )) } } } .buttonStyle(.borderedProminent) .disabled(addressInput.isEmpty || manager.isLoading) } .padding() // Carte avec marker Map(position: $position) { if let coord = manager.coordinate { Marker("Résultat", coordinate: coord) .tint(.red) } } .onTapGesture { screenCoord in // Pour le géocodage inverse, utiliser MapReader } if let error = manager.error { Text(error) .foregroundStyle(.red) .padding() } } } } ``` > **Limites du géocodage** > > Le géocodeur Apple impose des limites de requêtes. Pour des volumes importants, il est recommandé d'utiliser un cache local ou un service tiers comme Mapbox ou Google Maps. ## Patterns avancés et bonnes pratiques ### Question 10 : Comment optimiser les performances avec de nombreux markers ? L'affichage de centaines de markers peut impacter les performances. Le clustering et le chargement progressif sont des solutions efficaces. ```swift // ClusteringMapView.swift import SwiftUI import MapKit struct ClusteringMapView: View { @State private var position: MapCameraPosition = .automatic @State private var visiblePlaces: [Place] = [] @State private var visibleRegion: MKCoordinateRegion? // Toutes les données (potentiellement des milliers) let allPlaces: [Place] var body: some View { Map(position: $position) { // Affiche uniquement les lieux visibles ForEach(visiblePlaces) { place in Marker(place.name, coordinate: place.coordinate) } } .onMapCameraChange(frequency: .onEnd) { context in // Met à jour quand l'utilisateur arrête de bouger visibleRegion = context.region updateVisiblePlaces(in: context.region) } } // Filtre les lieux dans la région visible private func updateVisiblePlaces(in region: MKCoordinateRegion) { let minLat = region.center.latitude - region.span.latitudeDelta / 2 let maxLat = region.center.latitude + region.span.latitudeDelta / 2 let minLon = region.center.longitude - region.span.longitudeDelta / 2 let maxLon = region.center.longitude + region.span.longitudeDelta / 2 // Filtre spatial var filtered = allPlaces.filter { place in place.coordinate.latitude >= minLat && place.coordinate.latitude <= maxLat && place.coordinate.longitude >= minLon && place.coordinate.longitude <= maxLon } // Limite le nombre de markers affichés if filtered.count > 100 { // Échantillonnage ou clustering filtered = Array(filtered.prefix(100)) } visiblePlaces = filtered } } // Clustering manuel simplifié struct ClusteredPlace: Identifiable { let id = UUID() let coordinate: CLLocationCoordinate2D let count: Int let places: [Place] var isCluster: Bool { count > 1 } } @Observable class ClusterManager { var clusters: [ClusteredPlace] = [] // Regroupe les lieux proches selon le niveau de zoom func cluster(places: [Place], in region: MKCoordinateRegion) { let gridSize = region.span.latitudeDelta / 10 var grid: [String: [Place]] = [:] for place in places { // Clé de grille basée sur la position let gridX = Int(place.coordinate.longitude / gridSize) let gridY = Int(place.coordinate.latitude / gridSize) let key = "\(gridX),\(gridY)" grid[key, default: []].append(place) } // Convertit en clusters clusters = grid.map { (_, places) in let centerLat = places.map(\.coordinate.latitude).reduce(0, +) / Double(places.count) let centerLon = places.map(\.coordinate.longitude).reduce(0, +) / Double(places.count) return ClusteredPlace( coordinate: CLLocationCoordinate2D(latitude: centerLat, longitude: centerLon), count: places.count, places: places ) } } } struct ClusterAnnotationView: View { let cluster: ClusteredPlace var body: some View { if cluster.isCluster { // Affiche un cercle avec le nombre ZStack { Circle() .fill(.blue) .frame(width: 40, height: 40) Text("\(cluster.count)") .font(.system(size: 14, weight: .bold)) .foregroundStyle(.white) } } else { // Marker simple Image(systemName: "mappin.circle.fill") .font(.title) .foregroundStyle(.red) } } } ``` ### Question 11 : Comment intégrer MapKit avec d'autres services Apple ? MapKit s'intègre avec plusieurs frameworks Apple pour offrir une expérience enrichie : Look Around, CarPlay, WidgetKit. ```swift // LookAroundIntegration.swift import SwiftUI import MapKit struct LookAroundMapView: View { @State private var position: MapCameraPosition = .automatic @State private var selectedPlace: Place? @State private var lookAroundScene: MKLookAroundScene? @State private var showLookAround = false let places: [Place] var body: some View { Map(position: $position, selection: $selectedPlace) { ForEach(places) { place in Marker(place.name, coordinate: place.coordinate) } } .onChange(of: selectedPlace) { _, newPlace in if let place = newPlace { Task { await loadLookAroundScene(for: place.coordinate) } } } .sheet(isPresented: $showLookAround) { if let scene = lookAroundScene { LookAroundPreview(scene: scene) .frame(height: 300) } } .safeAreaInset(edge: .bottom) { if selectedPlace != nil && lookAroundScene != nil { Button("Voir en Look Around") { showLookAround = true } .buttonStyle(.borderedProminent) .padding() } } } // Charge la scène Look Around pour une position private func loadLookAroundScene(for coordinate: CLLocationCoordinate2D) async { let request = MKLookAroundSceneRequest(coordinate: coordinate) do { lookAroundScene = try await request.scene } catch { lookAroundScene = nil print("Look Around non disponible: \(error.localizedDescription)") } } } // Composant Look Around interactif struct LookAroundPreview: View { let scene: MKLookAroundScene @State private var isNavigating = false var body: some View { LookAroundPreviewRepresentable(scene: scene, isNavigating: $isNavigating) .overlay(alignment: .topTrailing) { Button { isNavigating.toggle() } label: { Image(systemName: isNavigating ? "stop.fill" : "play.fill") .padding() .background(.ultraThinMaterial) .clipShape(Circle()) } .padding() } } } // UIViewRepresentable pour Look Around struct LookAroundPreviewRepresentable: UIViewRepresentable { let scene: MKLookAroundScene @Binding var isNavigating: Bool func makeUIView(context: Context) -> MKLookAroundViewController { let controller = MKLookAroundViewController(scene: scene) controller.isNavigationEnabled = isNavigating return controller } func updateUIView(_ uiView: MKLookAroundViewController, context: Context) { uiView.isNavigationEnabled = isNavigating } } ``` ## Conclusion MapKit avec SwiftUI offre une API moderne et puissante pour intégrer la cartographie dans les applications iOS. La maîtrise des annotations, overlays, géolocalisation et recherche de lieux distingue les développeurs iOS confirmés lors des entretiens techniques. ### Checklist de révision - ✅ Afficher une carte avec `Map` et configurer `MapCameraPosition` - ✅ Utiliser les différents styles de carte (`standard`, `imagery`, `hybrid`) - ✅ Ajouter des `Marker` et `Annotation` personnalisées - ✅ Gérer les permissions de localisation avec `CLLocationManager` - ✅ Suivre la position en temps réel et tracer des parcours - ✅ Dessiner des overlays (`MapCircle`, `MapPolygon`, `MapPolyline`) - ✅ Calculer des itinéraires avec `MKDirections` - ✅ Implémenter la recherche de lieux avec `MKLocalSearch` - ✅ Effectuer du géocodage et géocodage inverse - ✅ Optimiser les performances avec le clustering de markers - ✅ Intégrer Look Around pour une vue immersive --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/ios/mapkit-swiftui-entretien-annotations-overlays-geolocalisation