mapkit-location

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

MapKit and CoreLocation

MapKit和CoreLocation

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.
面向iOS 17+版本,结合SwiftUI、MapKit与现代CoreLocation异步API构建基于地图和具备位置感知能力的功能。使用带有
MapContentBuilder
Map
组件构建视图,通过
CLLocationUpdate.liveUpdates()
获取流式位置数据,使用
CLMonitor
实现地理围栏功能。
查看
references/mapkit-patterns.md
获取更多MapKit使用模式,查看
references/corelocation-patterns.md
获取CoreLocation使用模式。

Workflow

工作流

1. Add a map with markers or annotations

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

  1. Import
    MapKit
    .
  2. Create a
    Map
    view with optional
    MapCameraPosition
    binding.
  3. Add
    Marker
    ,
    Annotation
    ,
    MapPolyline
    ,
    MapPolygon
    , or
    MapCircle
    inside the
    MapContentBuilder
    closure.
  4. Configure map style with
    .mapStyle()
    .
  5. Add map controls with
    .mapControls { }
    .
  6. Handle selection with a
    selection:
    binding.
  1. 导入
    MapKit
  2. 创建带有可选
    MapCameraPosition
    绑定的
    Map
    视图。
  3. MapContentBuilder
    闭包中添加
    Marker
    Annotation
    MapPolyline
    MapPolygon
    MapCircle
  4. 使用
    .mapStyle()
    配置地图样式。
  5. 通过
    .mapControls { }
    添加地图控件。
  6. 使用
    selection:
    绑定处理选中事件。

2. Track user location

2. 追踪用户位置

  1. Add
    NSLocationWhenInUseUsageDescription
    to Info.plist.
  2. On iOS 18+, create a
    CLServiceSession
    to manage authorization.
  3. Iterate
    CLLocationUpdate.liveUpdates()
    in a
    Task
    .
  4. Filter updates by distance or accuracy before updating the UI.
  5. Stop the task when location tracking is no longer needed.
  1. 在Info.plist中添加
    NSLocationWhenInUseUsageDescription
    配置。
  2. 在iOS 18+系统上,创建
    CLServiceSession
    管理权限授权。
  3. Task
    中迭代
    CLLocationUpdate.liveUpdates()
    获取位置更新。
  4. 更新UI前按距离或精度过滤位置更新。
  5. 不再需要位置追踪时停止任务。

3. Search for places

3. 搜索地点

  1. Configure
    MKLocalSearchCompleter
    for autocomplete suggestions.
  2. Debounce user input (at least 300ms) before setting the query.
  3. Convert selected completion to
    MKLocalSearch.Request
    for full results.
  4. Display results as markers or in a list.
  1. 配置
    MKLocalSearchCompleter
    实现自动补全建议。
  2. 设置查询前对用户输入做防抖处理(至少300ms)。
  3. 将选中的补全结果转换为
    MKLocalSearch.Request
    获取完整搜索结果。
  4. 将结果以标记或列表形式展示。

4. Get directions and display a route

4. 获取路线并展示

  1. Create an
    MKDirections.Request
    with source and destination
    MKMapItem
    .
  2. Set
    transportType
    (
    .automobile
    ,
    .walking
    ,
    .transit
    ,
    .cycling
    ).
  3. Await
    MKDirections.calculate()
    .
  4. Draw the route with
    MapPolyline(route.polyline)
    .
  1. 使用起点和终点
    MKMapItem
    创建
    MKDirections.Request
  2. 设置
    transportType
    .automobile
    驾车、
    .walking
    步行、
    .transit
    公共交通、
    .cycling
    骑行)。
  3. 等待
    MKDirections.calculate()
    返回结果。
  4. 使用
    MapPolyline(route.polyline)
    绘制路线。

5. Review existing map/location code

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

Run through the Review Checklist at the end of this file.
按照本文件末尾的审查清单逐一检查。

SwiftUI Map View (iOS 17+)

SwiftUI Map视图(iOS 17+)

swift
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()
        }
    }
}
swift
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 and Annotation

Marker与Annotation

swift
// 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)
}
swift
// 气球标记——钉选位置的最简单方式
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)
}

Overlays: Polyline, Polygon, Circle

覆盖层:折线、多边形、圆形

swift
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)
}
swift
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
controls what the map displays. Bind it to let the user interact and to programmatically move the camera.
swift
// 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(...))
MapCameraPosition
控制地图展示的内容。通过绑定既可以让用户交互控制,也可以通过代码移动相机。
swift
// 居中展示某个区域
@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(...))

Map Style

地图样式

swift
.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))
swift
.mapStyle(.standard)                                        // 默认道路地图
.mapStyle(.standard(elevation: .realistic, showsTraffic: true))
.mapStyle(.imagery)                                         // 卫星图
.mapStyle(.imagery(elevation: .realistic))                  // 3D卫星图
.mapStyle(.hybrid)                                          // 卫星图+标注
.mapStyle(.hybrid(elevation: .realistic, showsTraffic: true))

Map Interaction Modes

地图交互模式

swift
.mapInteractionModes(.all)           // Default: pan, zoom, rotate, pitch
.mapInteractionModes(.pan)           // Pan only
.mapInteractionModes([.pan, .zoom])  // Pan and zoom
.mapInteractionModes([])             // Static map (no interaction)
swift
.mapInteractionModes(.all)           // 默认:平移、缩放、旋转、俯仰
.mapInteractionModes(.pan)           // 仅允许平移
.mapInteractionModes([.pan, .zoom])  // 允许平移和缩放
.mapInteractionModes([])             // 静态地图(无交互)

Map Selection

地图选中

swift
@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
}
swift
@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 }
    // 处理选中事件
}

CoreLocation Modern API

CoreLocation现代API

CLLocationUpdate.liveUpdates() (iOS 17+)

CLLocationUpdate.liveUpdates()(iOS 17+)

Replace
CLLocationManagerDelegate
callbacks with a single async sequence. Each iteration yields a
CLLocationUpdate
containing an optional
CLLocation
.
swift
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
    }
}
使用单个异步序列替代
CLLocationManagerDelegate
回调。每次迭代返回一个包含可选
CLLocation
CLLocationUpdate
对象。
swift
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
    }
}

CLServiceSession (iOS 18+)

CLServiceSession(iOS 18+)

Declare authorization requirements for a feature's lifetime. Hold a reference to the session for as long as you need location services.
swift
// 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.
声明功能生命周期内的权限要求。只要你需要使用位置服务,就需要持有该会话的引用。
swift
// 使用时授权,优先获取全精度
let session = CLServiceSession(
    authorization: .whenInUse,
    fullAccuracyPurposeKey: "NearbySearchPurpose"
)
// 将`session`作为存储属性持有;使用完毕后释放
在iOS 18+系统上,如果你没有显式创建
CLServiceSession
CLLocationUpdate.liveUpdates()
CLMonitor
会隐式获取一个。当你需要
.always
授权或全精度时,请显式创建。

Authorization Flow

授权流程

swift
// Info.plist keys (required):
// NSLocationWhenInUseUsageDescription
// NSLocationAlwaysAndWhenInUseUsageDescription (only if .always needed)

// Check authorization and guide user to Settings when denied
func checkAuthorization() {
    let manager = CLLocationManager()
    switch manager.authorizationStatus {
    case .notDetermined:
        manager.requestWhenInUseAuthorization()
    case .denied, .restricted:
        // Show alert with Settings deep link
        if let url = URL(string: UIApplication.openSettingsURLString) {
            UIApplication.shared.open(url)
        }
    case .authorizedWhenInUse, .authorizedAlways:
        break // Ready to use
    @unknown default:
        break
    }
}
swift
// Info.plist必填项:
// NSLocationWhenInUseUsageDescription
// NSLocationAlwaysAndWhenInUseUsageDescription(仅当需要.always权限时配置)

// 检查授权状态,权限被拒绝时引导用户前往设置
func checkAuthorization() {
    let manager = CLLocationManager()
    switch manager.authorizationStatus {
    case .notDetermined:
        manager.requestWhenInUseAuthorization()
    case .denied, .restricted:
        // 展示携带设置页 deep link 的弹窗
        if let url = URL(string: UIApplication.openSettingsURLString) {
            UIApplication.shared.open(url)
        }
    case .authorizedWhenInUse, .authorizedAlways:
        break // 可正常使用
    @unknown default:
        break
    }
}

Geocoding

地理编码

CLGeocoder (iOS 8+)

CLGeocoder(iOS 8+)

swift
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: ", ")
}
swift
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 and MKReverseGeocodingRequest (iOS 26+)

MKGeocodingRequest与MKReverseGeocodingRequest(iOS 26+)

New MapKit-native geocoding that returns
MKMapItem
with richer data and
MKAddress
/
MKAddressRepresentations
for flexible address formatting.
swift
@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
}
MapKit原生的新型地理编码接口,返回携带更丰富数据的
MKMapItem
,以及支持灵活地址格式化的
MKAddress
/
MKAddressRepresentations
swift
@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
}

Search

搜索

MKLocalSearchCompleter (Autocomplete)

MKLocalSearchCompleter(自动补全)

swift
@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 = []
    }
}
swift
@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 (Full Search)

MKLocalSearch(完整搜索)

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

Directions

路线规划

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

Display Route on Map

在地图上展示路线

swift
@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)
}
swift
@State private var route: MKRoute?

Map {
    if let route {
        MapPolyline(route.polyline)
            .stroke(.blue, lineWidth: 5)
    }
    Marker("起点", coordinate: startCoord)
    Marker("终点", coordinate: endCoord)
}
.task {
    route = try? await getDirections(from: startItem, to: endItem)
}

ETA Calculation

预计到达时间计算

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

Cycling Directions (iOS 26+)

骑行路线规划(iOS 26+)

swift
@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
}
swift
@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+)

PlaceDescriptor(iOS 26+)

Create rich place references from coordinates or addresses without needing a Place ID. Requires
import GeoToolbox
.
swift
@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
}
无需地点ID即可通过坐标或地址创建丰富的地点引用。需要导入
GeoToolbox
swift
@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
}

Common Mistakes

常见错误

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.
不要: 一开始就请求
.authorizedAlways
权限——用户不信任宽泛的权限申请。 应当: 先使用
.requestWhenInUseAuthorization()
,仅当用户启用后台功能时再升级为
.always
权限。
不要: 在iOS 17+系统上使用
CLLocationManagerDelegate
实现简单的位置获取。 应当: 使用
CLLocationUpdate.liveUpdates()
异步流实现更简洁清晰的代码。
不要: 在地图/视图不可见时仍保持位置更新运行(会消耗电量)。 应当: 在SwiftUI中使用
.task { }
,这样视图消失时更新会自动取消。
不要: 强制解包
CLPlacemark
属性——它们都是可选类型。 应当: 使用空合运算符:
placemark.locality ?? "未知"
不要: 用户每次按键都触发
MKLocalSearchCompleter
查询。 应当: 使用
.task(id: searchText)
+
Task.sleep(for: .milliseconds(300))
做防抖处理。
不要: 位置授权被拒绝时静默失败。 应当: 检测到
.denied
状态时展示弹窗,附带跳转至设置页的链接。
不要: 假定地理编码总是成功——请处理空结果和网络错误。

Review Checklist

审查清单

  • Info.plist has
    NSLocationWhenInUseUsageDescription
    with specific reason
  • Authorization denial handled with Settings deep link
  • CLLocationUpdate
    task cancelled when not needed (battery)
  • Location accuracy appropriate for the use case
  • Map annotations use
    Identifiable
    data with stable IDs
  • Geocoding errors handled (network failure, no results)
  • Search completer input debounced
  • CLMonitor
    limited to 20 conditions, instance kept alive
  • Background location uses
    CLBackgroundActivitySession
  • Map tested with VoiceOver
  • Map annotation view models and location UI updates are
    @MainActor
    -isolated
  • Info.plist中已配置
    NSLocationWhenInUseUsageDescription
    并说明具体使用原因
  • 权限被拒绝时已处理,提供跳转至设置页的链接
  • 不需要时已取消
    CLLocationUpdate
    任务(节省电量)
  • 位置精度适配当前使用场景
  • 地图标注使用带有稳定ID的
    Identifiable
    数据
  • 已处理地理编码错误(网络失败、无结果)
  • 搜索补全输入已做防抖处理
  • CLMonitor
    限制在20个条件以内,实例已被持有不会被释放
  • 后台定位使用
    CLBackgroundActivitySession
  • 地图已通过VoiceOver无障碍测试
  • 地图标注视图模型和位置UI更新已通过
    @MainActor
    隔离

References

参考文献

  • references/mapkit-patterns.md
    — Map setup, annotations, search, routes, clustering, Look Around, snapshots.
  • references/corelocation-patterns.md
    — CLLocationUpdate, CLMonitor, CLServiceSession, background location, testing.
  • apple-docs MCP:
    /documentation/mapkit/map
    ,
    /documentation/corelocation/cllocationupdate
  • references/mapkit-patterns.md
    —— 地图配置、标注、搜索、路线、聚类、环顾功能、快照。
  • references/corelocation-patterns.md
    —— CLLocationUpdate、CLMonitor、CLServiceSession、后台定位、测试。
  • Apple官方文档MCP:
    /documentation/mapkit/map
    /documentation/corelocation/cllocationupdate