MapKit con SwiftUI: Map, Marker, Annotation, cameraBounds y búsqueda de lugares

MapKit en SwiftUI recibió una renovación completa con iOS 17. La nueva API declarativa con Map, Marker, Annotation y los overlays integrados permite construir mapas ricos en pocas líneas, sin necesidad de recurrir a UIViewRepresentable para la mayoría de casos de uso.

Map con MapCameraPosition

El contenedor principal es Map. Para controlar la región visible y la cámara, se usa MapCameraPosition:

import MapKit
import SwiftUI

struct MapaBasico: View {
    @State private var posicion: MapCameraPosition = .region(
        MKCoordinateRegion(
            center: CLLocationCoordinate2D(latitude: 40.4168, longitude: -3.7038),
            span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
        )
    )

    var body: some View {
        Map(position: $posicion)
            .ignoresSafeArea()
    }
}

Otras opciones de posición: .camera(MapCamera(centerCoordinate:distance:heading:pitch:)) para una cámara 3D, .automatic para que MapKit decida y .userLocation(fallback:) para centrar en el usuario.

Marker y Annotation: marcadores personalizados

Marker muestra un pin estándar del sistema con un icono opcional. Annotation permite una vista SwiftUI completamente personalizada:

struct PuntoInteres: Identifiable {
    let id = UUID()
    let nombre: String
    let coordenadas: CLLocationCoordinate2D
    let categoria: String
}

struct MapaPOI: View {
    let puntos: [PuntoInteres]
    @State private var posicion: MapCameraPosition = .automatic

    var body: some View {
        Map(position: $posicion) {
            ForEach(puntos) { punto in
                // Marker: pin estándar con título e icono SF Symbol
                Marker(punto.nombre, systemImage: iconoPara(punto.categoria),
                       coordinate: punto.coordenadas)
                    .tint(colorPara(punto.categoria))

                // O Annotation para vista personalizada
                Annotation(punto.nombre, coordinate: punto.coordenadas) {
                    VStack(spacing: 0) {
                        Image(systemName: iconoPara(punto.categoria))
                            .padding(8)
                            .background(.white)
                            .clipShape(Circle())
                            .shadow(radius: 4)
                        Image(systemName: "triangle.fill")
                            .font(.caption)
                            .foregroundStyle(.white)
                            .rotationEffect(.degrees(180))
                            .offset(y: -3)
                    }
                }
            }
        }
    }

    func iconoPara(_ categoria: String) -> String {
        switch categoria {
        case "restaurante": return "fork.knife"
        case "museo": return "building.columns"
        default: return "mappin"
        }
    }
}

Overlays: MapCircle y MapPolyline

Para dibujar áreas o rutas sobre el mapa, iOS 17 añade MapCircle, MapPolygon y MapPolyline como contenido declarativo del Map:

Map {
    // Círculo de 500 metros de radio
    MapCircle(
        center: CLLocationCoordinate2D(latitude: 40.4168, longitude: -3.7038),
        radius: 500
    )
    .foregroundStyle(.blue.opacity(0.2))
    .stroke(.blue, lineWidth: 2)

    // Ruta como polilínea
    let puntos = ruta.map { CLLocationCoordinate2D(latitude: $0.lat, longitude: $0.lon) }
    MapPolyline(coordinates: puntos)
        .stroke(.red, style: StrokeStyle(lineWidth: 4, lineCap: .round, lineJoin: .round))
}

Búsqueda de lugares: MKLocalSearch

MKLocalSearch permite buscar puntos de interés por texto o categoría:

func buscarLugares(texto: String, region: MKCoordinateRegion) async -> [MKMapItem] {
    let peticion = MKLocalSearch.Request()
    peticion.naturalLanguageQuery = texto
    peticion.region = region
    peticion.resultTypes = [.pointOfInterest, .address]

    do {
        let respuesta = try await MKLocalSearch(request: peticion).start()
        return respuesta.mapItems
    } catch {
        print("Error en búsqueda: (error)")
        return []
    }
}

// Buscar por categoría (iOS 18+)
let peticionCategoria = MKLocalSearch.Request()
peticionCategoria.pointOfInterestFilter = MKPointOfInterestFilter(including: [.restaurant, .cafe])
peticionCategoria.region = regionActual

Look Around preview

Look Around es el equivalente de Apple a Street View. Se activa con MKLookAroundSceneRequest:

struct VistaLookAround: View {
    let coordenadas: CLLocationCoordinate2D
    @State private var escena: MKLookAroundScene?
    @State private var mostrar = false

    var body: some View {
        VStack {
            if let escena {
                LookAroundPreview(scene: $escena)
                    .frame(height: 200)
                    .cornerRadius(12)
            }
            Button("Ver en Look Around") { mostrar = true }
        }
        .task {
            let peticion = MKLookAroundSceneRequest(coordinate: coordenadas)
            escena = try? await peticion.scene
        }
        .lookAroundViewer(isPresented: $mostrar, scene: $escena)
    }
}

Selección de anotaciones

iOS 17 añade soporte nativo para selección de anotaciones con el parámetro selection:

@State private var seleccionado: PuntoInteres?

Map(position: $posicion, selection: $seleccionado) {
    ForEach(puntos) { punto in
        Marker(punto.nombre, coordinate: punto.coordenadas)
            .tag(punto)  // Necesario para que selection funcione
    }
}
.onChange(of: seleccionado) { _, nuevo in
    if let nuevo {
        print("Seleccionado: (nuevo.nombre)")
    }
}

Resumen

MapKit en iOS 17 con SwiftUI convierte en declarativo lo que antes requería delegate y UIViewRepresentable. Map con MapCameraPosition controla la vista; Marker y Annotation añaden puntos personalizados; MapCircle y MapPolyline dibujan overlays; MKLocalSearch busca lugares por texto o categoría; y Look Around añade inmersión visual con pocas líneas. Para la mayoría de apps de mapas, la nueva API cubre el cien por cien de los casos de uso sin salir de SwiftUI.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP