mcpField
M
mapkit

mapkit

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

关于 mapkit

MapKit 是面向 SwiftUI 开发者的地图与定位功能模式指南,介绍 iOS 17+ 的 MapKit 和现代 CoreLocation 异步 API。内容包括地图标注、位置更新、地点搜索、地理编码及路线规划。

主要功能

  1. 01地图与标注、覆盖图
  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

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

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

evals/
evals.json
references/
mapkit-corelocation-patterns.md
mapkit-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. 添加带标记或注释的地图
  1. 导入 MapKit。
  2. 创建一个 Map 视图,可选绑定 MapCameraPosition。
  3. 在 MapContentBuilder 闭包中添加 Marker、Annotation、MapPolyline、MapPolygon 或 MapCircle。
  4. 使用 .mapStyle() 配置地图样式。
  5. 使用 .mapControls { } 添加地图控件。
  6. 使用 selection: 绑定处理选择。
2. 跟踪用户位置
  1. 将 NSLocationWhenInUseUsageDescription 添加到 Info.plist。
  2. 在 iOS 18+ 上,创建 CLServiceSession 来管理授权。
  3. 在 Task 中遍历 CLLocationUpdate.liveUpdates()。
  4. 更新 UI 前,按距离或精度筛选更新。
  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(...))
地图样式

默认使用 .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

参考资料

评论

支持 Markdown