Quellcode durchsuchen

Alarm editing gui improvements

Jonas Björkert vor 1 Jahr
Ursprung
Commit
03fda1915c

+ 16 - 0
LoopFollow.xcodeproj/project.pbxproj

@@ -14,6 +14,10 @@
 		DD0650EB2DCE8385004D3B41 /* LowBGCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650EA2DCE8385004D3B41 /* LowBGCondition.swift */; };
 		DD0650ED2DCE9371004D3B41 /* HighBgAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650EC2DCE9371004D3B41 /* HighBgAlarmEditor.swift */; };
 		DD0650EF2DCE96FF004D3B41 /* HighBGCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650EE2DCE96FF004D3B41 /* HighBGCondition.swift */; };
+		DD0650F12DCE9A9E004D3B41 /* MissedReadingCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650F02DCE9A9E004D3B41 /* MissedReadingCondition.swift */; };
+		DD0650F32DCE9B3D004D3B41 /* MissedReadingEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650F22DCE9B3D004D3B41 /* MissedReadingEditor.swift */; };
+		DD0650F52DCF303F004D3B41 /* AlarmStepperSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650F42DCF303F004D3B41 /* AlarmStepperSection.swift */; };
+		DD0650F72DCFDA26004D3B41 /* InfoBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650F62DCFDA26004D3B41 /* InfoBanner.swift */; };
 		DD07B5C929E2F9C400C6A635 /* NightscoutUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */; };
 		DD0C0C602C415B9D00DBADDF /* ProfileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */; };
 		DD0C0C622C4175FD00DBADDF /* NSProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C612C4175FD00DBADDF /* NSProfile.swift */; };
@@ -329,6 +333,10 @@
 		DD0650EA2DCE8385004D3B41 /* LowBGCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LowBGCondition.swift; sourceTree = "<group>"; };
 		DD0650EC2DCE9371004D3B41 /* HighBgAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighBgAlarmEditor.swift; sourceTree = "<group>"; };
 		DD0650EE2DCE96FF004D3B41 /* HighBGCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighBGCondition.swift; sourceTree = "<group>"; };
+		DD0650F02DCE9A9E004D3B41 /* MissedReadingCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissedReadingCondition.swift; sourceTree = "<group>"; };
+		DD0650F22DCE9B3D004D3B41 /* MissedReadingEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissedReadingEditor.swift; sourceTree = "<group>"; };
+		DD0650F42DCF303F004D3B41 /* AlarmStepperSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmStepperSection.swift; sourceTree = "<group>"; };
+		DD0650F62DCFDA26004D3B41 /* InfoBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoBanner.swift; sourceTree = "<group>"; };
 		DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUtils.swift; sourceTree = "<group>"; };
 		DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileManager.swift; sourceTree = "<group>"; };
 		DD0C0C612C4175FD00DBADDF /* NSProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSProfile.swift; sourceTree = "<group>"; };
@@ -676,6 +684,7 @@
 				DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */,
 				DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */,
 				DD0650EE2DCE96FF004D3B41 /* HighBGCondition.swift */,
+				DD0650F02DCE9A9E004D3B41 /* MissedReadingCondition.swift */,
 			);
 			path = AlarmCondition;
 			sourceTree = "<group>";
@@ -904,6 +913,8 @@
 		DDC7E53B2DBD8A1600EB1127 /* Components */ = {
 			isa = PBXGroup;
 			children = (
+				DD0650F62DCFDA26004D3B41 /* InfoBanner.swift */,
+				DD0650F42DCF303F004D3B41 /* AlarmStepperSection.swift */,
 				DD0650A82DCA8A10004D3B41 /* AlarmBGSection.swift */,
 				DDC7E5392DBD8A1600EB1127 /* AlarmEditorFields.swift */,
 				DDC7E53A2DBD8A1600EB1127 /* SoundFile.swift */,
@@ -917,6 +928,7 @@
 				DD0650EC2DCE9371004D3B41 /* HighBgAlarmEditor.swift */,
 				DDC7E53C2DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift */,
 				DDC7E53E2DBD8A1600EB1127 /* LowBgAlarmEditor.swift */,
+				DD0650F22DCE9B3D004D3B41 /* MissedReadingEditor.swift */,
 			);
 			path = Editors;
 			sourceTree = "<group>";
@@ -1646,6 +1658,7 @@
 				FCC6886924898FB100A0279D /* UserDefaultsValueGroups.swift in Sources */,
 				DDF2C0122BEFB733007A20E6 /* AppVersionManager.swift in Sources */,
 				DD7E19862ACDA59700DBD158 /* BGCheck.swift in Sources */,
+				DD0650F12DCE9A9E004D3B41 /* MissedReadingCondition.swift in Sources */,
 				DD16AF0F2C99592F00FB655A /* HKQuantityInputView.swift in Sources */,
 				DDFF3D7F2D1414A200BF9D9E /* BLEDevice.swift in Sources */,
 				DD9ACA042D32821400415D8A /* DeviceStatusTask.swift in Sources */,
@@ -1665,12 +1678,14 @@
 				DDDF6F472D479AD200884336 /* LoopOverrideViewModel.swift in Sources */,
 				DD5334272C61668800062F9D /* InfoDisplaySettingsViewModel.swift in Sources */,
 				DD0247592DB2E89600FCADF6 /* AlarmCondition.swift in Sources */,
+				DD0650F32DCE9B3D004D3B41 /* MissedReadingEditor.swift in Sources */,
 				DD48780E2C7B74A40048F05C /* TrioRemoteControlViewModel.swift in Sources */,
 				DDEF503A2D31615000999A5D /* LogManager.swift in Sources */,
 				DD4878172C7B75350048F05C /* BolusView.swift in Sources */,
 				DD493AE72ACF23CF009A6922 /* DeviceStatus.swift in Sources */,
 				DDC7E5162DBCFA7F00EB1127 /* SnoozerView.swift in Sources */,
 				FCFEECA2248857A600402A7F /* SettingsViewController.swift in Sources */,
+				DD0650F72DCFDA26004D3B41 /* InfoBanner.swift in Sources */,
 				DD4AFB612DB68BBC00BB593F /* AlarmListView.swift in Sources */,
 				DD7B0D442D730A3B0063DCB6 /* CycleHelper.swift in Sources */,
 				DD9ACA0C2D33BB8600415D8A /* CalendarTask.swift in Sources */,
@@ -1770,6 +1785,7 @@
 				DDCF979A24C14DB4002C9752 /* WatchSettingsViewController.swift in Sources */,
 				DD493AE52ACF2383009A6922 /* Treatments.swift in Sources */,
 				DD50C7552D0862770057AE6F /* ContactImageUpdater.swift in Sources */,
+				DD0650F52DCF303F004D3B41 /* AlarmStepperSection.swift in Sources */,
 				DDCF9A802D85FD0B004DF4DD /* Alarm.swift in Sources */,
 				DDD10F012C510C6B00D76A8E /* ObservableUserDefaults.swift in Sources */,
 				DD16AF0D2C98485400FB655A /* SecureStorageValue.swift in Sources */,

+ 29 - 0
LoopFollow/Alarm/AlarmCondition/MissedReadingCondition.swift

@@ -0,0 +1,29 @@
+//
+//  MissedReadingCondition.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-05-09.
+//  Copyright © 2025 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+
+/// Fires when the newest CGM reading is older than `threshold` minutes.
+struct MissedReadingCondition: AlarmCondition {
+    static let type: AlarmType = .missedReading
+    init() {}
+
+    func evaluate(alarm: Alarm, data: AlarmData) -> Bool {
+
+        // ────────────────────────────────
+        // 0. sanity checks
+        // ────────────────────────────────
+        guard let thresholdMinutes = alarm.threshold, thresholdMinutes > 0 else { return false }
+
+        // Skip if we have *no* readings
+        guard let last = data.bgReadings.last else { return false }
+
+        let secondsSinceLast = Date().timeIntervalSince(last.date)
+        return secondsSinceLast >= thresholdMinutes * 60
+    }
+}

+ 5 - 0
LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift

@@ -20,6 +20,11 @@ struct AlarmEditor: View {
             HighBgAlarmEditor(alarm: $alarm)
         case .low:
             LowBgAlarmEditor(alarm: $alarm)
+        case .missedReading:
+            MissedReadingEditor(alarm: $alarm)
+
+            // TODO: add other condition types here
+
         default:
             Text("No editor for \(alarm.type.rawValue)")
                 .padding()

+ 0 - 22
LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift

@@ -24,28 +24,6 @@ struct AlarmGeneralSection: View {
     }
 }
 
-struct AlarmStepperSection: View {
-    let title: String
-    let range: ClosedRange<Double>
-    let step: Double
-    let unitLabel: String
-    @Binding var value: Double
-
-    var body: some View {
-        Section(
-          header: Text(title),
-          footer: Text("Set \(title), \(Int(range.lowerBound))–\(Int(range.upperBound)) \(unitLabel)")
-        ) {
-            Stepper(
-                "\(title): \(Int(value)) \(unitLabel)",
-                value: $value,
-                in: range,
-                step: step
-            )
-        }
-    }
-}
-
 struct AlarmSnoozeSection: View {
     let title: String
     let range: ClosedRange<Double>

+ 56 - 0
LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift

@@ -0,0 +1,56 @@
+//
+//  AlarmStepperSection.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-05-10.
+//  Copyright © 2025 Jon Fawcett. All rights reserved.
+//
+
+import SwiftUI
+
+struct AlarmStepperSection: View {
+    let header: String?
+    let footer: String?
+    let title: String
+    let range: ClosedRange<Double>
+    let step: Double
+    let unitLabel: String
+    @Binding var value: Double
+
+    init(
+        header: String? = nil,
+        footer: String? = nil,
+        title: String,
+        range: ClosedRange<Double>,
+        step: Double,
+        unitLabel: String,
+        value: Binding<Double>
+    ) {
+        self.header = header
+        self.footer = footer
+        self.title = title
+        self.range = range
+        self.step = step
+        self.unitLabel = unitLabel
+        self._value = value
+    }
+
+/**
+ header: Text(title),
+ footer: Text("Set \(title), \(Int(range.lowerBound))–\(Int(range.upperBound)) \(unitLabel)")
+ **/
+
+    var body: some View {
+        Section(
+            header: header.map(Text.init),
+            footer: footer.map(Text.init)
+        ) {
+            Stepper(
+                "\(title): \(Int(value)) \(unitLabel)",
+                value: $value,
+                in: range,
+                step: step
+            )
+        }
+    }
+}

+ 47 - 0
LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift

@@ -0,0 +1,47 @@
+//
+//  InfoBanner.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-05-10.
+//  Copyright © 2025 Jon Fawcett. All rights reserved.
+//
+
+
+import SwiftUI
+
+/// Apple-style information banner you can drop into any `Form` / `List` row.
+///
+/// Usage:
+/// ```swift
+/// Form {
+///     InfoBanner("Triggers when no CGM reading is received for the time you set below.")
+///     
+///     AlarmGeneralSection(alarm: $alarm)
+///     …
+/// }
+/// ```
+struct InfoBanner: View {
+    /// The main explanatory text (can be a `String` or a localized key).
+    let text: String
+
+    /// Optional SFSymbol (defaults to “info.circle.fill” so you can omit it).
+    var systemImage: String = "info.circle.fill"
+    
+    /// Icon colour (defaults to `.accentColor` so it adapts to light/dark).
+    var iconColour: Color = .accentColor
+    
+    var body: some View {
+        HStack(alignment: .top, spacing: 12) {
+            Image(systemName: systemImage)
+                .font(.title3)
+                .foregroundColor(iconColour)
+            
+            Text(text)
+                .font(.callout)
+                .fixedSize(horizontal: false, vertical: true)
+        }
+        .padding()
+        .listRowInsets(EdgeInsets())
+        .padding(.bottom, 4)
+    }
+}

+ 27 - 22
LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift

@@ -13,33 +13,38 @@ struct BuildExpireAlarmEditor: View {
 
     var body: some View {
         Form {
-          AlarmGeneralSection(alarm: $alarm)
+            InfoBanner(
+                text: "Sends a reminder before the looping-app build you’re following reaches its "
+                + "TestFlight or Xcode expiry date. Currently only works for Trio 0.4 and later."
+            )
+            AlarmGeneralSection(alarm: $alarm)
 
-          AlarmStepperSection(
-            title: "Expires In",
-            range: 1...14,
-            step: 1,
-            unitLabel: alarm.type.timeUnit.label,
-            value: Binding(
-              get: { alarm.threshold ?? 1 },
-              set: { alarm.threshold = $0 }
+            AlarmStepperSection(
+                footer: "Choose how many days of notice you’d like before the build becomes unusable.",
+                title: "Expires In",
+                range: 1...14,
+                step: 1,
+                unitLabel: alarm.type.timeUnit.label,
+                value: Binding(
+                    get: { alarm.threshold ?? 1 },
+                    set: { alarm.threshold = $0 }
+                )
             )
-          )
 
-          AlarmSnoozeSection(
-            title: "Default Snooze",
-            range: 1...14,
-            step: 1,
-            unitLabel: alarm.type.timeUnit.label,
-            value: Binding(
-              get: { Double(alarm.snoozeDuration) },
-              set: { alarm.snoozeDuration = Int($0) }
+            AlarmStepperSection(
+                title: "Default Snooze",
+                range: 1...14,
+                step: 1,
+                unitLabel: alarm.type.timeUnit.label,
+                value: Binding(
+                    get: { Double(alarm.snoozeDuration) },
+                    set: { alarm.snoozeDuration = Int($0) }
+                )
             )
-          )
 
-          AlarmAudioSection(alarm: $alarm)
-          AlarmActiveSection(alarm: $alarm)
-          AlarmSnoozedUntilSection(alarm: $alarm)
+            AlarmAudioSection(alarm: $alarm)
+            AlarmActiveSection(alarm: $alarm)
+            AlarmSnoozedUntilSection(alarm: $alarm)
         }
         .navigationTitle(alarm.type.rawValue)
     }

+ 11 - 3
LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift

@@ -13,20 +13,29 @@ struct HighBgAlarmEditor: View {
 
     var body: some View {
         Form {
+            InfoBanner(
+                text: "Alerts when your CGM glucose stays above the limit "
+                + "you set below. Use Persistent if you want to ignore brief spikes."
+            )
+
             AlarmGeneralSection(alarm: $alarm)
 
             AlarmBGSection(
                 header: "Threshold",
+                footer: "The alarm becomes eligible once any reading is ≥ this value.",
                 title: "BG",
                 range: 120...350,
                 value: Binding(
-                    get: { alarm.threshold ?? 120 },
+                    get: { alarm.threshold ?? 180 },
                     set: { alarm.threshold = $0 }
                 )
             )
 
             AlarmStepperSection(
-                title: "Persistent",
+                header: "Persistent High",
+                footer: "How long glucose must remain above the threshold before the "
+                + "alarm actually fires.  Set to 0 for an immediate alert.",
+                title: "Persistent for",
                 range: 0...120,
                 step: 5,
                 unitLabel: alarm.type.timeUnit.label,
@@ -50,7 +59,6 @@ struct HighBgAlarmEditor: View {
             AlarmAudioSection(alarm: $alarm)
             AlarmActiveSection(alarm: $alarm)
             AlarmSnoozedUntilSection(alarm: $alarm)
-
         }
         .navigationTitle(alarm.type.rawValue)
     }

+ 9 - 2
LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift

@@ -14,6 +14,10 @@ struct LowBgAlarmEditor: View {
 
     var body: some View {
         Form {
+            InfoBanner(text: "Alerts when your current CGM value — "
+                       + "or any predicted value within the look-ahead window — "
+                       + "falls at or below the threshold you set.")
+
             AlarmGeneralSection(alarm: $alarm)
 
             AlarmBGSection(
@@ -27,6 +31,8 @@ struct LowBgAlarmEditor: View {
             )
 
             AlarmStepperSection(
+                footer: "Glucose must stay below the threshold for this many minutes "
+                + "before the alert sounds. Set 0 to alert immediately.",
                 title: "Persistent",
                 range: 0...120,
                 step: 5,
@@ -38,8 +44,10 @@ struct LowBgAlarmEditor: View {
             )
 
             AlarmStepperSection(
+                footer: "Look ahead this many minutes in Loop’s prediction; "
+                + "if any future value is at or below the threshold, "
+                + "you’ll be warned early. Set 0 to disable.",
                 title: "Predictive",
-                //footer: include this manu minutes of prediction
                 range: 0...60,
                 step: 5,
                 unitLabel: alarm.type.timeUnit.label,
@@ -63,7 +71,6 @@ struct LowBgAlarmEditor: View {
             AlarmAudioSection(alarm: $alarm)
             AlarmActiveSection(alarm: $alarm)
             AlarmSnoozedUntilSection(alarm: $alarm)
-
         }
         .navigationTitle(alarm.type.rawValue)
     }

+ 50 - 0
LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift

@@ -0,0 +1,50 @@
+//
+//  MissedReadingEditor.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-05-09.
+//  Copyright © 2025 Jon Fawcett. All rights reserved.
+//
+
+import SwiftUI
+
+struct MissedReadingEditor: View {
+    @Binding var alarm: Alarm
+
+    var body: some View {
+        Form {
+            InfoBanner(text: "The app notifies you when no CGM reading has been received for the time you choose below.")
+
+            AlarmGeneralSection(alarm: $alarm)
+
+            AlarmStepperSection(
+                footer: "Choose how long the app should wait before alerting.",
+                title: "No reading for",
+                range: 11...121,
+                step: 5,
+                unitLabel: alarm.type.timeUnit.label,
+                value: Binding(
+                    get: { alarm.threshold ?? 16 },
+                    set: { alarm.threshold = $0 }
+                )
+            )
+
+            AlarmStepperSection(
+                title: "Default Snooze",
+                range: 10...180,
+                step: 5,
+                unitLabel: alarm.type.timeUnit.label,
+                value: Binding(
+                    get: { Double(alarm.snoozeDuration) },
+                    set: { alarm.snoozeDuration = Int($0) }
+                )
+            )
+
+            AlarmAudioSection(alarm: $alarm)
+            AlarmActiveSection(alarm: $alarm)
+            AlarmSnoozedUntilSection(alarm: $alarm)
+
+        }
+        .navigationTitle(alarm.type.rawValue)
+    }
+}

+ 2 - 0
LoopFollow/Alarm/AlarmListView.swift

@@ -19,6 +19,7 @@ struct AlarmListView: View {
     var body: some View {
         NavigationView {
             List {
+                // TODO: sort these in the alarm prio order, as they are evaluated
                 ForEach(store.value) { alarm in
                     NavigationLink(alarm.name) {
                         AlarmEditor(alarm: binding(for: alarm))
@@ -44,6 +45,7 @@ struct AlarmListView: View {
                 }
             }
             // Step 1: pick a type for the new alarm
+            // TODO: Sort these in the type order
             .actionSheet(isPresented: $showingTypePicker) {
                 ActionSheet(
                     title: Text("Select Alarm Type"),