mcpField
M
mapkit-location

mapkit-location

github.com/dpearson2699开发1.2k461 次安装13 周前更新
代码仓库

关于 mapkit-location

mapkit-location 是用于开发地图与位置功能的 Swift 技能,采用 SwiftUI MapKit 和现代 CoreLocation API。它为开发者提供地图、位置跟踪、地理编码、地点搜索、路线规划及代码审查指引。

主要功能

  1. 01带标记与覆盖层的 SwiftUI 地图
  2. 02异步位置跟踪与地理围栏
  3. 03地址地理编码与地点搜索
  4. 04路线计算与地图展示
  5. 05实现检查清单与常见问题

适用场景

  1. 01构建带地点标记的地图
  2. 02在视图活动期间跟踪用户位置
  3. 03通过自动补全搜索地点
  4. 04计算并显示出行路线

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dpearson2699/swift-ios-skills --skill mapkit-location

安装后在 Claude Code 中输入/mapkit-location确认已加载。

附带文件references/corelocation-patterns.mdreferences/mapkit-patterns.md

详细介绍

MapKit 和 CoreLocation

使用 SwiftUI MapKit 和现代 CoreLocation 异步 API,为 iOS 17 及以上版本构建基于地图和位置感知的功能。使用 Map 和 MapContentBuilder 构建视图,使用 CLLocationUpdate.liveUpdates() 流式获取位置更新,并使用 CLMonitor 实现地理围栏。

如需了解扩展的 MapKit 模式,请参阅 references/mapkit-patterns.md;如需了解 CoreLocation 模式,请参阅 references/corelocation-patterns.md。

目录

工作流

1. 添加带标记或注释的地图
  1. 导入 MapKit。
  2. 创建一个 Map 视图,可选绑定 MapCameraPosition。
  3. 在 MapContentBuilder 闭包中添加 Marker、Annotation、MapPolyline、MapPolygon 或 MapCircle。
  4. 使用 .mapStyle() 配置地图样式。
  5. 使用 .mapControls { } 添加地图控件。
  6. 使用 selection: 绑定处理选择。
2. 跟踪用户位置
  1. 在 Info.plist 中添加 NSLocationWhenInUseUsageDescription。
  2. 在 iOS 18 及以上版本中,创建 CLServiceSession 来管理授权。
  3. 在 Task 中迭代 CLLocationUpdate.liveUpdates()。
  4. 更新界面前,按距离或精度筛选更新。
  5. 不再需要位置跟踪时,停止任务。
3. 搜索地点
  1. 配置 MKLocalSearchCompleter 以提供自动补全建议。
  2. 在设置查询前,对用户输入进行防抖处理(至少 300ms)。
  3. 将选中的补全项转换为 MKLocalSearch.Request,以获取完整结果。
  4. 将结果显示为地图标记或列表。
4. 获取路线并显示路线
  1. 使用起点和终点 MKMapItem 创建 MKDirections.Request。
  2. 设置 transportType(.automobile、.walking、.transit、.cycling)。
  3. 等待 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()
        }
    }
}
标记和注释
// 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(...))
地图样式
.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
}

CoreLocation 现代 API

CLLocationUpdate.liveUpdates()(iOS 17+)

使用单个异步序列替代 CLLocationManagerDelegate 回调。每次迭代都会生成一个 CLLocationUpdate,其中包含可选的 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
    }
}
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 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+)

无需 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 状态,并显示带有“设置”深层链接的警告。

**不要:**假设地理编码总能成功——要处理结果为空和网络错误的情况。

审查清单

  • Info.plist 包含 NSLocationWhenInUseUsageDescription,并说明具体原因
  • 授权被拒绝时,会显示“设置”深层链接
  • 不再需要时会取消 CLLocationUpdate 任务(节省电量)
  • 位置精度适合使用场景
  • 地图标注使用带有稳定 ID 的 Identifiable 数据
  • 已处理地理编码错误(网络故障、无结果)
  • 搜索补全器的输入已实现防抖
  • CLMonitor 最多监控 20 个条件,并保持实例存活
  • 后台定位使用 CLBackgroundActivitySession
  • 已使用 VoiceOver 测试地图
  • 地图标注视图模型和位置 UI 更新均隔离在 @MainActor 上

参考资料

  • references/mapkit-patterns.md — 地图设置、标注、搜索、路线、聚类、Look Around、快照。
  • references/corelocation-patterns.md — CLLocationUpdate、CLMonitor、CLServiceSession、后台定位、测试。

评论

支持 Markdown