Core Location 是 iOS 開發中用於處理地理位置相關功能的框架。今天的目標是了解如何使用 Core Location 來管理權限以及獲取用戶的當前位置。
首先我們先在專案資料夾建立一個新的 .swift 檔 LoactionManager.swift
。
可以在專案資料夾按快捷鍵 cmd + N
,選擇 iOS -> Swift File。
我們的目的是建立一個獨立的管理者 LocationManager,它會處理向使用者請求定位權限、接收座標更新、並將這些資訊即時「發布」給 SwiftUI 介面,讓畫面可以根據最新的位置或權限狀態自動更新。
import CoreLocation
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
// ....
}
NSObject: 繼承自 NSObject。因為 Core Location 框架是基於早期 Objective-C 的設計模式。
ObservableObject: 遵循 ObservableObject 協定。這是一個來自 Combine 框架的宣告,表示這個物件可以被 SwiftUI 的 View 所「觀察」。一旦物件內被 @Published
標記的屬性發生改變,它會自動通知所有正在觀察它的 View 進行更新。
Combine 框架是用來在 Swift 中以聲明式方式處理資料流與非同步事件的工具,簡單說就是你不用自己到處寫通知、代理或 callback 處理資料或事件的一套工具。
接著我們需要:
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
@Published var authorizationStatus: CLAuthorizationStatus = .notDetermined
@Published var lastSeenLocation: CLLocation?
// ...
}
因為我們只會在這裡存取 CLLocationManager 物件,故用 private 來初始化。@Published var authorizationStatus...
,@Published
是一個 Property Wrapper,每當 authorizationStatus
的值改變時,它會自動發出通知給訂閱者。
var authorizationStatus: CLAuthorizationStatus
,宣告一個變數來儲存當前的定位權限狀態(例如:尚未決定、已允許、已拒絕)。由於 App 剛啟動時,我們不知道權限狀態,故給它一個初始值 .notDetermined
。
@Published var lastSeenLocation: CLLocation?
,同樣地,宣告一個帶有 @Published
的變數,用來儲存最後一次收到的使用者位置。它的型別是 Optional,因為在獲得授權或收到更新之前,可能還沒有任何位置資訊。
override init() {
super.init()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
self.authorizationStatus = manager.authorizationStatus
}
在初始化方法中,manager.delegate = self
要告訴 CLLocationManager
(manager
),所有定位相關的事件(如權限改變、位置更新)都請通知我(self
,也就是這個 LocationManager
實例)。」
func requestPermission() {
manager.requestWhenInUseAuthorization()
}
此方法到時候可以方便我們從 View 中呼叫,呼叫 requestWhenInUseAuthorization()
,觸發 iOS 系統彈出「僅在使用 App 期間允許」的權限請求視窗。
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
let status = manager.authorizationStatus
DispatchQueue.main.async {
self.authorizationStatus = status
}
switch manager.authorizationStatus {
case .notDetermined:
manager.requestWhenInUseAuthorization()
case .authorizedWhenInUse:
manager.startUpdatingLocation()
manager.requestAlwaysAuthorization()
case .authorizedAlways:
manager.startUpdatingLocation()
default:
break
}
}
這是 CLLocationManagerDelegate 的 delegate 方法,每當 App 的定位權限狀態發生任何改變時,例如使用者按下「允許」或「拒絕」,這個方法就會被自動呼叫。
self.authorizationStatus = status
將 authorizationStatus
更新為最新的狀態,並觸發 @Published
發出通知,讓 SwiftUI View 更新畫面。
switch manager.authorizationStatus { ... }
則根據最新的權限狀態執行不同的邏輯:
.notDetermined
: 如果狀態是未知的,嘗試請求權限。.authorizedWhenInUse
: 如果使用者只允許了「使用時」,就開始更新位置,並接著請求「永遠允許」的權限(如果有需要)。.authorizedAlways
: 如果已經獲得了最高權限,直接開始更新位置。default
: 對於其他情況(如 .denied
或 .restricted
),不做任何事。接著,取得位置後要跟更新 lastSeenLocation:
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
DispatchQueue.main.async {
self.lastSeenLocation = locations.last
}
}
最後來個錯誤處理,以便開發中查錯:
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("定位失敗: \(error.localizedDescription)")
}
完整程式碼如下:
import Foundation
import CoreLocation
class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
@Published var authorizationStatus: CLAuthorizationStatus = .notDetermined
@Published var lastSeenLocation: CLLocation?
override init() {
super.init()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
self.authorizationStatus = manager.authorizationStatus
}
func requestPermission() {
manager.requestWhenInUseAuthorization()
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
let status = manager.authorizationStatus
DispatchQueue.main.async {
self.authorizationStatus = status
}
switch manager.authorizationStatus {
case .notDetermined:
manager.requestWhenInUseAuthorization()
case .authorizedWhenInUse:
manager.startUpdatingLocation()
manager.requestAlwaysAuthorization()
case .authorizedAlways:
manager.startUpdatingLocation()
default:
break
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
DispatchQueue.main.async {
self.lastSeenLocation = locations.last
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
print("定位失敗: \(error.localizedDescription)")
}
}
在處理我們的畫面之前,要需要處理用戶授權的問題,這也是我認為 iOS 開發有一點小複雜(惱人)的地方。只要你要取用用戶的任何權限,你必須要到 target 的 Info 去設置權限。
左側邊欄點選你的專案,右邊選擇你的 target,最上方的分頁選擇 Info,下方的「Custom iOS Target Properties」即是設定權限的地方。
這裡實際上編輯的是 Info.plist 的文件,這是一份基於 xml 格式的文件,每個 key 會有對應的 value,且 Xcode 看到的 key 名稱,與實際上在 Info.plist 裡的 key 名稱會不相同。
與定位有關的有三個,比較如下:
NSLocationWhenInUseUsageDescription
)僅在 App 前景使用時允許存取使用者位置,當呼叫 requestWhenInUseAuthorization()
時,跳出你於 Value 設定的文字。
NSLocationAlwaysUsageDescription
)允許 App 在前景與背景持續存取位置。已在 iOS 11 後被廢棄,新專案應改用下列「Location Always and When In Use Usage Description」。
NSLocationAlwaysAndWhenInUseUsageDescription
)允許 App 在前景與背景持續存取位置。當呼叫 requestAlwaysAuthorization()
(而且使用者已先通過 WhenInUse)時,跳出你於 Value 設定的文字。
結論是,如果你需要背景也取用使用者位置,你必須設定 1. 與 2.。
我們終於可以來處理畫面與邏輯了~
首先我們要先建立剛剛寫好的 LoactionManager 物件:
@StateObject var locationManager = LocationManager()
將 locationManager 加上 @StateObject
,SwiftUI 會持有並自動監控這個物件內所有被 @Published
標記的狀態,一旦那些狀態改變,View 就會自動重新渲染。
完整範例如下:
var body: some View {
VStack(spacing: 20) {
switch locationManager.authorizationStatus {
case .notDetermined:
ProgressView()
Text("正在請求定位權限...")
case .restricted, .denied:
Image(systemName: "location.slash.fill")
.font(.largeTitle)
.foregroundColor(.red)
Text("您的位置權限已被關閉。")
Text("請至「設定」App 中開啟權限。")
case .authorizedWhenInUse, .authorizedAlways:
Text("成功取得定位權限!")
if let coordinate = locationManager.lastSeenLocation?.coordinate {
Text("經度 (Longitude): \(coordinate.longitude)")
Text("緯度 (Latitude): \(coordinate.latitude)")
} else {
ProgressView()
Text("正在取得您的位置...")
}
@unknown default:
Text("發生未知的錯誤")
}
}
.multilineTextAlignment(.center)
.padding()
}
我們用 switch 來處理不同授權的狀況。
今天我們深入學習了 Core Location 的基本用法,從建立 LocationManager、處理權限,到即時取得並顯示使用者的地理位置。分點總結一下:
CLLocationManager
請求定位權限,並根據不同授權狀態做出對應處理@Published
和 @StateObject
讓 SwiftUI 介面自動反映資料變化明天我們將進一步探索 MapKit,學習如何將位置資訊在地圖上可視化。