Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/compile_check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ on:
- feat/quick-bolus-haptic
- chore/release-0.8.4
- feat/bolus-progress-clearer
- feat/bolus-initiating-border
paths-ignore:
- '**.md'
- '**/README'
Expand Down
4 changes: 4 additions & 0 deletions Trio.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@
BD11C001000000000000C003 /* SensorLifecycleArcView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11C000000000000000C003 /* SensorLifecycleArcView.swift */; };
BD11C001000000000000C004 /* SensorStatusTagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11C000000000000000C004 /* SensorStatusTagView.swift */; };
38AEE73D25F0200C0013F05B /* TrioSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AEE73C25F0200C0013F05B /* TrioSettings.swift */; };
AA11BB22CC33DD44EE55F002 /* BolusStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA11BB22CC33DD44EE55F001 /* BolusStatus.swift */; };
38AEE75225F022080013F05B /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AEE75125F022080013F05B /* SettingsManager.swift */; };
38AEE75725F0F18E0013F05B /* CarbsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38AEE75625F0F18E0013F05B /* CarbsStorage.swift */; };
38B17B6625DD90E0005CAE3D /* SwiftDate in Frameworks */ = {isa = PBXBuildFile; productRef = 38B17B6525DD90E0005CAE3D /* SwiftDate */; };
Expand Down Expand Up @@ -1194,6 +1195,7 @@
BD11C000000000000000C003 /* SensorLifecycleArcView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorLifecycleArcView.swift; sourceTree = "<group>"; };
BD11C000000000000000C004 /* SensorStatusTagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorStatusTagView.swift; sourceTree = "<group>"; };
38AEE73C25F0200C0013F05B /* TrioSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioSettings.swift; sourceTree = "<group>"; };
AA11BB22CC33DD44EE55F001 /* BolusStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusStatus.swift; sourceTree = "<group>"; };
38AEE75125F022080013F05B /* SettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsManager.swift; sourceTree = "<group>"; };
38AEE75625F0F18E0013F05B /* CarbsStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbsStorage.swift; sourceTree = "<group>"; };
38B4F3AE25E2979F00E76A18 /* IndexedCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndexedCollection.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2877,6 +2879,7 @@
DDEBB05B2D89E9050032305D /* TimeInRangeType.swift */,
1935363F28496F7D001E0B16 /* TrioCustomOrefVariables.swift */,
38AEE73C25F0200C0013F05B /* TrioSettings.swift */,
AA11BB22CC33DD44EE55F001 /* BolusStatus.swift */,
3811DE8E25C9D80400A708ED /* User.swift */,
DD3078672D42F5CE00DE0490 /* WatchGlucoseObject.swift */,
BD432CA02D2F4E3300D1EB79 /* WatchMessageKeys.swift */,
Expand Down Expand Up @@ -5032,6 +5035,7 @@ ECF21C66F4587B9A7FE37371 /* PresetChartViews.swift in Sources */,
19D466A329AA2B80004D5F33 /* MealSettingsDataFlow.swift in Sources */,
3811DEB225C9D88300A708ED /* KeychainItemAccessibility.swift in Sources */,
38AEE73D25F0200C0013F05B /* TrioSettings.swift in Sources */,
AA11BB22CC33DD44EE55F002 /* BolusStatus.swift in Sources */,
38FCF3FD25E997A80078B0D1 /* PumpHistoryStorage.swift in Sources */,
58645BA72CA2D390008AFCE7 /* ChartAxisSetup.swift in Sources */,
38D0B3B625EBE24900CB6E88 /* Battery.swift in Sources */,
Expand Down
8 changes: 5 additions & 3 deletions Trio/Sources/APS/APSManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,12 @@ final class BaseAPSManager: APSManager, Injectable {

deviceDataManager.bolusTrigger
.receive(on: processQueue)
.sink { [weak self] bolusing in
if bolusing {
.sink { [weak self] bolusStatus in
switch bolusStatus {
case .initiating,
.inProgress:
self?.createBolusReporter()
} else {
case .noBolus:
self?.clearBolusReporter()
}
}
Expand Down
15 changes: 9 additions & 6 deletions Trio/Sources/APS/DeviceDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ protocol DeviceDataManager: GlucoseSource {
var loopInProgress: Bool { get set }
var pumpDisplayState: CurrentValueSubject<PumpDisplayState?, Never> { get }
var recommendsLoop: PassthroughSubject<Void, Never> { get }
var bolusTrigger: PassthroughSubject<Bool, Never> { get }
var bolusTrigger: PassthroughSubject<BolusStatus, Never> { get }
var manualTempBasal: PassthroughSubject<Bool, Never> { get }
var scheduledBasal: PassthroughSubject<Bool?, Never> { get }
var suspended: PassthroughSubject<Bool, Never> { get }
Expand Down Expand Up @@ -69,7 +69,7 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
.distantPast

let recommendsLoop = PassthroughSubject<Void, Never>()
let bolusTrigger = PassthroughSubject<Bool, Never>()
let bolusTrigger = PassthroughSubject<BolusStatus, Never>()
let errorSubject = PassthroughSubject<Error, Never>()
let pumpNewStatus = PassthroughSubject<Void, Never>()
let manualTempBasal = PassthroughSubject<Bool, Never>()
Expand Down Expand Up @@ -473,10 +473,13 @@ extension BaseDeviceDataManager: PumpManagerDelegate {
debug(.deviceManager, "New pump status Bolus: \(status.bolusState)")
debug(.deviceManager, "New pump status Basal: \(String(describing: status.basalDeliveryState))")

if case .inProgress = status.bolusState {
bolusTrigger.send(true)
} else {
bolusTrigger.send(false)
switch status.bolusState {
case .initiating:
bolusTrigger.send(.initiating)
case .inProgress:
bolusTrigger.send(.inProgress)
default:
bolusTrigger.send(.noBolus)
}

switch status.basalDeliveryState {
Expand Down
11 changes: 11 additions & 0 deletions Trio/Sources/Models/BolusStatus.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/// Tracks where a bolus is in its lifecycle, derived from the pump's `bolusState`.
///
/// - `noBolus`: nothing is being delivered.
/// - `initiating`: the app has asked the pump to bolus, but delivery has not yet
/// started (no progress to report yet).
/// - `inProgress`: the pump is actively delivering and reporting progress.
enum BolusStatus {
case noBolus
case initiating
case inProgress
}
6 changes: 6 additions & 0 deletions Trio/Sources/Modules/Home/HomeStateModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ extension Home {
var tempBasals: [PumpEventStored] = []
var suspendAndResumeEvents: [PumpEventStored] = []
var batteryFromPersistence: [OpenAPS_Battery] = []
var bolusStatus: BolusStatus = .noBolus
var lastPumpBolus: PumpEventStored?
var overrides: [OverrideStored] = []
var overrideRunStored: [OverrideRunStored] = []
Expand Down Expand Up @@ -619,6 +620,11 @@ extension Home {
.weakAssign(to: \.bolusProgress, on: self)
.store(in: &lifetime)

provider.deviceManager.bolusTrigger
.receive(on: DispatchQueue.main)
.weakAssign(to: \.bolusStatus, on: self)
.store(in: &lifetime)

apsManager.pumpDisplayState
.receive(on: DispatchQueue.main)
.sink { [weak self] state in
Expand Down
98 changes: 59 additions & 39 deletions Trio/Sources/Modules/Home/View/HomeRootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -815,37 +815,49 @@ extension Home {
}

@ViewBuilder func bolusView(geo: GeometryProxy, _ progress: Decimal) -> some View {
/// ensure that state.lastPumpBolus has a value, i.e. there is a last bolus done by the pump and not an external bolus
/// - TRUE: show the pump bolus
/// - FALSE: do not show a progress bar at all
if let bolusTotal = state.lastPumpBolus?.bolus?.amount {
let isInitiating = state.bolusStatus == .initiating
let bolusTotal = state.lastPumpBolus?.bolus?.amount
/// Show the card for a real pump bolus (we have a total) or during the brief
/// `.initiating` window (bolus requested, pump not yet delivering).
if bolusTotal != nil || isInitiating {
let progressValue = (progress as NSDecimalNumber).doubleValue
let bolusFraction = progress * (bolusTotal as Decimal)
let bolusString =
(bolusProgressFormatter.string(from: bolusFraction as NSNumber) ?? "0")
+ String(localized: " of ", comment: "Bolus string partial message: 'x U of y U' in home view") +
(Formatter.decimalFormatterWithThreeFractionDigits.string(from: bolusTotal as NSNumber) ?? "0")
// The "x of y U" string is only meaningful once we're actually delivering.
let bolusString: String? = {
guard !isInitiating, let bolusTotal = bolusTotal else { return nil }
let bolusFraction = progress * (bolusTotal as Decimal)
return (bolusProgressFormatter.string(from: bolusFraction as NSNumber) ?? "0")
+ String(localized: " of ", comment: "Bolus string partial message: 'x U of y U' in home view")
+ (Formatter.decimalFormatterWithThreeFractionDigits.string(from: bolusTotal as NSNumber) ?? "0")
+ String(localized: " U", comment: "Insulin unit")
}()

let card = RoundedRectangle(cornerRadius: 15)
.fill(
colorScheme == .dark ? Color(red: 0.03921568627, green: 0.133333333, blue: 0.2156862745) : Color
.insulin
.opacity(0.2)
)
.clipShape(RoundedRectangle(cornerRadius: 15))
.frame(height: geo.size.height * 0.08)

ZStack {
/// rectangle as background
RoundedRectangle(cornerRadius: 15)
.fill(
colorScheme == .dark ? Color(red: 0.03921568627, green: 0.133333333, blue: 0.2156862745) : Color
.insulin
.opacity(0.2)
)
.clipShape(RoundedRectangle(cornerRadius: 15))
.frame(height: geo.size.height * 0.08)
// The card's full-width border is the progress meter: a bright fill
// runs left → right along the top and bottom edges equally (over a
// faint track) as the bolus completes.
.progressRoundedBorder(progress: progressValue, color: .insulin, cornerRadius: 15, lineWidth: 3)
.shadow(
color: colorScheme == .dark ? Color(red: 0.02745098039, green: 0.1098039216, blue: 0.1411764706) :
Color.black.opacity(0.33),
radius: 3
)
/// rectangle as background + border
Group {
if isInitiating {
// Delivery hasn't started: indeterminate spinning border, matching
// the pump reservoir and loop pills — no ProgressView spinner.
card.spinningRoundedBorder(isActive: true, color: .insulin, cornerRadius: 15, lineWidth: 3)
} else {
// Delivering: the border is the progress meter — a bright fill runs
// left → right along the top and bottom edges equally over a faint track.
card.progressRoundedBorder(progress: progressValue, color: .insulin, cornerRadius: 15, lineWidth: 3)
}
}
.shadow(
color: colorScheme == .dark ? Color(red: 0.02745098039, green: 0.1098039216, blue: 0.1411764706) :
Color.black.opacity(0.33),
radius: 3
)

/// actual bolus view
HStack {
Expand All @@ -855,22 +867,28 @@ extension Home {
Spacer()

VStack {
Text("Bolusing")
Text(isInitiating ? "Initiating…" : "Bolusing")
.font(.subheadline)
.frame(maxWidth: .infinity, alignment: .leading)
Text(bolusString)
.font(.caption)
.frame(maxWidth: .infinity, alignment: .leading)
if let bolusString = bolusString {
Text(bolusString)
.font(.caption)
.frame(maxWidth: .infinity, alignment: .leading)
}
}.padding(.leading, 5)

Spacer()

Button {
state.showProgressView()
state.cancelBolus()
} label: {
Image(systemName: "xmark.app")
.font(.system(size: 25))
// The cancel button only makes sense once delivery is actually running;
// during `.initiating` the spinning border conveys the state.
if state.bolusStatus == .inProgress {
Button {
state.showProgressView()
state.cancelBolus()
} label: {
Image(systemName: "xmark.app")
.font(.system(size: 25))
}
}
}.padding(.horizontal, 10)
.padding(.trailing, 8)
Expand Down Expand Up @@ -993,8 +1011,10 @@ extension Home {
activeProfileIndicator
.padding(.bottom)

if let progress = state.bolusProgress {
bolusView(geo: geo, progress)
if state.bolusStatus != .noBolus {
// Covers both `.initiating` (no progress yet → spinning border) and
// `.inProgress` (determinate progress fill).
bolusView(geo: geo, state.bolusProgress ?? 0)
.padding(.bottom, UIDevice.adjustPadding(min: nil, max: 40))
} else {
adjustmentView(geo: geo).padding(.bottom, UIDevice.adjustPadding(min: nil, max: 40))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ extension Treatments {
typealias PumpEvent = PumpEventStored.EventType

var bolusProgress: Decimal?
var isBolusInProgress: Bool { bolusProgress != nil }
var bolusStatus: BolusStatus = .noBolus
var lastPumpBolus: PumpEventStored?

func unsubscribe() {
Expand Down Expand Up @@ -263,6 +263,11 @@ extension Treatments {
.receive(on: DispatchQueue.main)
.weakAssign(to: \.bolusProgress, on: self)
.store(in: &lifetime)

provider.deviceManager.bolusTrigger
.receive(on: DispatchQueue.main)
.weakAssign(to: \.bolusStatus, on: self)
.store(in: &lifetime)
}

func cancelBolus() {
Expand Down
57 changes: 34 additions & 23 deletions Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ extension Treatments {
}

var treatmentButton: some View {
let shouldDisplayBolusProgress = state.isBolusInProgress && state.amount > 0 &&
let shouldDisplayBolusProgress = (state.bolusStatus != .noBolus) && state.amount > 0 &&
!state.externalInsulin && (state.carbs == 0 || state.fat == 0 || state.protein == 0)

var treatmentButtonBackground = Color(.systemBlue)
Expand Down Expand Up @@ -608,22 +608,29 @@ extension Treatments {
+ (Formatter.decimalFormatterWithThreeFractionDigits.string(from: bolusTotal as NSNumber) ?? "0")
+ String(localized: " U", comment: "Insulin unit")
}()
let bolusLabel = state.bolusStatus == .inProgress ? String(localized: "Bolusing") : String(localized: "Initiating…")

let backgroundCard = RoundedRectangle(cornerRadius: 15)
.fill(
colorScheme == .dark
? Color(red: 0.03921568627, green: 0.133333333, blue: 0.2156862745)
: Color.insulin.opacity(0.2)
)
.frame(height: 56)
.shadow(
color: colorScheme == .dark
? Color(red: 0.02745098039, green: 0.1098039216, blue: 0.1411764706)
: Color.black.opacity(0.33),
radius: 3
)

ZStack {
// background card
RoundedRectangle(cornerRadius: 15)
.fill(
colorScheme == .dark
? Color(red: 0.03921568627, green: 0.133333333, blue: 0.2156862745)
: Color.insulin.opacity(0.2)
)
.frame(height: 56)
.shadow(
color: colorScheme == .dark
? Color(red: 0.02745098039, green: 0.1098039216, blue: 0.1411764706)
: Color.black.opacity(0.33),
radius: 3
)
// background card — spinning border while initiating, in place of a spinner
if state.bolusStatus == .initiating {
backgroundCard.spinningRoundedBorder(isActive: true, color: .insulin, cornerRadius: 15, lineWidth: 3)
} else {
backgroundCard
}

// bolus content
HStack {
Expand All @@ -633,7 +640,7 @@ extension Treatments {
Spacer()

VStack {
Text("Bolusing")
Text(bolusLabel)
.font(.subheadline)
.frame(maxWidth: .infinity, alignment: .leading)
Text(bolusString)
Expand All @@ -644,12 +651,16 @@ extension Treatments {

Spacer()

Button { state.cancelBolus() } label: {
Image(systemName: "xmark.app")
.font(.system(size: 25))
}.tint(Color.tabBar)
.buttonStyle(.borderless)
.accessibilityLabel("Cancel bolus")
// During `.initiating` the spinning border conveys the state; the cancel
// button appears once delivery is actually running.
if state.bolusStatus == .inProgress {
Button { state.cancelBolus() } label: {
Image(systemName: "xmark.app")
.font(.system(size: 25))
}.tint(Color.tabBar)
.buttonStyle(.borderless)
.accessibilityLabel("Cancel bolus")
}
}
.padding(.horizontal, 10)
.padding(.trailing, 8)
Expand Down Expand Up @@ -730,7 +741,7 @@ extension Treatments {

private var disableTaskButton: Bool {
(
state.isBolusInProgress && state
(state.bolusStatus != .noBolus) && state
.amount > 0 && !state.externalInsulin && (state.carbs == 0 || state.fat == 0 || state.protein == 0)
) || state
.addButtonPressed || limitExceeded
Expand Down