Parcourir la source

Calendar settings

Jonas Björkert il y a 1 an
Parent
commit
59935e15e3

+ 4 - 4
LoopFollow.xcodeproj/project.pbxproj

@@ -125,6 +125,7 @@
 		DD8316482DE49EE5004467AA /* Storage+Migrate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8316472DE49EE5004467AA /* Storage+Migrate.swift */; };
 		DD83164A2DE4C504004467AA /* SettingsStepperRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8316492DE4C504004467AA /* SettingsStepperRow.swift */; };
 		DD83164C2DE4DB3A004467AA /* BinaryFloatingPoint+localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD83164B2DE4DB3A004467AA /* BinaryFloatingPoint+localized.swift */; };
+		DD83164E2DE4E093004467AA /* WatchSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD83164D2DE4E093004467AA /* WatchSettingsView.swift */; };
 		DD85E9952D739CFE001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */; };
 		DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */; };
 		DD98F54424BCEFEE0007425A /* ShareClientExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD98F54324BCEFEE0007425A /* ShareClientExtension.swift */; };
@@ -174,7 +175,6 @@
 		DDCC3A582DDC9655006F1C10 /* MissedBolusAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A572DDC9655006F1C10 /* MissedBolusAlarmEditor.swift */; };
 		DDCC3A5A2DDC988F006F1C10 /* CarbSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A592DDC988F006F1C10 /* CarbSample.swift */; };
 		DDCF979424C0D380002C9752 /* UIViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979324C0D380002C9752 /* UIViewExtension.swift */; };
-		DDCF979A24C14DB4002C9752 /* WatchSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979924C14DB4002C9752 /* WatchSettingsViewController.swift */; };
 		DDCF9A802D85FD0B004DF4DD /* Alarm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF9A7F2D85FD09004DF4DD /* Alarm.swift */; };
 		DDCF9A822D85FD15004DF4DD /* AlarmType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF9A812D85FD14004DF4DD /* AlarmType.swift */; };
 		DDCF9A882D85FD33004DF4DD /* AlarmData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF9A872D85FD33004DF4DD /* AlarmData.swift */; };
@@ -487,6 +487,7 @@
 		DD8316472DE49EE5004467AA /* Storage+Migrate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+Migrate.swift"; sourceTree = "<group>"; };
 		DD8316492DE4C504004467AA /* SettingsStepperRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsStepperRow.swift; sourceTree = "<group>"; };
 		DD83164B2DE4DB3A004467AA /* BinaryFloatingPoint+localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BinaryFloatingPoint+localized.swift"; sourceTree = "<group>"; };
+		DD83164D2DE4E093004467AA /* WatchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchSettingsView.swift; sourceTree = "<group>"; };
 		DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnipodDashHeartbeatBluetoothTransmitter.swift; sourceTree = "<group>"; };
 		DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseConversion.swift; sourceTree = "<group>"; };
 		DD98F54324BCEFEE0007425A /* ShareClientExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareClientExtension.swift; sourceTree = "<group>"; };
@@ -539,7 +540,6 @@
 		DDCC3ABF2DDE10B0006F1C10 /* Testing.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Testing.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/Testing.framework; sourceTree = DEVELOPER_DIR; };
 		DDCC3AD62DDE1790006F1C10 /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
 		DDCF979324C0D380002C9752 /* UIViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtension.swift; sourceTree = "<group>"; };
-		DDCF979924C14DB4002C9752 /* WatchSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchSettingsViewController.swift; sourceTree = "<group>"; };
 		DDCF9A7F2D85FD09004DF4DD /* Alarm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alarm.swift; sourceTree = "<group>"; };
 		DDCF9A812D85FD14004DF4DD /* AlarmType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmType.swift; sourceTree = "<group>"; };
 		DDCF9A872D85FD33004DF4DD /* AlarmData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmData.swift; sourceTree = "<group>"; };
@@ -841,6 +841,7 @@
 		DD1A97122D429495000DDC11 /* Settings */ = {
 			isa = PBXGroup;
 			children = (
+				DD83164D2DE4E093004467AA /* WatchSettingsView.swift */,
 				DD8316452DE49B09004467AA /* GraphSettingsView.swift */,
 				DD1A97152D4294B2000DDC11 /* AdvancedSettingsViewModel.swift */,
 				DD1A97132D4294A4000DDC11 /* AdvancedSettingsView.swift */,
@@ -1464,7 +1465,6 @@
 				FC97881B2485969B00A7906C /* MainViewController.swift */,
 				FC97881D2485969B00A7906C /* NightScoutViewController.swift */,
 				FCFEECA1248857A600402A7F /* SettingsViewController.swift */,
-				DDCF979924C14DB4002C9752 /* WatchSettingsViewController.swift */,
 			);
 			path = ViewControllers;
 			sourceTree = "<group>";
@@ -1976,6 +1976,7 @@
 				DDC6CA432DD8CED20060EE25 /* SensorAgeCondition.swift in Sources */,
 				DD7F4C212DD66BB200D449E9 /* FastRiseAlarmEditor.swift in Sources */,
 				FC97881A2485969B00A7906C /* SceneDelegate.swift in Sources */,
+				DD83164E2DE4E093004467AA /* WatchSettingsView.swift in Sources */,
 				DD0C0C662C46E54C00DBADDF /* InfoDataSeparator.swift in Sources */,
 				DD58171C2D299F940041FB98 /* BluetoothDevice.swift in Sources */,
 				DD7E198A2ACDA62600DBD158 /* SensorStart.swift in Sources */,
@@ -1995,7 +1996,6 @@
 				DD5334232C60ED3600062F9D /* IAge.swift in Sources */,
 				FCD2A27D24C9D044009F7B7B /* Globals.swift in Sources */,
 				DD0C0C642C45A59400DBADDF /* HKUnit+Extensions.swift in Sources */,
-				DDCF979A24C14DB4002C9752 /* WatchSettingsViewController.swift in Sources */,
 				DDC6CA412DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift in Sources */,
 				DD493AE52ACF2383009A6922 /* Treatments.swift in Sources */,
 				DD7F4C112DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift in Sources */,

+ 123 - 0
LoopFollow/Settings/WatchSettingsView.swift

@@ -0,0 +1,123 @@
+// LoopFollow
+// WatchSettingsView.swift
+// Created by Jonas Björkert on 2025-05-26.
+
+import EventKit
+import SwiftUI
+
+struct WatchSettingsView: View {
+    // MARK: Storage bindings
+
+    @ObservedObject private var writeCalendarEvent = Storage.shared.writeCalendarEvent
+    @ObservedObject private var calendarIdentifier = Storage.shared.calendarIdentifier
+    @ObservedObject private var watchLine1 = Storage.shared.watchLine1
+    @ObservedObject private var watchLine2 = Storage.shared.watchLine2
+
+    // MARK: Local state
+
+    @Environment(\.presentationMode) private var presentationMode
+    @State private var calendars: [EKCalendar] = []
+    @State private var accessDenied = false
+
+    // MARK: Body
+
+    var body: some View {
+        NavigationStack {
+            Form {
+                // ------------- Calendar write -------------
+                Section {
+                    Toggle("Save BG to Calendar",
+                           isOn: $writeCalendarEvent.value)
+                        .disabled(accessDenied) // prevent use when no access
+                } footer: {
+                    Text("""
+                    Add the Apple-Calendar complication to your watch or CarPlay \
+                    to see BG readings. Create a separate calendar (e.g. “Follow”) \
+                    — this view will **delete** events on the same calendar each time \
+                    it writes new readings.
+                    """)
+                }
+
+                // ------------- Access / calendar picker -------------
+                if accessDenied {
+                    Text("Calendar access denied")
+                        .foregroundColor(.red)
+                } else {
+                    if !calendars.isEmpty {
+                        Picker("Calendar",
+                               selection: $calendarIdentifier.value)
+                        {
+                            ForEach(calendars, id: \.calendarIdentifier) { cal in
+                                Text(cal.title).tag(cal.calendarIdentifier)
+                            }
+                        }
+                    }
+                }
+
+                // ------------- Template lines -------------
+                Section("Calendar Text") {
+                    TextField("Line 1", text: $watchLine1.value)
+                        .textInputAutocapitalization(.never)
+                        .disableAutocorrection(true)
+
+                    TextField("Line 2", text: $watchLine2.value)
+                        .textInputAutocapitalization(.never)
+                        .disableAutocorrection(true)
+                }
+
+                // ------------- Variable cheat-sheet -------------
+                Section("Available Variables") {
+                    ForEach(variableDescriptions, id: \.self) { desc in
+                        Text(desc)
+                    }
+                }
+            }
+            .navigationTitle("Calendar")
+            .toolbar {
+                ToolbarItem(placement: .confirmationAction) {
+                    Button("Done") { presentationMode.wrappedValue.dismiss() }
+                }
+            }
+            .task { // runs once on appear
+                await requestCalendarAccessAndLoad()
+            }
+        }
+    }
+
+    // MARK: - Helpers
+
+    /// Returns array of “%TOKEN% : Explanation” strings used in the cheat-sheet.
+    private var variableDescriptions: [String] {
+        [
+            "%BG% : Blood-glucose reading",
+            "%DIRECTION% : Dexcom trend arrow",
+            "%DELTA% : Difference from last reading",
+            "%IOB% : Insulin-on-Board",
+            "%COB% : Carbs-on-Board",
+            "%BASAL% : Current basal U/h",
+            "%LOOP% : Loop status symbol",
+            "%OVERRIDE% : Active override %",
+            "%MINAGO% : Minutes since last reading",
+        ]
+    }
+
+    /// Ask for calendar permission, then pull the user’s calendars.
+    private func requestCalendarAccessAndLoad() async {
+        let store = EKEventStore()
+        do {
+            try await store.requestAccess(to: .event)
+            accessDenied = false
+            calendars = store.calendars(for: .event)
+                .sorted { $0.title.localizedCaseInsensitiveCompare($1.title) == .orderedAscending }
+        } catch {
+            accessDenied = true
+        }
+
+        // If the previously-saved calendar no longer exists, blank it out
+        if !calendarIdentifier.value.isEmpty &&
+            !calendars.contains(where: { $0.calendarIdentifier == calendarIdentifier.value })
+        {
+            calendarIdentifier.value = ""
+        }
+    }
+}

+ 6 - 0
LoopFollow/Storage/Storage+Migrate.swift

@@ -176,5 +176,11 @@ extension Storage {
         moveFloatToDouble(UserDefaultsValue<Float>(key: "highLine", default: 180.0), into: highLine)
         move(UserDefaultsValue<Int>(key: "downloadDays", default: 1), into: downloadDays)
         // Remove this in a year later than the release of the new Alarms [END]
+
+        // ── Watch / Calendar ────────────────────────────────────────────────
+        move(UserDefaultsValue<Bool>(key: "writeCalendarEvent", default: false), into: writeCalendarEvent)
+        move(UserDefaultsValue<String>(key: "calendarIdentifier", default: ""), into: calendarIdentifier)
+        move(UserDefaultsValue<String>(key: "watchLine1", default: "%BG% %DIRECTION% %DELTA% %MINAGO%"), into: watchLine1)
+        move(UserDefaultsValue<String>(key: "watchLine2", default: "C:%COB% I:%IOB% B:%BASAL%"), into: watchLine2)
     }
 }

+ 7 - 0
LoopFollow/Storage/Storage.swift

@@ -100,6 +100,13 @@ class Storage {
     var downloadDays = StorageValue<Int>(key: "downloadDays", defaultValue: 1)
     // Graph Settings [END]
 
+    // Calendar entries [BEGIN]
+    var writeCalendarEvent = StorageValue<Bool>(key: "writeCalendarEvent", defaultValue: false)
+    var calendarIdentifier = StorageValue<String>(key: "calendarIdentifier", defaultValue: "")
+    var watchLine1 = StorageValue<String>(key: "watchLine1", defaultValue: "%BG% %DIRECTION% %DELTA% %MINAGO%")
+    var watchLine2 = StorageValue<String>(key: "watchLine2", defaultValue: "C:%COB% I:%IOB% B:%BASAL%")
+    // Calendar entries [END]
+
     static let shared = Storage()
     private init() {}
 }

+ 0 - 8
LoopFollow/Storage/UserDefaults.swift

@@ -95,14 +95,6 @@ class UserDefaultsRepository {
     static let graphCarbs = UserDefaultsValue<Bool>(key: "graphCarbs", default: true)
     static let bgUpdateDelay = UserDefaultsValue<Int>(key: "bgUpdateDelay", default: 10)
 
-    // Watch Calendar Settings
-    static let calendarIdentifier = UserDefaultsValue<String>(key: "calendarIdentifier", default: "")
-    static let savedEventID = UserDefaultsValue<String>(key: "savedEventID", default: "")
-    static let lastCalendarStartDate = UserDefaultsValue<Date?>(key: "lastCalendarStartDate", default: nil)
-    static let writeCalendarEvent = UserDefaultsValue<Bool>(key: "writeCalendarEvent", default: false)
-    static let watchLine1 = UserDefaultsValue<String>(key: "watchLine1", default: "%BG% %DIRECTION% %DELTA% %MINAGO%")
-    static let watchLine2 = UserDefaultsValue<String>(key: "watchLine2", default: "C:%COB% I:%IOB% B:%BASAL%")
-
     // Alarm Settings
     static let systemOutputVolume = UserDefaultsValue<Float>(key: "systemOutputVolume", default: 0.5)
     static let fadeInTimeInterval = UserDefaultsValue<TimeInterval>(key: "fadeInTimeInterval", default: 0)

+ 2 - 2
LoopFollow/Task/CalendarTask.swift

@@ -14,8 +14,8 @@ extension MainViewController {
     }
 
     func calendarTaskAction() {
-        if UserDefaultsRepository.writeCalendarEvent.value,
-           !UserDefaultsRepository.calendarIdentifier.value.isEmpty
+        if Storage.shared.writeCalendarEvent.value,
+           !Storage.shared.calendarIdentifier.value.isEmpty
         {
             writeCalendar()
         }

+ 6 - 7
LoopFollow/ViewControllers/MainViewController.swift

@@ -559,7 +559,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
     }
 
     func processCalendarUpdates() {
-        if UserDefaultsRepository.calendarIdentifier.value == "" { return }
+        if Storage.shared.calendarIdentifier.value == "" { return }
 
         if bgData.count < 1 { return }
 
@@ -589,9 +589,9 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
 
         var eventStartDate = Date(timeIntervalSince1970: bgData[bgData.count - 1].date)
         var eventEndDate = eventStartDate.addingTimeInterval(60 * 10)
-        var eventTitle = UserDefaultsRepository.watchLine1.value
-        if UserDefaultsRepository.watchLine2.value.count > 1 {
-            eventTitle += "\n" + UserDefaultsRepository.watchLine2.value
+        var eventTitle = Storage.shared.watchLine1.value
+        if Storage.shared.watchLine2.value.count > 1 {
+            eventTitle += "\n" + Storage.shared.watchLine2.value
         }
         eventTitle = eventTitle.replacingOccurrences(of: "%BG%", with: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)))
         eventTitle = eventTitle.replacingOccurrences(of: "%DIRECTION%", with: direction)
@@ -624,7 +624,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
         var deleteStartDate = Date().addingTimeInterval(-60 * 60 * 2)
         var deleteEndDate = Date().addingTimeInterval(60 * 60 * 2)
         // guard solves for some ios upgrades removing the calendar
-        guard let deleteCalendar = store.calendar(withIdentifier: UserDefaultsRepository.calendarIdentifier.value) as? EKCalendar else { return }
+        guard let deleteCalendar = store.calendar(withIdentifier: Storage.shared.calendarIdentifier.value) as? EKCalendar else { return }
         var predicate2 = store.predicateForEvents(withStart: deleteStartDate, end: deleteEndDate, calendars: [deleteCalendar])
         var eVDelete = store.events(matching: predicate2) as [EKEvent]?
         if eVDelete != nil {
@@ -642,13 +642,12 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
         event.title = eventTitle
         event.startDate = eventStartDate
         event.endDate = eventEndDate
-        event.calendar = store.calendar(withIdentifier: UserDefaultsRepository.calendarIdentifier.value)
+        event.calendar = store.calendar(withIdentifier: Storage.shared.calendarIdentifier.value)
         do {
             try store.save(event, span: .thisEvent, commit: true)
             lastCalendarWriteAttemptTime = now
 
             lastCalDate = bgData[bgData.count - 1].date
-            // UserDefaultsRepository.savedEventID.value = event.eventIdentifier //save event id to access this particular event later
         } catch {
             LogManager.shared.log(category: .calendar, message: "Error storing to the calendar")
         }

+ 8 - 4
LoopFollow/ViewControllers/SettingsViewController.swift

@@ -162,10 +162,14 @@ class SettingsViewController: FormViewController, NightscoutSettingsViewModelDel
                 $0.title = "Calendar"
                 $0.presentationMode = .show(
                     controllerProvider: .callback(builder: {
-                        let controller = WatchSettingsViewController()
-                        return controller
-                    }
-                    ), onDismiss: nil
+                        let host = UIHostingController(rootView: WatchSettingsView())
+                        host.modalPresentationStyle = .formSheet
+                        if Storage.shared.forceDarkMode.value {
+                            host.overrideUserInterfaceStyle = .dark
+                        }
+                        return host
+                    }),
+                    onDismiss: nil
                 )
             }
             <<< ButtonRow("contact") {

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 147
LoopFollow/ViewControllers/WatchSettingsViewController.swift