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.

Questions d'entretien MapKit SwiftUI pour développeurs iOS

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.

BasicMapView.swiftswift
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.

MapStyles.swiftswift
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).

MarkersAndAnnotations.swiftswift
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.

LocationManager.swiftswift
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.

RouteTrackingView.swiftswift
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)
        }
    }
}

Prêt à réussir tes entretiens iOS ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

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.

MapOverlays.swiftswift
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.

RouteDirections.swiftswift
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.

PlaceSearch.swiftswift
import SwiftUI
import MapKit

@Observable
class PlaceSearchManager {
    var searchResults: [MKMapItem] = []
    var isSearching = false
    var searchText = ""

    private var searchTask: Task<Void, Never>?

    // 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.

Geocoding.swiftswift
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.

ClusteringMapView.swiftswift
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.

LookAroundIntegration.swiftswift
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
    }
}

Prêt à réussir tes entretiens iOS ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

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

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#mapkit
#swiftui
#ios
#geolocalisation
#entretien

Partager

Articles similaires