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

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