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.

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.
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.
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.
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)
}
}
}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).
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.
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)
}
}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.
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.
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.
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.
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.
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()
}
}
}
}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.
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.
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
Mapet configurerMapCameraPosition - ✅ Utiliser les différents styles de carte (
standard,imagery,hybrid) - ✅ Ajouter des
MarkeretAnnotationpersonnalisé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
Partager
Articles similaires

SwiftUI NavigationStack : questions d'entretien sur les patterns de navigation 2026
Préparez vos entretiens iOS avec les questions essentielles sur NavigationStack, NavigationPath et les patterns de navigation SwiftUI modernes.

SwiftUI Performance : optimiser LazyVStack et listes complexes
Techniques d'optimisation pour LazyVStack et listes SwiftUI. Réduire la consommation mémoire, améliorer le scrolling et éviter les pièges de performance courants.

Swift Testing Framework en entretien 2026 : macros #expect et #require vs XCTest
Maîtrisez le nouveau Swift Testing Framework pour vos entretiens iOS : macros #expect et #require, migration depuis XCTest, patterns avancés et pièges à éviter.