Skip to content

Commit 3e4d849

Browse files
authored
Merge branch 'dev' into cameron/LOOP-5259-homescreen-charts-update
2 parents 45642d8 + 7cd4035 commit 3e4d849

13 files changed

+126
-31
lines changed

LoopKit.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,7 @@
670670
B4C004D12416961300B40429 /* GuidePage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C004B3241085DB00B40429 /* GuidePage.swift */; };
671671
B4C004D22416961300B40429 /* GuideNavigationButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C004B4241085DC00B40429 /* GuideNavigationButton.swift */; };
672672
B4C004D32416961300B40429 /* ActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C004B2241085DB00B40429 /* ActionButton.swift */; };
673+
B4CA4D3D2D79B252005F8FF3 /* BasalDisplayState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4CA4D3C2D79B252005F8FF3 /* BasalDisplayState.swift */; };
673674
B4CEE2A5256E912A0093111B /* DoseProgressTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4CEE2A4256E91290093111B /* DoseProgressTests.swift */; };
674675
B4CEE2E3257129780093111B /* UnfinalizedDoseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4CEE2E2257129780093111B /* UnfinalizedDoseTests.swift */; };
675676
B4CEE2E5257129780093111B /* MockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 89D2047221CC7BD7001238CC /* MockKit.framework */; };
@@ -1607,6 +1608,7 @@
16071608
B4C004B2241085DB00B40429 /* ActionButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionButton.swift; sourceTree = "<group>"; };
16081609
B4C004B3241085DB00B40429 /* GuidePage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GuidePage.swift; sourceTree = "<group>"; };
16091610
B4C004B4241085DC00B40429 /* GuideNavigationButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GuideNavigationButton.swift; sourceTree = "<group>"; };
1611+
B4CA4D3C2D79B252005F8FF3 /* BasalDisplayState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasalDisplayState.swift; sourceTree = "<group>"; };
16101612
B4CEE2A4256E91290093111B /* DoseProgressTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoseProgressTests.swift; sourceTree = "<group>"; };
16111613
B4CEE2E0257129780093111B /* MockKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MockKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
16121614
B4CEE2E2257129780093111B /* UnfinalizedDoseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnfinalizedDoseTests.swift; sourceTree = "<group>"; };
@@ -3161,6 +3163,7 @@
31613163
E9086B4624B5404D0062F5C8 /* Models */ = {
31623164
isa = PBXGroup;
31633165
children = (
3166+
B4CA4D3C2D79B252005F8FF3 /* BasalDisplayState.swift */,
31643167
14C9706F2C5A8E1500E8A01B /* ChartAxisValueDoubleCarbEntry.swift */,
31653168
A9D3FF0F2A6C19CA000C891D /* ChartAxisValueDoubleLog.swift */,
31663169
E9086B4724B5405E0062F5C8 /* ChartAxisValueDoubleUnit.swift */,
@@ -3879,6 +3882,7 @@
38793882
E949E38F24B3711E00024DA0 /* InsulinModelInformationView.swift in Sources */,
38803883
895FE08B22011F0C00FCF18A /* EmojiInputHeaderView.swift in Sources */,
38813884
895FE08322011F0C00FCF18A /* OverrideSelectionFooterView.swift in Sources */,
3885+
B4CA4D3D2D79B252005F8FF3 /* BasalDisplayState.swift in Sources */,
38823886
89AF78C22447E353002B4FCC /* Splat.swift in Sources */,
38833887
893C9F8C2447DBD900CD4185 /* CardBuilder.swift in Sources */,
38843888
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+
}

LoopKitUI/Views/Information Screens/BasalRatesInformationView.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public struct BasalRatesInformationView: View {
3737
Text(LocalizedString("Your Basal Rate of insulin is the number of units per hour that you want to use to cover your background insulin needs.", comment: "Information about basal rates"))
3838
Text(String(format: LocalizedString("%1$@ supports 1 to %2$@ rates per day.", comment: "Information about max number of basal rates (1: app name) (2: maximum schedule entry count)"), appName, String(describing: maximumScheduleEntryCount)))
3939
Text(LocalizedString("The schedule starts at midnight and cannot equal 0 U/day.", comment: "Information about basal rate scheduling"))
40-
}
40+
}.accessibilityIdentifier("text_BasalRatesInformation")
4141
}
4242
}
4343

LoopKitUI/Views/Information Screens/DeliveryLimitsInformationView.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public struct DeliveryLimitsInformationView: View {
5050
Text(LocalizedString("Some users choose a value 2, 3, or 4 times their highest scheduled basal rate.", comment: "Information about typical maximum basal rates"))
5151
Text(LocalizedString("Work with your healthcare provider to choose a value that is higher than your highest scheduled basal rate, but as conservative or aggressive as you feel comfortable.", comment: "Disclaimer"))
5252
}
53-
}
53+
}.accessibilityIdentifier("text_MaximumBasalRateInformation")
5454
}
5555

5656
private var maxBolusDescription: some View {
@@ -60,7 +60,7 @@ public struct DeliveryLimitsInformationView: View {
6060
VStack(alignment: .leading, spacing: 20) {
6161
Text(String(format: LocalizedString("Maximum Bolus is the highest bolus amount that you will allow %1$@ to recommend at one time to cover carbs or bring down high glucose.", comment: "Information about maximum bolus (1: app name)"), appName))
6262
}
63-
}
63+
}.accessibilityIdentifier("text_MaximumBolusInformation")
6464
}
6565
}
6666

LoopKitUI/Views/Information Screens/InsulinModelInformationView.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public struct InsulinModelInformationView: View {
2828
VStack (alignment: .leading, spacing: 20) {
2929
diaInfo
3030
modelPeakInfo
31-
}
31+
}.accessibilityIdentifier("text_InsulinModelInformation")
3232
},
3333
onExit: onExit ?? { self.presentationMode.wrappedValue.dismiss() },
3434
mode: mode

LoopKitUI/Views/Information Screens/InsulinSensitivityInformationView.swift

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public struct InsulinSensitivityInformationView: View {
3838
Text(LocalizedString("You can add different insulin sensitivities for different times of day by using the ➕.", comment: "Description of how to add a ratio"))
3939
}
4040
.accentColor(.secondary)
41+
.accessibilityIdentifier("text_InsulinSensitivitiesInformation")
4142
}
4243
}
4344

LoopKitUI/Views/Settings Editors/DeliveryLimitsEditor.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ public struct DeliveryLimitsEditor: View {
189189
}
190190
),
191191
leadingValueContent: {
192-
Text(DeliveryLimits.Setting.maximumBasalRate.title)
192+
Text(DeliveryLimits.Setting.maximumBasalRate.title).accessibilityIdentifier("text_MaximumBasalRateLimit")
193193
},
194194
trailingValueContent: {
195195
GuardrailConstrainedQuantityView(
@@ -239,7 +239,7 @@ public struct DeliveryLimitsEditor: View {
239239
}
240240
),
241241
leadingValueContent: {
242-
Text(DeliveryLimits.Setting.maximumBolus.title)
242+
Text(DeliveryLimits.Setting.maximumBolus.title).accessibilityIdentifier("text_MaximumBolusLimit")
243243
},
244244
trailingValueContent: {
245245
GuardrailConstrainedQuantityView(

LoopKitUI/Views/Settings Editors/InsulinModelSelection.swift

+2
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ public struct InsulinModelSelection: View {
160160
)
161161
.padding(.vertical, 4)
162162
.contentShape(Rectangle())
163+
.accessibilityIdentifier("item_RapidActingAdults")
163164
}
164165

165166
SectionDivider()
@@ -170,6 +171,7 @@ public struct InsulinModelSelection: View {
170171
)
171172
.padding(.vertical, 4)
172173
.padding(.bottom, 4)
174+
.accessibilityIdentifier("item_RapidActingChildren")
173175
}
174176
.buttonStyle(PlainButtonStyle()) // Disable row highlighting on selection
175177
}

LoopKitUI/Views/Settings Editors/TherapySettingsView.swift

+1
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ extension TherapySettingsView {
363363
.font(.body)
364364
.padding(.top, 5)
365365
.fixedSize(horizontal: false, vertical: true)
366+
.accessibilityIdentifier("text_InsulinModelTitle")
366367
Text(insulinModelPreset.subtitle)
367368
.font(.footnote)
368369
.foregroundColor(.secondary)

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)