mapkit-location by dpearson2699/swift-ios-skills
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill mapkit-location使用 SwiftUI MapKit 和现代 CoreLocation 异步 API,为 iOS 17+ 构建基于地图和位置感知的功能。使用 Map 配合 MapContentBuilder 来构建视图,使用 CLLocationUpdate.liveUpdates() 来流式获取位置,使用 CLMonitor 进行地理围栏。
有关扩展的 MapKit 模式,请参阅 references/mapkit-patterns.md;有关 CoreLocation 模式,请参阅 references/corelocation-patterns.md。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
MapKit。MapCameraPosition 绑定的 Map 视图。MapContentBuilder 闭包内添加 Marker、Annotation、MapPolyline、MapPolygon 或 MapCircle。.mapStyle() 配置地图样式。.mapControls { } 添加地图控件。selection: 绑定处理选择。NSLocationWhenInUseUsageDescription。CLServiceSession 来管理授权。Task 中遍历 CLLocationUpdate.liveUpdates()。MKLocalSearchCompleter 以获取自动补全建议。MKLocalSearch.Request 以获取完整结果。MKMapItem 创建 MKDirections.Request。transportType (.automobile、.walking、.transit、.cycling)。MKDirections.calculate()。MapPolyline(route.polyline) 绘制路线。运行本文件末尾的审查清单。
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("Cafe", systemImage: "cup.and.saucer.fill", coordinate: cafeCoord)
.tint(.brown)
// 标注 —— 在坐标处显示自定义 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)
}
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(...))
.mapStyle(.standard) // 默认道路地图
.mapStyle(.standard(elevation: .realistic, showsTraffic: true))
.mapStyle(.imagery) // 卫星图
.mapStyle(.imagery(elevation: .realistic)) // 3D 卫星图
.mapStyle(.hybrid) // 卫星图 + 标签
.mapStyle(.hybrid(elevation: .realistic, showsTraffic: true))
.mapInteractionModes(.all) // 默认:平移、缩放、旋转、倾斜
.mapInteractionModes(.pan) // 仅平移
.mapInteractionModes([.pan, .zoom]) // 平移和缩放
.mapInteractionModes([]) // 静态地图(无交互)
@State private var selectedMarker: MKMapItem?
Map(selection: $selectedMarker) {
ForEach(places) { place in
Marker(place.name, coordinate: place.coordinate)
.tag(place.mapItem) // 标签必须与选择类型匹配
}
}
.onChange(of: selectedMarker) { _, newValue in
guard let item = newValue else { return }
// 响应选择事件
}
使用单个异步序列替代 CLLocationManagerDelegate 回调。每次迭代产生一个包含可选 CLLocation 的 CLLocationUpdate。
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 }
// 按水平精度过滤
guard location.horizontalAccuracy < 50 else { continue }
await MainActor.run {
self.currentLocation = location
}
}
}
}
func stopTracking() {
updateTask?.cancel()
updateTask = nil
}
}
声明功能生命周期内的授权要求。在需要位置服务的整个期间,保持对会话的引用。
// 使用时授权,并偏好完全精度
let session = CLServiceSession(
authorization: .whenInUse,
fullAccuracyPurposeKey: "NearbySearchPurpose"
)
// 将 `session` 作为存储属性持有;完成后释放。
在 iOS 18+ 上,如果你没有显式创建一个 CLServiceSession,CLLocationUpdate.liveUpdates() 和 CLMonitor 会使用一个隐式的 CLServiceSession。当你需要 .always 授权或完全精度时,请显式创建一个。
// Info.plist 键(必需):
// NSLocationWhenInUseUsageDescription
// NSLocationAlwaysAndWhenInUseUsageDescription(仅在需要 .always 时)
// 检查授权状态,并在被拒绝时引导用户前往设置
struct LocationPermissionView: View {
@Environment(\.openURL) private var openURL
var body: some View {
ContentUnavailableView {
Label("位置访问被拒绝", systemImage: "location.slash")
} description: {
Text("请在设置中启用位置访问以使用此功能。")
} actions: {
Button("打开设置") {
if let url = URL(string: UIApplication.openSettingsURLString) {
openURL(url)
}
}
}
}
}
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: ", ")
}
新的 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
}
@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 = []
}
}
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: 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
}
@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
}
无需 Place ID,即可从坐标或地址创建丰富的地点引用。需要 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
}
不要: 一开始就请求 .authorizedAlways —— 用户不信任宽泛的权限。应该: 从 .requestWhenInUseAuthorization() 开始,仅在用户启用后台功能时才升级到 .always。
不要: 在 iOS 17+ 上使用 CLLocationManagerDelegate 进行简单的位置获取。应该: 使用 CLLocationUpdate.liveUpdates() 异步流,代码更清晰、更简洁。
不要: 在地图/视图不可见时仍保持位置更新运行(耗电)。应该: 在 SwiftUI 中使用 .task { },以便在视图消失时自动取消更新。
不要: 强制解包 CLPlacemark 属性 —— 它们都是可选的。应该: 使用空值合并:placemark.locality ?? "Unknown"。
不要: 每次按键都触发 MKLocalSearchCompleter 查询。应该: 使用 .task(id: searchText) + Task.sleep(for: .milliseconds(300)) 进行防抖。
不要: 当位置授权被拒绝时静默失败。应该: 检测 .denied 状态并显示带有设置深层链接的提示。
不要: 假设地理编码总是成功 —— 处理空结果和网络错误。
NSLocationWhenInUseUsageDescriptionCLLocationUpdate 任务在不需要时已取消(省电)Identifiable 数据CLMonitor 限制在 20 个条件内,实例保持存活CLBackgroundActivitySession@MainActorreferences/mapkit-patterns.md — 地图设置、标注、搜索、路线、聚类、Look Around、快照。references/corelocation-patterns.md — CLLocationUpdate、CLMonitor、CLServiceSession、后台位置、测试。每周安装量
382
代码仓库
GitHub 星标数
269
首次出现
2026年3月3日
安全审计
安装于
codex378
kimi-cli375
amp375
cline375
github-copilot375
opencode375
Build map-based and location-aware features targeting iOS 17+ with SwiftUI MapKit and modern CoreLocation async APIs. Use Map with MapContentBuilder for views, CLLocationUpdate.liveUpdates() for streaming location, and CLMonitor for geofencing.
See references/mapkit-patterns.md for extended MapKit patterns and references/corelocation-patterns.md for CoreLocation patterns.
MapKit.Map view with optional MapCameraPosition binding.Marker, Annotation, MapPolyline, MapPolygon, or MapCircle inside the MapContentBuilder closure..mapStyle()..mapControls { }.NSLocationWhenInUseUsageDescription to Info.plist.CLServiceSession to manage authorization.CLLocationUpdate.liveUpdates() in a Task.MKLocalSearchCompleter for autocomplete suggestions.MKLocalSearch.Request for full results.MKDirections.Request with source and destination MKMapItem.transportType (.automobile, .walking, .transit, .cycling).MKDirections.calculate().MapPolyline(route.polyline).Run through the Review Checklist at the end of this file.
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 controls what the map displays. Bind it to let the user interact and to programmatically move the camera.
// 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(...))
.mapStyle(.standard) // Default road map
.mapStyle(.standard(elevation: .realistic, showsTraffic: true))
.mapStyle(.imagery) // Satellite
.mapStyle(.imagery(elevation: .realistic)) // 3D satellite
.mapStyle(.hybrid) // Satellite + labels
.mapStyle(.hybrid(elevation: .realistic, showsTraffic: true))
.mapInteractionModes(.all) // Default: pan, zoom, rotate, pitch
.mapInteractionModes(.pan) // Pan only
.mapInteractionModes([.pan, .zoom]) // Pan and zoom
.mapInteractionModes([]) // Static map (no interaction)
@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
}
Replace CLLocationManagerDelegate callbacks with a single async sequence. Each iteration yields a CLLocationUpdate containing an optional CLLocation.
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 }
// Filter by horizontal accuracy
guard location.horizontalAccuracy < 50 else { continue }
await MainActor.run {
self.currentLocation = location
}
}
}
}
func stopTracking() {
updateTask?.cancel()
updateTask = nil
}
}
Declare authorization requirements for a feature's lifetime. Hold a reference to the session for as long as you need location services.
// 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.
On iOS 18+, CLLocationUpdate.liveUpdates() and CLMonitor take an implicit CLServiceSession if you do not create one explicitly. Create one explicitly when you need .always authorization or full accuracy.
// 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)
}
}
}
}
}
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: ", ")
}
New MapKit-native geocoding that returns MKMapItem with richer data and MKAddress / MKAddressRepresentations for flexible address formatting.
@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
}
@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 = []
}
}
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
}
@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
}
Create rich place references from coordinates or addresses without needing a Place ID. Requires 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
}
DON'T: Request .authorizedAlways upfront — users distrust broad permissions. DO: Start with .requestWhenInUseAuthorization(), escalate to .always only when the user enables a background feature.
DON'T: Use CLLocationManagerDelegate for simple location fetches on iOS 17+. DO: Use CLLocationUpdate.liveUpdates() async stream for cleaner, more concise code.
DON'T: Keep location updates running when the map/view is not visible (drains battery). DO: Use .task { } in SwiftUI so updates cancel automatically on disappear.
DON'T: Force-unwrap CLPlacemark properties — they are all optional. DO: Use nil-coalescing: placemark.locality ?? "Unknown".
DON'T: Fire MKLocalSearchCompleter queries on every keystroke. DO: Debounce with .task(id: searchText) + Task.sleep(for: .milliseconds(300)).
DON'T: Silently fail when location authorization is denied. DO: Detect .denied status and show an alert with a Settings deep link.
DON'T: Assume geocoding always succeeds — handle empty results and network errors.
NSLocationWhenInUseUsageDescription with specific reasonCLLocationUpdate task cancelled when not needed (battery)Identifiable data with stable IDsCLMonitor limited to 20 conditions, instance kept aliveCLBackgroundActivitySession@MainActor-isolatedreferences/mapkit-patterns.md — Map setup, annotations, search, routes, clustering, Look Around, snapshots.references/corelocation-patterns.md — CLLocationUpdate, CLMonitor, CLServiceSession, background location, testing.Weekly Installs
382
Repository
GitHub Stars
269
First Seen
Mar 3, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex378
kimi-cli375
amp375
cline375
github-copilot375
opencode375
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
105,000 周安装
selection: binding.