[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 分鐘

[Day 20] 里程定位與地圖顯示(六)- 佈局調整

今天我們先繼續把 UI 調整跟草圖接近一致。目前上半部的選項與下方的地圖區塊是分開的,為了讓選項卡片能夠疊在地圖上,我們要將這些既有元件放到 ZStack 裡。 var body: some View { ZStack(alignment: .top) { Map(position: $cameraPosition) { ForEach(pins) { pin in Annotation("", coordinate: pin.coordinate) { // ... } } } } VStack { VStack(spacing: 18) { Picker("公路類型", selection: $category) { ForEach(RoadCategory.allCases) { category in Text(category.rawValue).tag(category) } } .pickerStyle(.segmented) HStack { RoadPickerView( availableRoads: availableRoadNumbers, selection: $selectedRoad ) // ... TextField("輸入里程", text: $mileageText) // ... Button(action: { // ... }) { } } // 上半部卡片樣式 .padding(EdgeInsets(top: 25, leading: 16, bottom: 25, trailing: 16)) .background(.ultraThinMaterial) .cornerRadius(20) .shadow(color: .black.opacity(0.1), radius: 10, y: 5) } } } 最底層是 Map,接著是包含著公路類型選擇 Picker、道路選擇 Menu及里程輸入欄的 VStack。另外,使用 .background(.ultraThinMaterial) 套用一些毛玻璃效果與陰影,微微透視後面的 Map,整體而言會更自然,效果如下: ...

October 3, 2025 · 1 分鐘

[Day 19] 里程定位與地圖顯示(五)- 自訂元件樣式

今天繼續刻 UI! 在 SwiftUi 中自訂元件樣式 當你使用 SwiftUI 框架提供的元件,通常來說你能夠自訂的部分不多,不然就會受到許多限制。以 Picker 來說,你能改的部分大概就是選擇/未被選擇的字體顏色、背景等。如果你要完全客製化,那你可能得自己使用其他元件來自己刻。 除了自己刻之外,一般來說方法有: 調用 UIKit 方法 使用 UISegmentedControl.appearance()。 這是使用 UIKit 的方式來更改元件外觀,但會影響到 App 中 所有的 Segmented 外觀。可以在 View 的 init() 或在 Picker 的 .onAppear 修飾符來設定,例如: struct ContentView: View { // ... init() { UISegmentedControl.appearance().setTitleTextAttributes( [.foregroundColor: UIColor.systemTeal], for: .selected ) } // ... } 使用第三方函式庫 另一個方法是使用第三方函式庫(例如 Introspect) 若你不想更動 App 中所有的 Segmented,只希望針對特定的 Picker 進行修改,可以使用 Introspect 這類函式庫。它能讓你取得 SwiftUI 元件背後的 UIKit 元件,並直接對其進行設定。 ...

October 2, 2025 · 2 分鐘

[Day 18] 里程定位與地圖顯示(四)- Picker 滑動時會亂跳?開立 bug 單吧!

在我們昨天的進度中,我們建立了一個選擇道路的 Picker。但正當我興高采烈地測試時,發現了一個奇怪的 bug:當我快速滑動 Picker 的選項列表,手指一放開,列表的慣性滑動動畫到一半,它就自己重新整理了。 我的第一個反應是:「蝦米,這啥鬼?」我反覆檢查程式碼,Picker 的 selection 明明只綁定了 @State private var selectedRoad,在滑動過程中,這個變數的值也沒改變,為什麼它會自己跳回去?難道是 SwiftUI 的 bug? Troubleshooting SwiftUI View 的計算本質 經過一番研究(問 AI…XD),我發現問題的根源要回到 SwiftUI 對於 View 的本質: 在 SwiftUI 裡,View 不是靜態的畫面,而是由「狀態 (State)」推導出來的結果。 我們透過以下簡單的例子再一次複習 SwiftUI 畫面更新的概念: struct CounterView: View { @State private var count = 0 var body: some View { VStack { Text("Count: \(count)") Button("加一") { count += 1 } } } } 這裡的 body 是一個 compute property,它的職責是根據目前的狀態 count,回傳一個描述 UI 的藍圖。輸入是 count 的值,輸出是一個新的 View,只要 count 改變,整個 body就會重新被計算,畫面就會跟著更新。這表示,只要一個 View 所依賴的任何一個「狀態來源」(@State, @StateObject 等) 發生改變,SwiftUI 就會重新執行這個 View 的 body 屬性,計算出一個新的 View 結構。 ...

October 1, 2025 · 2 分鐘