[Day 17] 里程定位與地圖顯示(三)- 實作搜尋邏輯

今天要來填上核心的「搜尋」與「顯示」邏輯了,包含處理里程輸入,接收並驗證使用者輸入的公里數,並實作搜尋函式,處理不同里程格式的解析,找出最近的地理位置,最後在地圖上顯示結果。 實作搜尋邏輯 filter 高階函式 在搜尋按鈕按下之後,我們要執行的是 searchAction() 這個函式。第一步,透過 filter 此另一個 Swift 的高階函式來篩選我們要的資料: private func searchAction() { let candidates = dataManager.highwayMarkers.filter { $0.roadNumber == selectedRoad } } filter 函式的呼叫,它需要一個 closure 作為參數,這個 closure 就是你的「篩選條件」。filter 會依序將 highwayMarkers 陣列中的每一個元素傳入這個 closure。Closure 會回傳一個布林值,如果 closure 對某個元素回傳 true,filter 就會把這個元素保留下來,放進新的陣列;如果回傳 false,這個元素就會被丟棄。 因此,{ $0.roadNumber == selectedRoad } 表示,目前正在檢查的這個物件,它的 roadNumber 是否等於使用者選擇的 selectedRoad?假設要搜尋的是國道 1 號,最後 filter 會回傳一個新的陣列 candidates,裡面只包含所有 roadNumber 是國道 1號的 HighwayMileageMarker 物件。這個 candidates 陣列就是我們接下來要進行里程搜尋的目標資料。 泛型、Closure 與 KeyPath 在這個 App ,核心功能是根據使用者輸入的里程,從資料中找出最接近的地理位置。然而,國道省道的資料來源、格式不盡相同。例如,國道的里程牌面可能是「014K+800」,而省道則是「5.1」這樣的浮點數。如果為兩者各寫一套搜尋邏輯,會導致程式碼大量重複且難以維護。為了解決這個問題,因此可以設計一個通用的搜尋函式。 為了達到這個目的,必須運用到 Swift 的幾個功能: 泛型 (Generics):我們用泛型 <T> 來定義這個函式,使其不限定處理特定的資料型別。這讓函式的宣告看起來像這樣,其中 T 可以是任何我們想傳入的資料型別: private func findClosestMarker<T>( in items: [T], targetMile: Double, mileExtractor: (T) -> Double?, titleExtractor: (T) -> String, latKey: KeyPath<T, Double>, lonKey: KeyPath<T, Double>, maxAllowedDiffKm: Double? = nil ) -> // ... 回傳值 這樣一來,不論傳入的是國道標記陣列還是省道標記陣列都能處理。 ...

September 30, 2025 · 3 分鐘

[Day 16] 里程定位與地圖顯示(二)- Enum 與 Picker 搭配

今天要做另一個重要功能,我們要讓使用者能根據選擇的公路類型(國道/省道)和輸入的里程數,從預載的 CSV 資料中進行搜尋,並在地圖上精準標示出對應的地理位置。 可以在 Description 當中詳細描述這張 task 要完成些什麼事情,訂定「驗收標準(Acceptance Criteria)」。這是用來明確定義該 task 完成的條件和品質要求,確保開發人員和 user 對任務的完成有共同認知。即使是 Basic 架構,清楚的驗收標準依然能幫助提升開發效率和品質,避免誤解或遺漏需求。 那就來解決掉這張 task 吧! 任務拆分 里程搜尋這個任務,可以拆分成三大區塊: 輸入 選擇國道/省道 選擇道路 里程輸入(公里) 搜尋 從 CSV 解析後的物件中,篩出該道路的所有里程點 轉為可比較的「公里數」再找最近距離 設定一個最大容忍差距(例如 2 公里),超過就視為查無合理結果 顯示 地圖置中到結果的區域 放上一個大頭針(顯示牌面或公里數)。 但是,其實資料來源內容不太相同,解析規則要怎麼處理就會是個問題,例如國道牌面格式是「014K+800」,省道是「5.1」這種浮點數字串。重點是要將把人看得懂的牌面,轉成程式能比較的數字。哪些算、哪些不算,遇到異常如何處理,重點應該在這裡。 至於搜尋邏輯採「最近距離」而不是「完全匹配」的原因很簡單:資料可能不完整。這是資料源的限制,只能說這是一種取捨。如果最近的點也超過 2 公里,就直接回「查無合理結果」,避免在資料有缺或輸入不準時,硬給一個很遠的點誤導使用者。若兩個點距離一樣近,可以選「里程較小」或「較大」,比較符合沿著里程增加方向搜尋的直覺。 另外,搜尋邏輯的效能,先採取 linear time 就好,先把功能跑起來,目前手機端資料量還在可接受範圍內,未來若有進一步需要,效能不夠再談索引或空間資料結構,現階段主要先以完成 MVP 為主。 使用 Picker 建立道路選擇器 Enum 的運用 我們目前的資料有國道與省道,因為之後的資料、邏輯都會個別綁定在這兩個類型上,因此我們可以用 enum 來列舉這兩個項目: enum RoadCategory: String, CaseIterable, Identifiable { case highway = "國道" case provincial = "省道" var id: String { rawValue } } 這裡遵循了兩個特殊協定,CaseIterable 是為了讓 enum 能用 allCases 列出所有選項,方便做需要迭代的 Picker/Segment。而 Identifiable + var id 是讓每個選項有唯一識別,如此一來可以用 rawValue 當 id,可直接被 ForEach 使用,不必再加上 id: \.self。 ...

September 29, 2025 · 2 分鐘

[Day 15] 里程定位與地圖顯示(一)- 資料讀取

前言 第一個進行的 issue 是「里程定位與地圖顯示」,先用 VS code 來看一下這兩份 CSV(省道與國道)長什麼樣子: 我們現在的目標是,讀取 CSV 檔案,然後將其轉成我們定義好的物件類型。 Git 分支規劃 從 main 分出 develop 再從 develop 分出 feature/parse-csv 分支 於 feature/parse-csv 分支進行開發 資料結構 依據 CSV 欄位,定義我們需要的結構: struct ProvincialMileageMarker { let roadNumber: String // 公路編號 let county: String // 隸屬縣市 let wgs84Lon: Double // 坐標-E-WGS84 let wgs84Lat: Double // 坐標-N-WGS84 let township: String // 隸屬鄉鎮 let location: String // 設置位置 let content: String // 牌面內容 let condition: String // 現況 let direction: String // 牌面方向 } struct HighwayMileageMarker { let roadNumber: String // 國道編號 let county: String // 隸屬縣市 let wgs84Lon: Double // 坐標X-WGS84 let wgs84Lat: Double // 坐標Y-WGS84 let display: String // 牌面內容 let direction: String // 方向與備註 } 資料讀取與顯示 搭配 Combine,建立 RoadDataManager,實作資料背景讀取與在 Day 9 介紹過的 Combine 來自動更新畫面: ...

September 28, 2025 · 3 分鐘

[Day 12] 使用 Azure board 進行專案管理

前言 在 Day 1 的時候有提到,在我剛進公司時,公司正好在導入 Azure DevOps 作為專案管理工具。這讓我有機會從零開始學習如何使用這個工具,並且在實際的工作中應用它,也因此體會到了專案管理工具對於軟體開發的重要性與價值。例如,它能幫助團隊更清楚地了解專案進度、分配任務、追蹤 Bug 等等。在還沒有工具之前,我們可能常常用 Excel、Trello 或甚至是口頭溝通,結果就是「欸,那個功能上次是誰說要做的?」或是「這個 Bug 修好了嗎?」之類的對話層出不窮,資訊非常分散。每一個工作項目都能被清楚地記錄與追蹤,讓整個開發流程變得更有條理;每一個工作項目都能被清楚地記錄與追蹤,讓整個開發流程變得更有條理。而原本今年鐵人賽想單純撰寫 SwiftUI App 的計畫,但想了一下,或許我可以嘗試把我使用 Azure DevOps 的經驗也融入其中,也是個不錯的結合。 什麼是 Azure Boards? Azure Boards 是一個基於網頁的服務,讓團隊可以一起規劃、追蹤和討論整個開發過程的工作,並且支援敏捷式方法。它提供了可自訂的平台來管理各種工作項目,讓團隊能夠有效協作並簡化工作流程。 如果用更白話的方式來說,Azure Boards 就像是「專案進度白板」的進化版。想像你和同事要一起做一個 App,大家會把要做的事情(像是設計畫面、寫程式、修 bug)一條一條寫在便利貼上,貼在牆上,然後每天早上一起討論誰要負責什麼、做完了沒。Azure Boards 就是把這種「便利貼牆」搬到網路上,而且還加了很多自動化和追蹤功能。 舉個實際例子,假設你們團隊要開發一個新的功能,大家可以在 Azure Boards 上建立一個「Issue」,分配給負責的人,設定截止日期,還能隨時更新進度。如果有 bug,直接開一個新的工作項目,標記為「Bug」,讓大家都知道目前有什麼問題需要解決。你可以根據專案規模選擇不同的流程(像是 Basic 或 Agile),小型團隊可以用最簡單的方式管理,大型團隊則能用更細緻的分工。 總結來說,Azure Boards 就是幫助團隊把所有待辦事項、進度、問題都集中管理,讓大家不會漏掉重要的事,也能清楚看到專案的整體進展。 依據團隊與專案的不同,Azure Boards 提供四種流程選擇: Agile:使用敏捷式規劃方法時使用,適合追蹤 User Story,並可選擇性地在面板上追蹤 Bug。 Basic:適合小型團隊或個人專案,簡化工作項目結構,讓管理更直觀。 Scrum:適合正式執行 Scrum。 CMMI:當小組遵循更正式的專案方法時選擇。 詳細可參考微軟說明。 我們公司是使用 Agile 流程,但在這個專案中,我會使用 Basic 流程來進行管理。因為這個專案的規模較小,且我主要是個人開發,所以 Basic 流程就足夠了。如果用 Agile 或 Scrum,會多出像 User Story、Feature、等這些工作項目類型,對我一個人來說反而有點太複雜了,Basic 的三層架構(Epic → Issue → Task)對我來說最直覺。如果想要進一步了解差異,可以參考微軟官方文件。 ...

September 25, 2025 · 2 分鐘

[Day 3] Azure DevOps 設定與 Xcode 專案初始化

Azure DevOps 設定 什麼是 Azure DevOps? Azure DevOps 是微軟提供的開發工具服務平台,整合了版本控制、工作項目追蹤、自動化建置等功能。對於 iOS 開發者來說,它提供了: Azure Repos:Git 版本控制系統 Azure Pipelines:自動化建置和部署工具 Azure Boards:專案管理和工作追蹤 Azure Test Plans:測試管理工具 Azure Artifacts:套件管理系統 Azure DevOps 免費方案 依據官方說明,個人開發者可以享有以下免費額度: 無限量的私人 Git 儲存庫 最多 5 位 Azure DevOps 使用者 每月 1,800 分鐘的 Pipeline 執行時間 2GB 的 Azure Artifacts 儲存空間 這些免費額度對於個人專案來說已經相當充足。 建立 Azure DevOps 帳號 前往 Azure DevOps 官網 點擊「開始使用 Azure」按鈕 使用 Microsoft 帳號登入(如果沒有就註冊)。首次啟用 Azure 帳號時,系統會要求您填寫基本資料,依照畫面指示完成即可。 最後系統會要求綁定信用卡,這主要是為了驗證身分,在免費額度內並不會產生費用。如後續說明,Azure DevOps 的免費額度對個人專案來說綽綽有餘。 建立組織(Organization 登入後,第一步是建立一個組織。 ...

September 16, 2025 · 2 分鐘