Skip to content

Commit 3d2cb2f

Browse files
committed
Merge PR #609: Add inline documentation sheets to settings views
2 parents 9b5c83b + c0f96c7 commit 3d2cb2f

4 files changed

Lines changed: 384 additions & 9 deletions

File tree

LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ struct BackgroundRefreshSettingsView: View {
77
@ObservedObject var viewModel: BackgroundRefreshSettingsViewModel
88
@State private var forceRefresh = false
99
@State private var timer: Timer?
10+
@State private var showInfo = false
1011

1112
@ObservedObject var bleManager = BLEManager.shared
1213

@@ -20,6 +21,9 @@ struct BackgroundRefreshSettingsView: View {
2021
availableDevicesSection
2122
}
2223
}
24+
.sheet(isPresented: $showInfo) {
25+
backgroundRefreshInfoSheet
26+
}
2327
.onAppear {
2428
startTimer()
2529
}
@@ -34,7 +38,7 @@ struct BackgroundRefreshSettingsView: View {
3438
// MARK: - Subviews / Computed Properties
3539

3640
private var refreshTypeSection: some View {
37-
Section {
41+
Section(header: refreshTypeSectionHeader) {
3842
Picker("Background Refresh Type", selection: $viewModel.backgroundRefreshType) {
3943
ForEach(BackgroundRefreshType.allCases, id: \.self) { type in
4044
Text(type.rawValue).tag(type)
@@ -176,6 +180,64 @@ struct BackgroundRefreshSettingsView: View {
176180
}
177181
}
178182

183+
// MARK: - Section Header & Info Sheet
184+
185+
private var refreshTypeSectionHeader: some View {
186+
HStack(spacing: 4) {
187+
Text("Background Refresh")
188+
Button {
189+
showInfo = true
190+
} label: {
191+
Image(systemName: "info.circle")
192+
.foregroundStyle(Color.accentColor)
193+
}
194+
.buttonStyle(.plain)
195+
}
196+
}
197+
198+
private var backgroundRefreshInfoSheet: some View {
199+
NavigationStack {
200+
ScrollView {
201+
VStack(alignment: .leading, spacing: 16) {
202+
Text("LoopFollow needs to stay active in the background to check for alarms and update glucose values. There are several methods available:")
203+
204+
Text("Silent Tune")
205+
.font(.headline)
206+
Text("Plays a silent audio track to keep the app active. This has several drawbacks including battery drain and limited reliability — it may be interrupted by other apps.")
207+
208+
Text("Bluetooth Heartbeat")
209+
.font(.headline)
210+
Text("Uses an external Bluetooth device to keep LoopFollow awake. This can save significantly on battery and provides more reliable background operation.")
211+
212+
Text("Supported Bluetooth Devices")
213+
.font(.headline)
214+
Text(verbatim: """
215+
• Radiolink: RileyLink, OrangeLink, Emalink — heartbeat every minute
216+
• Dexcom G5/G6/ONE/Anubis transmitter — heartbeat every ~5 minutes
217+
• Dexcom G7/ONE+ sensor — heartbeat every ~5 minutes
218+
219+
Dexcom device batteries continue to provide Bluetooth power for months after they are no longer in service with a sensor.
220+
""")
221+
222+
Text("If the person using LoopFollow is also wearing a Dexcom or radiolink, they should choose their own device.")
223+
.font(.footnote)
224+
.foregroundColor(.secondary)
225+
}
226+
.padding()
227+
.frame(maxWidth: .infinity, alignment: .leading)
228+
}
229+
.navigationTitle("Background Refresh")
230+
.navigationBarTitleDisplayMode(.inline)
231+
.toolbar {
232+
ToolbarItem(placement: .confirmationAction) {
233+
Button("Done") { showInfo = false }
234+
}
235+
}
236+
}
237+
.presentationDetents([.large])
238+
.presentationDragIndicator(.visible)
239+
}
240+
179241
private func startTimer() {
180242
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
181243
self.forceRefresh.toggle()

LoopFollow/Nightscout/NightscoutSettingsView.swift

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import SwiftUI
55

66
struct NightscoutSettingsView: View {
77
@ObservedObject var viewModel: NightscoutSettingsViewModel
8+
@State private var activeInfoSheet: InfoSheet?
9+
10+
private enum InfoSheet: Identifiable {
11+
case url, token
12+
var id: Self { self }
13+
}
814

915
var body: some View {
1016
NavigationView {
@@ -14,6 +20,12 @@ struct NightscoutSettingsView: View {
1420
statusSection
1521
importSection
1622
}
23+
.sheet(item: $activeInfoSheet) { sheet in
24+
switch sheet {
25+
case .url: urlInfoSheet
26+
case .token: tokenInfoSheet
27+
}
28+
}
1729
.onDisappear {
1830
viewModel.dismiss()
1931
}
@@ -22,10 +34,10 @@ struct NightscoutSettingsView: View {
2234
.navigationBarTitle("Nightscout Settings", displayMode: .inline)
2335
}
2436

25-
// MARK: - Subviews / Computed Properties
37+
// MARK: - Sections
2638

2739
private var urlSection: some View {
28-
Section(header: Text("URL")) {
40+
Section(header: sectionHeader("URL", sheet: .url)) {
2941
TextField("Enter URL", text: $viewModel.nightscoutURL)
3042
.textContentType(.username)
3143
.autocapitalization(.none)
@@ -37,7 +49,7 @@ struct NightscoutSettingsView: View {
3749
}
3850

3951
private var tokenSection: some View {
40-
Section(header: Text("Token")) {
52+
Section(header: sectionHeader("Token", sheet: .token)) {
4153
HStack {
4254
Text("Access Token")
4355
TogglableSecureInput(
@@ -67,4 +79,89 @@ struct NightscoutSettingsView: View {
6779
}
6880
}
6981
}
82+
83+
// MARK: - Section Header
84+
85+
private func sectionHeader(_ title: String, sheet: InfoSheet) -> some View {
86+
HStack(spacing: 4) {
87+
Text(title)
88+
Button {
89+
activeInfoSheet = sheet
90+
} label: {
91+
Image(systemName: "info.circle")
92+
.foregroundStyle(Color.accentColor)
93+
}
94+
.buttonStyle(.plain)
95+
}
96+
}
97+
98+
// MARK: - Info Sheets
99+
100+
private var urlInfoSheet: some View {
101+
NavigationStack {
102+
ScrollView {
103+
Text(verbatim: """
104+
Enter your Nightscout site URL, for example:
105+
https://yoursite.yourprovider.com
106+
107+
You can copy your full URL (including the token) from Nightscout Admin Tools. When pasted here, LoopFollow automatically extracts both the URL and the token.
108+
109+
To find your URL, open your Nightscout site in a browser and copy the address from the address bar. Remove any trailing slashes or path components — just the base URL is needed.
110+
""")
111+
.padding()
112+
.frame(maxWidth: .infinity, alignment: .leading)
113+
}
114+
.navigationTitle("Nightscout URL")
115+
.navigationBarTitleDisplayMode(.inline)
116+
.toolbar {
117+
ToolbarItem(placement: .confirmationAction) {
118+
Button("Done") { activeInfoSheet = nil }
119+
}
120+
}
121+
}
122+
.presentationDetents([.medium])
123+
.presentationDragIndicator(.visible)
124+
}
125+
126+
private var tokenInfoSheet: some View {
127+
NavigationStack {
128+
ScrollView {
129+
VStack(alignment: .leading, spacing: 16) {
130+
Text("""
131+
A token controls what LoopFollow can access on your Nightscout site. Tokens are not the same as API keys — they are created within Nightscout itself.
132+
""")
133+
134+
Text("Creating a Token")
135+
.font(.headline)
136+
Text("""
137+
1. Open your Nightscout site in a browser
138+
2. Go to the hamburger menu (☰) and select Admin Tools
139+
3. Under "Subjects", tap "Add new Subject"
140+
4. Enter a name (e.g. "LoopFollow") and select a role
141+
5. Save, then copy the token (it looks like: loopfollow-1234567890abcdef)
142+
""")
143+
144+
Text("Which Role Do I Need?")
145+
.font(.headline)
146+
Text("""
147+
• Read — sufficient for most setups, including Loop and Trio remote control via APNS
148+
• Read & Write (Careportal) — required only for Nightscout Remote Control (Trio 0.2.x or older)
149+
150+
If your Nightscout site is publicly readable, you can leave the token empty. The status will show "OK (Read)".
151+
""")
152+
}
153+
.padding()
154+
.frame(maxWidth: .infinity, alignment: .leading)
155+
}
156+
.navigationTitle("Access Token")
157+
.navigationBarTitleDisplayMode(.inline)
158+
.toolbar {
159+
ToolbarItem(placement: .confirmationAction) {
160+
Button("Done") { activeInfoSheet = nil }
161+
}
162+
}
163+
}
164+
.presentationDetents([.large])
165+
.presentationDragIndicator(.visible)
166+
}
70167
}

0 commit comments

Comments
 (0)