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 內新增 Marker、Annotation、MapPolyline、MapPolygon 或 MapCircle。
  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. 建立包含起點與終點 MKMapItem 的 MKDirections.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 代理回呼。每次迭代都會傳回包含可選 CLLocation 的 CLLocationUpdate。在 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+ 上,若未顯式建立 CLServiceSession,CLLocationUpdate.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 -->