mapkit

mapkit

熱門

使用 MapKit 與 CoreLocation 在 iOS/macOS App 中實作、審查或改進地圖與定位功能。適用於處理地圖 View、標註(Annotations)、標記(Markers)、折線(Polylines)、使用者定位追蹤、地理編碼、反向地理編碼、搜尋/自動完成、路線規劃、地理圍欄、區域監控、CLLocationUpdate 非同步串流,或定位授權流程。亦適用於在 Swift App 中開發地圖、坐標、地址、地點、路線、距離計算或定位相關功能。

961星標
48分支
更新於 2026/7/31
SKILL.md
唯讀
名稱
mapkit
描述

使用 MapKit 與 CoreLocation 在 iOS/macOS App 中實作、審查或改進地圖與定位功能。適用於處理地圖 View、標註(Annotations)、標記(Markers)、折線(Polylines)、使用者定位追蹤、地理編碼、反向地理編碼、搜尋/自動完成、路線規劃、地理圍欄、區域監控、CLLocationUpdate 非同步串流,或定位授權流程。亦適用於在 Swift App 中開發地圖、坐標、地址、地點、路線、距離計算或定位相關功能。

MapKit

搭配 SwiftUI MapKit 與現代 CoreLocation 非同步 API,為 iOS 17+ 建置基於地圖與定位感知的功能。使用 Map 搭配 MapContentBuilder 建立 View,使用 CLLocationUpdate.liveUpdates() 進行定位串流,並使用 CLMonitor 處理地理圍欄。

當需要完整的地圖設定、搜尋、路線、Look Around、快照或 iOS 26 Place API 時,請參閱 references/mapkit-patterns.md。當任務涉及定位更新生命週期、地理圍欄、背景定位、測試或隱私權 Key 時,請參閱 references/mapkit-corelocation-patterns.md

Contents

工作流程

1. 新增帶有標記或標註的地圖

  1. 匯入 MapKit
  2. 建立帶有所選 MapCameraPosition Binding 的 Map View。
  3. MapContentBuilder Closure 內新增 MarkerAnnotationMapPolylineMapPolygonMapCircle
  4. 使用 .mapStyle() 設定地圖樣式。
  5. 使用 .mapControls { } 新增地圖控制元件。
  6. 使用 selection: Binding 處理選取事件。

2. 追蹤使用者位置

  1. 在 Info.plist 中新增 NSLocationWhenInUseUsageDescription
  2. 在 iOS 18+ 上,建立 CLServiceSession 來管理授權。
  3. Task 中迭代 CLLocationUpdate.liveUpdates()
  4. 在更新 UI 前,先按距離或精確度篩選更新。
  5. 當不再需要定位追蹤時停止該任務。

3. 搜尋地點

  1. 設定 MKLocalSearchCompleter 以提供自動完成建議。
  2. 在設定查詢字串前對使用者輸入進行防抖(Debounce,至少 300ms)。
  3. 將選定的完成項轉換為 MKLocalSearch.Request 以取得完整結果。
  4. 將結果顯示為標記或列表。

4. 取得路線規劃並顯示路線

  1. 建立包含起點與終點 MKMapItemMKDirections.Request
  2. 設定 transportType.automobile.walking.transit.cycling)。
  3. 非同步等待 MKDirections.calculate()
  4. 使用 MapPolyline(route.polyline) 繪製路線。

5. 審查現地圖/定位程式碼

請對照本檔案末尾的審查檢查清單逐項檢查。

SwiftUI 地圖 View (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()
        }
    }
}

Marker 與 Annotation

// 氣球標記 -- 釘選位置最簡單的方法
Marker("Cafe", systemImage: "cup.and.saucer.fill", coordinate: cafeCoord)
    .tint(.brown)

// Annotation -- 在特定坐標上的自訂 SwiftUI View
Annotation("You", coordinate: userCoord, anchor: .bottom) {
    Image(systemName: "figure.wave")
        .padding(6)
        .background(.blue.gradient, in: .circle)
        .foregroundStyle(.white)
}

覆蓋物 (Overlays):Polyline、Polygon、Circle

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 用來控制地圖顯示的內容。將其綁定(Bind)可允許使用者互動並以程式碼動態移動鏡頭。

// 置中於特定區域
@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)

預設為 .standard;僅在功能需要時才選擇 .imagery.hybrid、擬真海拔、交通狀況及興趣點(POI)篩選。請參閱 完整地圖 View 設定

地圖互動模式 (Map Interaction Modes)

互動式地圖請保持 .all。僅在需要特定手勢協調時才限制模式;嵌入式靜態地圖請使用 []。請參閱 List 或 ScrollView 中的地圖

地圖選取 (Map Selection)

@State private var selectedMarker: MKMapItem?

Map(selection: $selectedMarker) {
    ForEach(places) { place in
        Marker(place.name, coordinate: place.coordinate)
            .tag(place.mapItem)     // Tag 類型必須與 selection 類型符合
    }
}
.onChange(of: selectedMarker) { _, newValue in
    guard let item = newValue else { return }
    // 針對選取作出回應
}

CoreLocation 現代 API

CLLocationUpdate.liveUpdates() (iOS 17+)

使用單一步驟的非同步序列取代 CLLocationManagerDelegate 代理回呼。每次迭代都會傳回包含可選 CLLocationCLLocationUpdate。在 iOS 18+ 上,當遇到拒絕授權、全域停用定位服務、定位不可用以及使用中條件不足等診斷狀態時,應提供明確降級處理路徑,而非無休止地默默等待。請妥善保存該 Task 以便需要時取消,並在更新地圖 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 }
                    // 依據水平精確度篩選
                    guard location.horizontalAccuracy >= 0,
                          location.horizontalAccuracy < 50 else { continue }
                    currentLocation = location
                }
            } catch is CancellationError {
                // 停止追蹤時的預期行為。
            } catch {
                currentLocation = nil
            }
        }
    }

    func stopTracking() {
        updateTask?.cancel()
        updateTask = nil
    }
}

CLServiceSession (iOS 18+)

宣告功能生命週期內的定位授權需求。只要需要定位服務,請持續持有該 Session 的參考。

// 偏好最高精確度的使用中授權 (When-in-use authorization)
let session = CLServiceSession(
    authorization: .whenInUse,
    fullAccuracyPurposeKey: "NearbySearchPurpose"
)
// 將 `session` 存為屬性 (Stored Property);使用完畢後釋放。

在 iOS 18+ 上,若未顯式建立 CLServiceSessionCLLocationUpdate.liveUpdates()CLMonitor 會隱式建立一個。當需要 .always 授權或完全精確度時,請顯式建立。

授權流程 (Authorization Flow)

// Info.plist Key(必須):
// NSLocationWhenInUseUsageDescription
// NSLocationAlwaysAndWhenInUseUsageDescription(僅在需要 .always 時)

// 檢查授權並在被拒絕時引導使用者前往「設定」
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()

// 正向地理編碼:地址字串 -> 坐標
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 與 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
}

// 依據自然語言查詢字串在指定區域內搜尋周邊
func searchNearby(query: String, region: MKCoordinateRegion) async throws -> [MKMapItem] {
    let request = MKLocalSearch.Request()
    request.naturalLanguageQuery = query
    request.region = region
    let search = MKLocalSearch(request: req

<!-- truncated for translation batch; full body continues in source -->