mapkit

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

MapKit

MapKit

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/mapkit-corelocation-patterns.md for CoreLocation patterns.
基于 iOS 17+,使用 SwiftUI MapKit 和现代 CoreLocation 异步 API 构建地图类与位置感知功能。使用搭配
MapContentBuilder
Map
实现视图,通过
CLLocationUpdate.liveUpdates()
流式获取位置数据,使用
CLMonitor
实现地理围栏。
查看 references/mapkit-patterns.md 了解更多 MapKit 实践模式,查看 references/mapkit-corelocation-patterns.md 了解 CoreLocation 实践模式。

Contents

目录

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.
对照本文末尾的评审 Checklist 逐一检查。

SwiftUI Map View (iOS 17+)

SwiftUI 地图视图(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
// 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)
}

Overlays: Polyline, Polygon, Circle

覆盖层: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 {
    // 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)
}

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
// 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(...))

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)                                        // 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))

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)           // Default: pan, zoom, rotate, pitch
.mapInteractionModes(.pan)           // Pan only
.mapInteractionModes([.pan, .zoom])  // Pan and zoom
.mapInteractionModes([])             // Static map (no interaction)

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)     // Tag must match selection type
    }
}
.onChange(of: selectedMarker) { _, newValue in
    guard let item = newValue else { return }
    // React to selection
}

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 }
                // 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+)

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
// 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
授权或全精度定位时请显式创建会话。

Authorization Flow

授权流程

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

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()

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

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

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("Start", coordinate: startCoord)
    Marker("End", 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
}
无需 Place 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 ?? "Unknown"
不要: 每次按键都触发
MKLocalSearchCompleter
查询。 推荐: 通过
.task(id: searchText)
+
Task.sleep(for: .milliseconds(300))
实现防抖。
不要: 位置授权被拒绝时静默失败。 推荐: 检测到
.denied
状态时展示弹窗,提供跳转设置页的深链。
不要: 假设地理编码总是成功 —— 要处理空结果和网络错误。

Review Checklist

评审 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/mapkit-corelocation-patterns.md — CLLocationUpdate, CLMonitor, CLServiceSession, background location, testing.
  • references/mapkit-patterns.md —— 地图配置、标注、搜索、路线、聚类、环视、截图。
  • references/mapkit-corelocation-patterns.md —— CLLocationUpdate、CLMonitor、CLServiceSession、后台定位、测试。