关于 mapkit
MapKit 是面向 SwiftUI 开发者的地图与定位功能模式指南,介绍 iOS 17+ 的 MapKit 和现代 CoreLocation 异步 API。内容包括地图标注、位置更新、地点搜索、地理编码及路线规划。
主要功能
- 01地图与标注、覆盖图
- 02异步位置更新与围栏
- 03地点自动补全与搜索
- 04地址地理编码
- 05多种交通方式的路线规划
适用场景
- 01在地图上展示地点与路线
- 02构建附近地点搜索
- 03追踪用户位置
- 04创建地理围栏功能
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill mapkit安装后在 Claude Code 中输入/mapkit确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonreferences/mapkit-corelocation-patterns.mdmapkit-patterns.md详细介绍
MapKit
使用 SwiftUI MapKit 和现代 CoreLocation 异步 API,为 iOS 17+ 构建基于地图和位置感知的功能。使用 Map 和 MapContentBuilder 创建视图,使用 CLLocationUpdate.liveUpdates() 流式获取位置,并使用 CLMonitor 实现地理围栏。
需要完整的地图设置、搜索、路线、Look Around、快照或 iOS 26 地点 API 时,请阅读 references/mapkit-patterns.md。任务涉及位置更新生命周期、地理围栏、后台位置、测试或隐私键时,请阅读 references/mapkit-corelocation-patterns.md。
目录
工作流
1. 添加带标记或注释的地图
- 导入
MapKit。 - 创建一个
Map视图,可选绑定MapCameraPosition。 - 在
MapContentBuilder闭包中添加Marker、Annotation、MapPolyline、MapPolygon或MapCircle。 - 使用
.mapStyle()配置地图样式。 - 使用
.mapControls { }添加地图控件。 - 使用
selection:绑定处理选择。
2. 跟踪用户位置
- 将
NSLocationWhenInUseUsageDescription添加到 Info.plist。 - 在 iOS 18+ 上,创建
CLServiceSession来管理授权。 - 在
Task中遍历CLLocationUpdate.liveUpdates()。 - 更新 UI 前,按距离或精度筛选更新。
- 不再需要位置跟踪时停止任务。
3. 搜索地点
- 配置
MKLocalSearchCompleter以获取自动补全建议。 - 设置查询前,对用户输入进行防抖处理(至少 300ms)。
- 将所选补全项转换为
MKLocalSearch.Request,以获取完整结果。 - 将结果显示为标记或列表。
4. 获取路线并显示路线
- 使用起点和终点
MKMapItem创建MKDirections.Request。 - 设置
transportType(.automobile、.walking、.transit、.cycling)。 - 等待
MKDirections.calculate()完成。 - 使用
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()
}
}
}
标记和注释
// Balloon marker -- simplest way to pin a location
Marker("Cafe", systemImage: "cup.and.saucer.fill", coordinate: cafeCoord)
.tint(.brown)
// Annotation -- custom SwiftUI view at a coordinate
Annotation("You", coordinate: userCoord, anchor: .bottom) {
Image(systemName: "figure.wave")
.padding(6)
.background(.blue.gradient, in: .circle)
.foregroundStyle(.white)
}
覆盖层:折线、多边形、圆形
Map {
// Polyline from coordinates
MapPolyline(coordinates: routeCoords)
.stroke(.blue, lineWidth: 4)
// Polygon (area highlight)
MapPolygon(coordinates: parkBoundary)
.foregroundStyle(.green.opacity(0.3))
.stroke(.green, lineWidth: 2)
// Circle (radius around a point)
MapCircle(center: storeCoord, radius: 500)
.foregroundStyle(.red.opacity(0.15))
.stroke(.red, lineWidth: 1)
}
相机位置
MapCameraPosition 控制地图的显示内容。将其绑定,以便用户与地图交互,也可以通过代码移动相机。
// Center on a region
@State private var position: MapCameraPosition = .region(
MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 37.334, longitude: -122.009),
span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
)
)
// Follow user location
@State private var position: MapCameraPosition = .userLocation(fallback: .automatic)
// Specific camera angle (3D perspective)
@State private var position: MapCameraPosition = .camera(
MapCamera(centerCoordinate: applePark, distance: 1000, heading: 90, pitch: 60)
)
// Frame specific items
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 must match selection type
}
}
.onChange(of: selectedMarker) { _, newValue in
guard let item = newValue else { return }
// React to selection
}
CoreLocation 现代 API
CLLocationUpdate.liveUpdates()(iOS 17+)
使用单个异步序列替代 CLLocationManagerDelegate 回调。每次迭代都会返回一个 CLLocationUpdate,其中包含可选的 CLLocation。在 iOS 18+ 上,遇到诊断状态(例如授权被拒、全局关闭定位服务、位置不可用或使用条件不足)时,应通过明确可见的降级路径进行处理,而不是无限期地静默等待。保存任务,以便该功能可以取消任务;在驱动地图 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 }
// Filter by horizontal accuracy
guard location.horizontalAccuracy >= 0,
location.horizontalAccuracy < 50 else { continue }
currentLocation = location
}
} catch is CancellationError {
// Expected when tracking stops.
} catch {
currentLocation = nil
}
}
}
func stopTracking() {
updateTask?.cancel()
updateTask = nil
}
}
CLServiceSession(iOS 18+)
声明某项功能在整个生命周期内的授权要求。只要需要使用定位服务,就应持有对会话的引用。
// When-in-use authorization with full accuracy preference
let session = CLServiceSession(
authorization: .whenInUse,
fullAccuracyPurposeKey: "NearbySearchPurpose"
)
// Hold `session` as a stored property; release it when done.
在 iOS 18+ 上,如果你没有显式创建 CLServiceSession,CLLocationUpdate.liveUpdates() 和 CLMonitor 会隐式使用一个。需要 .always 授权或完整精度时,请显式创建一个。
授权流程
// Info.plist keys (required):
// NSLocationWhenInUseUsageDescription
// NSLocationAlwaysAndWhenInUseUsageDescription (only if .always needed)
// Check authorization and guide user to Settings when denied
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()
// Forward geocoding: address string -> coordinates
let placemarks = try await geocoder.geocodeAddressString("1 Apple Park Way, Cupertino")
if let location = placemarks.first?.location {
print(location.coordinate) // CLLocationCoordinate2D
}
// Reverse geocoding: coordinates -> placemark
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 原生的新地理编码功能,可返回包含更多数据的 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
}
// Search by natural language query within a region
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
}
路线
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
}
在地图上显示路线
@State private var route: MKRoute?
Map {
if let route {
MapPolyline(route.polyline)
.stroke(.blue, lineWidth: 5)
}
Marker("Start", coordinate: startCoord)
Marker("End", coordinate: endCoord)
}
.task {
route = try? await getDirections(from: startItem, to: endItem)
}
预计到达时间计算
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
}
骑行路线(iOS 14+)
将 .cycling 用作请求的交通方式类型;请参阅完整的骑行路线示例。
PlaceDescriptor(iOS 26+)
无需 Place ID,即可根据坐标或地址创建丰富的地点引用。需要 import GeoToolbox。
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
}
常见错误
不要: 一开始就请求始终授权。
应该: 先使用使用期间授权。在 iOS 18+ 上,在功能的整个生命周期内持有 CLServiceSession;只有需要在终止后由系统重新启动的后台功能,才请求 .always。
不要: 在 iOS 17+ 上使用 CLLocationManagerDelegate 获取简单的位置。
应该: 使用 CLLocationUpdate.liveUpdates() 异步流,让代码更简洁清晰。
不要: 忽略 CLLocationUpdate 的诊断信息,例如访问被拒、系统级禁用或位置不可用。
应该: 停止或降级该功能,显示恢复界面(例如设置引导),并确保搜索和手动操作流程仍可使用。
不要: 在地图/视图消失后,仍让 liveUpdates() 在不受管理的任务中运行。
应该: 保存 Task,在功能停止时取消它,并过滤无效、不准确、过时或不可能的移动位置数据。
不要: 对 CLPlacemark 属性使用强制解包——它们全部都是可选值。
应该: 使用空值合并:placemark.locality ?? "Unknown"。
不要: 每次按键时都触发 MKLocalSearchCompleter 查询。
应该: 使用 .task(id: searchText) + Task.sleep(for: .milliseconds(300)) 进行防抖。
不要: 在位置授权被拒时悄无声息地失败。
应该: 检测 .denied 状态,并显示包含设置深层链接的提醒。
不要: 假定地理编码总能成功——应处理结果为空和网络错误的情况。
审查清单
- Info.plist 包含
NSLocationWhenInUseUsageDescription,并说明具体原因 - 授权被拒时提供设置深层链接
- 不再需要时取消
CLLocationUpdate任务(节省电池) - 位置精度适合具体用例
- 地图标注使用带稳定 ID 的
Identifiable数据 - 处理地理编码错误(网络故障、无结果)
- 对搜索补全器输入进行防抖
-
CLMonitor限制为 20 个条件,并保持实例存活 - 后台定位使用
CLBackgroundActivitySession - 使用 VoiceOver 测试地图
- 地图标注视图模型和位置 UI 更新均隔离于
@MainActor
参考资料
- references/mapkit-patterns.md — 地图设置、标注、搜索、路线、聚类、Look Around、快照。
- references/mapkit-corelocation-patterns.md — CLLocationUpdate、CLMonitor、CLServiceSession、后台定位、测试。