使用 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
- 工作流程
- SwiftUI 地圖 View (iOS 17+)
- CoreLocation 現代 API
- 地理編碼
- 搜尋
- 路線規劃
- PlaceDescriptor (iOS 26+)
- 常見錯誤
- 審查檢查清單
- 參考資料
工作流程
1. 新增帶有標記或標註的地圖
- 匯入
MapKit。 - 建立帶有所選
MapCameraPositionBinding 的MapView。 - 在
MapContentBuilderClosure 內新增Marker、Annotation、MapPolyline、MapPolygon或MapCircle。 - 使用
.mapStyle()設定地圖樣式。 - 使用
.mapControls { }新增地圖控制元件。 - 使用
selection:Binding 處理選取事件。
2. 追蹤使用者位置
- 在 Info.plist 中新增
NSLocationWhenInUseUsageDescription。 - 在 iOS 18+ 上,建立
CLServiceSession來管理授權。 - 在
Task中迭代CLLocationUpdate.liveUpdates()。 - 在更新 UI 前,先按距離或精確度篩選更新。
- 當不再需要定位追蹤時停止該任務。
3. 搜尋地點
- 設定
MKLocalSearchCompleter以提供自動完成建議。 - 在設定查詢字串前對使用者輸入進行防抖(Debounce,至少 300ms)。
- 將選定的完成項轉換為
MKLocalSearch.Request以取得完整結果。 - 將結果顯示為標記或列表。
4. 取得路線規劃並顯示路線
- 建立包含起點與終點
MKMapItem的MKDirections.Request。 - 設定
transportType(.automobile、.walking、.transit、.cycling)。 - 非同步等待
MKDirections.calculate()。 - 使用
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 -->






