Skip to content

Commit 3917a98

Browse files
committed
Refresh device status on bolus screen load
- BGFetcher.swift: add completion handler to fetchDeviceStatus so bolus screen can await fresh IOB/COB - WatchBolusView.swift: fetch latest device status on appear with loading overlay to prevent stale COB double-counting
1 parent 40bd34f commit 3917a98

2 files changed

Lines changed: 139 additions & 123 deletions

File tree

LoopFollowWatch/BGFetcher.swift

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ class BGFetcher: ObservableObject {
324324

325325
// MARK: - Nightscout Device Status
326326

327-
func fetchDeviceStatus(config: WatchConfig) {
327+
func fetchDeviceStatus(config: WatchConfig, completion: (() -> Void)? = nil) {
328328
var components = URLComponents(string: config.nsURL)
329329
components?.path = "/api/v1/devicestatus.json"
330330

@@ -335,15 +335,20 @@ class BGFetcher: ObservableObject {
335335
queryItems.append(URLQueryItem(name: "count", value: "1"))
336336
components?.queryItems = queryItems
337337

338-
guard let url = components?.url else { return }
338+
guard let url = components?.url else {
339+
completion?()
340+
return
341+
}
339342

340343
var request = URLRequest(url: url)
341344
request.cachePolicy = .reloadIgnoringLocalCacheData
342-
request.timeoutInterval = 15
345+
request.timeoutInterval = 10
343346

344347
URLSession.shared.dataTask(with: request) { [weak self] data, _, error in
345-
guard let self = self, error == nil, let data = data else { return }
346-
self.parseDeviceStatus(data: data)
348+
if let self = self, error == nil, let data = data {
349+
self.parseDeviceStatus(data: data)
350+
}
351+
DispatchQueue.main.async { completion?() }
347352
}.resume()
348353
}
349354

LoopFollowWatch/WatchBolusView.swift

Lines changed: 129 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ struct WatchBolusView: View {
2626
@State private var isError = false
2727
@State private var showCalcDetail = false
2828
@State private var showCelebration = false
29+
@State private var isRefreshing = true
2930

3031
/// The displayed amount, snapped to 0.05U increments
3132
private var amount: Double {
@@ -35,148 +36,158 @@ struct WatchBolusView: View {
3536
}
3637

3738
var body: some View {
38-
VStack(spacing: 0) {
39-
if let result = resultMessage {
40-
ZStack {
41-
VStack {
42-
Spacer()
43-
Text(result)
44-
.font(.system(size: 24, weight: .bold))
45-
.foregroundColor(isError ? .red : .green)
46-
.multilineTextAlignment(.center)
47-
Spacer()
39+
ZStack {
40+
VStack(spacing: 0) {
41+
if let result = resultMessage {
42+
ZStack {
43+
VStack {
44+
Spacer()
45+
Text(result)
46+
.font(.system(size: 24, weight: .bold))
47+
.foregroundColor(isError ? .red : .green)
48+
.multilineTextAlignment(.center)
49+
Spacer()
50+
}
51+
CelebrationOverlay(isActive: $showCelebration)
4852
}
49-
CelebrationOverlay(isActive: $showCelebration)
50-
}
51-
} else if showConfirm {
52-
confirmSummary
53-
.padding(.bottom, 12)
53+
} else if showConfirm {
54+
confirmSummary
55+
.padding(.bottom, 12)
5456

55-
CrownConfirmView(label: confirmedAmount > 0 ? "to deliver" : "to send meal") {
56-
sendBolusAndMeal()
57-
}
58-
59-
} else {
60-
HStack {
61-
Button {
62-
rawCrown = max(rawCrown - 1.0, 0)
63-
WKInterfaceDevice.current().play(.click)
64-
} label: {
65-
Text("")
66-
.font(.system(size: 16, weight: .bold))
67-
.foregroundColor(.blue)
68-
.frame(width: 32, height: 32)
69-
.background(Color.blue.opacity(0.3))
70-
.clipShape(Circle())
57+
CrownConfirmView(label: confirmedAmount > 0 ? "to deliver" : "to send meal") {
58+
sendBolusAndMeal()
7159
}
72-
.buttonStyle(.plain)
73-
// Extra padding so the tap target doesn't bleed
74-
// into the system back button zone on small watches.
75-
.padding(.leading, 8)
7660

77-
Spacer()
61+
} else {
62+
HStack {
63+
Button {
64+
rawCrown = max(rawCrown - 1.0, 0)
65+
WKInterfaceDevice.current().play(.click)
66+
} label: {
67+
Text("")
68+
.font(.system(size: 16, weight: .bold))
69+
.foregroundColor(.blue)
70+
.frame(width: 32, height: 32)
71+
.background(Color.blue.opacity(0.3))
72+
.clipShape(Circle())
73+
}
74+
.buttonStyle(.plain)
75+
.padding(.leading, 8)
7876

79-
Text("Bolus")
80-
.font(.system(size: 16, weight: .semibold))
77+
Spacer()
8178

82-
Spacer()
79+
Text("Bolus")
80+
.font(.system(size: 16, weight: .semibold))
8381

84-
Button {
85-
rawCrown = min(rawCrown + 1.0, config.maxBolus / 0.25)
86-
WKInterfaceDevice.current().play(.click)
87-
} label: {
88-
Text("+")
89-
.font(.system(size: 16, weight: .bold))
90-
.foregroundColor(.blue)
91-
.frame(width: 32, height: 32)
92-
.background(Color.blue.opacity(0.3))
93-
.clipShape(Circle())
94-
}
95-
.buttonStyle(.plain)
96-
}
97-
.padding(.horizontal, 20)
98-
.padding(.top, 16)
82+
Spacer()
9983

100-
Text(String(format: "%.2f U", amount))
101-
.font(.system(size: 60, weight: .bold, design: .rounded))
102-
.foregroundColor(.blue)
103-
.lineLimit(1)
104-
.minimumScaleFactor(0.7)
84+
Button {
85+
rawCrown = min(rawCrown + 1.0, config.maxBolus / 0.25)
86+
WKInterfaceDevice.current().play(.click)
87+
} label: {
88+
Text("+")
89+
.font(.system(size: 16, weight: .bold))
90+
.foregroundColor(.blue)
91+
.frame(width: 32, height: 32)
92+
.background(Color.blue.opacity(0.3))
93+
.clipShape(Circle())
94+
}
95+
.buttonStyle(.plain)
96+
}
97+
.padding(.horizontal, 20)
98+
.padding(.top, 16)
10599

106-
HStack(spacing: 6) {
107-
Text("Calculated: \(String(format: "%.2f", bgFetcher.recommendedBolus))U")
108-
.font(.system(size: 16, weight: .medium))
100+
Text(String(format: "%.2f U", amount))
101+
.font(.system(size: 60, weight: .bold, design: .rounded))
109102
.foregroundColor(.blue)
110103
.lineLimit(1)
111104
.minimumScaleFactor(0.7)
112-
.onTapGesture {
113-
rawCrown = min(bgFetcher.recommendedBolus, config.maxBolus) / 0.25
114-
}
115-
if bgFetcher.bolusCalc != nil {
116-
Button {
117-
showCalcDetail = true
118-
} label: {
119-
Image(systemName: "info.circle")
120-
.font(.system(size: 20))
121-
.foregroundColor(.blue.opacity(0.8))
122-
.frame(width: 36, height: 36)
105+
106+
HStack(spacing: 6) {
107+
Text("Calculated: \(String(format: "%.2f", bgFetcher.recommendedBolus))U")
108+
.font(.system(size: 16, weight: .medium))
109+
.foregroundColor(.blue)
110+
.lineLimit(1)
111+
.minimumScaleFactor(0.7)
112+
.onTapGesture {
113+
rawCrown = min(bgFetcher.recommendedBolus, config.maxBolus) / 0.25
114+
}
115+
if bgFetcher.bolusCalc != nil {
116+
Button {
117+
showCalcDetail = true
118+
} label: {
119+
Image(systemName: "info.circle")
120+
.font(.system(size: 20))
121+
.foregroundColor(.blue.opacity(0.8))
122+
.frame(width: 36, height: 36)
123+
}
124+
.buttonStyle(.plain)
123125
}
124-
.buttonStyle(.plain)
125126
}
126-
}
127-
.padding(.leading, 8)
128-
.padding(.top, -8)
127+
.padding(.leading, 8)
128+
.padding(.top, -8)
129129

130-
Button(amount > 0 ? "Confirm" : (pendingMeal != nil ? "Skip" : "Confirm")) {
131-
confirmedAmount = amount
132-
showConfirm = true
130+
Button(amount > 0 ? "Confirm" : (pendingMeal != nil ? "Skip" : "Confirm")) {
131+
confirmedAmount = amount
132+
showConfirm = true
133+
}
134+
.buttonStyle(.borderedProminent)
135+
.tint(.blue)
136+
.disabled(amount <= 0 && pendingMeal == nil)
133137
}
134-
.buttonStyle(.borderedProminent)
135-
.tint(.blue)
136-
.disabled(amount <= 0 && pendingMeal == nil)
137138
}
138-
}
139-
.modifier(CrownRotationModifier(
140-
isActive: !showConfirm && resultMessage == nil,
141-
value: $rawCrown,
142-
from: 0,
143-
through: config.maxBolus / 0.25,
144-
by: 0.01,
145-
sensitivity: .low
146-
))
147-
.onChange(of: rawCrown) { _ in
148-
let current = amount
149-
if current != lastHapticAmount {
150-
lastHapticAmount = current
151-
WKInterfaceDevice.current().play(.click)
139+
.modifier(CrownRotationModifier(
140+
isActive: !showConfirm && resultMessage == nil,
141+
value: $rawCrown,
142+
from: 0,
143+
through: config.maxBolus / 0.25,
144+
by: 0.01,
145+
sensitivity: .low
146+
))
147+
.onChange(of: rawCrown) { _ in
148+
let current = amount
149+
if current != lastHapticAmount {
150+
lastHapticAmount = current
151+
WKInterfaceDevice.current().play(.click)
152+
}
152153
}
153-
}
154-
.sheet(isPresented: $showCalcDetail) {
155-
if let calc = bgFetcher.bolusCalc {
156-
BolusCalcDetailView(calc: calc, recommended: bgFetcher.recommendedBolus)
154+
.sheet(isPresented: $showCalcDetail) {
155+
if let calc = bgFetcher.bolusCalc {
156+
BolusCalcDetailView(calc: calc, recommended: bgFetcher.recommendedBolus)
157+
}
157158
}
158-
}
159-
.navigationBarBackButtonHidden(showConfirm)
160-
.toolbar {
161-
if showConfirm {
162-
ToolbarItem(placement: .cancellationAction) {
163-
Button {
164-
showConfirm = false
165-
} label: {
166-
Image(systemName: "chevron.left")
159+
.navigationBarBackButtonHidden(showConfirm)
160+
.toolbar {
161+
if showConfirm {
162+
ToolbarItem(placement: .cancellationAction) {
163+
Button {
164+
showConfirm = false
165+
} label: {
166+
Image(systemName: "chevron.left")
167+
}
167168
}
168169
}
169170
}
170-
}
171-
.onAppear {
172-
// If launched directly (not from meal entry), clear any stale pending carbs
173-
if pendingMeal == nil {
171+
.onAppear {
172+
if pendingMeal == nil {
173+
bgFetcher.pendingCarbs = 0
174+
}
175+
isRefreshing = true
176+
bgFetcher.fetchDeviceStatus(config: config) {
177+
bgFetcher.updateRecommendedBolus()
178+
isRefreshing = false
179+
}
180+
}
181+
.onDisappear {
174182
bgFetcher.pendingCarbs = 0
175183
}
176-
bgFetcher.updateRecommendedBolus()
177-
}
178-
.onDisappear {
179-
bgFetcher.pendingCarbs = 0
184+
185+
if isRefreshing {
186+
Color.black.opacity(0.6)
187+
.ignoresSafeArea()
188+
ProgressView()
189+
.tint(.blue)
190+
}
180191
}
181192
}
182193

0 commit comments

Comments
 (0)