MapKit
Construye funciones basadas en mapas y ubicación para iOS 17+ con SwiftUI, MapKit y las APIs asíncronas modernas de CoreLocation. Usa Map con MapContentBuilder para las vistas, CLLocationUpdate.liveUpdates() para transmitir la ubicación y CLMonitor para geocercas.
Consulta references/mapkit-patterns.md para patrones extendidos de MapKit y references/mapkit-corelocation-patterns.md para patrones de CoreLocation.
Contenido
- Flujo de trabajo
- Vista de Mapa en SwiftUI (iOS 17+)
- API Moderna de CoreLocation
- Geocodificación
- Búsqueda
- Direcciones
- PlaceDescriptor (iOS 26+)
- Errores Comunes
- Lista de Verificación
- Referencias
Flujo de trabajo
1. Agregar un mapa con marcadores o anotaciones
- Importa
MapKit. - Crea una vista
Mapcon un enlace opcionalMapCameraPosition. - Agrega
Marker,Annotation,MapPolyline,MapPolygonoMapCircledentro del closureMapContentBuilder. - Configura el estilo del mapa con
.mapStyle(). - Agrega controles de mapa con
.mapControls { }. - Maneja la selección con un enlace
selection:.
2. Rastrear la ubicación del usuario
- Agrega
NSLocationWhenInUseUsageDescriptional Info.plist. - En iOS 18+, crea un
CLServiceSessionpara gestionar la autorización. - Itera
CLLocationUpdate.liveUpdates()en unaTask. - Filtra las actualizaciones por distancia o precisión antes de actualizar la UI.
- Detén la tarea cuando ya no sea necesario el rastreo de ubicación.
3. Buscar lugares
- Configura
MKLocalSearchCompleterpara sugerencias de autocompletado. - Debounce la entrada del usuario (al menos 300ms) antes de establecer la consulta.
- Convierte la finalización seleccionada en un
MKLocalSearch.Requestpara obtener resultados completos. - Muestra los resultados como marcadores o en una lista.
4. Obtener direcciones y mostrar una ruta
- Crea un
MKDirections.Requestcon el origen y destinoMKMapItem. - Establece el
transportType(.automobile,.walking,.transit,.cycling). - Espera
MKDirections.calculate(). - Dibuja la ruta con
MapPolyline(route.polyline).
5. Revisar código de mapa/ubicación existente
Revisa la Lista de Verificación al final de este archivo.
Vista de Mapa en SwiftUI (iOS 17+)
import MapKit
import SwiftUI
struct PlaceMap: View {
@State private var position: MapCameraPosition = .automatic
var body: some View {
Map(position: $position) {
Marker("Apple Park", coordinate: applePark)
Marker("Infinite Loop", systemImage: "building.2",
coordinate: infiniteLoop)
}
.mapStyle(.standard(elevation: .realistic))
.mapControls {
MapUserLocationButton()
MapCompass()
MapScaleView()
}
}
}
Marcador y Anotación
// Marcador globo -- la forma más simple de fijar una ubicación
Marker("Café", systemImage: "cup.and.saucer.fill", coordinate: cafeCoord)
.tint(.brown)
// Anotación -- vista SwiftUI personalizada en una coordenada
Annotation("Tú", coordinate: userCoord, anchor: .bottom) {
Image(systemName: "figure.wave")
.padding(6)
.background(.blue.gradient, in: .circle)
.foregroundStyle(.white)
}
Superposiciones: Polilínea, Polígono, Círculo
Map {
// Polilínea a partir de coordenadas
MapPolyline(coordinates: routeCoords)
.stroke(.blue, lineWidth: 4)
// Polígono (resaltar área)
MapPolygon(coordinates: parkBoundary)
.foregroundStyle(.green.opacity(0.3))
.stroke(.green, lineWidth: 2)
// Círculo (radio alrededor de un punto)
MapCircle(center: storeCoord, radius: 500)
.foregroundStyle(.red.opacity(0.15))
.stroke(.red, lineWidth: 1)
}
Posición de la Cámara
MapCameraPosition controla lo que muestra el mapa. Enlázala para permitir la interacción del usuario y mover la cámara programáticamente.
// Centrar en una región
@State private var position: MapCameraPosition = .region(
MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 37.334, longitude: -122.009),
span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
)
)
// Seguir la ubicación del usuario
@State private var position: MapCameraPosition = .userLocation(fallback: .automatic)
// Ángulo de cámara específico (perspectiva 3D)
@State private var position: MapCameraPosition = .camera(
MapCamera(centerCoordinate: applePark, distance: 1000, heading: 90, pitch: 60)
)
// Encuadrar elementos específicos
position = .item(MKMapItem.forCurrentLocation())
position = .rect(MKMapRect(...))
Estilo del Mapa
.mapStyle(.standard) // Mapa de carreteras por defecto
.mapStyle(.standard(elevation: .realistic, showsTraffic: true))
.mapStyle(.imagery) // Satélite
.mapStyle(.imagery(elevation: .realistic)) // Satélite 3D
.mapStyle(.hybrid) // Satélite + etiquetas
.mapStyle(.hybrid(elevation: .realistic, showsTraffic: true))
Modos de Interacción del Mapa
.mapInteractionModes(.all) // Por defecto: desplazar, zoom, rotar, inclinar
.mapInteractionModes(.pan) // Solo desplazar
.mapInteractionModes([.pan, .zoom]) // Desplazar y zoom
.mapInteractionModes([]) // Mapa estático (sin interacción)
Selección en el Mapa
@State private var selectedMarker: MKMapItem?
Map(selection: $selectedMarker) {
ForEach(places) { place in
Marker(place.name, coordinate: place.coordinate)
.tag(place.mapItem) // La etiqueta debe coincidir con el tipo de selección
}
}
.onChange(of: selectedMarker) { _, newValue in
guard let item = newValue else { return }
// Reaccionar a la selección
}
API Moderna de CoreLocation
CLLocationUpdate.liveUpdates() (iOS 17+)
Reemplaza los callbacks de CLLocationManagerDelegate con una única secuencia asíncrona. Cada iteración produce un CLLocationUpdate que contiene un CLLocation opcional.
import CoreLocation
@Observable
final class LocationTracker: @unchecked Sendable {
var currentLocation: CLLocation?
private var updateTask: Task<Void, Never>?
func startTracking() {
updateTask = Task {
let updates = CLLocationUpdate.liveUpdates()
for try await update in updates {
guard let location = update.location else { continue }
// Filtrar por precisión horizontal
guard location.horizontalAccuracy < 50 else { continue }
await MainActor.run {
self.currentLocation = location
}
}
}
}
func stopTracking() {
updateTask?.cancel()
updateTask = nil
}
}
CLServiceSession (iOS 18+)
Declara los requisitos de autorización durante la vida útil de una función. Mantén una referencia a la sesión mientras necesites servicios de ubicación.
// Autorización de uso mientras la aplicación está en uso con preferencia de precisión completa
let session = CLServiceSession(
authorization: .whenInUse,
fullAccuracyPurposeKey: "NearbySearchPurpose"
)
// Mantén `session` como una propiedad almacenada; libérala cuando termines.
En iOS 18+, CLLocationUpdate.liveUpdates() y CLMonitor toman un CLServiceSession implícito si no creas uno explícitamente. Crea uno explícito cuando necesites autorización .always o precisión completa.
Flujo de Autorización
// Claves Info.plist (requeridas):
// NSLocationWhenInUseUsageDescription
// NSLocationAlwaysAndWhenInUseUsageDescription (solo si se necesita .always)
// Verificar la autorización y guiar al usuario a Configuración cuando se deniega
struct LocationPermissionView: View {
@Environment(\.openURL) private var openURL
var body: some View {
ContentUnavailableView {
Label("Acceso a Ubicación Denegado", systemImage: "location.slash")
} description: {
Text("Habilita el acceso a la ubicación en Configuración para usar esta función.")
} actions: {
Button("Abrir Configuración") {
if let url = URL(string: UIApplication.openSettingsURLString) {
openURL(url)
}
}
}
}
}
Geocodificación
CLGeocoder (iOS 8+)
let geocoder = CLGeocoder()
// Geocodificación directa: cadena de dirección -> coordenadas
let placemarks = try await geocoder.geocodeAddressString("1 Apple Park Way, Cupertino")
if let location = placemarks.first?.location {
print(location.coordinate) // CLLocationCoordinate2D
}
// Geocodificación inversa: coordenadas -> lugar
let location = CLLocation(latitude: 37.3349, longitude: -122.0090)
let placemarks = try await geocoder.reverseGeocodeLocation(location)
if let placemark = placemarks.first {
let address = [placemark.name, placemark.locality, placemark.administrativeArea]
.compactMap { $0 }
.joined(separator: ", ")
}
MKGeocodingRequest y MKReverseGeocodingRequest (iOS 26+)
Nueva geocodificación nativa de MapKit que devuelve MKMapItem con datos más ricos y MKAddress / MKAddressRepresentations para un formato de dirección flexible.
@available(iOS 26, *)
func reverseGeocode(location: CLLocation) async throws -> MKMapItem? {
guard let request = MKReverseGeocodingRequest(location: location) else {
return nil
}
let mapItems = try await request.mapItems
return mapItems.first
}
@available(iOS 26, *)
func forwardGeocode(address: String) async throws -> [MKMapItem] {
guard let request = MKGeocodingRequest(addressString: address) else { return [] }
return try await request.mapItems
}
Búsqueda
MKLocalSearchCompleter (Autocompletado)
@Observable
final class SearchCompleter: NSObject, MKLocalSearchCompleterDelegate {
var results: [MKLocalSearchCompletion] = []
var query: String = "" { didSet { completer.queryFragment = query } }
private let completer = MKLocalSearchCompleter()
override init() {
super.init()
completer.delegate = self
completer.resultTypes = [.address, .pointOfInterest]
}
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
results = completer.results
}
func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
results = []
}
}
MKLocalSearch (Búsqueda Completa)
func search(for completion: MKLocalSearchCompletion) async throws -> [MKMapItem] {
let request = MKLocalSearch.Request(completion: completion)
request.resultTypes = [.pointOfInterest, .address]
let search = MKLocalSearch(request: request)
let response = try await search.start()
return response.mapItems
}
// Búsqueda por consulta en lenguaje natural dentro de una región
func searchNearby(query: String, region: MKCoordinateRegion) async throws -> [MKMapItem] {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query
request.region = region
let search = MKLocalSearch(request: request)
let response = try await search.start()
return response.mapItems
}
Direcciones
func getDirections(from source: MKMapItem, to destination: MKMapItem,
transport: MKDirectionsTransportType = .automobile) async throws -> MKRoute? {
let request = MKDirections.Request()
request.source = source
request.destination = destination
request.transportType = transport
let directions = MKDirections(request: request)
let response = try await directions.calculate()
return response.routes.first
}
Mostrar Ruta en el Mapa
@State private var route: MKRoute?
Map {
if let route {
MapPolyline(route.polyline)
.stroke(.blue, lineWidth: 5)
}
Marker("Inicio", coordinate: startCoord)
Marker("Fin", coordinate: endCoord)
}
.task {
route = try? await getDirections(from: startItem, to: endItem)
}
Cálculo de ETA
func getETA(from source: MKMapItem, to destination: MKMapItem) async throws -> TimeInterval {
let request = MKDirections.Request()
request.source = source
request.destination = destination
let directions = MKDirections(request: request)
let response = try await directions.calculateETA()
return response.expectedTravelTime
}
Direcciones para Ciclistas (iOS 26+)
@available(iOS 26, *)
func getCyclingDirections(to destination: MKMapItem) async throws -> MKRoute? {
let request = MKDirections.Request()
request.source = MKMapItem.forCurrentLocation()
request.destination = destination
request.transportType = .cycling
let directions = MKDirections(request: request)
let response = try await directions.calculate()
return response.routes.first
}
PlaceDescriptor (iOS 26+)
Crea referencias de lugar enriquecidas a partir de coordenadas o direcciones sin necesidad de un ID de lugar. Requiere import GeoToolbox.
@available(iOS 26, *)
func lookupPlace(name: String, coordinate: CLLocationCoordinate2D) async throws -> MKMapItem {
let descriptor = PlaceDescriptor(
representations: [.coordinate(coordinate)],
commonName: name
)
let request = MKMapItemRequest(placeDescriptor: descriptor)
return try await request.mapItem
}
Errores Comunes
NO: Solicites .authorizedAlways por adelantado: los usuarios desconfían de los permisos amplios.
SÍ: Comienza con .requestWhenInUseAuthorization(), escala a .always solo cuando el usuario active una función en segundo plano.
NO: Uses CLLocationManagerDelegate para obtener ubicaciones simples en iOS 17+.
SÍ: Usa el flujo asíncrono CLLocationUpdate.liveUpdates() para un código más limpio y conciso.
NO: Mantengas las actualizaciones de ubicación ejecutándose cuando el mapa/vista no es visible (drena la batería).
SÍ: Usa .task { } en SwiftUI para que las actualizaciones se cancelen automáticamente al desaparecer.
NO: Desenvuelvas forzosamente las propiedades de CLPlacemark (todas son opcionales).
SÍ: Usa coalescencia nil: placemark.locality ?? "Desconocido".
NO: Disparar MKLocalSearch con cada pulsación de tecla; satura el límite de la API.
SÍ: Debounce con Combine en al menos 300ms.
NO: Dibujar una ruta sin manejar errores en el cálculo de direcciones (carreteras cortadas, sin ruta, etc.).
SÍ: Manejar MKDirections.Error y mostrar un mensaje al usuario.
NO: Ignorar el argumento fullAccuracyPurposeKey en CLServiceSession.
SÍ: Proporcionar una clave de propósito definida en Info.plist en NSLocationTemporaryUsageDescriptionDictionary.
NO: Depender de la segmentación de carreteras de MKRoute (puede no coincidir con el camino real).
SÍ: Para navegación paso a paso, usa MKDirections.calculateETA() en su lugar.
Lista de Verificación
Fundamentos de la Vista de Mapa
- ¿Se usan vistas SwiftUI
Map(iOS 17+) en lugar deMKMapViewmedianteUIViewRepresentable? - ¿Se utiliza
MapContentBuilderpara marcadores, anotaciones y superposiciones? - ¿Se usan
MarkeryAnnotationadecuadamente? (Marker para etiqueta simple, Annotation para vistas personalizadas) - ¿Están
mapControlsagregados (brújula, escala, botón de ubicación)? - ¿Se configuró
mapStyle(estándar, imagen, híbrido, elevación)? - ¿Se está pasando
MapCameraPositionpara control programático de la cámara? - ¿Están los modos de interacción restringidos cuando se desea?
Autorización y Privacidad
- ¿Están
NSLocationWhenInUseUsageDescription(y.alwayssi aplica) enInfo.plist? - ¿Están las descripciones de autorización escritas para el usuario final?
- ¿Se usa
CLServiceSessionen iOS 18+ para definir los requisitos de autorización? - ¿Se guía al usuario a Configuración cuando se deniega la autorización de ubicación?
- ¿Se solicita
.alwayssolo cuando el usuario activa una función en segundo plano?
Rendimiento de la Ubicación
- ¿Se usan
CLLocationUpdate.liveUpdates()en unTasken lugar del delegado? - ¿Se filtran las actualizaciones por
horizontalAccuracyantes de actualizar la UI? - ¿Se detiene el rastreo de ubicación cuando la vista desaparece?
- ¿Se usan
desiredAccuracyyactivityTypecuando se crea unCLServiceSession? - ¿Se evita la actualización de la UI a 60fps para actualizaciones de ubicación? (20fps es suficiente)
Búsqueda y Autocompletado
- ¿Se debouncea la entrada de búsqueda (mín. 300ms) antes de establecer
queryFragment? - ¿Se configuró
resultTypesenMKLocalSearchCompleter(.address,.pointOfInterest, etc.)? - ¿Se maneja la superación del límite de tasa de
MKLocalSearch? - ¿Se filtra
regionen las solicitudes de búsqueda geográfica? - ¿Se muestran en el mapa los resultados como marcadores después de la selección?
Direcciones y Enrutamiento
- ¿Se verificó que el origen/destino existe antes de calcular las direcciones?
- ¿Se configuró
transportType(automóvil, caminar, transporte público, bicicleta) según corresponda? - ¿Se manejan los casos de error (sin ruta, restricciones de tráfico, etc.)?
- ¿Se limita la línea de ruta al ancho y color adecuados?
- ¿Se usa
calculateETA()en lugar decalculate()si solo se necesita el tiempo de viaje?
Precisión y Confiabilidad
- ¿Se manejan los opcionales de
CLPlacemarkde forma segura con coalescencia nil? - ¿Se proporciona una alternativa cuando la geocodificación falla o no devuelve resultados?
- ¿Están las escalas del mapa limitadas a un rango razonable?
- ¿Se usa
MKMapItemRequestconPlaceDescriptoren lugar de geocodificación para coordenadas? - ¿Se realizan pruebas de la función en diferentes configuraciones regionales y tipos de datos de ubicación?
Geocercas (usando CLMonitor)
- ¿Se usa
CLMonitoren lugar del obsoletoCLCircularRegion+startMonitoring(for:)? - ¿Se llama
CLMonitor.StreamedEventspara eventos de geocerca en vivo? - ¿Se maneja la autorización de ubicación
.alwayspara la entrega de eventos en segundo plano? - ¿Se define un radio mínimo razonable (al menos 100m)?
- ¿Se limpian las geocercas cuando la función ya no está en uso?
Referencias
- WWDC 2023: Meet MapKit for SwiftUI
- WWDC 2024: What's new in MapKit
- WWDC 2024: Meet CoreLocation Update Streams
- MapKit | Apple Developer Documentation
- CoreLocation | Apple Developer Documentation
- Human Interface Guidelines: Location
Ver también: references/mapkit-patterns.md para patrones extendidos de MapKit y references/mapkit-corelocation-patterns.md para patrones de CoreLocation.