mapkit

mapkit

热门

使用 MapKit 和 CoreLocation 在 iOS/macOS 应用中实现、审查或优化地图与定位功能。适用于处理地图视图(Map views)、标注(annotations)、标记(markers)、折线(polylines)、用户位置追踪、地理编码(geocoding)、反向地理编码(reverse geocoding)、搜索与自动补全(search/autocomplete)、路线规划与导航(directions and routes)、地理围栏(geofencing)、区域监测(region monitoring)、CLLocationUpdate 异步流或位置授权流程。同样适用于在 Swift 应用中开发地图、坐标、地址、地点、路线规划、距离计算或基于位置的相关功能。

961Star
48Fork
更新于 2026/7/31
SKILL.md
只读
名称
mapkit
描述

使用 MapKit 和 CoreLocation 在 iOS/macOS 应用中实现、审查或优化地图与定位功能。适用于处理地图视图(Map views)、标注(annotations)、标记(markers)、折线(polylines)、用户位置追踪、地理编码(geocoding)、反向地理编码(reverse geocoding)、搜索与自动补全(search/autocomplete)、路线规划与导航(directions and routes)、地理围栏(geofencing)、区域监测(region monitoring)、CLLocationUpdate 异步流或位置授权流程。同样适用于在 Swift 应用中开发地图、坐标、地址、地点、路线规划、距离计算或基于位置的相关功能。

MapKit

基于 SwiftUI MapKit 和现代 CoreLocation 异步 API 构建适配 iOS 17+ 的地图与位置感知功能。视图层面推荐结合 MapContentBuilder 使用 Map,位置流监听使用 CLLocationUpdate.liveUpdates(),地理围栏使用 CLMonitor

当需要完整的地图配置、搜索、路线规划、Look Around(街景)、快照生成或 iOS 26 Place API 时,请参阅 references/mapkit-patterns.md。当任务涉及位置更新生命周期、地理围栏、后台定位、测试或隐私权限配置时,请参阅 references/mapkit-corelocation-patterns.md

目录

开发流程

1. 添加带有标记或标注的地图

  1. 导入 MapKit
  2. 创建 Map 视图,可选绑定 MapCameraPosition
  3. MapContentBuilder 闭包内添加 MarkerAnnotationMapPolylineMapPolygonMapCircle
  4. 使用 .mapStyle() 配置地图样式。
  5. 使用 .mapControls { } 添加地图控件。
  6. 使用 selection: 绑定处理选中事件。

2. 追踪用户位置

  1. 在 Info.plist 中添加 NSLocationWhenInUseUsageDescription 权限说明。
  2. 在 iOS 18+ 上,创建 CLServiceSession 来管理权限授权。
  3. Task 中异步遍历 CLLocationUpdate.liveUpdates()
  4. 更新 UI 前,先按距离或精度过滤定位数据。
  5. 当不再需要位置追踪时,及时停止该任务。

3. 搜索地点

  1. 配置 MKLocalSearchCompleter 以实现自动补全建议。
  2. 在设置查询词之前,对用户输入进行防抖处理(至少 300ms)。
  3. 将选中的补全项转换为 MKLocalSearch.Request 以获取完整结果。
  4. 将结果以标记(markers)形式显示在地图上或展示在列表中。

4. 获取导航与路线展示

  1. 使用起点和终点的 MKMapItem 创建 MKDirections.Request
  2. 设置交通路线类型 transportType(如 .automobile.walking.transit.cycling)。
  3. await 调用 MKDirections.calculate()
  4. 使用 MapPolyline(route.polyline) 绘制路线。

5. 审查现有地图/定位代码

参考本文末尾的审查清单逐项检查。

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()
        }
    }
}

Marker 与 Annotation

// 气泡标记 -- 在坐标处标注位置最简单的方式
Marker("Cafe", systemImage: "cup.and.saucer.fill", coordinate: cafeCoord)
    .tint(.brown)

// Annotation -- 指定坐标处的自定义 SwiftUI 视图
Annotation("You", coordinate: userCoord, anchor: .bottom) {
    Image(systemName: "figure.wave")
        .padding(6)
        .background(.blue.gradient, in: .circle)
        .foregroundStyle(.white)
}

图层覆盖物:折线、多边形、圆环

Map {
    // 根据坐标绘制折线
    MapPolyline(coordinates: routeCoords)
        .stroke(.blue, lineWidth: 4)

    // 多边形(区域高亮)
    MapPolygon(coordinates: parkBoundary)
        .foregroundStyle(.green.opacity(0.3))
        .stroke(.green, lineWidth: 2)

    // 圆形(围绕某点的半径区域)
    MapCircle(center: storeCoord, radius: 500)
        .foregroundStyle(.red.opacity(0.15))
        .stroke(.red, lineWidth: 1)
}

视角位置(Camera Position)

MapCameraPosition 决定了地图的展示范围。将其绑定后既能允许用户交互,也能通过代码动态移动镜头。

// 以特定区域为中心
@State private var position: MapCameraPosition = .region(
    MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 37.334, longitude: -122.009),
        span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
    )
)

// 追踪用户当前位置
@State private var position: MapCameraPosition = .userLocation(fallback: .automatic)

// 特定镜头角度(3D 视角)
@State private var position: MapCameraPosition = .camera(
    MapCamera(centerCoordinate: applePark, distance: 1000, heading: 90, pitch: 60)
)

// 聚焦到指定项目
position = .item(MKMapItem.forCurrentLocation())
position = .rect(MKMapRect(...))

地图样式

默认建议使用 .standard;仅在功能确实需要时才选择 .imagery(卫星图)、.hybrid(混合图)、逼真高程、交通状况及兴趣点过滤。详见 完整地图视图设置

地图交互模式

交互式地图请保持 .all。仅在需要有意识协调手势时才限制模式;如果是静态嵌入式地图,则传入 []。详见 列表或 ScrollView 中的地图

地图选中项绑定

@State private var selectedMarker: MKMapItem?

Map(selection: $selectedMarker) {
    ForEach(places) { place in
        Marker(place.name, coordinate: place.coordinate)
            .tag(place.mapItem)     // Tag 类型必须与 selection 绑定的类型一致
    }
}
.onChange(of: selectedMarker) { _, newValue in
    guard let item = newValue else { return }
    // 响应选中操作
}

CoreLocation 现代 API

CLLocationUpdate.liveUpdates() (iOS 17+)

使用单条异步序列替代传统的 CLLocationManagerDelegate 回调。每次迭代均会返回一个包含可选 CLLocationCLLocationUpdate。在 iOS 18+ 上,对于诸如权限被拒绝、系统定位服务未开启、定位不可用或未满足使用时前台条件等诊断状态,应当提供清晰可见的降级兜底路径,而不是无限期静默等待。建议保存该 Task 以便在功能结束时进行取消,并在驱动地图 UI 或后台任务前过滤掉无效、低精度、过期或不可用的位置移动数据。

import CoreLocation

@MainActor
@Observable
final class LocationTracker {
    var currentLocation: CLLocation?
    private var updateTask: Task<Void, Never>?

    func startTracking() {
        updateTask = Task {
            do {
                let updates = CLLocationUpdate.liveUpdates()
                for try await update in updates {
                    guard let location = update.location else { continue }
                    // 按水平精度过滤
                    guard location.horizontalAccuracy >= 0,
                          location.horizontalAccuracy < 50 else { continue }
                    currentLocation = location
                }
            } catch is CancellationError {
                // 停止追踪时的预期行为
            } catch {
                currentLocation = nil
            }
        }
    }

    func stopTracking() {
        updateTask?.cancel()
        updateTask = nil
    }
}

CLServiceSession (iOS 18+)

在功能的生命周期内声明定位授权需求。只要需要使用定位服务,就请保持对 session 的强引用。

// 使用使用期间(When-in-use)授权并指定最高精度选项
let session = CLServiceSession(
    authorization: .whenInUse,
    fullAccuracyPurposeKey: "NearbySearchPurpose"
)
// 将 `session` 保存为存储属性,使用完毕后释放

在 iOS 18+ 上,如果你未显式创建 CLServiceSessionCLLocationUpdate.liveUpdates()CLMonitor 会隐式创建一个。但当你需要 .always(始终授权)或完全精确度时,仍需显式创建。

授权流程

// Info.plist 必须配置的键名:
// NSLocationWhenInUseUsageDescription
// NSLocationAlwaysAndWhenInUseUsageDescription(仅在需要 .always 时配置)

// 检查授权状态;若被拒绝则引导用户前往“设置”
struct LocationPermissionView: View {
    @Environment(\.openURL) private var openURL

    var body: some View {
        ContentUnavailableView {
            Label("Location Access Denied", systemImage: "location.slash")
        } description: {
            Text("Enable location access in Settings to use this feature.")
        } actions: {
            Button("Open Settings") {
                if let url = URL(string: UIApplication.openSettingsURLString) {
                    openURL(url)
                }
            }
        }
    }
}

地理编码

CLGeocoder (iOS 8+)

let geocoder = CLGeocoder()

// 正向地理编码:地址字符串 -> 坐标
let placemarks = try await geocoder.geocodeAddressString("1 Apple Park Way, Cupertino")
if let location = placemarks.first?.location {
    print(location.coordinate) // CLLocationCoordinate2D
}

// 反向地理编码:坐标 -> 地标对象
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 与 MKReverseGeocodingRequest (iOS 26+)

MapKit 原生的全新地理编码 API,可返回包含更丰富数据的 MKMapItem 以及用于灵活格式化地址的 MKAddress / MKAddressRepresentations

@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
}

搜索

MKLocalSearchCompleter(自动补全)

@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(完整搜索)

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
}

// 在特定区域内按自然语言查询词进行搜索
func searchNearby(query: String, region: MKCoordinateRegion) async throws -> [MKMapItem] {
    let request = MKLocalSearch.Request()
    request.naturalLanguageQuery = query
    request.region = region
    let search = MKLocalSearch(request: req

<!-- truncated for translation batch; full body continues in source -->