[Day 25] 使用 XCTest 來建立單元測試

前言 CSV 資料的讀取與解析,這項功能是 App 的核心之一。然而,隨著專案越來越複雜,我們很可能會在未來的某個時候不小心改動到相關程式碼,或是 CSV 檔案有異動,導致這個功能壞掉。為了確保它能一直穩定運作,我們需要為它建立一道防線——單元測試。 單元測試可以幫助我們驗證一小段程式碼(一個「單元」)的行為是否符合預期。今天,我們將使用 Apple 官方的測試框架 XCTest 來為 RoadDataManager 撰寫測試,確保它在面對正常與異常資料時,都能做出正確的反應。 將 XCTest 加入專案 如果你的專案一開始建立時沒有勾選 Include Tests,我們需要手動將其加入。 首先,在 Xcode 專案導覽器中點擊最上層的專案檔案,進入設定頁面。在「TARGETS」區域點擊左下角的「+」按鈕,選擇新增「Unit Testing Bundle」。你可以為這個測試目標命名,例如 RoadMileLocatorTests,Xcode 便會為你建立好測試所需的環境。 撰寫你的測試案例 建立測試目標後,Xcode 會自動生成一個樣板檔案。雖然可以直接使用,但更好的做法是為特定的類別建立專屬的測試檔案。這樣一來,當 RoadDataManager 的功能需要擴充或修改時,我們就能立刻找到對應的測試案例來進行驗證。 import XCTest final class RoadMileLocatorTests: XCTestCase { override func setUpWithError() throws { // Put setup code here. This method is called before the invocation of each test method in the class. } override func tearDownWithError() throws { // Put teardown code here. This method is called after the invocation of each test method in the class. } func testExample() throws { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. // Any test you write for XCTest can be annotated as throws and async. // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. } func testPerformanceExample() throws { // This is an example of a performance test case. measure { // Put the code you want to measure the time of here. } } } 這個是由 Xcode 自動產生的單元測試的 Template: ...

October 8, 2025 · 2 分鐘

[Day 24] 地理圍欄通知(二)

前言 首先簡要回顧前幾天的進度: Day 11:我們實作了地理圍欄的核心功能,讓 App 能夠在用戶進入或離開特定區域時觸發事件 。 Day 21 & 22:我們優化了地圖互動,實現了點擊圖釘後彈出詳細資訊視窗(Sheet),並加入了跳轉至 Apple Maps 或 Google Maps 的功能 。 Day 23:我們導入了 UserDefaults 和 Codable,實現了追蹤狀態的持久化。現在 App 即使關閉重啟,也能記得正在追蹤的地點 。 但程式碼跑起來是一回事,但在真實世界中是否可運作又是另一回事,所以今天的核心任務是驗證。我們將設計一系列的測試情境,徹底檢驗地理圍欄通知的穩定性與準確性,並記錄測試過程與結果。 測試準備 使用 GPX 檔案進行定位模擬 新增 GPX 檔案 為了確保測試的準確性與可重複性,我們需要一個可靠的方法來模擬使用者的移動。在 Day 11 測試地理圍欄功能時,我們是透過直接修改模擬器定位座標的方式來進行,雖然可行,但操作上較為繁瑣且不易模擬連續的路徑。今天我們將採用另一種方式,那就是在 Xcode 專案中匯入 GPX 檔案,用它來模擬「進入區域」與「離開區域」的完整行為。 GPX(GPS Exchange Format)是一種標準的 XML 格式,可以用來記錄 GPS 座標點。你可以在 Xcode 裡面直接新增一個 GPX 檔案,Xcode 會提供一個標出蘋果總部的座標預設的模板。 <?xml version="1.0"?> <gpx version="1.1" creator="Xcode"> <!-- Provide one or more waypoints containing a latitude/longitude pair. If you provide one waypoint, Xcode will simulate that specific location. If you provide multiple waypoints, Xcode will simulate a route visiting each waypoint. --> <wpt lat="37.331705" lon="-122.030237"> <name>Cupertino</name> <!-- Optionally provide a time element for each waypoint. Xcode will interpolate movement at a rate of speed based on the time elapsed between each waypoint. If you do not provide a time element, then Xcode will use a fixed rate of speed. Waypoints must be sorted by time in ascending order. --> <time>2014-09-24T14:55:37Z</time> </wpt> </gpx> 這個模板讓我們可以直接修改 <wpt> 標籤中的 lat 和 lon數值,將其變更為我們需要的任何地點。不過,若要模擬一條完整的路線,手動輸入每個路徑點的座標顯然不切實際。因此,這裡我們不打算自己逐點加入,而是借助第三方工具來更快速地產出包含連續路徑的 GPX 檔案。 ...

October 7, 2025 · 2 分鐘

[Day 23] 地理圍欄通知(一)

前言 今天要來完成的是地理圍欄(Geofencing)功能,讓使用者可以針對選定的里程位置訂閱通知,當進入/離開該區域時,會收到推播通知。 關於在 LoactionManager 裡面實作有關包含定位權限、通知權限以及建立與監控 Geofencing 的邏輯,我們在 Day 11 時已經有相當程度的介紹了,因此今天主要是將 UI 建立好,並串接以上的邏輯,而重點會放在「如何管理追蹤狀態」這件事情上,我們等等進一步談這部分。 新增啟用追蹤按鈕 昨天在 PinDetailSheet 裡面加上了 Apple Maps 與 Google Maps 的按鈕,現在我們要加上讓使用者控制啟用追蹤的按鈕: struct PinDetailSheet: View { // ... let isTracking: Bool // 判斷是否正在追蹤 let onToggleTracking: () -> Void // 點擊追蹤按鈕後執行的邏輯 // ... var body: some View { // ... VStack(spacing: 12) { HStack(spacing: 12) { // Apple Maps Button // Google Maps Button } Button(action: onToggleTracking) { HStack { Image(systemName: isTracking ? "bell.slash.fill" : "bell.fill") Text(isTracking ? "取消追蹤" : "啟用追蹤") } .frame(maxWidth: .infinity) .padding(.vertical, 4) } .buttonStyle(.borderedProminent) .tint(isTracking ? .red : .orange) .controlSize(.large) } } .padding(EdgeInsets(top: 12, leading: 16, bottom: 20, trailing: 16)) } ...

October 6, 2025 · 4 分鐘

[Day 22] 導向地圖 App - URL Scheme

今天要來完成將圖標地點用 Apple Maps 或 Google Maps 來開啟。 URL Scheme 要在 App 之間進行跳轉,就如同網站一般,URL Scheme 是一種特殊的網址格式,它允許 App之間互相溝通、啟動或直接跳轉到 App 內的特定功能頁面。你可以將它想像成專門用來定位 App 功能的網址。 一個 URL Scheme 的結構大概是:scheme://host/path?query,scheme 是 App 的唯一識別符,例如 google maps 的 URL Scheme 為 comgooglemaps://。Host / Path 是主機與路徑,用來指定 App 內部的特定功能或頁面。Query 為查詢參數,用於傳遞資料給 App。例如,在購物 App 中打開特定商品頁面,可以透過參數傳遞商品編號。 開發者在 App 中註冊一個專屬的 URL Scheme。當使用者在手機的任何地方(例如另一個 App 或網頁瀏覽器)點擊這個特殊格式的 URL 時,作業系統(如 iOS 或 Android)會識別這個 Scheme,並啟動對應的 App,同時將後續的路徑和參數傳遞給它處理。 iOS 中 URL Scheme 的基本實踐 在 iOS 開發中,我們常需要透過 URL Scheme 來啟動另一個 App 或跳轉到其特定頁面。核心的實作會圍繞著 UIApplication 的兩個主要方法:open(_:options:completionHandler:) 和 canOpenURL(_:)。 ...

October 5, 2025 · 3 分鐘

[Day 21] 里程定位與地圖顯示(七)- TapGesture & Sheet

接續昨天的進度,現在地圖上的圖標已經能按照資料集的經緯度精確標出位置,並在圖標上方顯示道路名稱與里程數。然而,僅有這些資訊是不夠的,我們需要提供一個更豐富的互動方式,讓使用者能深入了解每個地點的詳情。 一個常見的解決方案是,讓使用者點擊圖標後,從螢幕下方彈出一個包含詳細資訊的 sheet。這種做法不僅能呈現更多內容,還能確保使用者無需離開當前頁面,從而避免割裂感,提供流暢的使用體驗。 選擇最適合的 Sheet 呈現方式 要使用 SwiftUI 彈出 sheet,蘋果官方提供了幾種不同的方法,其中最常見的兩種是: sheet(isPresented:onDismiss:content:) 這個方式是綁定一個 Bool 參數,來判斷是否需要 present sheet。 sheet(item:onDismiss:content:) 這裡的 item 綁定的是一個可選型別 (Optional) 的物件,透過當這個參數傳入的物件為是否為有值來決定彈出 sheet 與否。這應該是會比較適合我們的實作方式,因為我們的 sheet 內容是要與圖標物件綁定的。 考慮到我們的 sheet 需要顯示特定圖標的詳細資訊,第二種方法顯然更適合我們的實作場景。 步驟一:綁定狀態與觸發事件 首先,我們在 MapView 中宣告一個 @State 變數 selectedPin,它的型別是 MarkerPin?(可選的 MarkerPin)。因為使用者一開始尚未選擇任何圖標,所以它的初始值為 nil。 @State private var selectedPin: MarkerPin? = nil 接著,我們在 MapView 的最外層容器 ZStack 上附加 .sheet 修飾符,並將它的 item 參數綁定到 $selectedPin。 var body: some View { ZStack(alignment: .top) { Map(position: $cameraPosition) { // .. } } .sheet(item: $selectedPin) { pin in // 在這個閉包中,`pin` 就是使用者所點擊的那個 MarkerPin 物件 // 我們將在這裡建構 sheet 的內容 } } 現在,我們只需要在使用者點擊圖標時,將該圖標的物件指派給 selectedPin 即可。我們回到 Annotation 的程式碼,為其加上 .onTapGesture 事件: ...

October 4, 2025 · 2 分鐘