Skip to content

Commit 7cd4035

Browse files
authored
[LOOP-5281] added basal display state model (#648)
* added basal display state model * updated mock pump settings to display basal state
1 parent 318cfb9 commit 7cd4035

File tree

6 files changed

+116
-25
lines changed

6 files changed

+116
-25
lines changed

LoopKit.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,7 @@
666666
B4C004D12416961300B40429 /* GuidePage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C004B3241085DB00B40429 /* GuidePage.swift */; };
667667
B4C004D22416961300B40429 /* GuideNavigationButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C004B4241085DC00B40429 /* GuideNavigationButton.swift */; };
668668
B4C004D32416961300B40429 /* ActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C004B2241085DB00B40429 /* ActionButton.swift */; };
669+
B4CA4D3D2D79B252005F8FF3 /* BasalDisplayState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4CA4D3C2D79B252005F8FF3 /* BasalDisplayState.swift */; };
669670
B4CEE2A5256E912A0093111B /* DoseProgressTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4CEE2A4256E91290093111B /* DoseProgressTests.swift */; };
670671
B4CEE2E3257129780093111B /* UnfinalizedDoseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4CEE2E2257129780093111B /* UnfinalizedDoseTests.swift */; };
671672
B4CEE2E5257129780093111B /* MockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 89D2047221CC7BD7001238CC /* MockKit.framework */; };
@@ -1599,6 +1600,7 @@
15991600
B4C004B2241085DB00B40429 /* ActionButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionButton.swift; sourceTree = "<group>"; };
16001601
B4C004B3241085DB00B40429 /* GuidePage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GuidePage.swift; sourceTree = "<group>"; };
16011602
B4C004B4241085DC00B40429 /* GuideNavigationButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GuideNavigationButton.swift; sourceTree = "<group>"; };
1603+
B4CA4D3C2D79B252005F8FF3 /* BasalDisplayState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasalDisplayState.swift; sourceTree = "<group>"; };
16021604
B4CEE2A4256E91290093111B /* DoseProgressTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoseProgressTests.swift; sourceTree = "<group>"; };
16031605
B4CEE2E0257129780093111B /* MockKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MockKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
16041606
B4CEE2E2257129780093111B /* UnfinalizedDoseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnfinalizedDoseTests.swift; sourceTree = "<group>"; };
@@ -3149,6 +3151,7 @@
31493151
E9086B4624B5404D0062F5C8 /* Models */ = {
31503152
isa = PBXGroup;
31513153
children = (
3154+
B4CA4D3C2D79B252005F8FF3 /* BasalDisplayState.swift */,
31523155
14C9706F2C5A8E1500E8A01B /* ChartAxisValueDoubleCarbEntry.swift */,
31533156
A9D3FF0F2A6C19CA000C891D /* ChartAxisValueDoubleLog.swift */,
31543157
E9086B4724B5405E0062F5C8 /* ChartAxisValueDoubleUnit.swift */,
@@ -3867,6 +3870,7 @@
38673870
E949E38F24B3711E00024DA0 /* InsulinModelInformationView.swift in Sources */,
38683871
895FE08B22011F0C00FCF18A /* EmojiInputHeaderView.swift in Sources */,
38693872
895FE08322011F0C00FCF18A /* OverrideSelectionFooterView.swift in Sources */,
3873+
B4CA4D3D2D79B252005F8FF3 /* BasalDisplayState.swift in Sources */,
38703874
89AF78C22447E353002B4FCC /* Splat.swift in Sources */,
38713875
893C9F8C2447DBD900CD4185 /* CardBuilder.swift in Sources */,
38723876
B4D4C20D25F95A8700DA809D /* DisplayGlucosePreference.swift in Sources */,

LoopKit/DailyValueSchedule.swift

+7-1
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,14 @@ public protocol DailySchedule {
7171

7272

7373
public extension DailySchedule {
74+
func scheduleSegment(at time: Date) -> AbsoluteScheduleValue<T> {
75+
return between(start: time, end: time).first!
76+
}
7477
func value(at time: Date) -> T {
75-
return between(start: time, end: time).first!.value
78+
return scheduleSegment(at: time).value
79+
}
80+
func startDate(at time: Date) -> Date {
81+
return scheduleSegment(at: time).startDate
7682
}
7783
}
7884

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// BasalDisplayState.swift
3+
// LoopKit
4+
//
5+
// Created by Nathaniel Hamming on 2025-03-06.
6+
// Copyright © 2025 LoopKit Authors. All rights reserved.
7+
//
8+
9+
public enum BasalDisplayState: Equatable {
10+
case basalTempManual(Double)
11+
case basalScheduled
12+
case basalTempAutoAbove
13+
case basalTempAutoBelow
14+
case basalTempAutoNoDelivery
15+
16+
public var imageName: String? {
17+
switch self {
18+
case .basalScheduled: return "arrow.right.square.fill"
19+
case .basalTempAutoAbove: return "arrow.up.square.fill"
20+
case .basalTempAutoBelow, .basalTempAutoNoDelivery: return "arrow.down.square.fill"
21+
default:
22+
return nil
23+
}
24+
}
25+
}

MockKit/UnfinalizedDose.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public struct UnfinalizedDose: RawRepresentable, Equatable, CustomStringConverti
3737
public var units: Double
3838
var scheduledUnits: Double? // Tracks the scheduled units, as boluses may be canceled before finishing, at which point units would reflect actual delivered volume.
3939
var scheduledTempRate: Double? // Tracks the original temp rate, as during finalization the units are discretized to pump pulses, changing the actual rate
40-
let startTime: Date
40+
public let startTime: Date
4141
var duration: TimeInterval
4242
let insulinType: InsulinType?
4343
let automatic: Bool?

MockKitUI/ViewModel/MockPumpManagerSettingsViewModel.swift

+58-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import SwiftUI
1010
import LoopKit
11+
import LoopKitUI
1112
import MockKit
1213

1314
class MockPumpManagerSettingsViewModel: ObservableObject {
@@ -75,6 +76,24 @@ class MockPumpManagerSettingsViewModel: ObservableObject {
7576
}
7677

7778
@Published private(set) var basalDeliveryRate: Double?
79+
80+
@Published private(set) var basalDeliveryRateDate: Date?
81+
var basalDeliveryRateDateString: String? {
82+
guard let basalDeliveryRateDate else { return nil }
83+
return Self.shortTimeFormatter.string(from: basalDeliveryRateDate)
84+
}
85+
86+
@Published private(set) var basalDisplayState: BasalDisplayState
87+
var basalDisplayStateString: String {
88+
switch basalDisplayState {
89+
case .basalScheduled:
90+
return LocalizedString("Scheduled\nbasal", comment: "Label for scheduled basal")
91+
case .basalTempAutoAbove:
92+
return LocalizedString("More than\nscheduled", comment: "Label for when temp basal is above the scheduled basal")
93+
default:
94+
return LocalizedString("Less than\nscheduled", comment: "Label for when temp basal is below the scheduled basal")
95+
}
96+
}
7897

7998
@Published private(set) var presentDeliveryWarning: Bool?
8099

@@ -99,9 +118,12 @@ class MockPumpManagerSettingsViewModel: ObservableObject {
99118
init(pumpManager: MockPumpManager) {
100119
self.pumpManager = pumpManager
101120

121+
let now = Date()
102122
isDeliverySuspended = pumpManager.status.basalDeliveryState?.isSuspended == true
103123
basalDeliveryState = pumpManager.status.basalDeliveryState
104-
basalDeliveryRate = pumpManager.state.basalDeliveryRate(at: Date())
124+
basalDeliveryRate = pumpManager.state.basalDeliveryRate(at: now)
125+
basalDeliveryRateDate = now
126+
basalDisplayState = pumpManager.state.basalDisplayState(at: now) ?? .basalScheduled
105127
setSuspenededAtString()
106128

107129
pumpManager.addStateObserver(self, queue: .main)
@@ -148,8 +170,11 @@ class MockPumpManagerSettingsViewModel: ObservableObject {
148170
extension MockPumpManagerSettingsViewModel: MockPumpManagerStateObserver {
149171
func mockPumpManager(_ manager: MockKit.MockPumpManager, didUpdate state: MockKit.MockPumpManagerState) {
150172
guard !transitioningSuspendResumeInsulinDelivery else { return }
151-
basalDeliveryRate = state.basalDeliveryRate(at: Date())
173+
let now = Date()
174+
basalDeliveryRateDate = now
175+
basalDeliveryRate = state.basalDeliveryRate(at: now)
152176
basalDeliveryState = manager.status.basalDeliveryState
177+
basalDisplayState = state.basalDisplayState(at: now) ?? basalDisplayState
153178
}
154179

155180
func mockPumpManager(_ manager: MockKit.MockPumpManager, didUpdate status: LoopKit.PumpManagerStatus, oldStatus: LoopKit.PumpManagerStatus) {
@@ -172,4 +197,35 @@ extension MockPumpManagerState {
172197
return nil
173198
}
174199
}
200+
201+
func basalDisplayState(at now: Date) -> BasalDisplayState? {
202+
guard let scheduledBasalRate = basalRateSchedule?.value(at: now),
203+
let currentBasalRate = basalDeliveryRate(at: now)
204+
else {
205+
return nil
206+
}
207+
208+
if currentBasalRate == scheduledBasalRate {
209+
return .basalScheduled
210+
} else if currentBasalRate == 0 {
211+
return .basalTempAutoNoDelivery
212+
} else if currentBasalRate < scheduledBasalRate {
213+
return .basalTempAutoBelow
214+
} else {
215+
return .basalTempAutoAbove
216+
}
217+
}
218+
219+
func basalDeliveryStartDate(at now: Date) -> Date? {
220+
switch suspendState {
221+
case .resumed:
222+
if let tempBasal = unfinalizedTempBasal, !tempBasal.isFinished(at: now) {
223+
return tempBasal.startTime
224+
} else {
225+
return basalRateSchedule?.startDate(at: now)
226+
}
227+
case .suspended:
228+
return nil
229+
}
230+
}
175231
}

MockKitUI/Views/InsulinStatusView.swift

+21-21
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@
99
import SwiftUI
1010
import LoopAlgorithm
1111
import LoopKit
12+
import LoopKitUI
1213

1314
struct InsulinStatusView: View {
1415
@Environment(\.guidanceColors) var guidanceColors
1516
@Environment(\.insulinTintColor) var insulinTintColor
1617

1718
@ObservedObject var viewModel: MockPumpManagerSettingsViewModel
1819

19-
private let subViewSpacing: CGFloat = 14
20+
private let subViewSpacing: CGFloat = 16
2021

2122
var body: some View {
2223
HStack(alignment: .top, spacing: 0) {
@@ -48,7 +49,7 @@ struct InsulinStatusView: View {
4849
}
4950

5051
private var deliveryStatusSpacing: CGFloat {
51-
return subViewSpacing
52+
return subViewSpacing - 8
5253
}
5354

5455
var deliveryStatus: some View {
@@ -58,8 +59,10 @@ struct InsulinStatusView: View {
5859
.fixedSize(horizontal: false, vertical: true)
5960
if viewModel.isDeliverySuspended {
6061
insulinSuspended
61-
} else if let basalRate = viewModel.basalDeliveryRate {
62-
basalRateView(basalRate)
62+
} else if let basalRate = viewModel.basalDeliveryRate,
63+
let date = viewModel.basalDeliveryRateDate
64+
{
65+
basalRateView(basalRate, at: date)
6366
} else {
6467
noDelivery
6568
}
@@ -79,34 +82,31 @@ struct InsulinStatusView: View {
7982
}
8083
}
8184

82-
private func basalRateView(_ basalRate: Double) -> some View {
85+
private func basalRateView(_ basalRate: Double, at date: Date) -> some View {
8386
HStack(alignment: .center) {
8487
VStack(alignment: .leading) {
85-
HStack(alignment: .lastTextBaseline, spacing: 3) {
86-
let unit = LoopUnit.internationalUnitsPerHour
87-
let quantity = LoopQuantity(unit: unit, doubleValue: basalRate)
88+
HStack(spacing: 3) {
8889
if viewModel.presentDeliveryWarning == true {
8990
Image(systemName: "exclamationmark.circle.fill")
9091
.foregroundColor(guidanceColors.warning)
9192
.font(.system(size: 28))
9293
.fixedSize()
9394
}
94-
Text(basalRateFormatter.string(from: quantity, includeUnit: false) ?? "")
95-
.font(.system(size: 28))
95+
if let basalStateImageName = viewModel.basalDisplayState.imageName {
96+
Image(systemName: basalStateImageName)
97+
.font(.largeTitle)
98+
.foregroundColor(.accentColor)
99+
}
100+
Text(viewModel.basalDisplayStateString)
101+
.lineSpacing(1)
102+
.font(.callout)
96103
.fontWeight(.heavy)
97-
.fixedSize()
98-
Text(basalRateFormatter.localizedUnitStringWithPlurality(forQuantity: quantity))
99-
.foregroundColor(.secondary)
100104
}
101-
Group {
102-
if viewModel.isScheduledBasal {
103-
Text("Scheduled\(String.nonBreakingSpace)Basal")
104-
} else if viewModel.isTempBasal {
105-
Text("Temporary\(String.nonBreakingSpace)Basal")
106-
}
105+
if let basalDeliveryRateDateString = viewModel.basalDeliveryRateDateString {
106+
Text("at \(basalDeliveryRateDateString)")
107+
.font(.footnote)
108+
.foregroundColor(.accentColor)
107109
}
108-
.font(.footnote)
109-
.foregroundColor(.accentColor)
110110
}
111111
}
112112
}

0 commit comments

Comments
 (0)