MapKit
使用 SwiftUI MapKit 和現代化 CoreLocation 非同步 API,建構支援 iOS 17+ 的地圖與位置感知功能。使用 Map 搭配 MapContentBuilder 來建立視圖,使用 CLLocationUpdate.liveUpdates() 進行位置串流,並使用 CLMonitor 進行地理圍欄。
更多 MapKit 模式請參閱 references/mapkit-patterns.md,CoreLocation 模式請參閱 references/mapkit-corelocation-patterns.md。
目錄
- 工作流程
- SwiftUI Map 視圖 (iOS 17+)
- CoreLocation 現代化 API
- 地理編碼
- 搜尋
- 路線規劃
- PlaceDescriptor (iOS 26+)
- 常見錯誤
- 審查清單
- 參考資料
工作流程
1. 新增帶有標記或註釋的地圖
- 匯入
MapKit。 - 建立一個
Map視圖,並選擇性地綁定MapCameraPosition。 - 在
MapContentBuilder閉包內加入Marker、Annotation、MapPolyline、MapPolygon或MapCircle。 - 使用
.mapStyle()設定地圖樣式。 - 使用
.mapControls { }加入地圖控制項。 - 使用
selection:綁定來處理選取操作。
2. 追蹤使用者位置
- 在 Info.plist 中加入
NSLocationWhenInUseUsageDescription。 - 在 iOS 18+ 上,建立一個
CLServiceSession來管理授權。 - 在
Task中疊代CLLocationUpdate.liveUpdates()。 - 在更新 UI 前,根據距離或準確度過濾更新內容。
- 當不再需要位置追蹤時,停止該任務。
3. 搜尋地點
- 設定
MKLocalSearchCompleter以獲得自動完成建議。 - 在設定查詢前,對使用者輸入進行防抖處理(至少 300 毫秒)。
- 將選定的完成項目轉換為
MKLocalSearch.Request以取得完整結果。 - 將結果顯示為標記或列表。
4. 取得路線並顯示路徑
- 建立一個
MKDirections.Request,並設定起點和終點MKMapItem。 - 設定
transportType(.automobile、.walking、.transit、.cycling)。 - 等待
MKDirections.calculate()完成。 - 使用
MapPolyline(route.polyline)繪製路線。
5. 審查現有的地圖/位置程式碼
按照此文件末尾的審查清單進行檢查。
SwiftUI Map 視圖 (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("咖啡廳", systemImage: "cup.and.saucer.fill", coordinate: cafeCoord)
.tint(.brown)
// 註釋 — 在座標上放置自訂 SwiftUI 視圖
Annotation("你", coordinate: userCoord, anchor: .bottom) {
Image(systemName: "figure.wave")
.padding(6)
.background(.blue.gradient, in: .circle)
.foregroundStyle(.white)
}
疊加層:折線、多邊形、圓形
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)
}
相機位置
MapCameraPosition 控制地圖顯示的內容。綁定它可以讓使用者互動,或以程式方式移動相機。
// 以某個區域為中心
@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(...))
地圖樣式
.mapStyle(.standard) // 預設道路地圖
.mapStyle(.standard(elevation: .realistic, showsTraffic: true))
.mapStyle(.imagery) // 衛星
.mapStyle(.imagery(elevation: .realistic)) // 3D 衛星
.mapStyle(.hybrid) // 衛星 + 標籤
.mapStyle(.hybrid(elevation: .realistic, showsTraffic: true))
地圖互動模式
.mapInteractionModes(.all) // 預設:平移、縮放、旋轉、傾斜
.mapInteractionModes(.pan) // 僅平移
.mapInteractionModes([.pan, .zoom]) // 平移和縮放
.mapInteractionModes([]) // 靜態地圖(無互動)
地圖選取
@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 現代化 API
CLLocationUpdate.liveUpdates() (iOS 17+)
用單一的非同步序列取代 CLLocationManagerDelegate 回呼。每次疊代都會產生一個包含可選 CLLocation 的 CLLocationUpdate。
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+)
在某項功能的生命週期內宣告授權需求。只要你需要位置服務,就保留對 session 的參考。
// 使用中授權,並偏好完整準確度
let session = CLServiceSession(
authorization: .whenInUse,
fullAccuracyPurposeKey: "NearbySearchPurpose"
)
// 將 `session` 保留為儲存屬性;使用完畢後釋放。
在 iOS 18+ 上,如果你沒有明確建立 CLServiceSession,CLLocationUpdate.liveUpdates() 和 CLMonitor 會隱式使用一個。當你需要 .always 授權或完整準確度時,請明確建立一個。
授權流程
// Info.plist 鍵(必要):
// NSLocationWhenInUseUsageDescription
// NSLocationAlwaysAndWhenInUseUsageDescription(僅在需要 .always 時)
// 檢查授權狀態,並在被拒絕時引導使用者前往設定
struct LocationPermissionView: View {
@Environment(\.openURL) private var openURL
var body: some View {
ContentUnavailableView {
Label("位置存取被拒", systemImage: "location.slash")
} description: {
Text("請在「設定」中啟用位置存取以使用此功能。")
} actions: {
Button("開啟設定") {
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: request)
let response = try await search.start()
return response.mapItems
}
路線規劃
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
}
在地圖上顯示路線
@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)
}
預估到達時間計算
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
}
自行車路線 (iOS 26+)
@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+)
從座標或地址建立豐富的地點參考,無需地點 ID。需要 import GeoToolbox。
@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
}
常見錯誤
不要: 一開始就請求 .authorizedAlways — 使用者不信任過度的權限。
要: 從 .requestWhenInUseAuthorization() 開始,僅當使用者啟用背景功能時才升級到 .always。
不要: 在 iOS 17+ 上為了簡單的位置獲取而使用 CLLocationManagerDelegate。
要: 使用 CLLocationUpdate.liveUpdates() 非同步串流,以獲得更簡潔、清晰的程式碼。
不要: 當地圖/視圖不可見時仍保持位置更新(會耗盡電池)。
要: 在 SwiftUI 中使用 .task { },以便在視圖消失時自動取消更新。
不要: 強制解包 CLPlacemark 的屬性 — 它們都是可選的。
要: 使用 nil 合併運算子:placemark.locality ?? "未知"。
不要: 在文字變更時立即觸發 MKLocalSearch。
要: 對文字輸入進行防抖處理(300-500 毫秒),然後使用 MKLocalSearchCompleter 取得建議。
不要: 在建立 MKDirections.Request 時略過 transportType。
要: 明確設定運輸類型,以獲得準確的路線和到達時間。
不要: 假設 CLLocation 的 speed 和 course 始終可用。
要: 在使用前檢查 speed >= 0 和 course >= 0。
不要: 在 iOS 18+ 上建立多個 CLServiceSession 實例。
要: 為應用程式的位置相關功能保留一個 session 參考,以簡化授權管理。
審查清單
- 資訊清單(Info.plist)是否包含位置授權的說明?
- 是否使用
.task { }和CLLocationUpdate.liveUpdates()而非CLLocationManagerDelegate? - 是否在設定查詢前對
MKLocalSearchCompleter的輸入進行了防抖處理? -
MKMapItem的標記是否與選取類型(.tag)相符? - 路線和到達時間計算是否在背景佇列上執行?
- 地圖疊加層(折線/多邊形/圓形)是否使用了正確的樣式(線條顏色、填充、不透明度)?
- 所有
CLPlacemark屬性在使用前是否都進行了安全解包? - 當視圖消失時,位置更新任務是否會取消?
- 對於 iOS 18+,是否使用
CLServiceSession來管理授權? - 對於 iOS 26+,是否考慮使用
PlaceDescriptor和MKGeocodingRequest? - 是否已處理位置授權被拒絕的情況?
- 地圖互動模式是否與預期行為相符?
- 地圖樣式是否適合應用程式的設計?
- 如果返回
async搜尋結果,呼叫端是否處理了錯誤? - 地理編碼請求是否會頻繁觸發?考慮加入快取機制。
參考資料
- references/mapkit-patterns.md — 地圖與 CoreLocation 的擴展模式
- references/mapkit-corelocation-patterns.md — 現代化 CoreLocation 的使用方式
- MapKit 建議:使用
MapContentBuilder、MapCameraPosition、MapStyle - CoreLocation 建議:使用非同步串流 (
CLLocationUpdate.liveUpdates) 和CLServiceSession MKLookAroundViewController/MKLookAroundSceneRequest用於街景預覽(iOS 17+)MKMapItemopenInMaps(launchOptions:)用於啟動 Apple 地圖