時隔九年,繼續折騰…
這次折騰了個什麼東西?
在 iOS 開發裡,定位跟導航 是不少 App 的必備功能,無論是外送、騎行、旅遊、甚至只是單純的運動記錄,幾乎都離不開 GPS。但如果你直接用 CoreLocation
和 MapKit
,你很快就會發現:
- 代碼到處都是:每個模組都要請求定位、計算路線,重複寫得心煩。
- 測試困難:
CLLocationManager
和MKDirections
這些 API 太「系統級」,沒辦法好好 Mock,單元測試變成地獄。 - 擴展性差:如果哪天想接 Google Maps 或 Mapbox,很可能得把原來的代碼大卸八塊。
為了讓這些問題迎刃而解,我們封裝了一個 模組化的 iOS 導航 SDK——NavigationKit
,它把 定位、路線規劃、地圖顯示 分開管理,並且採用 Swift Package Manager(SPM),讓整個流程乾淨、清晰、好維護。
這篇文章就來聊聊這次的 封裝過程、踩坑經驗,以及一些有趣的細節,看看怎麼把導航模組做得更優雅!😎
架構設計
在開始寫代碼之前,先想想怎麼拆模組比較合理。這次我們把 NavigationKit
拆成三個部分:
LocationKit
—— 負責 設備定位,封裝CLLocationManager
,並且支援 SwiftUI 綁定。RouteKit
—— 負責 路線規劃 & 軌跡記錄,封裝MKDirections
,提供更簡單的 API。MapKitWrapper
—— 包裝MapKit
UI 相關邏輯,讓 SwiftUI 直接用。
NavigationKit/
├── Sources/
│ ├── NavigationKit/ # 入口模組
│ ├── LocationKit/ # 定位功能
│ ├── RouteKit/ # 路徑計算
│ ├── MapKitWrapper/ # MapKit UI 封裝
├── Tests/
│ ├── NavigationKitTests/
│ ├── LocationKitTests/
│ ├── RouteKitTests/
這麼拆的好處
低耦合:每個模組只專注一件事,避免一坨 NavigationManager
神級類別。
易擴展:未來如果要接 Google Maps,只要改 MapKitWrapper
,不用動 LocationKit
或 RouteKit
。
SwiftUI 友善:內建 ObservableObject
,讓數據流更自然。
LocationKit
—— 乾淨俐落的定位 API
CLLocationManager
這玩意兒不好用,因為:
- 需要處理 授權、背景模式、準確度設定,代碼很瑣碎。
- 如果直接塞到視圖裡面,UI 層就變得很雜。
所以我們封裝了一個 LocationService
來簡化這些操作:
import Foundationimport CoreLocation
public final class LocationService: NSObject, ObservableObject {
private let locationManager = CLLocationManager()
@Published public private(set) var currentLocation: CLLocation?
public override init() {
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
}
public func startUpdatingLocation() {
locationManager.requestWhenInUseAuthorization()
locationManager.startUpdatingLocation()
}
}
extension LocationService: CLLocationManagerDelegate {
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
self.currentLocation = locations.last
}
}
為什麼這麼寫?
@Published
讓 SwiftUI 視圖可以直接訂閱currentLocation
,更新界面不用 callback。- 避免暴露
CLLocationManager
,讓LocationService
負責一切底層細節,別的模組只管拿數據。 - 未來要擴展背景模式、地理圍欄(Geofencing)時,這個架構會更靈活。
RouteKit
—— 不再手寫 MKDirections
iOS 內建 MKDirections
可以計算路線,但 API 太低階,還得自己處理請求、解析結果。
我們希望直接來個:
routeManager.calculateRoute(from: 起點, to: 終點) { route in
顯示路線
}
所以我們封裝了 RouteManager
:
import MapKit
public final class RouteManager {
public func calculateRoute(from source: CLLocationCoordinate2D, to destination: CLLocationCoordinate2D, completion: @escaping (MKRoute?) -> Void) {
let request = MKDirections.Request()
request.source = MKMapItem(placemark: MKPlacemark(coordinate: source))
request.destination = MKMapItem(placemark: MKPlacemark(coordinate: destination))
request.transportType = .automobile
MKDirections(request: request).calculate { response, error in
completion(response?.routes.first)
}
}
}
這樣封裝的好處
- 代碼清爽:不需要每次都手寫
MKDirections.Request()
。 - 擴展性強:如果之後要支援 Google Maps,只要改
RouteManager
,調用方不用改。
SPM化
既然要模組化,當然要讓 NavigationKit
能夠 像正規 SDK 一樣使用。所以我們用 Swift Package Manager(SPM) 來管理它:
swift package init --type library
然後 Package.swift
長這樣:
let package = Package(
name: "NavigationKit",
platforms: [.iOS(.v15)],
products: [
.library(name: "NavigationKit", targets: ["NavigationKit"])
],
dependencies: [],
targets: [
.target(name: "NavigationKit", dependencies: []),
.testTarget(name: "NavigationKitTests", dependencies: ["NavigationKit"]),
]
)
這樣我們就可以 直接在其他專案中用 NavigationKit
,而不用每次 copy-paste 代碼了。 🎉
結論
這次封裝 NavigationKit
,讓 iOS 的導航功能變得更 模組化、清晰、易用:
✔ LocationKit
提供 SwiftUI 友善的 GPS 定位 API
✔ RouteKit
讓路線計算變得簡單直覺
✔ Swift Package 讓整個 SDK 可重用、易維護
🔜 下一步計畫
- 支援 Google Maps & Mapbox
- 加入離線導航功能
- 讓
RouteKit
支援更多類型的路徑規劃(步行、騎行、貨車路線)
這次的封裝算是解決了很多 導航 API 使用上的痛點,如果你也有類似的需求,或者對 iOS 開發有興趣,歡迎交流!