[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 14] UI/UX 規劃 (二) - App 畫面草圖

前言 今天我們來畫 App 畫面草圖,有了草圖,之後才有辦法按圖施工~ 手繪 Wireframe or AI 工具? Wireframe 一般來說,最初會從手繪草圖開始。你只需要一支筆和一張紙(或平板),可以快速畫出多種版本的設計,不滿意就立刻劃掉重來,幾乎沒有任何時間和金錢成本。 這個階段,我們完全不用考慮顏色、字體或精美的圖示。所有的精力都集中在「畫面上該放什麼元件?」、「按鈕應該在哪裡?」以及「使用者如何從 A 畫面跳到 B 畫面?」這些核心問題上。它能幫助你將腦中模糊的想法具體化,並檢查流程是否順暢。這樣的好處是,你的思考是自由的,你不會被工具的功能或好看的的 icon 分心。 手繪草圖其實就是 Wireframe (線框圖) 的一種形式。Wireframe(線框圖)是 App 的低保真度設計原型,像是 App 的骨架或藍圖。它通常由簡單的線條和方框組成,用來呈現: 內容佈局:各個區塊(如圖片、文字、列表)在畫面中的位置。 核心元件:包含哪些按鈕、輸入框、導航欄等。 資訊架構:如何組織畫面上顯示的資訊。 Wireframe 主要著重於功能,而非視覺設計,同時也確保開發者和設計師在動手前對 App 的樣貌和流程有一致的理解。 AI 工具的新可能 現在有一些 AI 工具,可以讓你用文字描述你想要的畫面,然後自動生成視覺設計稿或 Wireframe,甚至直接給你完整的 UI 設計。這類工具(如 Figma AI 等)非常適合作為初期發想的輔助,幫助你快速探索不同的設計方向。若你的 App 很簡單,或是只是想快速做個 MVP,那或許很適合直接請 AI 幫你產出畫面設計。 但不論是自己全手動繪製,或是請 AI 產出,本質上你都還是得必須先透過自己思考你的 UI 佈局,畢竟請 AI 產,你還是得給予具一定精準度的提示,否則產出結果可能不如預期。 而且,AI 產出的設計需要專業評估。你需要檢查功能完整性,是否涵蓋所有必要功能?流程是否符合使用者習慣?設計是否能實際開發實現?或是否符合你的 App 定位? 我的實際嘗試 手繪先行 先手繪基本流程,用平板快速勾勒頁面架構。 搜尋畫面:國道/省道、道路選擇、地圖。 追蹤元件:這個頁面會顯示正在被監控的地理圍欄點,且包含: 里程標資訊: 例如「台19線 89公里」。 當前狀態: 顯示該點目前是「在範圍內」還是「在範圍外」。 停止追蹤/啟用追蹤功能 AI 設計的評估與調整 接著,試著請 AI 來產一份 UI 設計,看看成果如何。 ...

September 27, 2025 · 1 分鐘

[Day 13] UI/UX 規劃 (一) - 使用者流程

前言 在我們開始繪製使用者介面之前,我們將先聚焦在使用者流程 (User Flow)。 這就好比建築師在蓋房子前,不會先煩惱沙發要買什麼顏色,而是會先畫出整棟建築的平面圖與動線規劃。使用者流程圖幫助我們釐清,使用者為了達成某個特定目標,需要依序經過哪些畫面、點擊哪些按鈕。 預先規劃好完整的流程,再針對每一個環節去設計對應的畫面,才能確保 App 的功能環環相扣,既不會做出無用的設計,也不會遺漏掉關鍵的操作步驟。 什麼是使用者流程圖? 使用者流程圖(User Flow Diagram)它描繪了使用者為達成特定目標時,從進入 App 開始到完成任務的完整路徑。流程圖聚焦在: 使用者如何在不同畫面/功能之間移動 每個操作觸發的系統反應與後續分支 各種異常或錯誤狀況下的備援流程 決策點與不同選擇導致的結果 它與 Wireframe 不同在於,流程圖專注於流程的邏輯性與連貫性,而 Wireframe 則專注於畫面的佈局與排版。 流程圖的價值 當你在規劃流程圖時,你會用使用者的角度去看待操作過程,同時會意識到過程中的可能的瑕疵或邏輯不順的地方。如果沒有事先思考就開始開發,你很可能開發到一半才發現有問題,例如「啊,使用者從這個頁面要怎麼回到上一頁?」或是「等等,如果使用者沒登入,這個按鈕按下去會發生什麼事?」 此時修改的成本可能會相當地高,最慘甚至可能導致整個功能必需重新設計。 而從另一個角度來看,事先進行這個過程,同時也會思考如何提升使用者體驗,確保每個使用者路徑都有清楚的起點、過程與結果,讓使用者在使用 App 時感到順暢自然。 工具 - draw.io 我通常使用 draw.io 這個工具來建立流程圖,它是免費、功能完整且支援多種匯出格式的流程圖工具。流程圖的圖示皆有代表的意義,例如: 圓角矩形(開始/結束):代表流程的起點與終點 矩形(畫面/動作):代表具體的畫面或使用者動作 菱形(決策點):代表需要判斷或選擇的節點 箭頭(流向):表示流程的方向與順序 流程圖的重點是釐清邏輯,而不是畫得漂亮。就算一開始用筆和紙在便條紙上畫草稿也完全沒問題。工具只是輔助,能幫助我們把腦中的邏輯具象化,才是最重要的。 規劃專案 App 的流程 在我們的 App 構想中,包含了地理圍欄通知、搜尋歷程等功能,但在本篇文章中,我們將以核心功能公路里程搜尋為例,從頭到尾走一遍使用者流程圖的規劃過程。掌握了這個概念,實作其他功能的流程圖時,原理都是一樣的。 範例:搜尋特定里程點 App 最基本的功能流程,這裡我就使用 draw.io 來繪製: 可以輸出成圖片: 流程從綠色的「開始」到紅色的「結束」。頁面與動作 (矩形)則代表使用者看到的畫面或執行的操作。抉擇點 (菱形)則為使用者需要做選擇,且不同的選擇會導向不同結果的地方。 開始 → 主畫面 使用者打開 App,看到地圖及上方的搜尋欄。 點擊「道路類型」→ 抉擇:國道或省道? 這是使用者的第一個岔路,根據使用者的選擇(國道/省道),系統會載入不同的道路列表。 顯示列表 → 道路選擇 + 里程輸入 使用者從列表中選定一條具體的道路(如:國道一號),並在下方的欄位輸入里程數字。 ...

September 26, 2025 · 1 分鐘