Skip to content

Commit afc1c64

Browse files
authored
Add search to the Menu (#695)
* Add search to the Menu covering Settings sub-items Make SettingsRoute the single source of truth for the Settings list by moving each row's title, icon and search keywords onto the route, and grouping them via menuSections(nightscoutConfigured:). SettingsMenuView now renders from that data. Add .searchable to the Menu. Search results are assembled from the same sources that build the menu — the Settings routes, the tab features and the static rows — so they stay in sync automatically. A settings result deep-links straight into its sub-screen; features, log and support-link results reuse the menu's existing navigation. * Make bottom-level settings findable in Menu search Add a leaves list to SettingsRoute describing the individual settings inside each sub-screen (row titles plus search synonyms), and include them in the Menu search index after the screen-level items. A leaf result shows a Settings → <screen> subtitle and opens the screen that contains the setting, e.g. searching "basal" finds Graph Basal under Advanced. Info Display leaves are derived from InfoType so they follow the app's info rows automatically. NavigationRow gains an optional subtitle to render these results.
1 parent d520f23 commit afc1c64

3 files changed

Lines changed: 494 additions & 163 deletions

File tree

LoopFollow/Helpers/Views/NavigationRow.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import SwiftUI
55

66
struct NavigationRow<Value: Hashable>: View {
77
let title: String
8+
var subtitle: String? = nil
89
let icon: String
910
var iconTint: Color = .white
1011
let value: Value
@@ -13,7 +14,14 @@ struct NavigationRow<Value: Hashable>: View {
1314
NavigationLink(value: value) {
1415
HStack {
1516
Glyph(symbol: icon, tint: iconTint)
16-
Text(title)
17+
VStack(alignment: .leading, spacing: 2) {
18+
Text(title)
19+
if let subtitle {
20+
Text(subtitle)
21+
.font(.caption)
22+
.foregroundStyle(.secondary)
23+
}
24+
}
1725
}
1826
}
1927
}

LoopFollow/Settings/SettingsMenuView.swift

Lines changed: 246 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -9,100 +9,44 @@ struct SettingsMenuView: View {
99

1010
var body: some View {
1111
List {
12-
dataSection
13-
14-
Section("Display Settings") {
15-
NavigationRow(title: "General",
16-
icon: "gearshape",
17-
value: SettingsRoute.general)
18-
NavigationRow(title: "Graph",
19-
icon: "chart.xyaxis.line",
20-
value: SettingsRoute.graph)
21-
22-
if !nightscoutURL.value.isEmpty {
23-
NavigationRow(title: "Information Display",
24-
icon: "info.circle",
25-
value: SettingsRoute.infoDisplay)
26-
}
27-
28-
NavigationRow(title: "Units and Metrics",
29-
icon: "scalemass",
30-
value: SettingsRoute.units)
31-
32-
NavigationRow(title: "Tabs",
33-
icon: "rectangle.3.group",
34-
value: SettingsRoute.tabSettings)
35-
}
36-
37-
Section("App Settings") {
38-
NavigationRow(title: "Background Refresh",
39-
icon: "arrow.clockwise",
40-
value: SettingsRoute.backgroundRefresh)
41-
42-
NavigationRow(title: "Import/Export",
43-
icon: "square.and.arrow.down",
44-
value: SettingsRoute.importExport)
45-
46-
NavigationRow(title: "APN",
47-
icon: "bell.and.waves.left.and.right",
48-
value: SettingsRoute.apn)
49-
50-
#if !targetEnvironment(macCatalyst)
51-
NavigationRow(title: "Live Activity",
52-
icon: "dot.radiowaves.left.and.right",
53-
value: SettingsRoute.liveActivity)
54-
#endif
55-
56-
if !nightscoutURL.value.isEmpty {
57-
NavigationRow(title: "Remote",
58-
icon: "antenna.radiowaves.left.and.right",
59-
value: SettingsRoute.remote)
12+
ForEach(SettingsRoute.menuSections(nightscoutConfigured: !nightscoutURL.value.isEmpty), id: \.0) { section, routes in
13+
Section(section.rawValue) {
14+
ForEach(routes) { route in
15+
NavigationRow(title: route.title,
16+
icon: route.icon,
17+
value: route)
18+
}
6019
}
6120
}
62-
63-
Section("Alarms") {
64-
NavigationRow(title: "Alarms",
65-
icon: "bell.badge",
66-
value: SettingsRoute.alarmSettings)
67-
}
68-
69-
Section("Integrations") {
70-
NavigationRow(title: "Calendar",
71-
icon: "calendar",
72-
value: SettingsRoute.calendar)
73-
74-
NavigationRow(title: "Contact",
75-
icon: "person.circle",
76-
value: SettingsRoute.contact)
77-
}
78-
79-
Section("Advanced Settings") {
80-
NavigationRow(title: "Advanced",
81-
icon: "exclamationmark.shield",
82-
value: SettingsRoute.advanced)
83-
}
8421
}
8522
.navigationTitle("Settings")
8623
.navigationBarTitleDisplayMode(.large)
8724
}
25+
}
8826

89-
// MARK: – Section builders
27+
// MARK: – Sheet routing
9028

91-
@ViewBuilder
92-
private var dataSection: some View {
93-
Section("Data Settings") {
94-
NavigationRow(title: "Nightscout",
95-
icon: "network",
96-
value: SettingsRoute.nightscout)
97-
98-
NavigationRow(title: "Dexcom",
99-
icon: "sensor.tag.radiowaves.forward",
100-
value: SettingsRoute.dexcom)
101-
}
102-
}
29+
enum SettingsSection: String, CaseIterable, Hashable {
30+
case data = "Data Settings"
31+
case display = "Display Settings"
32+
case app = "App Settings"
33+
case alarms = "Alarms"
34+
case integrations = "Integrations"
35+
case advanced = "Advanced Settings"
10336
}
10437

105-
// MARK: – Sheet routing
38+
/// A single setting inside a settings screen, exposed to Menu search. Leaves
39+
/// are not navigable on their own — a search hit opens the screen
40+
/// (`SettingsRoute`) that contains it.
41+
struct SettingsLeaf: Hashable {
42+
let title: String
43+
let keywords: [String]
44+
45+
init(_ title: String, _ keywords: [String] = []) {
46+
self.title = title
47+
self.keywords = keywords
48+
}
49+
}
10650

10751
enum SettingsRoute: Hashable, Identifiable {
10852
case settings
@@ -125,6 +69,224 @@ enum SettingsRoute: Hashable, Identifiable {
12569

12670
var id: Self { self }
12771

72+
// MARK: – Row presentation (single source of truth)
73+
74+
/// Title shown in the Settings list and used for search matching.
75+
/// Non-row cases (`.settings`, `.aggregatedStats`) return "" and are never
76+
/// included in `menuSections`.
77+
var title: String {
78+
switch self {
79+
case .nightscout: return "Nightscout"
80+
case .dexcom: return "Dexcom"
81+
case .general: return "General"
82+
case .graph: return "Graph"
83+
case .infoDisplay: return "Information Display"
84+
case .units: return "Units and Metrics"
85+
case .tabSettings: return "Tabs"
86+
case .backgroundRefresh: return "Background Refresh"
87+
case .importExport: return "Import/Export"
88+
case .apn: return "APN"
89+
#if !targetEnvironment(macCatalyst)
90+
case .liveActivity: return "Live Activity"
91+
#endif
92+
case .remote: return "Remote"
93+
case .alarmSettings: return "Alarms"
94+
case .calendar: return "Calendar"
95+
case .contact: return "Contact"
96+
case .advanced: return "Advanced"
97+
case .settings, .aggregatedStats: return ""
98+
}
99+
}
100+
101+
var icon: String {
102+
switch self {
103+
case .nightscout: return "network"
104+
case .dexcom: return "sensor.tag.radiowaves.forward"
105+
case .general: return "gearshape"
106+
case .graph: return "chart.xyaxis.line"
107+
case .infoDisplay: return "info.circle"
108+
case .units: return "scalemass"
109+
case .tabSettings: return "rectangle.3.group"
110+
case .backgroundRefresh: return "arrow.clockwise"
111+
case .importExport: return "square.and.arrow.down"
112+
case .apn: return "bell.and.waves.left.and.right"
113+
#if !targetEnvironment(macCatalyst)
114+
case .liveActivity: return "dot.radiowaves.left.and.right"
115+
#endif
116+
case .remote: return "antenna.radiowaves.left.and.right"
117+
case .alarmSettings: return "bell.badge"
118+
case .calendar: return "calendar"
119+
case .contact: return "person.circle"
120+
case .advanced: return "exclamationmark.shield"
121+
case .settings, .aggregatedStats: return ""
122+
}
123+
}
124+
125+
/// Extra synonyms so search finds a page by related terms, not just its title.
126+
var keywords: [String] {
127+
switch self {
128+
case .graph: return ["chart"]
129+
case .units: return ["mmol", "mgdl", "metrics"]
130+
case .infoDisplay: return ["info"]
131+
case .apn: return ["push", "notification"]
132+
case .liveActivity: return ["dynamic island", "lock screen"]
133+
case .importExport: return ["import", "export", "backup"]
134+
default: return []
135+
}
136+
}
137+
138+
/// The individual settings inside this screen, so Menu search can find a
139+
/// bottom-level setting (e.g. "Graph Basal") and open the screen containing
140+
/// it. Titles mirror the row (or section) titles in each screen's view —
141+
/// keep them in sync when adding or renaming rows.
142+
var leaves: [SettingsLeaf] {
143+
switch self {
144+
case .nightscout: return [
145+
SettingsLeaf("URL"),
146+
SettingsLeaf("Access Token", ["token"]),
147+
SettingsLeaf("Enable WebSocket", ["websocket", "real-time"]),
148+
]
149+
case .dexcom: return [
150+
SettingsLeaf("User Name", ["username"]),
151+
SettingsLeaf("Password"),
152+
SettingsLeaf("Server"),
153+
]
154+
case .general: return [
155+
SettingsLeaf("Display App Badge", ["badge"]),
156+
SettingsLeaf("Persistent Notification"),
157+
SettingsLeaf("Appearance", ["dark mode", "light mode", "theme"]),
158+
SettingsLeaf("Display Stats"),
159+
SettingsLeaf("Display Small Graph"),
160+
SettingsLeaf("Color BG Text"),
161+
SettingsLeaf("Keep Screen Active", ["screen lock", "screenlock"]),
162+
SettingsLeaf("Show Display Name"),
163+
SettingsLeaf("Snoozer emoji"),
164+
SettingsLeaf("Force portrait mode", ["orientation", "landscape"]),
165+
SettingsLeaf("Time Zone Override", ["timezone"]),
166+
SettingsLeaf("Speak BG", ["voice", "speech"]),
167+
SettingsLeaf("Send anonymous usage stats", ["telemetry", "diagnostics"]),
168+
]
169+
case .graph: return [
170+
SettingsLeaf("Display Dots"),
171+
SettingsLeaf("Display Lines"),
172+
SettingsLeaf("Show DIA Lines", ["dia"]),
173+
SettingsLeaf("Show −30 min Line", ["-30"]),
174+
SettingsLeaf("Show −90 min Line", ["-90"]),
175+
SettingsLeaf("Show Yesterday's BG", ["yesterday"]),
176+
SettingsLeaf("Show Midnight Lines"),
177+
SettingsLeaf("Show Carb/Bolus Values", ["carbs"]),
178+
SettingsLeaf("Show Carb Absorption"),
179+
SettingsLeaf("Treatments on Small Graph"),
180+
SettingsLeaf("Small Graph Height", ["height"]),
181+
SettingsLeaf("Hours of Prediction", ["prediction"]),
182+
SettingsLeaf("Prediction Style"),
183+
SettingsLeaf("Min Basal", ["basal scale"]),
184+
SettingsLeaf("Min BG Scale"),
185+
SettingsLeaf("Show Days Back", ["history", "days back"]),
186+
]
187+
case .infoDisplay:
188+
return [SettingsLeaf("Hide Information Table")]
189+
+ InfoType.allCases.map { SettingsLeaf($0.name) }
190+
case .units: return [
191+
SettingsLeaf("Glucose Unit"),
192+
SettingsLeaf("Range Mode", ["tir", "titr", "time in range"]),
193+
SettingsLeaf("Glycemic Metrics", ["hba1c", "ehba1c", "gmi"]),
194+
SettingsLeaf("Variability", ["standard deviation", "cv"]),
195+
]
196+
case .backgroundRefresh: return [
197+
SettingsLeaf("Background Refresh Type", ["silent tune", "bluetooth", "rileylink", "omnipod", "heartbeat"]),
198+
]
199+
case .importExport: return [
200+
SettingsLeaf("Scan QR Code to Import Settings", ["qr"]),
201+
SettingsLeaf("Export Settings To QR Code", ["qr"]),
202+
]
203+
case .apn: return [
204+
SettingsLeaf("APNS Key ID", ["apns"]),
205+
SettingsLeaf("APNS Key", ["apns", "p8"]),
206+
]
207+
#if !targetEnvironment(macCatalyst)
208+
case .liveActivity: return [
209+
SettingsLeaf("Enable Live Activity"),
210+
SettingsLeaf("Restart Live Activity"),
211+
SettingsLeaf("Grid Slots", ["carplay", "watch"]),
212+
]
213+
#endif
214+
case .remote: return [
215+
SettingsLeaf("Loop Remote Control"),
216+
SettingsLeaf("Trio Remote Control", ["trc"]),
217+
SettingsLeaf("Meal with Bolus"),
218+
SettingsLeaf("Meal with Fat/Protein"),
219+
SettingsLeaf("Guardrails", ["max bolus", "max carbs", "max fat", "max protein"]),
220+
SettingsLeaf("Bolus Increment"),
221+
SettingsLeaf("Shared Secret"),
222+
SettingsLeaf("QR Code URL", ["qr"]),
223+
]
224+
case .alarmSettings: return [
225+
SettingsLeaf("All Alerts Snoozed", ["snooze all"]),
226+
SettingsLeaf("All Sounds Muted", ["mute all"]),
227+
SettingsLeaf("Day starts", ["schedule", "day/night"]),
228+
SettingsLeaf("Night starts", ["schedule", "day/night"]),
229+
SettingsLeaf("Override System Volume", ["volume"]),
230+
SettingsLeaf("Audio During Calls"),
231+
SettingsLeaf("Ignore Zero BG"),
232+
SettingsLeaf("Auto-Snooze CGM Start", ["autosnooze"]),
233+
SettingsLeaf("Volume Buttons Snooze Alarms"),
234+
]
235+
case .calendar: return [
236+
SettingsLeaf("Save BG to Calendar", ["watch", "carplay"]),
237+
SettingsLeaf("Calendar Text"),
238+
]
239+
case .contact: return [
240+
SettingsLeaf("Enable Contact BG Updates", ["watch face"]),
241+
SettingsLeaf("Background Color"),
242+
SettingsLeaf("Color Mode"),
243+
SettingsLeaf("Text Color"),
244+
SettingsLeaf("Show Trend"),
245+
SettingsLeaf("Show Delta"),
246+
SettingsLeaf("Show IOB"),
247+
]
248+
case .advanced: return [
249+
SettingsLeaf("Download Treatments"),
250+
SettingsLeaf("Download Prediction"),
251+
SettingsLeaf("Graph Basal"),
252+
SettingsLeaf("Graph Bolus"),
253+
SettingsLeaf("Graph Carbs"),
254+
SettingsLeaf("Graph Other Treatments"),
255+
SettingsLeaf("BG Update Delay", ["delay"]),
256+
SettingsLeaf("Debug Log Level", ["logging"]),
257+
]
258+
case .tabSettings, .settings, .aggregatedStats: return []
259+
}
260+
}
261+
262+
/// Ordered, grouped list of the routes shown as rows in the Settings menu.
263+
/// Encodes the conditional visibility in one place so both the list and the
264+
/// Menu search stay in sync.
265+
static func menuSections(nightscoutConfigured: Bool) -> [(SettingsSection, [SettingsRoute])] {
266+
var display: [SettingsRoute] = [.general, .graph]
267+
if nightscoutConfigured {
268+
display.append(.infoDisplay)
269+
}
270+
display += [.units, .tabSettings]
271+
272+
var app: [SettingsRoute] = [.backgroundRefresh, .importExport, .apn]
273+
#if !targetEnvironment(macCatalyst)
274+
app.append(.liveActivity)
275+
#endif
276+
if nightscoutConfigured {
277+
app.append(.remote)
278+
}
279+
280+
return [
281+
(.data, [.nightscout, .dexcom]),
282+
(.display, display),
283+
(.app, app),
284+
(.alarms, [.alarmSettings]),
285+
(.integrations, [.calendar, .contact]),
286+
(.advanced, [.advanced]),
287+
]
288+
}
289+
128290
@ViewBuilder
129291
var destination: some View {
130292
switch self {

0 commit comments

Comments
 (0)