SPM實戰:封裝 NavigationKit 以提升導航模塊的可復用性

時隔九年,繼續折騰…

這次折騰了個什麼東西?

在 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 開發有興趣,歡迎交流!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注