From 6411d6e53241f984bf8e33ef26e0013cf5d7f6a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 26 Apr 2025 11:31:29 +0200 Subject: [PATCH 001/138] Alarm refactoring --- LoopFollow.xcodeproj/project.pbxproj | 238 +++++++++++++++++- .../xcschemes/LoopFollow.xcscheme | 13 + LoopFollow/Alarm/Alarm.swift | 154 ++++++++++++ .../Alarm/AlarmCondition/AlarmCondition.swift | 49 ++++ .../AlarmCondition/BuildExpireCondition.swift | 24 ++ LoopFollow/Alarm/AlarmConfiguration.swift | 34 +++ LoopFollow/Alarm/AlarmContext.swift | 15 ++ LoopFollow/Alarm/AlarmData.swift | 23 ++ .../Alarm/AlarmEditing/AlarmEditor.swift | 28 +++ .../Components/AlarmEditorFields.swift | 211 ++++++++++++++++ .../AlarmEditing/Components/SoundFile.swift | 129 ++++++++++ .../Editors/BuildExpireAlarmEditor.swift | 46 ++++ .../Editors/HighBgAlarmEditor.swift | 35 +++ .../Editors/LowBgAlarmEditor.swift | 35 +++ LoopFollow/Alarm/AlarmListView.swift | 78 ++++++ LoopFollow/Alarm/AlarmManager.swift | 57 +++++ LoopFollow/Alarm/AlarmSettingsView.swift | 188 ++++++++++++++ LoopFollow/Alarm/AlarmType.swift | 96 +++++++ LoopFollow/Alarm/SnoozeState.swift | 14 ++ LoopFollow/Controllers/Nightscout/CAge.swift | 2 +- .../Controllers/Nightscout/NSProfile.swift | 2 + .../Nightscout/ProfileManager.swift | 6 + LoopFollow/Extensions/Binding+Optional.swift | 20 ++ LoopFollow/Extensions/UUID+Identifiable.swift | 13 + LoopFollow/Helpers/NightscoutUtils.swift | 20 ++ LoopFollow/Helpers/TimeOfDay.swift | 26 ++ LoopFollow/Snoozer/SnoozerView.swift | 84 +++++++ LoopFollow/Storage/Observable.swift | 7 + LoopFollow/Storage/Storage.swift | 7 + LoopFollow/Task/AlarmTask.swift | 10 +- .../SettingsViewController.swift | 50 +++- LoopFollowTests/AlwaysTrueCondition.swift | 77 ++++++ .../BuildExpireConditionTests.swift | 51 ++++ 33 files changed, 1835 insertions(+), 7 deletions(-) create mode 100644 LoopFollow/Alarm/Alarm.swift create mode 100644 LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift create mode 100644 LoopFollow/Alarm/AlarmCondition/BuildExpireCondition.swift create mode 100644 LoopFollow/Alarm/AlarmConfiguration.swift create mode 100644 LoopFollow/Alarm/AlarmContext.swift create mode 100644 LoopFollow/Alarm/AlarmData.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift create mode 100644 LoopFollow/Alarm/AlarmListView.swift create mode 100644 LoopFollow/Alarm/AlarmManager.swift create mode 100644 LoopFollow/Alarm/AlarmSettingsView.swift create mode 100644 LoopFollow/Alarm/AlarmType.swift create mode 100644 LoopFollow/Alarm/SnoozeState.swift create mode 100644 LoopFollow/Extensions/Binding+Optional.swift create mode 100644 LoopFollow/Extensions/UUID+Identifiable.swift create mode 100644 LoopFollow/Helpers/TimeOfDay.swift create mode 100644 LoopFollow/Snoozer/SnoozerView.swift create mode 100644 LoopFollowTests/AlwaysTrueCondition.swift create mode 100644 LoopFollowTests/BuildExpireConditionTests.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 5b25e5ffe..a5646a495 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -3,11 +3,13 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ 3F1335F351590E573D8E6962 /* Pods_LoopFollow.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */; }; + DD0247592DB2E89600FCADF6 /* AlarmCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */; }; + DD0247712DB4337700FCADF6 /* BuildExpireCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.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 */; }; @@ -54,6 +56,15 @@ DD493AE52ACF2383009A6922 /* Treatments.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE42ACF2383009A6922 /* Treatments.swift */; }; DD493AE72ACF23CF009A6922 /* DeviceStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE62ACF23CF009A6922 /* DeviceStatus.swift */; }; DD493AE92ACF2445009A6922 /* BGData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE82ACF2445009A6922 /* BGData.swift */; }; + DD4AFB3B2DB55CB600BB593F /* TimeOfDay.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB3A2DB55CB600BB593F /* TimeOfDay.swift */; }; + DD4AFB3D2DB55D2900BB593F /* AlarmConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB3C2DB55D2900BB593F /* AlarmConfiguration.swift */; }; + DD4AFB3F2DB55EA700BB593F /* AlarmContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB3E2DB55EA700BB593F /* AlarmContext.swift */; }; + DD4AFB422DB5655700BB593F /* BuildExpireConditionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB412DB5652400BB593F /* BuildExpireConditionTests.swift */; }; + DD4AFB432DB5655D00BB593F /* AlwaysTrueCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB402DB5651500BB593F /* AlwaysTrueCondition.swift */; }; + DD4AFB492DB576C200BB593F /* AlarmSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB482DB576C200BB593F /* AlarmSettingsView.swift */; }; + DD4AFB612DB68BBC00BB593F /* AlarmListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB602DB68BBC00BB593F /* AlarmListView.swift */; }; + DD4AFB672DB68C5500BB593F /* UUID+Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB662DB68C5500BB593F /* UUID+Identifiable.swift */; }; + DD4AFB6B2DB6BF2A00BB593F /* Binding+Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB6A2DB6BF2A00BB593F /* Binding+Optional.swift */; }; DD50C7502D0828800057AE6F /* ContactSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD50C74F2D0828800057AE6F /* ContactSettingsViewModel.swift */; }; DD50C7532D0828D10057AE6F /* ContactSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD50C7522D0828D10057AE6F /* ContactSettingsView.swift */; }; DD50C7552D0862770057AE6F /* ContactImageUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD50C7542D0862770057AE6F /* ContactImageUpdater.swift */; }; @@ -102,6 +113,10 @@ DDCF979824C1489C002C9752 /* GraphSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */; }; DDCF979A24C14DB4002C9752 /* WatchSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979924C14DB4002C9752 /* WatchSettingsViewController.swift */; }; DDCF979E24C2382A002C9752 /* AppStateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979D24C2382A002C9752 /* AppStateController.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 */; }; + DDCF9A8C2D86005E004DF4DD /* AlarmManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF9A8B2D86005E004DF4DD /* AlarmManager.swift */; }; DDCFCAF22B17273200BE5751 /* LoopFollowDisplayNameConfig.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = DDCFCAF12B17273200BE5751 /* LoopFollowDisplayNameConfig.xcconfig */; }; DDD10EFF2C510C3C00D76A8E /* ObservableUserDefaultsValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD10EFE2C510C3C00D76A8E /* ObservableUserDefaultsValue.swift */; }; DDD10F012C510C6B00D76A8E /* ObservableUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD10F002C510C6B00D76A8E /* ObservableUserDefaults.swift */; }; @@ -285,9 +300,22 @@ FCFEECA2248857A600402A7F /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCFEECA1248857A600402A7F /* SettingsViewController.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + DD0247652DB2EB9A00FCADF6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = FC97880C2485969B00A7906C /* Project object */; + proxyType = 1; + remoteGlobalIDString = FC9788132485969B00A7906C; + remoteInfo = LoopFollow; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ 059B0FA59AABFE72FE13DDDA /* Pods-LoopFollow.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LoopFollow.release.xcconfig"; path = "Target Support Files/Pods-LoopFollow/Pods-LoopFollow.release.xcconfig"; sourceTree = ""; }; A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LoopFollow.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmCondition.swift; sourceTree = ""; }; + DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildExpireCondition.swift; sourceTree = ""; }; + DD0247612DB2EB9A00FCADF6 /* LoopFollowTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LoopFollowTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUtils.swift; sourceTree = ""; }; DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileManager.swift; sourceTree = ""; }; DD0C0C612C4175FD00DBADDF /* NSProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSProfile.swift; sourceTree = ""; }; @@ -333,6 +361,15 @@ DD493AE42ACF2383009A6922 /* Treatments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Treatments.swift; sourceTree = ""; }; DD493AE62ACF23CF009A6922 /* DeviceStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatus.swift; sourceTree = ""; }; DD493AE82ACF2445009A6922 /* BGData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGData.swift; sourceTree = ""; }; + DD4AFB3A2DB55CB600BB593F /* TimeOfDay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeOfDay.swift; sourceTree = ""; }; + DD4AFB3C2DB55D2900BB593F /* AlarmConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmConfiguration.swift; sourceTree = ""; }; + DD4AFB3E2DB55EA700BB593F /* AlarmContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmContext.swift; sourceTree = ""; }; + DD4AFB402DB5651500BB593F /* AlwaysTrueCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlwaysTrueCondition.swift; sourceTree = ""; }; + DD4AFB412DB5652400BB593F /* BuildExpireConditionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildExpireConditionTests.swift; sourceTree = ""; }; + DD4AFB482DB576C200BB593F /* AlarmSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSettingsView.swift; sourceTree = ""; }; + DD4AFB602DB68BBC00BB593F /* AlarmListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmListView.swift; sourceTree = ""; }; + DD4AFB662DB68C5500BB593F /* UUID+Identifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UUID+Identifiable.swift"; sourceTree = ""; }; + DD4AFB6A2DB6BF2A00BB593F /* Binding+Optional.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+Optional.swift"; sourceTree = ""; }; DD50C74F2D0828800057AE6F /* ContactSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactSettingsViewModel.swift; sourceTree = ""; }; DD50C7522D0828D10057AE6F /* ContactSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactSettingsView.swift; sourceTree = ""; }; DD50C7542D0862770057AE6F /* ContactImageUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactImageUpdater.swift; sourceTree = ""; }; @@ -382,6 +419,10 @@ DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphSettingsViewController.swift; sourceTree = ""; }; DDCF979924C14DB4002C9752 /* WatchSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchSettingsViewController.swift; sourceTree = ""; }; DDCF979D24C2382A002C9752 /* AppStateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateController.swift; sourceTree = ""; }; + DDCF9A7F2D85FD09004DF4DD /* Alarm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alarm.swift; sourceTree = ""; }; + DDCF9A812D85FD14004DF4DD /* AlarmType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmType.swift; sourceTree = ""; }; + DDCF9A872D85FD33004DF4DD /* AlarmData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmData.swift; sourceTree = ""; }; + DDCF9A8B2D86005E004DF4DD /* AlarmManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmManager.swift; sourceTree = ""; }; DDCFCAF12B17273200BE5751 /* LoopFollowDisplayNameConfig.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = LoopFollowDisplayNameConfig.xcconfig; sourceTree = ""; }; DDD10EFE2C510C3C00D76A8E /* ObservableUserDefaultsValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableUserDefaultsValue.swift; sourceTree = ""; }; DDD10F002C510C6B00D76A8E /* ObservableUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableUserDefaults.swift; sourceTree = ""; }; @@ -570,7 +611,19 @@ FCFEECA1248857A600402A7F /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + DD4AFB4A2DB684A200BB593F /* AlarmEditing */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = AlarmEditing; sourceTree = ""; }; + DD4AFB6C2DBCDA6B00BB593F /* Snoozer */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Snoozer; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ + DD02475E2DB2EB9A00FCADF6 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; FC9788112485969B00A7906C /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -603,6 +656,24 @@ path = Pods; sourceTree = ""; }; + DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = { + isa = PBXGroup; + children = ( + DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */, + DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */, + ); + path = AlarmCondition; + sourceTree = ""; + }; + DD02476E2DB4321000FCADF6 /* LoopFollowTests */ = { + isa = PBXGroup; + children = ( + DD4AFB412DB5652400BB593F /* BuildExpireConditionTests.swift */, + DD4AFB402DB5651500BB593F /* AlwaysTrueCondition.swift */, + ); + path = LoopFollowTests; + sourceTree = ""; + }; DD0C0C692C4852A100DBADDF /* Metric */ = { isa = PBXGroup; children = ( @@ -769,6 +840,8 @@ DDCF979324C0D380002C9752 /* UIViewExtension.swift */, DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */, DD0C0C632C45A59400DBADDF /* HKUnit+Extensions.swift */, + DD4AFB662DB68C5500BB593F /* UUID+Identifiable.swift */, + DD4AFB6A2DB6BF2A00BB593F /* Binding+Optional.swift */, ); path = Extensions; sourceTree = ""; @@ -803,6 +876,23 @@ path = Scripts; sourceTree = ""; }; + DDCF9A7E2D85FCE6004DF4DD /* Alarm */ = { + isa = PBXGroup; + children = ( + DD4AFB602DB68BBC00BB593F /* AlarmListView.swift */, + DD4AFB4A2DB684A200BB593F /* AlarmEditing */, + DD4AFB482DB576C200BB593F /* AlarmSettingsView.swift */, + DD4AFB3E2DB55EA700BB593F /* AlarmContext.swift */, + DD4AFB3C2DB55D2900BB593F /* AlarmConfiguration.swift */, + DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */, + DDCF9A8B2D86005E004DF4DD /* AlarmManager.swift */, + DDCF9A872D85FD33004DF4DD /* AlarmData.swift */, + DDCF9A812D85FD14004DF4DD /* AlarmType.swift */, + DDCF9A7F2D85FD09004DF4DD /* Alarm.swift */, + ); + path = Alarm; + sourceTree = ""; + }; DDDF6F412D479A8E00884336 /* Loop */ = { isa = PBXGroup; children = ( @@ -1044,6 +1134,8 @@ FC8DEEE32485D1680075863F /* LoopFollow */ = { isa = PBXGroup; children = ( + DD4AFB6C2DBCDA6B00BB593F /* Snoozer */, + DDCF9A7E2D85FCE6004DF4DD /* Alarm */, DD1A97122D429495000DDC11 /* Settings */, DD2C2E522D3C36A8006413A5 /* Dexcom */, DD2C2E4D2D3B8ACF006413A5 /* Nightscout */, @@ -1078,6 +1170,7 @@ FC3AE7B3249E8E0E00AAE1E0 /* LoopFollow.xcdatamodeld */, FC5A5C3C2497B229009C550E /* Config.xcconfig */, FC8DEEE32485D1680075863F /* LoopFollow */, + DD02476E2DB4321000FCADF6 /* LoopFollowTests */, FC9788152485969B00A7906C /* Products */, 8E32230C453C93FDCE59C2B9 /* Pods */, 6A5880E0B811AF443B05AB02 /* Frameworks */, @@ -1088,6 +1181,7 @@ isa = PBXGroup; children = ( FC9788142485969B00A7906C /* Loop Follow.app */, + DD0247612DB2EB9A00FCADF6 /* LoopFollowTests.xctest */, ); name = Products; sourceTree = ""; @@ -1112,6 +1206,7 @@ FCC688542489367300A0279D /* Helpers */ = { isa = PBXGroup; children = ( + DD4AFB3A2DB55CB600BB593F /* TimeOfDay.swift */, DD7B0D432D730A320063DCB6 /* CycleHelper.swift */, DDF6999C2C5AAA4C0058A8D9 /* Views */, FCC6886E2489A53800A0279D /* AppConstants.swift */, @@ -1155,6 +1250,26 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + DD0247602DB2EB9A00FCADF6 /* LoopFollowTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = DD0247672DB2EB9A00FCADF6 /* Build configuration list for PBXNativeTarget "LoopFollowTests" */; + buildPhases = ( + DD02475D2DB2EB9A00FCADF6 /* Sources */, + DD02475E2DB2EB9A00FCADF6 /* Frameworks */, + DD02475F2DB2EB9A00FCADF6 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + DD0247662DB2EB9A00FCADF6 /* PBXTargetDependency */, + ); + name = LoopFollowTests; + packageProductDependencies = ( + ); + productName = LoopFollowTests; + productReference = DD0247612DB2EB9A00FCADF6 /* LoopFollowTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; FC9788132485969B00A7906C /* LoopFollow */ = { isa = PBXNativeTarget; buildConfigurationList = FC97882D2485969C00A7906C /* Build configuration list for PBXNativeTarget "LoopFollow" */; @@ -1170,6 +1285,9 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + DD4AFB4A2DB684A200BB593F /* AlarmEditing */, + ); name = LoopFollow; packageProductDependencies = ( DD48781B2C7DAF140048F05C /* SwiftJWT */, @@ -1184,10 +1302,14 @@ FC97880C2485969B00A7906C /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1150; + LastSwiftUpdateCheck = 1630; LastUpgradeCheck = 1140; ORGANIZATIONNAME = "Jon Fawcett"; TargetAttributes = { + DD0247602DB2EB9A00FCADF6 = { + CreatedOnToolsVersion = 16.3; + TestTargetID = FC9788132485969B00A7906C; + }; FC9788132485969B00A7906C = { CreatedOnToolsVersion = 11.4.1; }; @@ -1210,11 +1332,19 @@ projectRoot = ""; targets = ( FC9788132485969B00A7906C /* LoopFollow */, + DD0247602DB2EB9A00FCADF6 /* LoopFollowTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + DD02475F2DB2EB9A00FCADF6 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; FC9788122485969B00A7906C /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1356,10 +1486,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks.sh\"\n"; @@ -1408,6 +1542,15 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + DD02475D2DB2EB9A00FCADF6 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DD4AFB432DB5655D00BB593F /* AlwaysTrueCondition.swift in Sources */, + DD4AFB422DB5655700BB593F /* BuildExpireConditionTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; FC9788102485969B00A7906C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1416,13 +1559,17 @@ FCC68850248935D800A0279D /* AlarmViewController.swift in Sources */, DD48781E2C7DAF2F0048F05C /* PushNotificationManager.swift in Sources */, DD2C2E562D3C3917006413A5 /* DexcomSettingsView.swift in Sources */, + DD4AFB672DB68C5500BB593F /* UUID+Identifiable.swift in Sources */, DD9ED0CA2D355257000D2A63 /* LogView.swift in Sources */, DD9ACA082D32F68B00415D8A /* BGTask.swift in Sources */, DD9ACA102D34129200415D8A /* Task.swift in Sources */, + DD4AFB492DB576C200BB593F /* AlarmSettingsView.swift in Sources */, DD4878202C7DAF890048F05C /* PushMessage.swift in Sources */, FC7CE59F248D8D23001F83B8 /* SnoozeViewController.swift in Sources */, + DD0247712DB4337700FCADF6 /* BuildExpireCondition.swift in Sources */, DDF6999B2C5AA32E0058A8D9 /* TempTargetPreset.swift in Sources */, DD9ACA0E2D340BFF00415D8A /* AlarmTask.swift in Sources */, + DDCF9A822D85FD15004DF4DD /* AlarmType.swift in Sources */, DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */, FCC6886724898F8000A0279D /* UserDefaultsValue.swift in Sources */, DDF699942C555B310058A8D9 /* ViewControllerManager.swift in Sources */, @@ -1439,6 +1586,7 @@ DDF2C0102BEFA991007A20E6 /* GitHubService.swift in Sources */, FC16A97A24996673003D6245 /* NightScout.swift in Sources */, DD07B5C929E2F9C400C6A635 /* NightscoutUtils.swift in Sources */, + DDCF9A8C2D86005E004DF4DD /* AlarmManager.swift in Sources */, FCC6886924898FB100A0279D /* UserDefaultsValueGroups.swift in Sources */, DDF2C0122BEFB733007A20E6 /* AppVersionManager.swift in Sources */, DD7E19862ACDA59700DBD158 /* BGCheck.swift in Sources */, @@ -1460,11 +1608,13 @@ DDD10F0B2C54192A00D76A8E /* TemporaryTarget.swift in Sources */, DDDF6F472D479AD200884336 /* LoopOverrideViewModel.swift in Sources */, DD5334272C61668800062F9D /* InfoDisplaySettingsViewModel.swift in Sources */, + DD0247592DB2E89600FCADF6 /* AlarmCondition.swift in Sources */, DD48780E2C7B74A40048F05C /* TrioRemoteControlViewModel.swift in Sources */, DDEF503A2D31615000999A5D /* LogManager.swift in Sources */, DD4878172C7B75350048F05C /* BolusView.swift in Sources */, DD493AE72ACF23CF009A6922 /* DeviceStatus.swift in Sources */, FCFEECA2248857A600402A7F /* SettingsViewController.swift in Sources */, + DD4AFB612DB68BBC00BB593F /* AlarmListView.swift in Sources */, DD7B0D442D730A3B0063DCB6 /* CycleHelper.swift in Sources */, DD9ACA0C2D33BB8600415D8A /* CalendarTask.swift in Sources */, DD13BC792C3FE63A0062313B /* InfoManager.swift in Sources */, @@ -1494,6 +1644,7 @@ DD1A97142D4294A5000DDC11 /* AdvancedSettingsView.swift in Sources */, DD9ACA0A2D33095600415D8A /* MinAgoTask.swift in Sources */, DD9ED0CC2D35526E000D2A63 /* SearchBar.swift in Sources */, + DD4AFB3F2DB55EA700BB593F /* AlarmContext.swift in Sources */, DDD10EFF2C510C3C00D76A8E /* ObservableUserDefaultsValue.swift in Sources */, DD58171A2D299EF80041FB98 /* DexcomHeartbeatBluetoothDevice.swift in Sources */, DDFF3D872D14280500BF9D9E /* BackgroundRefreshSettingsViewModel.swift in Sources */, @@ -1509,19 +1660,23 @@ FC9788182485969B00A7906C /* AppDelegate.swift in Sources */, DDD10F072C529DE800D76A8E /* Observable.swift in Sources */, DDFF3D852D14279B00BF9D9E /* BackgroundRefreshSettingsView.swift in Sources */, + DDCF9A882D85FD33004DF4DD /* AlarmData.swift in Sources */, DD608A0C2C27415C00F91132 /* BackgroundAlertManager.swift in Sources */, DD50C7532D0828D10057AE6F /* ContactSettingsView.swift in Sources */, DD4878082C7B30BF0048F05C /* RemoteSettingsView.swift in Sources */, + DD4AFB3B2DB55CB600BB593F /* TimeOfDay.swift in Sources */, DDEF503C2D31BE2D00999A5D /* TaskScheduler.swift in Sources */, DD0C0C6B2C48562000DBADDF /* InsulinMetric.swift in Sources */, DD493AD92ACF2171009A6922 /* Carbs.swift in Sources */, DD493AE92ACF2445009A6922 /* BGData.swift in Sources */, FCC6886B24898FD800A0279D /* ObservationToken.swift in Sources */, + DD4AFB6B2DB6BF2A00BB593F /* Binding+Optional.swift in Sources */, DD608A082C1F584900F91132 /* DeviceStatusLoop.swift in Sources */, DD9ACA062D32AF7900415D8A /* TreatmentsTask.swift in Sources */, DD98F54424BCEFEE0007425A /* ShareClientExtension.swift in Sources */, DD9ED0CE2D35587A000D2A63 /* LogEntry.swift in Sources */, DD0C0C602C415B9D00DBADDF /* ProfileManager.swift in Sources */, + DD4AFB3D2DB55D2900BB593F /* AlarmConfiguration.swift in Sources */, DDF699962C5582290058A8D9 /* TextFieldWithToolBar.swift in Sources */, DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */, DDDF6F452D479AB100884336 /* LoopOverrideView.swift in Sources */, @@ -1550,6 +1705,7 @@ DDCF979A24C14DB4002C9752 /* WatchSettingsViewController.swift in Sources */, DD493AE52ACF2383009A6922 /* Treatments.swift in Sources */, DD50C7552D0862770057AE6F /* ContactImageUpdater.swift in Sources */, + DDCF9A802D85FD0B004DF4DD /* Alarm.swift in Sources */, DDD10F012C510C6B00D76A8E /* ObservableUserDefaults.swift in Sources */, DD16AF0D2C98485400FB655A /* SecureStorageValue.swift in Sources */, DD2C2E4F2D3B8AF1006413A5 /* NightscoutSettingsView.swift in Sources */, @@ -1567,6 +1723,14 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + DD0247662DB2EB9A00FCADF6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = FC9788132485969B00A7906C /* LoopFollow */; + targetProxy = DD0247652DB2EB9A00FCADF6 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ FC97881F2485969B00A7906C /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -1587,6 +1751,59 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + DD0247682DB2EB9A00FCADF6 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.--unique-id-.LoopFollowTests--app-suffix-.LoopFollowTests"; + PRODUCT_MODULE_NAME = LoopFollowTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Loop Follow.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Loop Follow"; + }; + name = Debug; + }; + DD0247692DB2EB9A00FCADF6 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.--unique-id-.LoopFollowTests--app-suffix-.LoopFollowTests"; + PRODUCT_MODULE_NAME = LoopFollowTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Loop Follow.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Loop Follow"; + }; + name = Release; + }; FC97882B2485969C00A7906C /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = FC5A5C3C2497B229009C550E /* Config.xcconfig */; @@ -1622,6 +1839,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -1642,6 +1860,7 @@ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; + PRODUCT_MODULE_NAME = LoopFollow; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -1683,6 +1902,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -1696,6 +1916,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 15.6; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; + PRODUCT_MODULE_NAME = LoopFollow; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; @@ -1707,10 +1928,10 @@ isa = XCBuildConfiguration; baseConfigurationReference = ECA3EFB4037410B4973BB632 /* Pods-LoopFollow.debug.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "LoopFollow/Loop Follow.entitlements"; CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; INFOPLIST_FILE = LoopFollow/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -1729,10 +1950,10 @@ isa = XCBuildConfiguration; baseConfigurationReference = 059B0FA59AABFE72FE13DDDA /* Pods-LoopFollow.release.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = "LoopFollow/Loop Follow.entitlements"; CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; INFOPLIST_FILE = LoopFollow/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -1750,6 +1971,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + DD0247672DB2EB9A00FCADF6 /* Build configuration list for PBXNativeTarget "LoopFollowTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DD0247682DB2EB9A00FCADF6 /* Debug */, + DD0247692DB2EB9A00FCADF6 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; FC97880F2485969B00A7906C /* Build configuration list for PBXProject "LoopFollow" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/LoopFollow.xcworkspace/xcshareddata/xcschemes/LoopFollow.xcscheme b/LoopFollow.xcworkspace/xcshareddata/xcschemes/LoopFollow.xcscheme index ea95fcdf1..e8ee3d9e4 100644 --- a/LoopFollow.xcworkspace/xcshareddata/xcschemes/LoopFollow.xcscheme +++ b/LoopFollow.xcworkspace/xcshareddata/xcschemes/LoopFollow.xcscheme @@ -28,6 +28,19 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" shouldAutocreateTestPlan = "YES"> + + + + + + Bool { + return false + } + + func trigger() { + // TODO: play sound / update UI / schedule snooze etc. + print("🔔 Alarm “\(name)” triggered! Playing \(soundFile.displayName)") + } + + init(type: AlarmType) { + self.type = type + self.name = type.rawValue + + switch type { + case .buildExpire: + /// Alert 7 days before the build expires + self.threshold = 7 + self.soundFile = .wrongAnswer + self.snoozeDuration = 1 + self.repeatSoundOption = .always + case .low: + soundFile = .indeed + case .iob: + soundFile = .alertToneRingtone1 + case .bolus: + soundFile = .dholShuffleloop + case .cob: + soundFile = .alertToneRingtone2 + case .high: + soundFile = .timeHasCome + case .fastDrop: + soundFile = .bigClockTicking + case .fastRise: + soundFile = .cartoonFailStringsTrumpet + case .missedReading: + soundFile = .cartoonTipToeSneakyWalk + case .notLooping: + soundFile = .sciFiEngineShutDown + case .missedBolus: + soundFile = .dholShuffleloop + case .sensorChange: + soundFile = .wakeUpWillYou + case .pumpChange: + soundFile = .wakeUpWillYou + case .pump: + soundFile = .marimbaDescend + case .battery: + soundFile = .machineCharge + case .batteryDrop: + soundFile = .machineCharge + case .recBolus: + soundFile = .dholShuffleloop + case .overrideStart: + soundFile = .endingReached + case .overrideEnd: + soundFile = .alertToneBusy + case .tempTargetStart: + soundFile = .endingReached + case .tempTargetEnd: + soundFile = .alertToneBusy + } + } +} diff --git a/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift b/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift new file mode 100644 index 000000000..4034d06cb --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift @@ -0,0 +1,49 @@ +// +// AlarmCondition.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-04-18. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import Foundation + +protocol AlarmCondition { + static var type: AlarmType { get } + init() + /// pure, per-alarm logic against `AlarmData` + func evaluate(alarm: Alarm, data: AlarmData) -> Bool +} + +extension AlarmCondition { + /// applies every global & per-alarm guard exactly once + func shouldFire(alarm: Alarm, data: AlarmData, context: AlarmContext) -> Bool { + // master on/off + guard alarm.isEnabled else { return false } + // global mute + if let until = context.config.muteUntil, until > context.now { return false } + // per-alarm snooze + if let snooze = alarm.snoozedUntil, snooze > context.now { return false } + + // time-of-day guard + let comps = Calendar.current.dateComponents([.hour, .minute], from: context.now) + let nowMin = (comps.hour! * 60) + comps.minute! + let dStart = context.config.dayStart.minutesSinceMidnight + let nStart = context.config.nightStart.minutesSinceMidnight + let isNight = (nowMin < dStart) || (nowMin >= nStart) + + switch alarm.activeOption { + case .always: + break + case .day: + // only fire in day + guard !isNight else { return false } + case .night: + // only fire in night + guard isNight else { return false } + } + + // finally, run the type-specific logic + return evaluate(alarm: alarm, data: data) + } +} diff --git a/LoopFollow/Alarm/AlarmCondition/BuildExpireCondition.swift b/LoopFollow/Alarm/AlarmCondition/BuildExpireCondition.swift new file mode 100644 index 000000000..6b28c8e53 --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/BuildExpireCondition.swift @@ -0,0 +1,24 @@ +// +// ExpireCondition.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-04-18. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import Foundation + +struct BuildExpireCondition: AlarmCondition { + static let type: AlarmType = .buildExpire + init() {} + + func evaluate(alarm: Alarm, data: AlarmData) -> Bool { + guard let expiry = data.expireDate else { return false } + guard let thresholdDays = alarm.threshold else { return false } + + let thresholdSeconds = TimeInterval(thresholdDays) * TimeUnit.day.seconds + let thresholdDate = expiry.addingTimeInterval(-thresholdSeconds) + + return Date() >= thresholdDate + } +} diff --git a/LoopFollow/Alarm/AlarmConfiguration.swift b/LoopFollow/Alarm/AlarmConfiguration.swift new file mode 100644 index 000000000..93bda3ecf --- /dev/null +++ b/LoopFollow/Alarm/AlarmConfiguration.swift @@ -0,0 +1,34 @@ +// AlarmConfiguration.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025‑04‑20. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import Foundation + +struct AlarmConfiguration: Codable, Equatable { + // MARK: Core + var snoozeUntil: Date? + var muteUntil: Date? + var dayStart: TimeOfDay + var nightStart: TimeOfDay + + // MARK: System audio overrides + var overrideSystemOutputVolume: Bool + var forcedOutputVolume: Float // 0 … 1 + var audioDuringCalls: Bool + var ignoreZeroBG: Bool + var autoSnoozeCGMStart: Bool + + static let `default` = AlarmConfiguration( + muteUntil: nil, + dayStart: TimeOfDay(hour: 6, minute: 0), + nightStart: TimeOfDay(hour: 22, minute: 0), + overrideSystemOutputVolume: true, + forcedOutputVolume: 0.5, + audioDuringCalls: true, + ignoreZeroBG: true, + autoSnoozeCGMStart: false, + ) +} diff --git a/LoopFollow/Alarm/AlarmContext.swift b/LoopFollow/Alarm/AlarmContext.swift new file mode 100644 index 000000000..27cb93fae --- /dev/null +++ b/LoopFollow/Alarm/AlarmContext.swift @@ -0,0 +1,15 @@ +// +// AlarmContext.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-04-20. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + + +import Foundation + +struct AlarmContext { + let now: Date + let config: AlarmConfiguration +} diff --git a/LoopFollow/Alarm/AlarmData.swift b/LoopFollow/Alarm/AlarmData.swift new file mode 100644 index 000000000..ecc1ab9fd --- /dev/null +++ b/LoopFollow/Alarm/AlarmData.swift @@ -0,0 +1,23 @@ +// +// AlarmData.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-03-15. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import Foundation + +struct AlarmData { +// let bgReadings: [ShareGlucoseData] +// let iob: Double? +// let cob: Double? +// let predictionData: [ShareGlucoseData] +// let latestBoluses: [BolusEntry] +// let batteryLevel: Double? +// let latestCarbs: [CarbEntry] +// let overrideData: [OverrideEntry] +// let tempTargetData: [TempTargetEntry] +// let pumpVolume: Double? + let expireDate: Date? +} diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift new file mode 100644 index 000000000..d0d84fcee --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -0,0 +1,28 @@ +// +// AlarmEditor.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-04-21. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + + +import SwiftUI + +struct AlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + switch alarm.type { + case .buildExpire: + BuildExpireAlarmEditor(alarm: $alarm) + case .high: + HighBgAlarmEditor(alarm: $alarm) + case .low: + LowBgAlarmEditor(alarm: $alarm) + default: + Text("No editor for \(alarm.type.rawValue)") + .padding() + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift new file mode 100644 index 000000000..6c91caf02 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift @@ -0,0 +1,211 @@ +// +// AlarmEditorFields.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-04-21. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import SwiftUI + +struct AlarmGeneralSection: View { + @Binding var alarm: Alarm + + var body: some View { + Section(header: Text("General")) { + HStack { + Text("Name") + TextField("Alarm Name", text: $alarm.name) + .multilineTextAlignment(.trailing) + .textFieldStyle(.plain) + } + Toggle("Enabled", isOn: $alarm.isEnabled) + } + } +} + +struct AlarmThresholdSection: View { + let title: String + let range: ClosedRange + let step: Double + let unitLabel: String + @Binding var value: Double + + var body: some View { + Section( + header: Text(title), + footer: Text("\(title) in \(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 + let step: Double + let unitLabel: String + @Binding var value: Double + + var body: some View { + Section( + header: Text(title), + footer: Text("How long to snooze after firing \(Int(range.lowerBound))–\(Int(range.upperBound)) \(unitLabel)") + ) { + Stepper( + "\(title): \(Int(value)) \(unitLabel)", + value: $value, + in: range, + step: step + ) + } + } +} + +import SwiftUI + +struct AlarmAudioSection: View { + @Binding var alarm: Alarm + @State private var showingTonePicker = false + + var body: some View { + Section(header: Text("Alert Sound")) { + // ——— Tone Row ——— + Button { + showingTonePicker = true + } label: { + HStack { + Text("Tone") + Spacer() + Text(alarm.soundFile.displayName) + .foregroundColor(.secondary) + Image(systemName: "chevron.right") + .foregroundColor(.secondary) + } + } + .sheet(isPresented: $showingTonePicker) { + NavigationView { + List { + ForEach(SoundFile.allCases) { tone in + Button { + alarm.soundFile = tone + // play test tone + AlarmSound.setSoundFile(str: tone.rawValue) + AlarmSound.stop() + AlarmSound.playTest() + } label: { + HStack { + Text(tone.displayName) + if alarm.soundFile == tone { + Spacer() + Image(systemName: "checkmark") + .foregroundColor(.accentColor) + } + } + } + } + } + .navigationTitle("Choose Tone") + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Done") { + AlarmSound.stop() + showingTonePicker = false + } + } + } + } + } + + // ——— Play / Repeat Toggles ——— + VStack(alignment: .leading, spacing: 8) { + Text("Play") + .font(.subheadline) + .foregroundColor(.secondary) + + Picker("", selection: $alarm.playSoundOption) { + ForEach(PlaySoundOption.allCases, id: \.self) { opt in + Text(opt.rawValue.capitalized).tag(opt) + } + } + .pickerStyle(.segmented) + } + + VStack(alignment: .leading, spacing: 8) { + Text("Repeat") + .font(.subheadline) + .foregroundColor(.secondary) + + Picker("", selection: $alarm.repeatSoundOption) { + ForEach(RepeatSoundOption.allCases, id: \.self) { opt in + Text(opt.rawValue.capitalized).tag(opt) + } + } + .pickerStyle(.segmented) + } + } + } +} + +struct AlarmActiveSection: View { + @Binding var alarm: Alarm + + var body: some View { + Section(header: Text("Active During")) { + Picker("Active", selection: $alarm.activeOption) { + Text("Always").tag(ActiveOption.always) + Text("Day").tag(ActiveOption.day) + Text("Night").tag(ActiveOption.night) + } + .pickerStyle(.segmented) + } + } +} + +struct AlarmSnoozedUntilSection: View { + @Binding var alarm: Alarm + + private var isSnoozed: Binding { + Binding( + get: { + if let until = alarm.snoozedUntil, until > Date() { + return true + } + return false + }, + set: { on in + if on { + // keep existing future snooze or set default ahead + if let until = alarm.snoozedUntil, until > Date() { + alarm.snoozedUntil = until + } else { + let secs = alarm.type.timeUnit.seconds + alarm.snoozedUntil = Date().addingTimeInterval(Double(alarm.snoozeDuration) * secs) + } + } else { + alarm.snoozedUntil = nil + } + } + ) + } + + var body: some View { + Section(header: Text("Snoozed Until")) { + Toggle("Snoozed", isOn: isSnoozed) + if isSnoozed.wrappedValue, let until = alarm.snoozedUntil { + DatePicker("Until", selection: Binding( + get: { until }, + set: { alarm.snoozedUntil = $0 } + ), displayedComponents: [.date, .hourAndMinute]) + } + } + } +} + + diff --git a/LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift b/LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift new file mode 100644 index 000000000..c2020b0d6 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift @@ -0,0 +1,129 @@ +// +// File.swift +// SoundFile +// +// Created by Jonas Björkert on 2025-04-21. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import Foundation + +enum SoundFile: String, CaseIterable, Identifiable, Codable { + case alarmBuzzer = "Alarm_Buzzer" + case alarmClock = "Alarm_Clock" + case alertToneBusy = "Alert_Tone_Busy" + case alertToneRingtone1 = "Alert_Tone_Ringtone_1" + case alertToneRingtone2 = "Alert_Tone_Ringtone_2" + case alienSiren = "Alien_Siren" + case ambulance = "Ambulance" + case analogWatchAlarm = "Analog_Watch_Alarm" + case bigClockTicking = "Big_Clock_Ticking" + case burglarAlarmSiren1 = "Burglar_Alarm_Siren_1" + case burglarAlarmSiren2 = "Burglar_Alarm_Siren_2" + case cartoonAscendClimbSneaky = "Cartoon_Ascend_Climb_Sneaky" + case cartoonAscendThenDescend = "Cartoon_Ascend_Then_Descend" + case cartoonBounceToCeiling = "Cartoon_Bounce_To_Ceiling" + case cartoonDreamyGlissandoHarp = "Cartoon_Dreamy_Glissando_Harp" + case cartoonFailStringsTrumpet = "Cartoon_Fail_Strings_Trumpet" + case cartoonMachineClumsyLoop = "Cartoon_Machine_Clumsy_Loop" + case cartoonSiren = "Cartoon_Siren" + case cartoonTipToeSneakyWalk = "Cartoon_Tip_Toe_Sneaky_Walk" + case cartoonUhOh = "Cartoon_Uh_Oh" + case cartoonVillainHorns = "Cartoon_Villain_Horns" + case cellPhoneRingTone = "Cell_Phone_Ring_Tone" + case chimesGlassy = "Chimes_Glassy" + case computerMagic = "Computer_Magic" + case csfx2Alarm = "CSFX-2_Alarm" + case cuckooClock = "Cuckoo_Clock" + case dholShuffleloop = "Dhol_Shuffleloop" + case discreet = "Discreet" + case earlySunrise = "Early_Sunrise" + case emergencyAlarmCarbonMonoxide = "Emergency_Alarm_Carbon_Monoxide" + case emergencyAlarmSiren = "Emergency_Alarm_Siren" + case emergencyAlarm = "Emergency_Alarm" + case endingReached = "Ending_Reached" + case fly = "Fly" + case ghostHover = "Ghost_Hover" + case goodMorning = "Good_Morning" + case hellYeahSomewhatCalmer = "Hell_Yeah_Somewhat_Calmer" + case inAHurry = "In_A_Hurry" + case indeed = "Indeed" + case insistently = "Insistently" + case jingleAllTheWay = "Jingle_All_The_Way" + case laserShoot = "Laser_Shoot" + case machineCharge = "Machine_Charge" + case magicalTwinkle = "Magical_Twinkle" + case marchingHeavyFootedFatElephants = "Marching_Heavy_Footed_Fat_Elephants" + case marimbaDescend = "Marimba_Descend" + case marimbaFlutterOrShake = "Marimba_Flutter_or_Shake" + case martianGun = "Martian_Gun" + case martianScanner = "Martian_Scanner" + case metallic = "Metallic" + case nightguard = "Nightguard" + case notKiddin = "Not_Kiddin" + case openYourEyesAndSee = "Open_Your_Eyes_And_See" + case orchestralHorns = "Orchestral_Horns" + case oringz = "Oringz" + case pagerBeeps = "Pager_Beeps" + case remembersMeOfAsia = "Remembers_Me_Of_Asia" + case riseAndShine = "Rise_And_Shine" + case rush = "Rush" + case sciFiAirRaidAlarm = "Sci-Fi_Air_Raid_Alarm" + case sciFiAlarmLoop1 = "Sci-Fi_Alarm_Loop_1" + case sciFiAlarmLoop2 = "Sci-Fi_Alarm_Loop_2" + case sciFiAlarmLoop3 = "Sci-Fi_Alarm_Loop_3" + case sciFiAlarmLoop4 = "Sci-Fi_Alarm_Loop_4" + case sciFiAlarm = "Sci-Fi_Alarm" + case sciFiComputerConsoleAlarm = "Sci-Fi_Computer_Console_Alarm" + case sciFiConsoleAlarm = "Sci-Fi_Console_Alarm" + case sciFiEerieAlarm = "Sci-Fi_Eerie_Alarm" + case sciFiEngineShutDown = "Sci-Fi_Engine_Shut_Down" + case sciFiIncomingMessageAlert = "Sci-Fi_Incoming_Message_Alert" + case sciFiSpaceshipMessage = "Sci-Fi_Spaceship_Message" + case sciFiSpaceshipWarmUp = "Sci-Fi_Spaceship_Warm_Up" + case sciFiWarning = "Sci-Fi_Warning" + case signatureCorporate = "Signature_Corporate" + case siriAlertCalibrationNeeded = "Siri_Alert_Calibration_Needed" + case siriAlertDeviceMuted = "Siri_Alert_Device_Muted" + case siriAlertGlucoseDroppingFast = "Siri_Alert_Glucose_Dropping_Fast" + case siriAlertGlucoseRisingFast = "Siri_Alert_Glucose_Rising_Fast" + case siriAlertHighGlucose = "Siri_Alert_High_Glucose" + case siriAlertLowGlucose = "Siri_Alert_Low_Glucose" + case siriAlertMissedReadings = "Siri_Alert_Missed_Readings" + case siriAlertTransmitterBatteryLow = "Siri_Alert_Transmitter_Battery_Low" + case siriAlertUrgentHighGlucose = "Siri_Alert_Urgent_High_Glucose" + case siriAlertUrgentLowGlucose = "Siri_Alert_Urgent_Low_Glucose" + case siriCalibrationNeeded = "Siri_Calibration_Needed" + case siriDeviceMuted = "Siri_Device_Muted" + case siriGlucoseDroppingFast = "Siri_Glucose_Dropping_Fast" + case siriGlucoseRisingFast = "Siri_Glucose_Rising_Fast" + case siriHighGlucose = "Siri_High_Glucose" + case siriLowGlucose = "Siri_Low_Glucose" + case siriMissedReadings = "Siri_Missed_Readings" + case siriTransmitterBatteryLow = "Siri_Transmitter_Battery_Low" + case siriUrgentHighGlucose = "Siri_Urgent_High_Glucose" + case siriUrgentLowGlucose = "Siri_Urgent_Low_Glucose" + case softMarimbaPadPositive = "Soft_Marimba_Pad_Positive" + case softWarmAiryOptimistic = "Soft_Warm_Airy_Optimistic" + case softWarmAiryReassuring = "Soft_Warm_Airy_Reassuring" + case storeDoorChime = "Store_Door_Chime" + case sunny = "Sunny" + case thunderSoundFX = "Thunder_Sound_FX" + case timeHasCome = "Time_Has_Come" + case tornadoSiren = "Tornado_Siren" + case twoTurtleDoves = "Two_Turtle_Doves" + case unpaved = "Unpaved" + case wakeUpWillYou = "Wake_Up_Will_You" + case winGain = "Win_Gain" + case wrongAnswer = "Wrong_Answer" + + // Identifiable conformance + var id: SoundFile { self } + + /// Human-friendly name (spaces instead of underscores) + var displayName: String { + rawValue + .replacingOccurrences(of: "_", with: " ") + .replacingOccurrences(of: " ", with: " ") + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift new file mode 100644 index 000000000..7b036c5d4 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift @@ -0,0 +1,46 @@ +// +// BuildExpireAlarmEditor.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-04-21. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import SwiftUI + +struct BuildExpireAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Form { + AlarmGeneralSection(alarm: $alarm) + + AlarmThresholdSection( + title: "Expires In", + range: 1...14, + step: 1, + unitLabel: alarm.type.timeUnit.label, + value: Binding( + get: { Double(alarm.threshold ?? 1) }, + set: { alarm.threshold = Float($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) } + ) + ) + + AlarmAudioSection(alarm: $alarm) + AlarmActiveSection(alarm: $alarm) + AlarmSnoozedUntilSection(alarm: $alarm) + } + .navigationTitle(alarm.type.rawValue) + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift new file mode 100644 index 000000000..a22a16486 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift @@ -0,0 +1,35 @@ +// +// HighBgAlarmEditor.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-04-21. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + + +import SwiftUI + +struct HighBgAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Form {/* + AlarmNameField(alarm: $alarm) + EnabledToggle(alarm: $alarm) + ValueStepper( + title: "BG Above", + value: Binding( + get: { Double(alarm.threshold ?? 0) }, + set: { alarm.threshold = Float($0) } + ), + range: 0...500, step: 1, + formatter: { "\(Int($0))" } + ) + DayNightToggle(alarm: $alarm) + SoundPicker(alarm: $alarm) + SnoozeDatePicker(alarm: $alarm) + SnoozeDurationStepper(alarm: $alarm)*/ + } + .navigationTitle("High BG Alert") + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift new file mode 100644 index 000000000..8995c4052 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift @@ -0,0 +1,35 @@ +// +// LowBgAlarmEditor.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-04-21. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + + +import SwiftUI + +struct LowBgAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Form {/* + AlarmNameField(alarm: $alarm) + EnabledToggle(alarm: $alarm) + ValueStepper( + title: "BG Below", + value: Binding( + get: { Double(alarm.threshold ?? 0) }, + set: { alarm.threshold = Float($0) } + ), + range: 0...500, step: 1, + formatter: { "\(Int($0))" } + ) + DayNightToggle(alarm: $alarm) + SoundPicker(alarm: $alarm) + SnoozeDatePicker(alarm: $alarm) + SnoozeDurationStepper(alarm: $alarm)*/ + } + .navigationTitle("Low BG Alert") + } +} diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift new file mode 100644 index 000000000..5d930e9ab --- /dev/null +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -0,0 +1,78 @@ +// +// AlarmListView.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-04-21. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + + +import SwiftUI + +/// Displays all configured alarms and allows adding a new one by selecting its type. +struct AlarmListView: View { + @ObservedObject private var store = Storage.shared.alarms + @Environment(\.presentationMode) var presentationMode + @State private var showingTypePicker = false + @State private var editingAlarmID: UUID? + + var body: some View { + NavigationView { + List { + ForEach(store.value) { alarm in + NavigationLink(alarm.name) { + AlarmEditor(alarm: binding(for: alarm)) + } + } + .onDelete { idxs in + store.value.remove(atOffsets: idxs) + } + } + .navigationTitle("Alarms") + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button { + showingTypePicker = true + } label: { + Image(systemName: "plus") + } + } + ToolbarItem(placement: .navigationBarTrailing) { + Button("Done") { + presentationMode.wrappedValue.dismiss() + } + } + } + // Step 1: pick a type for the new alarm + .actionSheet(isPresented: $showingTypePicker) { + ActionSheet( + title: Text("Select Alarm Type"), + buttons: AlarmType.allCases.map { type in + .default(Text(type.rawValue)) { + let newAlarm = Alarm(type: type) + store.value.append(newAlarm) + editingAlarmID = newAlarm.id + } + } + [.cancel()] + ) + } + // Step 2: when an ID is set, present the editor + .sheet(item: $editingAlarmID) { id in + if let idx = store.value.firstIndex(where: { $0.id == id }) { + AlarmEditor(alarm: $store.value[idx]) + } else { + Text("Alarm not found") + .padding() + } + } + } + } + + /// Find and return a binding to the given alarm in the store + private func binding(for alarm: Alarm) -> Binding { + guard let idx = store.value.firstIndex(where: { $0.id == alarm.id }) else { + fatalError("Alarm not found in store") + } + return $store.value[idx] + } +} diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift new file mode 100644 index 000000000..dcc1d0cd0 --- /dev/null +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -0,0 +1,57 @@ +// +// AlarmManager.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-03-15. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import Foundation + +class AlarmManager { + static let shared = AlarmManager() + + private let evaluators: [AlarmType: AlarmCondition] + private let config: AlarmConfiguration + + private init( + config: AlarmConfiguration = .default, + conditionTypes: [AlarmCondition.Type] = [ + BuildExpireCondition.self + // …add your other condition types here + ] + ) { + self.config = config + var dict = [AlarmType: AlarmCondition]() + conditionTypes.forEach { dict[$0.type] = $0.init() } + evaluators = dict + } + + func checkAlarms(data: AlarmData) { + let context = AlarmContext(now: Date(), config: config) + let alarms = Storage.shared.alarms.value + + let sorted = alarms.sorted { lhs, rhs in + // Primary: type priority + if lhs.type.priority != rhs.type.priority { + return lhs.type.priority < rhs.type.priority + } + // Secondary: threshold ordering if applicable + if let asc = lhs.type.thresholdSortAscending { + let leftVal = lhs.threshold ?? (asc ? Float.infinity : -Float.infinity) + let rightVal = rhs.threshold ?? (asc ? Float.infinity : -Float.infinity) + return asc ? leftVal < rightVal : leftVal > rightVal + } + // Tertiary: fallback to insertion order + return false + } + + for alarm in sorted { + guard let checker = evaluators[alarm.type], + checker.shouldFire(alarm: alarm, data: data, context: context) + else { continue } + alarm.trigger() + break + } + } +} diff --git a/LoopFollow/Alarm/AlarmSettingsView.swift b/LoopFollow/Alarm/AlarmSettingsView.swift new file mode 100644 index 000000000..7b2ef325c --- /dev/null +++ b/LoopFollow/Alarm/AlarmSettingsView.swift @@ -0,0 +1,188 @@ +// +// AlarmSettingsView.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025‑04‑20. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import SwiftUI + +struct AlarmSettingsView: View { + @ObservedObject private var cfgStore = Storage.shared.alarmConfiguration + @Environment(\.presentationMode) var presentationMode + + /// Helper to bind an optional Date? into a non‑optional Date for DatePicker + private func optDateBinding(_ b: Binding) -> Binding { + Binding( + get: { b.wrappedValue ?? Date() }, + set: { b.wrappedValue = $0 } + ) + } + + private var dayBinding: Binding { + Binding( + get: { + var c = Calendar.current.dateComponents([.year, .month, .day], from: Date()) + c.hour = cfgStore.value.dayStart.hour + c.minute = cfgStore.value.dayStart.minute + return Calendar.current.date(from: c)! + }, + set: { d in + let hc = Calendar.current.dateComponents([.hour, .minute], from: d) + cfgStore.value.dayStart = TimeOfDay(hour: hc.hour!, minute: hc.minute!) + } + ) + } + + private var nightBinding: Binding { + Binding( + get: { + var c = Calendar.current.dateComponents([.year, .month, .day], from: Date()) + c.hour = cfgStore.value.nightStart.hour + c.minute = cfgStore.value.nightStart.minute + return Calendar.current.date(from: c)! + }, + set: { d in + let hc = Calendar.current.dateComponents([.hour, .minute], from: d) + cfgStore.value.nightStart = TimeOfDay(hour: hc.hour!, minute: hc.minute!) + } + ) + } + + var body: some View { + NavigationView { + Form { + Section( + header: Text("Snooze & Mute Options"), + footer: Text(""" + Snooze All turns everything off, \ + Mute All turns off phone sounds but leaves vibration \ + and iOS notifications on + """) + ) { + // Snooze All Until + DatePicker( + "Snooze All Until", + selection: optDateBinding( + Binding( + get: { cfgStore.value.snoozeUntil }, + set: { cfgStore.value.snoozeUntil = $0 } + ) + ), + displayedComponents: [.date, .hourAndMinute] + ) + + Toggle( + "All Alerts Snoozed", + isOn: Binding( + get: { + if let until = cfgStore.value.snoozeUntil { + return until > Date() + } + return false + }, + set: { newOn in + if newOn { + // if turning on, set a default 1h snooze if none or expired + if cfgStore.value.snoozeUntil == nil || cfgStore.value.snoozeUntil! <= Date() { + cfgStore.value.snoozeUntil = Date().addingTimeInterval(3600) + } + } else { + cfgStore.value.snoozeUntil = nil + } + } + ) + ) + + // Mute All Until + DatePicker( + "Mute All Until", + selection: optDateBinding( + Binding( + get: { cfgStore.value.muteUntil }, + set: { cfgStore.value.muteUntil = $0 } + ) + ), + displayedComponents: [.date, .hourAndMinute] + ) + + Toggle( + "All Sounds Muted", + isOn: Binding( + get: { + if let until = cfgStore.value.muteUntil { + return until > Date() + } + return false + }, + set: { newOn in + if newOn { + if cfgStore.value.muteUntil == nil || cfgStore.value.muteUntil! <= Date() { + cfgStore.value.muteUntil = Date().addingTimeInterval(3600) + } + } else { + cfgStore.value.muteUntil = nil + } + } + ) + ) + } + + Section(header: Text("Alarm Settings")) { + Toggle( + "Override System Volume", + isOn: Binding( + get: { cfgStore.value.overrideSystemOutputVolume }, + set: { cfgStore.value.overrideSystemOutputVolume = $0 } + ) + ) + + if cfgStore.value.overrideSystemOutputVolume { + Stepper( + "Volume Level: \(Int(cfgStore.value.forcedOutputVolume * 100))%", + value: Binding( + get: { Double(cfgStore.value.forcedOutputVolume) }, + set: { cfgStore.value.forcedOutputVolume = Float($0) } + ), + in: 0...1, + step: 0.05 + ) + } + + Toggle( + "Audio During Calls", + isOn: Binding( + get: { cfgStore.value.audioDuringCalls }, + set: { cfgStore.value.audioDuringCalls = $0 } + ) + ) + + Toggle( + "Ignore Zero BG", + isOn: Binding( + get: { cfgStore.value.ignoreZeroBG }, + set: { cfgStore.value.ignoreZeroBG = $0 } + ) + ) + + Toggle( + "Auto‑Snooze CGM Start", + isOn: Binding( + get: { cfgStore.value.autoSnoozeCGMStart }, + set: { cfgStore.value.autoSnoozeCGMStart = $0 } + ) + ) + } + } + .navigationTitle("Alarm Settings") + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Done") { + presentationMode.wrappedValue.dismiss() + } + } + } + } + } +} diff --git a/LoopFollow/Alarm/AlarmType.swift b/LoopFollow/Alarm/AlarmType.swift new file mode 100644 index 000000000..dc10eee90 --- /dev/null +++ b/LoopFollow/Alarm/AlarmType.swift @@ -0,0 +1,96 @@ +// +// AlarmType.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-03-15. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import Foundation + +/// Categorizes alarms into distinct types, prioritized in the order they appear here. +/// Multiple user-defined alarms may share the same type but differ in configuration. +enum AlarmType: String, CaseIterable, Codable { + case iob = "IOB Alert" + case bolus = "Bolus Alert" + case cob = "COB Alert" + case low = "Low Alert" + case high = "High Alert" + case fastDrop = "Fast Drop Alert" + case fastRise = "Fast Rise Alert" + case missedReading = "Missed Reading Alert" + case notLooping = "Not Looping Alert" + case missedBolus = "Missed Bolus Alert" + case sensorChange = "Sensor Change Alert" + case pumpChange = "Pump Change Alert" + case pump = "Low Insulin Alert" + case battery = "Low Battery" + case batteryDrop = "Battery Drop" + case recBolus = "Rec. Bolus" + case overrideStart = "Override Started" + case overrideEnd = "Override Ended" + case tempTargetStart = "Temp Target Started" + case tempTargetEnd = "Temp Target Ended" + case buildExpire = "Looping app expiration" +} + +extension AlarmType { + var priority: Int { + return AlarmType.allCases.firstIndex(of: self) ?? 0 + } +} + +extension AlarmType { + /// Should alarms of this type sort their thresholds ascending (true) or descending (false) + var thresholdSortAscending: Bool? { + switch self { + case .low, .fastDrop, .fastRise, .missedReading, .notLooping, .missedBolus, .buildExpire: + return true + case .high, .iob, .cob: + return false + default: + return nil + } + } +} + +extension AlarmType { + /// What “unit” we use for snoozeDuration for this alarmType. + var timeUnit: TimeUnit { + switch self { + case .buildExpire: + return .day + case .low, .high, .fastDrop, .fastRise, + .missedReading, .notLooping, .missedBolus, + .iob, .bolus, .cob, .recBolus, + .overrideStart, .overrideEnd, .tempTargetStart, + .tempTargetEnd: + return .minute + case .battery, .batteryDrop, .sensorChange, .pumpChange, + .pump: + return .hour + } + } +} + +enum TimeUnit { + case minute, hour, day + + /// How many seconds in one “unit” + var seconds: TimeInterval { + switch self { + case .minute: return 60 + case .hour: return 60 * 60 + case .day: return 60 * 60 * 24 + } + } + + /// A user-facing label + var label: String { + switch self { + case .minute: return "minutes" + case .hour: return "hours" + case .day: return "days" + } + } +} diff --git a/LoopFollow/Alarm/SnoozeState.swift b/LoopFollow/Alarm/SnoozeState.swift new file mode 100644 index 000000000..42d3e14eb --- /dev/null +++ b/LoopFollow/Alarm/SnoozeState.swift @@ -0,0 +1,14 @@ +// +// SnoozeState.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-03-15. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import Foundation + +struct SnoozeState: Codable { + var isSnoozed: Bool = false + var snoozeUntil: Date? +} diff --git a/LoopFollow/Controllers/Nightscout/CAge.swift b/LoopFollow/Controllers/Nightscout/CAge.swift index b5546e457..41306385e 100644 --- a/LoopFollow/Controllers/Nightscout/CAge.swift +++ b/LoopFollow/Controllers/Nightscout/CAge.swift @@ -37,7 +37,7 @@ extension MainViewController { currentCage = data[0] let lastCageString = data[0].created_at - + let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withFullDate, .withTime, diff --git a/LoopFollow/Controllers/Nightscout/NSProfile.swift b/LoopFollow/Controllers/Nightscout/NSProfile.swift index 9c31947a9..1c318533c 100644 --- a/LoopFollow/Controllers/Nightscout/NSProfile.swift +++ b/LoopFollow/Controllers/Nightscout/NSProfile.swift @@ -46,6 +46,7 @@ struct NSProfile: Decodable { let isAPNSProduction: Bool? let deviceToken: String? let teamID: String? + let expirationDate: String? struct TrioOverrideEntry: Decodable { let name: String @@ -94,5 +95,6 @@ struct NSProfile: Decodable { case trioOverrides = "overridePresets" case loopSettings = "loopSettings" case teamID + case expirationDate } } diff --git a/LoopFollow/Controllers/Nightscout/ProfileManager.swift b/LoopFollow/Controllers/Nightscout/ProfileManager.swift index 0977c40d7..c31c4f9d5 100644 --- a/LoopFollow/Controllers/Nightscout/ProfileManager.swift +++ b/LoopFollow/Controllers/Nightscout/ProfileManager.swift @@ -105,6 +105,12 @@ final class ProfileManager { } Storage.shared.deviceToken.value = profileData.deviceToken ?? "" + if let expirationDate = profileData.expirationDate { + Storage.shared.expirationDate.value = NightscoutUtils.parseDate(expirationDate) + Storage.shared.expirationDate.value = NightscoutUtils.parseDate("2025-04-25T17:54:56.000Z") + } else { + Storage.shared.expirationDate.value = nil + } Storage.shared.bundleId.value = profileData.bundleIdentifier ?? "" Storage.shared.productionEnvironment.value = profileData.isAPNSProduction ?? false Storage.shared.teamId.value = profileData.teamID ?? Storage.shared.teamId.value ?? "" diff --git a/LoopFollow/Extensions/Binding+Optional.swift b/LoopFollow/Extensions/Binding+Optional.swift new file mode 100644 index 000000000..5a3264902 --- /dev/null +++ b/LoopFollow/Extensions/Binding+Optional.swift @@ -0,0 +1,20 @@ +// +// Binding+Optional.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-04-21. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import Foundation +import SwiftUI + +extension Binding where Value: Equatable { + /// Create a Binding out of a Binding by substituting `defaultValue` when nil. + init(_ source: Binding, replacingNilWith defaultValue: Value) { + self.init( + get: { source.wrappedValue ?? defaultValue }, + set: { source.wrappedValue = $0 } + ) + } +} diff --git a/LoopFollow/Extensions/UUID+Identifiable.swift b/LoopFollow/Extensions/UUID+Identifiable.swift new file mode 100644 index 000000000..f7eccb74a --- /dev/null +++ b/LoopFollow/Extensions/UUID+Identifiable.swift @@ -0,0 +1,13 @@ +// +// UUID+Identifiable.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-04-21. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import Foundation + +extension UUID: @retroactive Identifiable { + public var id: UUID { self } +} diff --git a/LoopFollow/Helpers/NightscoutUtils.swift b/LoopFollow/Helpers/NightscoutUtils.swift index ede541bb6..0a3c900e5 100644 --- a/LoopFollow/Helpers/NightscoutUtils.swift +++ b/LoopFollow/Helpers/NightscoutUtils.swift @@ -90,7 +90,27 @@ class NightscoutUtils { DispatchQueue.main.async { completion(.success(decodedObject)) } + } + catch let decodingError as DecodingError { + print("[ERROR] Failed to decode \(T.self):") + switch decodingError { + case .typeMismatch(let type, let context): + print("Type mismatch for type \(type), context: \(context.debugDescription)") + print("Coding path:", context.codingPath) + case .valueNotFound(let type, let context): + print("Value not found for type \(type), context: \(context.debugDescription)") + print("Coding path:", context.codingPath) + case .keyNotFound(let key, let context): + print("Key '\(key.stringValue)' not found, context: \(context.debugDescription)") + print("Coding path:", context.codingPath) + case .dataCorrupted(let context): + print("Data corrupted, context: \(context.debugDescription)") + @unknown default: + print("Unknown decoding error") + } + completion(.failure(decodingError)) } catch { + print("[ERROR] General error:", error) completion(.failure(error)) } } diff --git a/LoopFollow/Helpers/TimeOfDay.swift b/LoopFollow/Helpers/TimeOfDay.swift new file mode 100644 index 000000000..8b5641db1 --- /dev/null +++ b/LoopFollow/Helpers/TimeOfDay.swift @@ -0,0 +1,26 @@ +// +// TimeOfDay.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-04-20. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + + +import Foundation + +/// A time‐of‐day independent of any date +struct TimeOfDay: Codable, Equatable { + let hour: Int // 0…23 + let minute: Int // 0…59 + + /// total minutes since midnight + var minutesSinceMidnight: Int { hour * 60 + minute } + + init(hour: Int, minute: Int) { + precondition((0...23).contains(hour)) + precondition((0...59).contains(minute)) + self.hour = hour + self.minute = minute + } +} diff --git a/LoopFollow/Snoozer/SnoozerView.swift b/LoopFollow/Snoozer/SnoozerView.swift new file mode 100644 index 000000000..f22ab761b --- /dev/null +++ b/LoopFollow/Snoozer/SnoozerView.swift @@ -0,0 +1,84 @@ +// +// SnoozerView.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-04-26. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + + +import SwiftUI +import Combine + +struct SnoozerView: View { + @ObservedObject var bg = Observable.shared.bgValue + @ObservedObject var trend = Observable.shared.trendArrow + @ObservedObject var delta = Observable.shared.delta + @ObservedObject var minutesAgo = Observable.shared.minutesAgo + @ObservedObject var alarmTitle = Observable.shared.alarmTitle + + @State private var snoozeMinutes = 10 + @State private var currentTime = Date() + + private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() + + var bgColor: Color { + switch bg.value { + case ..<4.0: + return .red + case 4.0..<10: + return .yellow + default: + return .blue + } + } + + var body: some View { + VStack(spacing: 20) { + Text(String(format: "%.1f", bg.value).replacingOccurrences(of: ".", with: ",")) + .font(.system(size: 100, weight: .bold)) + .foregroundColor(bgColor) + + Text(trend.value) + .font(.system(size: 40)) + + Text(String(format: "%+.1f", delta.value)) + .font(.title2) + + Text("\(minutesAgo.value) min ago") + .font(.subheadline) + + Text(currentTimeFormatted) + .font(.largeTitle) + .onReceive(timer) { _ in currentTime = Date() } + + if let alarm = alarmTitle.value { + Text(alarm) + .font(.title2) + .foregroundColor(.red) + .padding(.top) + + Stepper("Snooze for \(snoozeMinutes) min", value: $snoozeMinutes, in: 5...60, step: 5) + .padding(.horizontal) + + Button("Snooze") { + // Call snooze logic + print("Snoozing \(alarm) for \(snoozeMinutes) minutes") + } + .padding() + .background(Color.gray.opacity(0.3)) + .cornerRadius(10) + } + } + .padding() + .background(Color.black) + .foregroundColor(.white) + .ignoresSafeArea() + } + + private var currentTimeFormatted: String { + let formatter = DateFormatter() + formatter.dateFormat = "HH:mm" + return formatter.string(from: currentTime) + } +} \ No newline at end of file diff --git a/LoopFollow/Storage/Observable.swift b/LoopFollow/Storage/Observable.swift index 8cc97e140..242ce330a 100644 --- a/LoopFollow/Storage/Observable.swift +++ b/LoopFollow/Storage/Observable.swift @@ -16,5 +16,12 @@ class Observable { var override = ObservableValue(default: nil) var lastRecBolusTriggered = ObservableValue(default: nil) + // Work in progress here.. + var bgValue = ObservableValue(default: 0.0) + var trendArrow = ObservableValue(default: "→") + var delta = ObservableValue(default: 0.0) + var minutesAgo = ObservableValue(default: 0) + var alarmTitle = ObservableValue(default: nil) + private init() {} } diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index ba760ef53..771f1ce46 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -12,6 +12,7 @@ import HealthKit class Storage { var remoteType = StorageValue(key: "remoteType", defaultValue: .nightscout) var deviceToken = StorageValue(key: "deviceToken", defaultValue: "") + var expirationDate = StorageValue(key: "expirationDate", defaultValue: nil) var sharedSecret = StorageValue(key: "sharedSecret", defaultValue: "") var productionEnvironment = StorageValue(key: "productionEnvironment", defaultValue: true) var apnsKey = StorageValue(key: "apnsKey", defaultValue: "") @@ -45,6 +46,12 @@ class Storage { var sensorScheduleOffset = StorageValue(key: "sensorScheduleOffset", defaultValue: nil) + var alarms = StorageValue<[Alarm]>(key: "alarms", defaultValue: []) + var alarmConfiguration = StorageValue( + key: "alarmConfiguration", + defaultValue: .default + ) + static let shared = Storage() private init() { } diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index 8fe64b1c9..991fbb0e1 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -19,6 +19,14 @@ extension MainViewController { func alarmTaskAction() { DispatchQueue.main.async { + let alarmData = AlarmData( + expireDate: Storage.shared.expirationDate.value + ) + + LogManager.shared.log(category: .alarm, message: "Checking alarms based on \(alarmData)", isDebug: true) + + AlarmManager.shared.checkAlarms(data: alarmData) + /* if self.bgData.count > 0 { self.checkAlarms(bgs: self.bgData) } @@ -27,7 +35,7 @@ extension MainViewController { } if self.tempTargetGraphData.count > 0 { self.checkTempTargetAlarms() - } + }*/ TaskScheduler.shared.rescheduleTask(id: .alarmCheck, to: Date().addingTimeInterval(30)) } diff --git a/LoopFollow/ViewControllers/SettingsViewController.swift b/LoopFollow/ViewControllers/SettingsViewController.swift index de97da484..1a6d4d9b6 100644 --- a/LoopFollow/ViewControllers/SettingsViewController.swift +++ b/LoopFollow/ViewControllers/SettingsViewController.swift @@ -127,7 +127,7 @@ class SettingsViewController: FormViewController, NightscoutSettingsViewModelDel } ), onDismiss: nil) } - <<< ButtonRow("alarmsSettings") { + <<< ButtonRow("alarmsSettingstobedeleted") { $0.title = "Alarms" $0.presentationMode = .show( controllerProvider: .callback(builder: { @@ -137,6 +137,30 @@ class SettingsViewController: FormViewController, NightscoutSettingsViewModelDel return alarmVC }), onDismiss: nil) } + + + <<< ButtonRow("alarmsList") { + $0.title = "Alarms" + $0.presentationMode = .show( + controllerProvider: .callback(builder: { + self.presentAlarmList() + return UIViewController() + }), + onDismiss: nil + ) + } + + <<< ButtonRow("alarmsSettings") { + $0.title = "Alarm Settings" + $0.presentationMode = .show( + controllerProvider: .callback(builder: { + self.presentAlarmSettings() + return UIViewController() + }), + onDismiss: nil + ) + } + <<< ButtonRow("remoteSettings") { $0.title = "Remote Settings" $0.presentationMode = .show( @@ -288,6 +312,30 @@ class SettingsViewController: FormViewController, NightscoutSettingsViewModelDel present(hostingController, animated: true, completion: nil) } + func presentAlarmSettings() { + let settingsView = AlarmSettingsView() + let hostingController = UIHostingController(rootView: settingsView) + hostingController.modalPresentationStyle = .formSheet + + if UserDefaultsRepository.forceDarkMode.value { + hostingController.overrideUserInterfaceStyle = .dark + } + + present(hostingController, animated: true, completion: nil) + } + + func presentAlarmList() { + let settingsView = AlarmListView() + let hostingController = UIHostingController(rootView: settingsView) + hostingController.modalPresentationStyle = .formSheet + + if UserDefaultsRepository.forceDarkMode.value { + hostingController.overrideUserInterfaceStyle = .dark + } + + present(hostingController, animated: true, completion: nil) + } + func presentContactSettings() { let viewModel = ContactSettingsViewModel() let contactSettingsView = ContactSettingsView(viewModel: viewModel) diff --git a/LoopFollowTests/AlwaysTrueCondition.swift b/LoopFollowTests/AlwaysTrueCondition.swift new file mode 100644 index 000000000..d15f00dd0 --- /dev/null +++ b/LoopFollowTests/AlwaysTrueCondition.swift @@ -0,0 +1,77 @@ +// +// AlwaysTrueCondition.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-04-20. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import XCTest +@testable import LoopFollow + +struct AlwaysTrueCondition: AlarmCondition { + static let type: AlarmType = .low + init() {} + func evaluate(alarm: Alarm, data: AlarmData) -> Bool { true } +} + +final class CommonAlarmGuardsTests: XCTestCase { + var now: Date! + var config: AlarmConfiguration! + var context: AlarmContext! + var alarm: Alarm! + var data: AlarmData! + var cond: AlwaysTrueCondition! + + override func setUp() { + super.setUp() + now = Date() + config = .default + context = AlarmContext(now: now, config: config) + + alarm = Alarm(type: .low) + alarm.name = "test" + alarm.isEnabled = true + alarm.snoozedUntil = nil + alarm.playSoundOption = .always + alarm.activeOption = .always + alarm.snoozeDuration = 0 + + data = AlarmData(expireDate: nil) + cond = AlwaysTrueCondition() + } + + func testMuteUntil() { + config.muteUntil = now.addingTimeInterval(60) + context = AlarmContext(now: now, config: config) + XCTAssertFalse(cond.shouldFire(alarm: alarm, data: data, context: context)) + } + + func testDisabledAlarm() { + alarm.isEnabled = false + XCTAssertFalse(cond.shouldFire(alarm: alarm, data: data, context: context)) + } + + func testSnoozedAlarm() { + alarm.snoozedUntil = now.addingTimeInterval(60) + XCTAssertFalse(cond.shouldFire(alarm: alarm, data: data, context: context)) + } + + func testRespectsNightFlag() { + let night = Calendar.current.date(bySettingHour: 23, minute: 0, second: 0, of: now)! + alarm.activeOption = .day // alarm should be inactive at night + context = AlarmContext(now: night, config: config) + XCTAssertFalse(cond.shouldFire(alarm: alarm, data: data, context: context)) + } + + func testRespectsDayFlag() { + let day = Calendar.current.date(bySettingHour: 9, minute: 0, second: 0, of: now)! + alarm.activeOption = .night // alarm should be inactive during day + context = AlarmContext(now: day, config: config) + XCTAssertFalse(cond.shouldFire(alarm: alarm, data: data, context: context)) + } + + func testAllGuardsPass() { + XCTAssertTrue(cond.shouldFire(alarm: alarm, data: data, context: context)) + } +} diff --git a/LoopFollowTests/BuildExpireConditionTests.swift b/LoopFollowTests/BuildExpireConditionTests.swift new file mode 100644 index 000000000..f188b34e0 --- /dev/null +++ b/LoopFollowTests/BuildExpireConditionTests.swift @@ -0,0 +1,51 @@ +// +// BuildExpireConditionTests.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-04-20. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import XCTest +@testable import LoopFollow + +final class BuildExpireConditionTests: XCTestCase { + let cond = BuildExpireCondition() + var alarm: Alarm! + + override func setUp() { + super.setUp() + alarm = Alarm(type: .buildExpire) + alarm.threshold = 7 // 7 days before expiration + } + + func testEvaluateWhenWithinThreshold() { + let now = Date() + // Build expires in 6 days → within threshold (7 days) + let expiryDate = Calendar.current.date(byAdding: .day, value: 6, to: now)! + let data = AlarmData(expireDate: expiryDate) + XCTAssertTrue(cond.evaluate(alarm: alarm, data: data)) + } + + func testEvaluateWhenOutsideThreshold() { + let now = Date() + // Build expires in 10 days → outside threshold (7 days) + let expiryDate = Calendar.current.date(byAdding: .day, value: 10, to: now)! + let data = AlarmData(expireDate: expiryDate) + XCTAssertFalse(cond.evaluate(alarm: alarm, data: data)) + } + + func testEvaluateWhenNoExpireDate() { + let data = AlarmData(expireDate: nil) + XCTAssertFalse(cond.evaluate(alarm: alarm, data: data)) + } + + func testEvaluateWhenNoThreshold() { + // Clear threshold + alarm.threshold = nil + let now = Date() + let expiryDate = Calendar.current.date(byAdding: .day, value: 5, to: now)! + let data = AlarmData(expireDate: expiryDate) + XCTAssertFalse(cond.evaluate(alarm: alarm, data: data)) + } +} From e171dc43f103d50acd510851982d8a490bbe8e50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 26 Apr 2025 12:17:32 +0200 Subject: [PATCH 002/138] SnoozerViewController --- LoopFollow.xcodeproj/project.pbxproj | 14 ++- LoopFollow/Snoozer/SnoozerView.swift | 94 ++++++++----------- .../Snoozer/SnoozerViewController.swift | 67 +++++++++++++ 3 files changed, 117 insertions(+), 58 deletions(-) create mode 100644 LoopFollow/Snoozer/SnoozerViewController.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index a5646a495..abf816b55 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -414,6 +414,8 @@ DDB0AF512BB1A8BE00AFA48B /* BuildDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildDetails.swift; sourceTree = ""; }; DDB0AF542BB1B24A00AFA48B /* BuildDetails.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = BuildDetails.plist; sourceTree = ""; }; DDBE3ABC2CB5A961006B37DC /* OverrideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideView.swift; sourceTree = ""; }; + DDC7E5122DBCE1B900EB1127 /* SnoozerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozerView.swift; sourceTree = ""; }; + DDC7E5132DBCE1B900EB1127 /* SnoozerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozerViewController.swift; sourceTree = ""; }; DDCF979324C0D380002C9752 /* UIViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtension.swift; sourceTree = ""; }; DDCF979524C1443C002C9752 /* GeneralSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsViewController.swift; sourceTree = ""; }; DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphSettingsViewController.swift; sourceTree = ""; }; @@ -613,7 +615,6 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ DD4AFB4A2DB684A200BB593F /* AlarmEditing */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = AlarmEditing; sourceTree = ""; }; - DD4AFB6C2DBCDA6B00BB593F /* Snoozer */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Snoozer; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -876,6 +877,15 @@ path = Scripts; sourceTree = ""; }; + DDC7E5142DBCE1B900EB1127 /* Snoozer */ = { + isa = PBXGroup; + children = ( + DDC7E5122DBCE1B900EB1127 /* SnoozerView.swift */, + DDC7E5132DBCE1B900EB1127 /* SnoozerViewController.swift */, + ); + path = Snoozer; + sourceTree = ""; + }; DDCF9A7E2D85FCE6004DF4DD /* Alarm */ = { isa = PBXGroup; children = ( @@ -1134,7 +1144,7 @@ FC8DEEE32485D1680075863F /* LoopFollow */ = { isa = PBXGroup; children = ( - DD4AFB6C2DBCDA6B00BB593F /* Snoozer */, + DDC7E5142DBCE1B900EB1127 /* Snoozer */, DDCF9A7E2D85FCE6004DF4DD /* Alarm */, DD1A97122D429495000DDC11 /* Settings */, DD2C2E522D3C36A8006413A5 /* Dexcom */, diff --git a/LoopFollow/Snoozer/SnoozerView.swift b/LoopFollow/Snoozer/SnoozerView.swift index f22ab761b..abca3abf1 100644 --- a/LoopFollow/Snoozer/SnoozerView.swift +++ b/LoopFollow/Snoozer/SnoozerView.swift @@ -8,77 +8,59 @@ import SwiftUI -import Combine struct SnoozerView: View { - @ObservedObject var bg = Observable.shared.bgValue - @ObservedObject var trend = Observable.shared.trendArrow - @ObservedObject var delta = Observable.shared.delta - @ObservedObject var minutesAgo = Observable.shared.minutesAgo - @ObservedObject var alarmTitle = Observable.shared.alarmTitle + @ObservedObject var bgValue: ObservableValue + @ObservedObject var deltaValue: ObservableValue + @ObservedObject var direction: ObservableValue + @ObservedObject var age: ObservableValue + @ObservedObject var time: ObservableValue + @ObservedObject var alarmText: ObservableValue - @State private var snoozeMinutes = 10 - @State private var currentTime = Date() - - private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() - - var bgColor: Color { - switch bg.value { - case ..<4.0: - return .red - case 4.0..<10: - return .yellow - default: - return .blue - } - } + @Binding var snoozeMinutes: Int + var onSnooze: () -> Void var body: some View { - VStack(spacing: 20) { - Text(String(format: "%.1f", bg.value).replacingOccurrences(of: ".", with: ",")) - .font(.system(size: 100, weight: .bold)) - .foregroundColor(bgColor) + VStack(spacing: 12) { + Text(bgValue.value) + .font(.system(size: 72, weight: .bold)) + .foregroundColor(.yellow) + + if let alarm = alarmText.value, !alarm.isEmpty { + Text(alarm) + .font(.title2) + .foregroundColor(.red) + } - Text(trend.value) - .font(.system(size: 40)) + Text(direction.value) + .font(.title) - Text(String(format: "%+.1f", delta.value)) + Text(deltaValue.value) .font(.title2) + .foregroundColor(.white.opacity(0.8)) - Text("\(minutesAgo.value) min ago") + Text(age.value + " ago") .font(.subheadline) + .foregroundColor(.white.opacity(0.6)) - Text(currentTimeFormatted) - .font(.largeTitle) - .onReceive(timer) { _ in currentTime = Date() } + Text(time.value) + .font(.title3) - if let alarm = alarmTitle.value { - Text(alarm) - .font(.title2) - .foregroundColor(.red) - .padding(.top) - - Stepper("Snooze for \(snoozeMinutes) min", value: $snoozeMinutes, in: 5...60, step: 5) - .padding(.horizontal) + HStack { + Text("Snooze for \(snoozeMinutes) min") + Stepper("", value: $snoozeMinutes, in: 1...60) + .labelsHidden() + } + .padding(.top) - Button("Snooze") { - // Call snooze logic - print("Snoozing \(alarm) for \(snoozeMinutes) minutes") - } + Button("Snooze", action: onSnooze) .padding() - .background(Color.gray.opacity(0.3)) - .cornerRadius(10) - } + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(8) } .padding() .background(Color.black) - .foregroundColor(.white) - .ignoresSafeArea() - } - - private var currentTimeFormatted: String { - let formatter = DateFormatter() - formatter.dateFormat = "HH:mm" - return formatter.string(from: currentTime) + .edgesIgnoringSafeArea(.all) } -} \ No newline at end of file +} diff --git a/LoopFollow/Snoozer/SnoozerViewController.swift b/LoopFollow/Snoozer/SnoozerViewController.swift new file mode 100644 index 000000000..fe891782d --- /dev/null +++ b/LoopFollow/Snoozer/SnoozerViewController.swift @@ -0,0 +1,67 @@ +// +// SnoozerViewController.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-04-26. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + + +import UIKit +import SwiftUI +import Combine + +class SnoozerViewController: UIViewController { + private var hostingController: UIHostingController? + + private var bgValue = ObservableValue(default: "8,9") + private var deltaValue = ObservableValue(default: "-0,7") + private var direction = ObservableValue(default: "→") + private var age = ObservableValue(default: "4 min") + private var time = ObservableValue(default: "10:28") + private var alarmText = ObservableValue(default: "High Alert") + @State private var snoozeMinutes = 15 + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .black + + let snoozerView = SnoozerView( + bgValue: bgValue, + deltaValue: deltaValue, + direction: direction, + age: age, + time: time, + alarmText: alarmText, + snoozeMinutes: Binding(get: { self.snoozeMinutes }, set: { self.snoozeMinutes = $0 }), + onSnooze: { + // Trigger snooze logic here (e.g., update UserDefaultsRepository, stop alarm, etc.) + print("Snoozed for \(self.snoozeMinutes) minutes") + } + ) + + let hosting = UIHostingController(rootView: snoozerView) + self.hostingController = hosting + addChild(hosting) + view.addSubview(hosting.view) + hosting.view.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + hosting.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + hosting.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + hosting.view.topAnchor.constraint(equalTo: view.topAnchor), + hosting.view.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + + hosting.didMove(toParent: self) + } + + // ✅ Only this screen supports landscape + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + return [.portrait, .landscapeLeft, .landscapeRight] + } + + override var shouldAutorotate: Bool { + return true + } +} From 9c1b6adda7fcb4a27c39c23f65afd0e210286873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 26 Apr 2025 18:41:16 +0200 Subject: [PATCH 003/138] Snoozer details --- LoopFollow.xcodeproj/project.pbxproj | 4 + .../Application/Base.lproj/Main.storyboard | 169 ++---------------- LoopFollow/Application/SceneDelegate.swift | 2 + LoopFollow/Controllers/Alarms.swift | 6 +- .../Controllers/Nightscout/BGData.swift | 28 +-- LoopFollow/Info.plist | 20 +-- LoopFollow/Snoozer/SnoozerView.swift | 112 ++++++++---- .../Snoozer/SnoozerViewController.swift | 26 +-- LoopFollow/Storage/Observable.swift | 15 +- LoopFollow/Task/MinAgoTask.swift | 15 +- .../ViewControllers/MainViewController.swift | 39 ++-- .../SnoozeViewController.swift | 2 +- 12 files changed, 172 insertions(+), 266 deletions(-) diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index abf816b55..54a6a49e8 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -108,6 +108,8 @@ DDB0AF522BB1A8BE00AFA48B /* BuildDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB0AF512BB1A8BE00AFA48B /* BuildDetails.swift */; }; DDB0AF552BB1B24A00AFA48B /* BuildDetails.plist in Resources */ = {isa = PBXBuildFile; fileRef = DDB0AF542BB1B24A00AFA48B /* BuildDetails.plist */; }; DDBE3ABD2CB5A961006B37DC /* OverrideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDBE3ABC2CB5A961006B37DC /* OverrideView.swift */; }; + DDC7E5152DBCFA7900EB1127 /* SnoozerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5132DBCE1B900EB1127 /* SnoozerViewController.swift */; }; + DDC7E5162DBCFA7F00EB1127 /* SnoozerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5122DBCE1B900EB1127 /* SnoozerView.swift */; }; DDCF979424C0D380002C9752 /* UIViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979324C0D380002C9752 /* UIViewExtension.swift */; }; DDCF979624C1443C002C9752 /* GeneralSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979524C1443C002C9752 /* GeneralSettingsViewController.swift */; }; DDCF979824C1489C002C9752 /* GraphSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */; }; @@ -1590,6 +1592,7 @@ DD608A0A2C23593900F91132 /* SMB.swift in Sources */, DDCF979824C1489C002C9752 /* GraphSettingsViewController.swift in Sources */, FC3AE7B5249E8E0E00AAE1E0 /* LoopFollow.xcdatamodeld in Sources */, + DDC7E5152DBCFA7900EB1127 /* SnoozerViewController.swift in Sources */, DDDF6F492D479AF000884336 /* NoRemoteView.swift in Sources */, FCC6886F2489A53800A0279D /* AppConstants.swift in Sources */, DD7E19842ACDA50C00DBD158 /* Overrides.swift in Sources */, @@ -1623,6 +1626,7 @@ 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 */, DD4AFB612DB68BBC00BB593F /* AlarmListView.swift in Sources */, DD7B0D442D730A3B0063DCB6 /* CycleHelper.swift in Sources */, diff --git a/LoopFollow/Application/Base.lproj/Main.storyboard b/LoopFollow/Application/Base.lproj/Main.storyboard index 06af2b7c7..2278db6db 100644 --- a/LoopFollow/Application/Base.lproj/Main.storyboard +++ b/LoopFollow/Application/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -13,13 +13,13 @@ - + - + @@ -114,21 +114,21 @@ - + - + - + - + @@ -321,150 +321,14 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -473,13 +337,13 @@ - + - + @@ -508,7 +372,7 @@ - + @@ -525,7 +389,7 @@ - + @@ -541,7 +405,7 @@ - + @@ -582,11 +446,8 @@ - - - - + diff --git a/LoopFollow/Application/SceneDelegate.swift b/LoopFollow/Application/SceneDelegate.swift index 00ab1aba6..24c5d03b5 100644 --- a/LoopFollow/Application/SceneDelegate.swift +++ b/LoopFollow/Application/SceneDelegate.swift @@ -44,9 +44,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { if let vc = viewControllers[i] as? AlarmViewController { vc.appStateController = appStateController } + /* TODO if let vc = viewControllers[i] as? SnoozeViewController { vc.appStateController = appStateController } + */ if let vc = viewControllers[i] as? debugViewController { vc.appStateController = appStateController } diff --git a/LoopFollow/Controllers/Alarms.swift b/LoopFollow/Controllers/Alarms.swift index 6778d78f8..e44a3f9e4 100644 --- a/LoopFollow/Controllers/Alarms.swift +++ b/LoopFollow/Controllers/Alarms.swift @@ -672,7 +672,7 @@ extension MainViewController { if !UserDefaultsRepository.alertAudioDuringPhone.value && isOnPhoneCall() { audioDuringCall = false } guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } - snoozer.updateDisplayWhenTriggered(bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: latestDirectionString ?? "", deltaVal: latestDeltaString, minAgoVal: latestMinAgoString ?? "", alertLabelVal: AlarmSound.whichAlarm) + snoozer.updateDisplayWhenTriggered(bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: Observable.shared.directionText.value, deltaVal: latestDeltaString, minAgoVal: Observable.shared.minAgoText.value, alertLabelVal: AlarmSound.whichAlarm) if audio && !UserDefaultsRepository.alertMuteAllIsMuted.value && audioDuringCall{ AlarmSound.setSoundFile(str: sound) AlarmSound.play(overrideVolume: overrideVolume, numLoops: numLoops) @@ -688,7 +688,7 @@ extension MainViewController { if !UserDefaultsRepository.alertAudioDuringPhone.value && isOnPhoneCall() { audioDuringCall = false } guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } - snoozer.updateDisplayWhenTriggered(bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: latestDirectionString ?? "", deltaVal: latestDeltaString, minAgoVal: latestMinAgoString ?? "", alertLabelVal: AlarmSound.whichAlarm) + snoozer.updateDisplayWhenTriggered(bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: Observable.shared.directionText.value, deltaVal: latestDeltaString, minAgoVal: Observable.shared.minAgoText.value, alertLabelVal: AlarmSound.whichAlarm) snoozer.SnoozeButton.isHidden = false snoozer.AlertLabel.isHidden = false snoozer.clockLabel.isHidden = true @@ -721,7 +721,7 @@ extension MainViewController { AlarmSound.whichAlarm = "none" guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } - snoozer.updateDisplayWhenTriggered(bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: latestDirectionString ?? "", deltaVal: latestDeltaString, minAgoVal: latestMinAgoString ?? "", alertLabelVal: AlarmSound.whichAlarm) + snoozer.updateDisplayWhenTriggered(bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: Observable.shared.directionText.value, deltaVal: latestDeltaString, minAgoVal: Observable.shared.minAgoText.value, alertLabelVal: AlarmSound.whichAlarm) snoozer.SnoozeButton.isHidden = true snoozer.AlertLabel.isHidden = true if AlarmSound.isPlaying { diff --git a/LoopFollow/Controllers/Nightscout/BGData.swift b/LoopFollow/Controllers/Nightscout/BGData.swift index f67c12fbe..e2e243455 100644 --- a/LoopFollow/Controllers/Nightscout/BGData.swift +++ b/LoopFollow/Controllers/Nightscout/BGData.swift @@ -240,24 +240,19 @@ extension MainViewController { let deltaTime = (TimeInterval(Date().timeIntervalSince1970) - lastBGTime) / 60 self.updateServerText(with: sourceName) - var snoozerBG = "" - var snoozerDirection = "" var snoozerDelta = "" // Set BGText with the latest BG value - self.BGText.text = Localizer.toDisplayUnits(String(latestBG)) - snoozerBG = Localizer.toDisplayUnits(String(latestBG)) self.setBGTextColor() + Observable.shared.bgText.value = Localizer + .toDisplayUnits(String(latestBG)) + // Direction handling if let directionBG = entries[latestEntryIndex].direction { - self.DirectionText.text = self.bgDirectionGraphic(directionBG) - snoozerDirection = self.bgDirectionGraphic(directionBG) - self.latestDirectionString = self.bgDirectionGraphic(directionBG) + Observable.shared.directionText.value = self.bgDirectionGraphic(directionBG) } else { - self.DirectionText.text = "" - snoozerDirection = "" - self.latestDirectionString = "" + Observable.shared.directionText.value = "" } // Delta handling @@ -284,14 +279,21 @@ extension MainViewController { self.BGText.attributedText = attributeString // Snoozer Display + /* + TODO guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } - snoozer.BGLabel.text = snoozerBG snoozer.DirectionLabel.text = snoozerDirection snoozer.DeltaLabel.text = snoozerDelta - +*/ // Update contact if Storage.shared.contactEnabled.value { - self.contactImageUpdater.updateContactImage(bgValue: bgTextStr, trend: snoozerDirection, delta: snoozerDelta, stale: deltaTime >= 12) + self.contactImageUpdater + .updateContactImage( + bgValue: bgTextStr, + trend: Observable.shared.directionText.value, + delta: snoozerDelta, + stale: deltaTime >= 12 + ) } } } diff --git a/LoopFollow/Info.plist b/LoopFollow/Info.plist index 8916329b5..5ab0c28b5 100644 --- a/LoopFollow/Info.plist +++ b/LoopFollow/Info.plist @@ -2,8 +2,6 @@ - NSHumanReadableCopyright - AppGroupIdentifier group.com.$(unique_id).LoopFollow$(app_suffix) BGTaskSchedulerPermittedIdentifiers @@ -47,6 +45,8 @@ 13.0 LSRequiresIPhoneOS + LSSupportsOpeningDocumentsInPlace + NSBluetoothAlwaysUsageDescription This app uses Bluetooth to connect to devices for managing background operations. NSCalendarsFullAccessUsageDescription @@ -57,6 +57,8 @@ This app requires access to contacts to update a contact image with real-time blood glucose information. NSFaceIDUsageDescription This app requires Face ID for secure authentication. + NSHumanReadableCopyright + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes @@ -82,6 +84,8 @@ processing bluetooth-central + UIFileSharingEnabled + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -102,18 +106,10 @@ UISupportedInterfaceOrientations - UIInterfaceOrientationPortrait - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown - UIFileSharingEnabled - - LSSupportsOpeningDocumentsInPlace - diff --git a/LoopFollow/Snoozer/SnoozerView.swift b/LoopFollow/Snoozer/SnoozerView.swift index abca3abf1..f2706453d 100644 --- a/LoopFollow/Snoozer/SnoozerView.swift +++ b/LoopFollow/Snoozer/SnoozerView.swift @@ -6,61 +6,97 @@ // Copyright © 2025 Jon Fawcett. All rights reserved. // - import SwiftUI struct SnoozerView: View { - @ObservedObject var bgValue: ObservableValue - @ObservedObject var deltaValue: ObservableValue - @ObservedObject var direction: ObservableValue - @ObservedObject var age: ObservableValue - @ObservedObject var time: ObservableValue - @ObservedObject var alarmText: ObservableValue + @ObservedObject var minAgoText = Observable.shared.minAgoText + @ObservedObject var bgText = Observable.shared.bgText + @ObservedObject var bgTextcolor = Observable.shared.bgTextColor + @ObservedObject var directionText = Observable.shared.directionText @Binding var snoozeMinutes: Int var onSnooze: () -> Void var body: some View { - VStack(spacing: 12) { - Text(bgValue.value) - .font(.system(size: 72, weight: .bold)) - .foregroundColor(.yellow) + ZStack { + Color.black + .edgesIgnoringSafeArea(.all) - if let alarm = alarmText.value, !alarm.isEmpty { - Text(alarm) - .font(.title2) - .foregroundColor(.red) - } + VStack(spacing: 0) { + // MARK: Main numbers block + VStack(spacing: 0) { + Text(bgText.value) + .font(.system(size: 220, weight: .black)) + .minimumScaleFactor(0.5) + .foregroundColor(Observable.shared.bgTextColor.value) + .frame(maxWidth: .infinity, maxHeight: 167) - Text(direction.value) - .font(.title) + Text(directionText.value) + .font(.system(size: 110, weight: .black)) + .minimumScaleFactor(0.5) + .foregroundColor(.white) + .frame(maxWidth: .infinity, maxHeight: 96) - Text(deltaValue.value) - .font(.title2) - .foregroundColor(.white.opacity(0.8)) + Text(""/*deltaValue.value*/) + .font(.system(size: 70)) + .minimumScaleFactor(0.5) + .foregroundColor(.white.opacity(0.8)) + .frame(maxWidth: .infinity, maxHeight: 78) - Text(age.value + " ago") - .font(.subheadline) - .foregroundColor(.white.opacity(0.6)) + Text(minAgoText.value) + .font(.system(size: 70)) + .minimumScaleFactor(0.5) + .foregroundColor(.white.opacity(0.6)) + .frame(maxWidth: .infinity, maxHeight: 48) + } - Text(time.value) - .font(.title3) + // MARK: Clock + optional alert label + VStack(spacing: 8) { + Text("19:59"/*time.value*/) + .font(.system(size: 70)) + .minimumScaleFactor(0.5) + .foregroundColor(.white) + .frame(height: 78) +/* + if let alarm = ""/*alarmText.value*/, !alarm.isEmpty { + Text(alarm) + .font(.system(size: 40, weight: .semibold)) + .foregroundColor(.red) + .minimumScaleFactor(0.5) + .lineLimit(1) + .frame(height: 48) + }*/ + } + .padding(.vertical, 16) - HStack { - Text("Snooze for \(snoozeMinutes) min") - Stepper("", value: $snoozeMinutes, in: 1...60) - .labelsHidden() - } - .padding(.top) + Spacer() - Button("Snooze", action: onSnooze) - .padding() - .background(Color.blue) + // MARK: Snooze controls + HStack(spacing: 12) { + Text("Snooze for") + .font(.system(size: 20)) + .foregroundColor(.white) + Text("\(snoozeMinutes) min") + .font(.system(size: 20, weight: .semibold)) + .foregroundColor(.white) + Spacer() + Stepper("", value: $snoozeMinutes, in: 1...60) + .labelsHidden() + } + .padding(.horizontal, 32) + .frame(height: 44) + + Button(action: onSnooze) { + Text("Snooze") + .font(.system(size: 24, weight: .bold)) + .frame(maxWidth: .infinity, minHeight: 56) + } + .background(Color(white: 0.15)) .foregroundColor(.white) .cornerRadius(8) + .padding(.horizontal, 32) + .padding(.bottom, 32) + } } - .padding() - .background(Color.black) - .edgesIgnoringSafeArea(.all) } } diff --git a/LoopFollow/Snoozer/SnoozerViewController.swift b/LoopFollow/Snoozer/SnoozerViewController.swift index fe891782d..6ee9a6cc7 100644 --- a/LoopFollow/Snoozer/SnoozerViewController.swift +++ b/LoopFollow/Snoozer/SnoozerViewController.swift @@ -14,30 +14,17 @@ import Combine class SnoozerViewController: UIViewController { private var hostingController: UIHostingController? - private var bgValue = ObservableValue(default: "8,9") - private var deltaValue = ObservableValue(default: "-0,7") - private var direction = ObservableValue(default: "→") - private var age = ObservableValue(default: "4 min") - private var time = ObservableValue(default: "10:28") - private var alarmText = ObservableValue(default: "High Alert") @State private var snoozeMinutes = 15 override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .black - let snoozerView = SnoozerView( - bgValue: bgValue, - deltaValue: deltaValue, - direction: direction, - age: age, - time: time, - alarmText: alarmText, - snoozeMinutes: Binding(get: { self.snoozeMinutes }, set: { self.snoozeMinutes = $0 }), + let snoozerView = SnoozerView(snoozeMinutes: $snoozeMinutes, onSnooze: { // Trigger snooze logic here (e.g., update UserDefaultsRepository, stop alarm, etc.) print("Snoozed for \(self.snoozeMinutes) minutes") - } + }, ) let hosting = UIHostingController(rootView: snoozerView) @@ -55,13 +42,4 @@ class SnoozerViewController: UIViewController { hosting.didMove(toParent: self) } - - // ✅ Only this screen supports landscape - override var supportedInterfaceOrientations: UIInterfaceOrientationMask { - return [.portrait, .landscapeLeft, .landscapeRight] - } - - override var shouldAutorotate: Bool { - return true - } } diff --git a/LoopFollow/Storage/Observable.swift b/LoopFollow/Storage/Observable.swift index 242ce330a..0d0fc5dd2 100644 --- a/LoopFollow/Storage/Observable.swift +++ b/LoopFollow/Storage/Observable.swift @@ -8,6 +8,7 @@ import Foundation import HealthKit +import SwiftUI class Observable { static let shared = Observable() @@ -16,11 +17,17 @@ class Observable { var override = ObservableValue(default: nil) var lastRecBolusTriggered = ObservableValue(default: nil) - // Work in progress here.. - var bgValue = ObservableValue(default: 0.0) + var minAgoText = ObservableValue(default: "?? min ago") + var bgText = ObservableValue(default: "BG") + //TODO + var bgTextStale = ObservableValue(default: true) + var bgTextColor = ObservableValue(default: .yellow) + var directionText = ObservableValue(default: "-") + var deltaText = ObservableValue(default: "+0") + + + // Work in progress here.. var trendArrow = ObservableValue(default: "→") - var delta = ObservableValue(default: 0.0) - var minutesAgo = ObservableValue(default: 0) var alarmTitle = ObservableValue(default: nil) private init() {} diff --git a/LoopFollow/Task/MinAgoTask.swift b/LoopFollow/Task/MinAgoTask.swift index 31c8b5653..b7bb88a5e 100644 --- a/LoopFollow/Task/MinAgoTask.swift +++ b/LoopFollow/Task/MinAgoTask.swift @@ -23,12 +23,13 @@ extension MainViewController { DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.MinAgoText.text = "" - self.latestMinAgoString = "" + Observable.shared.minAgoText.value = "" + Observable.shared.bgText.value = "" + /*TODO if let snoozer = self.tabBarController?.viewControllers?[2] as? SnoozeViewController { - snoozer.MinAgoLabel.text = "" - snoozer.BGLabel.text = "" snoozer.BGLabel.attributedText = NSAttributedString(string: "") } + */ } TaskScheduler.shared.rescheduleTask(id: .minAgoUpdate, to: Date().addingTimeInterval(1)) return @@ -54,15 +55,14 @@ extension MainViewController { let minAgoDisplayText = formattedDuration + " min ago" // Update UI only if the display text has changed - if minAgoDisplayText != latestMinAgoString { + if minAgoDisplayText != Observable.shared.minAgoText.value { DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.MinAgoText.text = minAgoDisplayText - self.latestMinAgoString = minAgoDisplayText + Observable.shared.minAgoText.value = minAgoDisplayText + /*TODO if let snoozer = self.tabBarController?.viewControllers?[2] as? SnoozeViewController { - snoozer.MinAgoLabel.text = minAgoDisplayText - let bgLabelText = snoozer.BGLabel.text ?? "" let attributeString = NSMutableAttributedString(string: bgLabelText) attributeString.addAttribute(.strikethroughStyle, @@ -73,6 +73,7 @@ extension MainViewController { range: NSRange(location: 0, length: attributeString.length)) snoozer.BGLabel.attributedText = attributeString } + */ } } diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 86ed34d9f..0c97c0a0a 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -13,6 +13,8 @@ import ShareClient import UserNotifications import AVFAudio import CoreBluetooth +import Combine +import SwiftUI func IsNightscoutEnabled() -> Bool { return !ObservableUserDefaults.shared.url.value.isEmpty @@ -105,8 +107,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele var deviceBatteryData: [DataStructs.batteryStruct] = [] var newBGPulled = false var lastCalDate: Double = 0 - var latestDirectionString = "" - var latestMinAgoString = "" var latestDeltaString = "" var latestLoopStatusString = "" var latestCOB: CarbMetric? @@ -144,6 +144,8 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele let contactImageUpdater = ContactImageUpdater() + private var cancellables = Set() + override func viewDidLoad() { super.viewDidLoad() @@ -194,8 +196,9 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } // Load the snoozer tab - guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } - snoozer.loadViewIfNeeded() + //TODO: + //guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } + //snoozer.loadViewIfNeeded() // Trigger foreground and background functions let notificationCenter = NotificationCenter.default @@ -235,8 +238,23 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele refreshScrollView.delegate = self NotificationCenter.default.addObserver(self, selector: #selector(refresh), name: NSNotification.Name("refresh"), object: nil) + + Observable.shared.bgText.$value + .receive(on: DispatchQueue.main) + .sink { [weak self] newValue in + self?.BGText.text = newValue + } + .store(in: &cancellables) + + Observable.shared.directionText + .$value + .receive(on: DispatchQueue.main) + .sink { [weak self] newValue in + self?.DirectionText.text = newValue + } + .store(in: &cancellables) } - + deinit { NotificationCenter.default.removeObserver(self, name: NSNotification.Name("refresh"), object: nil) } @@ -269,7 +287,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } MinAgoText.text = "Refreshing" - latestMinAgoString = "Refreshing" + Observable.shared.minAgoText.value = "Refreshing" scheduleAllTasks() currentCage = nil @@ -520,27 +538,27 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele func setBGTextColor() { if bgData.count > 0 { - guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } let latestBG = bgData[bgData.count - 1].sgv var color: NSUIColor = NSUIColor.label if UserDefaultsRepository.colorBGText.value { if Float(latestBG) >= UserDefaultsRepository.highLine.value { color = NSUIColor.systemYellow + Observable.shared.bgTextColor.value = .yellow } else if Float(latestBG) <= UserDefaultsRepository.lowLine.value { color = NSUIColor.systemRed + Observable.shared.bgTextColor.value = .red } else { color = NSUIColor.systemGreen + Observable.shared.bgTextColor.value = .green } } BGText.textColor = color - snoozer.BGLabel.textColor = color } } func bgDirectionGraphic(_ value:String)->String { - if value == nil { return "-" } let //graphics:[String:String]=["Flat":"\u{2192}","DoubleUp":"\u{21C8}","SingleUp":"\u{2191}","FortyFiveUp":"\u{2197}\u{FE0E}","FortyFiveDown":"\u{2198}\u{FE0E}","SingleDown":"\u{2193}","DoubleDown":"\u{21CA}","None":"-","NOT COMPUTABLE":"-","RATE OUT OF RANGE":"-"] graphics:[String:String]=["Flat":"→","DoubleUp":"↑↑","SingleUp":"↑","FortyFiveUp":"↗","FortyFiveDown":"↘︎","SingleDown":"↓","DoubleDown":"↓↓","None":"-","NONE":"-","NOT COMPUTABLE":"-","RATE OUT OF RANGE":"-", "": "-"] return graphics[value]! @@ -658,8 +676,9 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele func persistentNotification(bgTime: TimeInterval) { if UserDefaultsRepository.persistentNotification.value && bgTime > UserDefaultsRepository.persistentNotificationLastBGTime.value && bgData.count > 0 { +/*TODO guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } - snoozer.sendNotification(self, bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: latestDirectionString, deltaVal: latestDeltaString, minAgoVal: latestMinAgoString, alertLabelVal: "Latest BG") + snoozer.sendNotification(self, bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: Observable.shared.directionText.value, deltaVal: latestDeltaString, minAgoVal: Observable.shared.minAgoText.value, alertLabelVal: "Latest BG")*/ } } diff --git a/LoopFollow/ViewControllers/SnoozeViewController.swift b/LoopFollow/ViewControllers/SnoozeViewController.swift index ebe48c819..652fe1179 100644 --- a/LoopFollow/ViewControllers/SnoozeViewController.swift +++ b/LoopFollow/ViewControllers/SnoozeViewController.swift @@ -9,7 +9,7 @@ import UIKit import UserNotifications - +//TODO class SnoozeViewController: UIViewController, UNUserNotificationCenterDelegate { var appStateController: AppStateController? var snoozeTabItem: UITabBarItem = UITabBarItem() From 7309f162b0ecb0713f636447536d605efc6b3ba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 26 Apr 2025 20:21:57 +0200 Subject: [PATCH 004/138] iOS 16 and stale bg --- LoopFollow.xcodeproj/project.pbxproj | 2 ++ LoopFollow/Controllers/Alarms.swift | 6 +++--- LoopFollow/Controllers/Nightscout/BGData.swift | 18 +++--------------- LoopFollow/Snoozer/SnoozerView.swift | 14 ++++++++++---- LoopFollow/Storage/Observable.swift | 3 +-- .../ViewControllers/MainViewController.swift | 11 ++++++++--- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 54a6a49e8..043fcaa21 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -1948,6 +1948,7 @@ DEFINES_MODULE = YES; DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; INFOPLIST_FILE = LoopFollow/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1970,6 +1971,7 @@ DEFINES_MODULE = YES; DEVELOPMENT_TEAM = "$(LF_DEVELOPMENT_TEAM)"; INFOPLIST_FILE = LoopFollow/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/LoopFollow/Controllers/Alarms.swift b/LoopFollow/Controllers/Alarms.swift index e44a3f9e4..ec07603c7 100644 --- a/LoopFollow/Controllers/Alarms.swift +++ b/LoopFollow/Controllers/Alarms.swift @@ -672,7 +672,7 @@ extension MainViewController { if !UserDefaultsRepository.alertAudioDuringPhone.value && isOnPhoneCall() { audioDuringCall = false } guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } - snoozer.updateDisplayWhenTriggered(bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: Observable.shared.directionText.value, deltaVal: latestDeltaString, minAgoVal: Observable.shared.minAgoText.value, alertLabelVal: AlarmSound.whichAlarm) + snoozer.updateDisplayWhenTriggered(bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: Observable.shared.directionText.value, deltaVal: Observable.shared.deltaText.value, minAgoVal: Observable.shared.minAgoText.value, alertLabelVal: AlarmSound.whichAlarm) if audio && !UserDefaultsRepository.alertMuteAllIsMuted.value && audioDuringCall{ AlarmSound.setSoundFile(str: sound) AlarmSound.play(overrideVolume: overrideVolume, numLoops: numLoops) @@ -688,7 +688,7 @@ extension MainViewController { if !UserDefaultsRepository.alertAudioDuringPhone.value && isOnPhoneCall() { audioDuringCall = false } guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } - snoozer.updateDisplayWhenTriggered(bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: Observable.shared.directionText.value, deltaVal: latestDeltaString, minAgoVal: Observable.shared.minAgoText.value, alertLabelVal: AlarmSound.whichAlarm) + snoozer.updateDisplayWhenTriggered(bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: Observable.shared.directionText.value, deltaVal: Observable.shared.deltaText.value, minAgoVal: Observable.shared.minAgoText.value, alertLabelVal: AlarmSound.whichAlarm) snoozer.SnoozeButton.isHidden = false snoozer.AlertLabel.isHidden = false snoozer.clockLabel.isHidden = true @@ -721,7 +721,7 @@ extension MainViewController { AlarmSound.whichAlarm = "none" guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } - snoozer.updateDisplayWhenTriggered(bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: Observable.shared.directionText.value, deltaVal: latestDeltaString, minAgoVal: Observable.shared.minAgoText.value, alertLabelVal: AlarmSound.whichAlarm) + snoozer.updateDisplayWhenTriggered(bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: Observable.shared.directionText.value, deltaVal: Observable.shared.deltaText.value, minAgoVal: Observable.shared.minAgoText.value, alertLabelVal: AlarmSound.whichAlarm) snoozer.SnoozeButton.isHidden = true snoozer.AlertLabel.isHidden = true if AlarmSound.isPlaying { diff --git a/LoopFollow/Controllers/Nightscout/BGData.swift b/LoopFollow/Controllers/Nightscout/BGData.swift index e2e243455..0d210d708 100644 --- a/LoopFollow/Controllers/Nightscout/BGData.swift +++ b/LoopFollow/Controllers/Nightscout/BGData.swift @@ -240,8 +240,6 @@ extension MainViewController { let deltaTime = (TimeInterval(Date().timeIntervalSince1970) - lastBGTime) / 60 self.updateServerText(with: sourceName) - var snoozerDelta = "" - // Set BGText with the latest BG value self.setBGTextColor() @@ -257,13 +255,10 @@ extension MainViewController { // Delta handling if deltaBG < 0 { - self.latestDeltaString = Localizer.toDisplayUnits(String(deltaBG)) - + Observable.shared.deltaText.value = Localizer.toDisplayUnits(String(deltaBG)) } else { - self.latestDeltaString = "+" + Localizer.toDisplayUnits(String(deltaBG)) + Observable.shared.deltaText.value = "+" + Localizer.toDisplayUnits(String(deltaBG)) } - self.DeltaText.text = self.latestDeltaString - snoozerDelta = self.latestDeltaString // Apply strikethrough to BGText based on the staleness of the data let bgTextStr = self.BGText.text ?? "" @@ -278,20 +273,13 @@ extension MainViewController { } self.BGText.attributedText = attributeString - // Snoozer Display - /* - TODO - guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } - snoozer.DirectionLabel.text = snoozerDirection - snoozer.DeltaLabel.text = snoozerDelta -*/ // Update contact if Storage.shared.contactEnabled.value { self.contactImageUpdater .updateContactImage( bgValue: bgTextStr, trend: Observable.shared.directionText.value, - delta: snoozerDelta, + delta: Observable.shared.deltaText.value, stale: deltaTime >= 12 ) } diff --git a/LoopFollow/Snoozer/SnoozerView.swift b/LoopFollow/Snoozer/SnoozerView.swift index f2706453d..9216bc7e7 100644 --- a/LoopFollow/Snoozer/SnoozerView.swift +++ b/LoopFollow/Snoozer/SnoozerView.swift @@ -11,8 +11,10 @@ import SwiftUI struct SnoozerView: View { @ObservedObject var minAgoText = Observable.shared.minAgoText @ObservedObject var bgText = Observable.shared.bgText - @ObservedObject var bgTextcolor = Observable.shared.bgTextColor + @ObservedObject var bgTextColor = Observable.shared.bgTextColor @ObservedObject var directionText = Observable.shared.directionText + @ObservedObject var deltaText = Observable.shared.deltaText + @ObservedObject var bgStale = Observable.shared.bgStale @Binding var snoozeMinutes: Int var onSnooze: () -> Void @@ -23,12 +25,16 @@ struct SnoozerView: View { .edgesIgnoringSafeArea(.all) VStack(spacing: 0) { - // MARK: Main numbers block VStack(spacing: 0) { Text(bgText.value) .font(.system(size: 220, weight: .black)) .minimumScaleFactor(0.5) - .foregroundColor(Observable.shared.bgTextColor.value) + .foregroundColor(bgTextColor.value) + .strikethrough( + bgStale.value, + pattern: .solid, + color: bgStale.value ? .red : .clear + ) .frame(maxWidth: .infinity, maxHeight: 167) Text(directionText.value) @@ -37,7 +43,7 @@ struct SnoozerView: View { .foregroundColor(.white) .frame(maxWidth: .infinity, maxHeight: 96) - Text(""/*deltaValue.value*/) + Text(deltaText.value) .font(.system(size: 70)) .minimumScaleFactor(0.5) .foregroundColor(.white.opacity(0.8)) diff --git a/LoopFollow/Storage/Observable.swift b/LoopFollow/Storage/Observable.swift index 0d0fc5dd2..01f463bbb 100644 --- a/LoopFollow/Storage/Observable.swift +++ b/LoopFollow/Storage/Observable.swift @@ -19,8 +19,7 @@ class Observable { var minAgoText = ObservableValue(default: "?? min ago") var bgText = ObservableValue(default: "BG") - //TODO - var bgTextStale = ObservableValue(default: true) + var bgStale = ObservableValue(default: true) var bgTextColor = ObservableValue(default: .yellow) var directionText = ObservableValue(default: "-") var deltaText = ObservableValue(default: "+0") diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 0c97c0a0a..d4485c215 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -107,7 +107,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele var deviceBatteryData: [DataStructs.batteryStruct] = [] var newBGPulled = false var lastCalDate: Double = 0 - var latestDeltaString = "" var latestLoopStatusString = "" var latestCOB: CarbMetric? var latestBasal = "" @@ -246,13 +245,19 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } .store(in: &cancellables) - Observable.shared.directionText - .$value + Observable.shared.directionText.$value .receive(on: DispatchQueue.main) .sink { [weak self] newValue in self?.DirectionText.text = newValue } .store(in: &cancellables) + + Observable.shared.deltaText.$value + .receive(on: DispatchQueue.main) + .sink { [weak self] newValue in + self?.DeltaText.text = newValue + } + .store(in: &cancellables) } deinit { From 1f4a80a8d466c2ec8cdbb3310d5ee1fc7b926015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 26 Apr 2025 20:52:57 +0200 Subject: [PATCH 005/138] Landscape snoozer --- LoopFollow/Controllers/NightScout.swift | 15 -- .../Controllers/Nightscout/BGData.swift | 8 +- LoopFollow/Snoozer/SnoozerView.swift | 181 ++++++++++-------- LoopFollow/Storage/Observable.swift | 5 - 4 files changed, 112 insertions(+), 97 deletions(-) diff --git a/LoopFollow/Controllers/NightScout.swift b/LoopFollow/Controllers/NightScout.swift index 90c321b10..9e57a384f 100644 --- a/LoopFollow/Controllers/NightScout.swift +++ b/LoopFollow/Controllers/NightScout.swift @@ -51,21 +51,6 @@ extension MainViewController { var absorptionTime: Int } - func isStaleData() -> Bool { - if bgData.count > 0 { - let now = dateTimeUtils.getNowTimeIntervalUTC() - let lastReadingTime = bgData.last!.date - let secondsAgo = now - lastReadingTime - if secondsAgo >= 20*60 { - return true - } else { - return false - } - } else { - return false - } - } - func clearOldTempBasal() { basalData.removeAll() diff --git a/LoopFollow/Controllers/Nightscout/BGData.swift b/LoopFollow/Controllers/Nightscout/BGData.swift index 0d210d708..419654eb7 100644 --- a/LoopFollow/Controllers/Nightscout/BGData.swift +++ b/LoopFollow/Controllers/Nightscout/BGData.swift @@ -260,11 +260,15 @@ extension MainViewController { Observable.shared.deltaText.value = "+" + Localizer.toDisplayUnits(String(deltaBG)) } + // Stale + Observable.shared.bgStale.value = deltaTime >= 12 + // Apply strikethrough to BGText based on the staleness of the data + // Also clear badge if bgvalue is stale let bgTextStr = self.BGText.text ?? "" let attributeString = NSMutableAttributedString(string: bgTextStr) attributeString.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: NSRange(location: 0, length: attributeString.length)) - if deltaTime >= 12 { // Data is stale + if Observable.shared.bgStale.value { // Data is stale attributeString.addAttribute(.strikethroughColor, value: UIColor.systemRed, range: NSRange(location: 0, length: attributeString.length)) self.updateBadge(val: 0) } else { // Data is fresh @@ -280,7 +284,7 @@ extension MainViewController { bgValue: bgTextStr, trend: Observable.shared.directionText.value, delta: Observable.shared.deltaText.value, - stale: deltaTime >= 12 + stale: Observable.shared.bgStale.value ) } } diff --git a/LoopFollow/Snoozer/SnoozerView.swift b/LoopFollow/Snoozer/SnoozerView.swift index 9216bc7e7..f84c1be83 100644 --- a/LoopFollow/Snoozer/SnoozerView.swift +++ b/LoopFollow/Snoozer/SnoozerView.swift @@ -10,99 +10,130 @@ import SwiftUI struct SnoozerView: View { @ObservedObject var minAgoText = Observable.shared.minAgoText - @ObservedObject var bgText = Observable.shared.bgText + @ObservedObject var bgText = Observable.shared.bgText @ObservedObject var bgTextColor = Observable.shared.bgTextColor @ObservedObject var directionText = Observable.shared.directionText - @ObservedObject var deltaText = Observable.shared.deltaText - @ObservedObject var bgStale = Observable.shared.bgStale + @ObservedObject var deltaText = Observable.shared.deltaText + @ObservedObject var bgStale = Observable.shared.bgStale @Binding var snoozeMinutes: Int var onSnooze: () -> Void var body: some View { - ZStack { - Color.black - .edgesIgnoringSafeArea(.all) + GeometryReader { geo in + ZStack { + Color.black + .edgesIgnoringSafeArea(.all) - VStack(spacing: 0) { - VStack(spacing: 0) { - Text(bgText.value) - .font(.system(size: 220, weight: .black)) - .minimumScaleFactor(0.5) - .foregroundColor(bgTextColor.value) - .strikethrough( - bgStale.value, - pattern: .solid, - color: bgStale.value ? .red : .clear - ) - .frame(maxWidth: .infinity, maxHeight: 167) + Group { + if geo.size.width > geo.size.height { + // Landscape: two columns + HStack(spacing: 0) { + leftColumn + rightColumn + } + } else { + // Portrait: single column + VStack(spacing: 0) { + leftColumn + rightColumn + } + } + } + .frame(width: geo.size.width, height: geo.size.height) + } + } + } - Text(directionText.value) - .font(.system(size: 110, weight: .black)) - .minimumScaleFactor(0.5) - .foregroundColor(.white) - .frame(maxWidth: .infinity, maxHeight: 96) + // MARK: - Left Column (BG / Direction / Delta / Age) + private var leftColumn: some View { + VStack(spacing: 0) { + Text(bgText.value) + .font(.system(size: 220, weight: .black)) + .minimumScaleFactor(0.5) + .foregroundColor(bgTextColor.value) + .strikethrough( + bgStale.value, + pattern: .solid, + color: bgStale.value ? .red : .clear + ) + .frame(maxWidth: .infinity, maxHeight: 167) - Text(deltaText.value) - .font(.system(size: 70)) - .minimumScaleFactor(0.5) - .foregroundColor(.white.opacity(0.8)) - .frame(maxWidth: .infinity, maxHeight: 78) + Text(directionText.value) + .font(.system(size: 110, weight: .black)) + .minimumScaleFactor(0.5) + .foregroundColor(.white) + .frame(maxWidth: .infinity, maxHeight: 96) - Text(minAgoText.value) - .font(.system(size: 70)) - .minimumScaleFactor(0.5) - .foregroundColor(.white.opacity(0.6)) - .frame(maxWidth: .infinity, maxHeight: 48) - } + Text(deltaText.value) + .font(.system(size: 70)) + .minimumScaleFactor(0.5) + .foregroundColor(.white.opacity(0.8)) + .frame(maxWidth: .infinity, maxHeight: 78) + + Text(minAgoText.value) + .font(.system(size: 70)) + .minimumScaleFactor(0.5) + .foregroundColor(.white.opacity(0.6)) + .frame(maxWidth: .infinity, maxHeight: 48) + } + .padding(.top, 16) + .padding(.horizontal, 16) + } + + // MARK: - Right Column (Clock/Alert + Snooze Controls) + private var rightColumn: some View { + VStack(spacing: 0) { + Spacer() - // MARK: Clock + optional alert label - VStack(spacing: 8) { - Text("19:59"/*time.value*/) - .font(.system(size: 70)) + // Clock and (optional) alert + VStack(spacing: 8) { + Text("19:59" /* replace with time.value */) + .font(.system(size: 70)) + .minimumScaleFactor(0.5) + .foregroundColor(.white) + .frame(height: 78) + + /* + if let alarm = alarmText.value, !alarm.isEmpty { + Text(alarm) + .font(.system(size: 40, weight: .semibold)) + .foregroundColor(.red) .minimumScaleFactor(0.5) - .foregroundColor(.white) - .frame(height: 78) -/* - if let alarm = ""/*alarmText.value*/, !alarm.isEmpty { - Text(alarm) - .font(.system(size: 40, weight: .semibold)) - .foregroundColor(.red) - .minimumScaleFactor(0.5) - .lineLimit(1) - .frame(height: 48) - }*/ + .lineLimit(1) + .frame(height: 48) } - .padding(.vertical, 16) + */ + } - Spacer() + Spacer() - // MARK: Snooze controls - HStack(spacing: 12) { - Text("Snooze for") - .font(.system(size: 20)) - .foregroundColor(.white) - Text("\(snoozeMinutes) min") - .font(.system(size: 20, weight: .semibold)) - .foregroundColor(.white) - Spacer() - Stepper("", value: $snoozeMinutes, in: 1...60) - .labelsHidden() - } - .padding(.horizontal, 32) - .frame(height: 44) + // Snooze controls + HStack(spacing: 12) { + Text("Snooze for") + .font(.system(size: 20)) + .foregroundColor(.white) + Text("\(snoozeMinutes) min") + .font(.system(size: 20, weight: .semibold)) + .foregroundColor(.white) + Spacer() + Stepper("", value: $snoozeMinutes, in: 1...60) + .labelsHidden() + } + .padding(.horizontal, 32) + .frame(height: 44) - Button(action: onSnooze) { - Text("Snooze") - .font(.system(size: 24, weight: .bold)) - .frame(maxWidth: .infinity, minHeight: 56) - } - .background(Color(white: 0.15)) - .foregroundColor(.white) - .cornerRadius(8) - .padding(.horizontal, 32) - .padding(.bottom, 32) + Button(action: onSnooze) { + Text("Snooze") + .font(.system(size: 24, weight: .bold)) + .frame(maxWidth: .infinity, minHeight: 56) } + .background(Color(white: 0.15)) + .foregroundColor(.white) + .cornerRadius(8) + .padding(.horizontal, 32) + .padding(.bottom, 32) } + .padding(.top, 16) } } diff --git a/LoopFollow/Storage/Observable.swift b/LoopFollow/Storage/Observable.swift index 01f463bbb..82e49053f 100644 --- a/LoopFollow/Storage/Observable.swift +++ b/LoopFollow/Storage/Observable.swift @@ -24,10 +24,5 @@ class Observable { var directionText = ObservableValue(default: "-") var deltaText = ObservableValue(default: "+0") - - // Work in progress here.. - var trendArrow = ObservableValue(default: "→") - var alarmTitle = ObservableValue(default: nil) - private init() {} } From 03c930c0c185611870d69a0f6624ed760ae43514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 26 Apr 2025 21:00:47 +0200 Subject: [PATCH 006/138] Removal of dead code --- .../ViewControllers/MainViewController.swift | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index d4485c215..c62669184 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -51,31 +51,15 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele var appStateController: AppStateController? // Variables for BG Charts - public var numPoints: Int = 13 - public var linePlotData: [Double] = [] - public var linePlotDataTime: [Double] = [] var firstGraphLoad: Bool = true - var firstBasalGraphLoad: Bool = true - var minAgoBG: Double = 0.0 var currentOverride = 1.0 - // Vars for NS Pull - var mmol = false as Bool - var apnsKey = UserDefaultsRepository.token.value as String - var defaults : UserDefaults? - let consoleLogging = true - var timeofLastBGUpdate = 0 as TimeInterval var currentSage : sageData? var currentCage : cageData? var currentIage : iageData? var backgroundTask = BackgroundTask() - - // Refresh NS Data - var timer = Timer() - // check every 30 Seconds whether new bgvalues should be retrieved - let timeInterval: TimeInterval = 30.0 - + // Check Alarms Timer // Don't check within 1 minute of alarm triggering to give the snoozer time to save data var checkAlarmTimer = Timer() @@ -105,7 +89,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele var noteGraphData: [DataStructs.noteStruct] = [] var chartData = LineChartData() var deviceBatteryData: [DataStructs.batteryStruct] = [] - var newBGPulled = false var lastCalDate: Double = 0 var latestLoopStatusString = "" var latestCOB: CarbMetric? @@ -131,8 +114,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele // calendar setup let store = EKEventStore() - var snoozeTabItem: UITabBarItem = UITabBarItem() - // Stores the time of the last speech announcement to prevent repeated announcements. // This is a temporary safeguard until the issue with multiple calls to speakBG is fixed. var lastSpeechTime: Date? From d7c970899ead982d04bdd9dc8021f76da3cb8066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 26 Apr 2025 23:22:40 +0200 Subject: [PATCH 007/138] Show actual time --- LoopFollow/Snoozer/SnoozerView.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/LoopFollow/Snoozer/SnoozerView.swift b/LoopFollow/Snoozer/SnoozerView.swift index f84c1be83..040c94ca3 100644 --- a/LoopFollow/Snoozer/SnoozerView.swift +++ b/LoopFollow/Snoozer/SnoozerView.swift @@ -86,13 +86,16 @@ struct SnoozerView: View { VStack(spacing: 0) { Spacer() - // Clock and (optional) alert VStack(spacing: 8) { - Text("19:59" /* replace with time.value */) + TimelineView(.periodic(from: .now, by: 1)) { context in + Text(context.date, format: + Date.FormatStyle(date: .omitted, time: .shortened) + ) .font(.system(size: 70)) .minimumScaleFactor(0.5) .foregroundColor(.white) .frame(height: 78) + } /* if let alarm = alarmText.value, !alarm.isEmpty { From 637e62a130aab57c592a9cb5109ba39834b1abcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 3 May 2025 17:55:37 +0200 Subject: [PATCH 008/138] Work in progress --- LoopFollow.xcodeproj/project.pbxproj | 100 +- LoopFollow/Alarm/Alarm.swift | 93 +- .../Alarm/AlarmCondition/AlarmCondition.swift | 12 +- LoopFollow/Alarm/AlarmContext.swift | 15 - LoopFollow/Alarm/AlarmManager.swift | 44 +- LoopFollow/Alarm/AlarmType.swift | 15 +- LoopFollow/Controllers/AlarmSound.swift | 43 +- LoopFollow/Controllers/Alarms.swift | 1157 ----------------- LoopFollow/Controllers/SpeakBG.swift | 169 +++ LoopFollow/Controllers/Timers.swift | 46 +- LoopFollow/Helpers/isOnPhoneCall.swift | 16 + .../ObservableUserDefaultsValue.swift | 0 .../{ => Framework}/ObservableValue.swift | 0 .../{ => Framework}/SecureStorageValue.swift | 0 .../{ => Framework}/StorageValue.swift | 0 .../UserData.xcdatamodel/contents | 0 .../{ => Framework}/UserDefaultsValue.swift | 0 .../UserDefaultsValueGroups.swift | 0 LoopFollow/Storage/Observable.swift | 4 + .../Storage/ObservableUserDefaults.swift | 4 + LoopFollow/Storage/Storage.swift | 6 +- LoopFollow/Storage/UserDefaults.swift | 4 + LoopFollow/Task/AlarmTask.swift | 19 +- .../ViewControllers/MainViewController.swift | 7 - .../SnoozeViewController.swift | 498 ------- LoopFollowTests/AlwaysTrueCondition.swift | 3 +- 26 files changed, 456 insertions(+), 1799 deletions(-) delete mode 100644 LoopFollow/Alarm/AlarmContext.swift delete mode 100644 LoopFollow/Controllers/Alarms.swift create mode 100644 LoopFollow/Controllers/SpeakBG.swift create mode 100644 LoopFollow/Helpers/isOnPhoneCall.swift rename LoopFollow/Storage/{ => Framework}/ObservableUserDefaultsValue.swift (100%) rename LoopFollow/Storage/{ => Framework}/ObservableValue.swift (100%) rename LoopFollow/Storage/{ => Framework}/SecureStorageValue.swift (100%) rename LoopFollow/Storage/{ => Framework}/StorageValue.swift (100%) rename LoopFollow/Storage/{ => Framework}/UserData.xcdatamodeld/UserData.xcdatamodel/contents (100%) rename LoopFollow/Storage/{ => Framework}/UserDefaultsValue.swift (100%) rename LoopFollow/Storage/{ => Framework}/UserDefaultsValueGroups.swift (100%) delete mode 100644 LoopFollow/ViewControllers/SnoozeViewController.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 043fcaa21..7dba17289 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 70; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -58,7 +58,6 @@ DD493AE92ACF2445009A6922 /* BGData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE82ACF2445009A6922 /* BGData.swift */; }; DD4AFB3B2DB55CB600BB593F /* TimeOfDay.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB3A2DB55CB600BB593F /* TimeOfDay.swift */; }; DD4AFB3D2DB55D2900BB593F /* AlarmConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB3C2DB55D2900BB593F /* AlarmConfiguration.swift */; }; - DD4AFB3F2DB55EA700BB593F /* AlarmContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB3E2DB55EA700BB593F /* AlarmContext.swift */; }; DD4AFB422DB5655700BB593F /* BuildExpireConditionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB412DB5652400BB593F /* BuildExpireConditionTests.swift */; }; DD4AFB432DB5655D00BB593F /* AlwaysTrueCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB402DB5651500BB593F /* AlwaysTrueCondition.swift */; }; DD4AFB492DB576C200BB593F /* AlarmSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB482DB576C200BB593F /* AlarmSettingsView.swift */; }; @@ -110,6 +109,13 @@ DDBE3ABD2CB5A961006B37DC /* OverrideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDBE3ABC2CB5A961006B37DC /* OverrideView.swift */; }; DDC7E5152DBCFA7900EB1127 /* SnoozerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5132DBCE1B900EB1127 /* SnoozerViewController.swift */; }; DDC7E5162DBCFA7F00EB1127 /* SnoozerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5122DBCE1B900EB1127 /* SnoozerView.swift */; }; + DDC7E5382DBD887400EB1127 /* isOnPhoneCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5372DBD887400EB1127 /* isOnPhoneCall.swift */; }; + DDC7E5422DBD8A1600EB1127 /* AlarmEditorFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5392DBD8A1600EB1127 /* AlarmEditorFields.swift */; }; + DDC7E5432DBD8A1600EB1127 /* SoundFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E53A2DBD8A1600EB1127 /* SoundFile.swift */; }; + DDC7E5442DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E53C2DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift */; }; + DDC7E5452DBD8A1600EB1127 /* HighBgAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E53D2DBD8A1600EB1127 /* HighBgAlarmEditor.swift */; }; + DDC7E5462DBD8A1600EB1127 /* LowBgAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E53E2DBD8A1600EB1127 /* LowBgAlarmEditor.swift */; }; + DDC7E5472DBD8A1600EB1127 /* AlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5402DBD8A1600EB1127 /* AlarmEditor.swift */; }; DDCF979424C0D380002C9752 /* UIViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979324C0D380002C9752 /* UIViewExtension.swift */; }; DDCF979624C1443C002C9752 /* GeneralSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979524C1443C002C9752 /* GeneralSettingsViewController.swift */; }; DDCF979824C1489C002C9752 /* GraphSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */; }; @@ -150,7 +156,7 @@ DDFF3D892D1429AB00BF9D9E /* BackgroundRefreshType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFF3D882D1429AB00BF9D9E /* BackgroundRefreshType.swift */; }; FC16A97A24996673003D6245 /* NightScout.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC16A97924996673003D6245 /* NightScout.swift */; }; FC16A97B249966A3003D6245 /* AlarmSound.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC7CE589248ABEA3001F83B8 /* AlarmSound.swift */; }; - FC16A97D24996747003D6245 /* Alarms.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC16A97C24996747003D6245 /* Alarms.swift */; }; + FC16A97D24996747003D6245 /* SpeakBG.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC16A97C24996747003D6245 /* SpeakBG.swift */; }; FC16A97F249969E2003D6245 /* Graphs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC16A97E249969E2003D6245 /* Graphs.swift */; }; FC16A98124996C07003D6245 /* DateTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC16A98024996C07003D6245 /* DateTime.swift */; }; FC1BDD2B24A22650001B652C /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC1BDD2A24A22650001B652C /* Stats.swift */; }; @@ -273,7 +279,6 @@ FC7CE58D248ABEF2001F83B8 /* alarm.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE58B248ABEF1001F83B8 /* alarm.mp3 */; }; FC7CE58E248ABEF2001F83B8 /* alarm-notification.m4a in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE58C248ABEF1001F83B8 /* alarm-notification.m4a */; }; FC7CE59C248D33A9001F83B8 /* dragbar.png in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE59B248D33A9001F83B8 /* dragbar.png */; }; - FC7CE59F248D8D23001F83B8 /* SnoozeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC7CE59E248D8D23001F83B8 /* SnoozeViewController.swift */; }; FC8589BF252B54F500C8FC73 /* Mobileprovision.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC8589BE252B54F500C8FC73 /* Mobileprovision.swift */; }; FC9788182485969B00A7906C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC9788172485969B00A7906C /* AppDelegate.swift */; }; FC97881A2485969B00A7906C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC9788192485969B00A7906C /* SceneDelegate.swift */; }; @@ -365,7 +370,6 @@ DD493AE82ACF2445009A6922 /* BGData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGData.swift; sourceTree = ""; }; DD4AFB3A2DB55CB600BB593F /* TimeOfDay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeOfDay.swift; sourceTree = ""; }; DD4AFB3C2DB55D2900BB593F /* AlarmConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmConfiguration.swift; sourceTree = ""; }; - DD4AFB3E2DB55EA700BB593F /* AlarmContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmContext.swift; sourceTree = ""; }; DD4AFB402DB5651500BB593F /* AlwaysTrueCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlwaysTrueCondition.swift; sourceTree = ""; }; DD4AFB412DB5652400BB593F /* BuildExpireConditionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildExpireConditionTests.swift; sourceTree = ""; }; DD4AFB482DB576C200BB593F /* AlarmSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSettingsView.swift; sourceTree = ""; }; @@ -418,6 +422,13 @@ DDBE3ABC2CB5A961006B37DC /* OverrideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideView.swift; sourceTree = ""; }; DDC7E5122DBCE1B900EB1127 /* SnoozerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozerView.swift; sourceTree = ""; }; DDC7E5132DBCE1B900EB1127 /* SnoozerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozerViewController.swift; sourceTree = ""; }; + DDC7E5372DBD887400EB1127 /* isOnPhoneCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = isOnPhoneCall.swift; sourceTree = ""; }; + DDC7E5392DBD8A1600EB1127 /* AlarmEditorFields.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmEditorFields.swift; sourceTree = ""; }; + DDC7E53A2DBD8A1600EB1127 /* SoundFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundFile.swift; sourceTree = ""; }; + DDC7E53C2DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildExpireAlarmEditor.swift; sourceTree = ""; }; + DDC7E53D2DBD8A1600EB1127 /* HighBgAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighBgAlarmEditor.swift; sourceTree = ""; }; + DDC7E53E2DBD8A1600EB1127 /* LowBgAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LowBgAlarmEditor.swift; sourceTree = ""; }; + DDC7E5402DBD8A1600EB1127 /* AlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmEditor.swift; sourceTree = ""; }; DDCF979324C0D380002C9752 /* UIViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtension.swift; sourceTree = ""; }; DDCF979524C1443C002C9752 /* GeneralSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsViewController.swift; sourceTree = ""; }; DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphSettingsViewController.swift; sourceTree = ""; }; @@ -458,7 +469,7 @@ DDFF3D882D1429AB00BF9D9E /* BackgroundRefreshType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundRefreshType.swift; sourceTree = ""; }; ECA3EFB4037410B4973BB632 /* Pods-LoopFollow.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LoopFollow.debug.xcconfig"; path = "Target Support Files/Pods-LoopFollow/Pods-LoopFollow.debug.xcconfig"; sourceTree = ""; }; FC16A97924996673003D6245 /* NightScout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightScout.swift; sourceTree = ""; }; - FC16A97C24996747003D6245 /* Alarms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alarms.swift; sourceTree = ""; }; + FC16A97C24996747003D6245 /* SpeakBG.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeakBG.swift; sourceTree = ""; }; FC16A97E249969E2003D6245 /* Graphs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Graphs.swift; sourceTree = ""; }; FC16A98024996C07003D6245 /* DateTime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTime.swift; sourceTree = ""; }; FC1BDD2A24A22650001B652C /* Stats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stats.swift; sourceTree = ""; }; @@ -581,7 +592,6 @@ FC7CE58B248ABEF1001F83B8 /* alarm.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = alarm.mp3; sourceTree = ""; }; FC7CE58C248ABEF1001F83B8 /* alarm-notification.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = "alarm-notification.m4a"; sourceTree = ""; }; FC7CE59B248D33A9001F83B8 /* dragbar.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = dragbar.png; sourceTree = ""; }; - FC7CE59E248D8D23001F83B8 /* SnoozeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozeViewController.swift; sourceTree = ""; }; FC8589BE252B54F500C8FC73 /* Mobileprovision.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mobileprovision.swift; sourceTree = ""; }; FC8DEEE62485D1ED0075863F /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; FC9788142485969B00A7906C /* Loop Follow.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Loop Follow.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -615,10 +625,6 @@ FCFEECA1248857A600402A7F /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ -/* Begin PBXFileSystemSynchronizedRootGroup section */ - DD4AFB4A2DB684A200BB593F /* AlarmEditing */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = AlarmEditing; sourceTree = ""; }; -/* End PBXFileSystemSynchronizedRootGroup section */ - /* Begin PBXFrameworksBuildPhase section */ DD02475E2DB2EB9A00FCADF6 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -888,13 +894,53 @@ path = Snoozer; sourceTree = ""; }; + DDC7E53B2DBD8A1600EB1127 /* Components */ = { + isa = PBXGroup; + children = ( + DDC7E5392DBD8A1600EB1127 /* AlarmEditorFields.swift */, + DDC7E53A2DBD8A1600EB1127 /* SoundFile.swift */, + ); + path = Components; + sourceTree = ""; + }; + DDC7E53F2DBD8A1600EB1127 /* Editors */ = { + isa = PBXGroup; + children = ( + DDC7E53C2DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift */, + DDC7E53D2DBD8A1600EB1127 /* HighBgAlarmEditor.swift */, + DDC7E53E2DBD8A1600EB1127 /* LowBgAlarmEditor.swift */, + ); + path = Editors; + sourceTree = ""; + }; + DDC7E5412DBD8A1600EB1127 /* AlarmEditing */ = { + isa = PBXGroup; + children = ( + DDC7E53B2DBD8A1600EB1127 /* Components */, + DDC7E53F2DBD8A1600EB1127 /* Editors */, + DDC7E5402DBD8A1600EB1127 /* AlarmEditor.swift */, + ); + path = AlarmEditing; + sourceTree = ""; + }; + DDC7E5CD2DC6637800EB1127 /* Storage */ = { + isa = PBXGroup; + children = ( + FCC688512489363F00A0279D /* Framework */, + FCC6886424898EEE00A0279D /* UserDefaults.swift */, + DDD10F002C510C6B00D76A8E /* ObservableUserDefaults.swift */, + DDD10F062C529DE800D76A8E /* Observable.swift */, + DD4878042C7B2C970048F05C /* Storage.swift */, + ); + path = Storage; + sourceTree = ""; + }; DDCF9A7E2D85FCE6004DF4DD /* Alarm */ = { isa = PBXGroup; children = ( DD4AFB602DB68BBC00BB593F /* AlarmListView.swift */, - DD4AFB4A2DB684A200BB593F /* AlarmEditing */, + DDC7E5412DBD8A1600EB1127 /* AlarmEditing */, DD4AFB482DB576C200BB593F /* AlarmSettingsView.swift */, - DD4AFB3E2DB55EA700BB593F /* AlarmContext.swift */, DD4AFB3C2DB55D2900BB593F /* AlarmConfiguration.swift */, DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */, DDCF9A8B2D86005E004DF4DD /* AlarmManager.swift */, @@ -1000,7 +1046,7 @@ DDCF979D24C2382A002C9752 /* AppStateController.swift */, FC7CE589248ABEA3001F83B8 /* AlarmSound.swift */, FC16A97924996673003D6245 /* NightScout.swift */, - FC16A97C24996747003D6245 /* Alarms.swift */, + FC16A97C24996747003D6245 /* SpeakBG.swift */, FC16A97E249969E2003D6245 /* Graphs.swift */, FC1BDD2A24A22650001B652C /* Stats.swift */, FC1BDD2C24A23204001B652C /* StatsView.swift */, @@ -1166,7 +1212,7 @@ DD98F54224BCEF190007425A /* Extensions */, FC16A9782499657E003D6245 /* Controllers */, FCC688542489367300A0279D /* Helpers */, - FCC688512489363F00A0279D /* Storage */, + DDC7E5CD2DC6637800EB1127 /* Storage */, FC16A97624995FEE003D6245 /* Application */, ); path = LoopFollow; @@ -1198,21 +1244,17 @@ name = Products; sourceTree = ""; }; - FCC688512489363F00A0279D /* Storage */ = { + FCC688512489363F00A0279D /* Framework */ = { isa = PBXGroup; children = ( FCC6886624898F8000A0279D /* UserDefaultsValue.swift */, FCC6886824898FB100A0279D /* UserDefaultsValueGroups.swift */, - FCC6886424898EEE00A0279D /* UserDefaults.swift */, DDD10EFE2C510C3C00D76A8E /* ObservableUserDefaultsValue.swift */, - DDD10F002C510C6B00D76A8E /* ObservableUserDefaults.swift */, DDD10F042C529DA200D76A8E /* ObservableValue.swift */, - DDD10F062C529DE800D76A8E /* Observable.swift */, DD4878022C7B297E0048F05C /* StorageValue.swift */, - DD4878042C7B2C970048F05C /* Storage.swift */, DD16AF0C2C98485400FB655A /* SecureStorageValue.swift */, ); - path = Storage; + path = Framework; sourceTree = ""; }; FCC688542489367300A0279D /* Helpers */ = { @@ -1239,6 +1281,7 @@ DDF2C00F2BEFA991007A20E6 /* GitHubService.swift */, DDF2C0112BEFB733007A20E6 /* AppVersionManager.swift */, DDF699952C5582290058A8D9 /* TextFieldWithToolBar.swift */, + DDC7E5372DBD887400EB1127 /* isOnPhoneCall.swift */, ); path = Helpers; sourceTree = ""; @@ -1251,7 +1294,6 @@ FCD49B6B24AA536E007879DC /* DebugViewController.swift */, FCFEECA1248857A600402A7F /* SettingsViewController.swift */, FCC6884F248935D800A0279D /* AlarmViewController.swift */, - FC7CE59E248D8D23001F83B8 /* SnoozeViewController.swift */, DDCF979524C1443C002C9752 /* GeneralSettingsViewController.swift */, DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */, DDCF979924C14DB4002C9752 /* WatchSettingsViewController.swift */, @@ -1297,9 +1339,6 @@ ); dependencies = ( ); - fileSystemSynchronizedGroups = ( - DD4AFB4A2DB684A200BB593F /* AlarmEditing */, - ); name = LoopFollow; packageProductDependencies = ( DD48781B2C7DAF140048F05C /* SwiftJWT */, @@ -1577,7 +1616,6 @@ DD9ACA102D34129200415D8A /* Task.swift in Sources */, DD4AFB492DB576C200BB593F /* AlarmSettingsView.swift in Sources */, DD4878202C7DAF890048F05C /* PushMessage.swift in Sources */, - FC7CE59F248D8D23001F83B8 /* SnoozeViewController.swift in Sources */, DD0247712DB4337700FCADF6 /* BuildExpireCondition.swift in Sources */, DDF6999B2C5AA32E0058A8D9 /* TempTargetPreset.swift in Sources */, DD9ACA0E2D340BFF00415D8A /* AlarmTask.swift in Sources */, @@ -1606,7 +1644,7 @@ DD16AF0F2C99592F00FB655A /* HKQuantityInputView.swift in Sources */, DDFF3D7F2D1414A200BF9D9E /* BLEDevice.swift in Sources */, DD9ACA042D32821400415D8A /* DeviceStatusTask.swift in Sources */, - FC16A97D24996747003D6245 /* Alarms.swift in Sources */, + FC16A97D24996747003D6245 /* SpeakBG.swift in Sources */, DDFD5C532CB167DA00D3FD68 /* TRCCommandType.swift in Sources */, DD16AF112C997B4600FB655A /* LoadingButtonView.swift in Sources */, DD5817172D2710E90041FB98 /* BLEDeviceSelectionView.swift in Sources */, @@ -1637,7 +1675,14 @@ FCFEECA02488157B00402A7F /* Chart.swift in Sources */, DDCF979424C0D380002C9752 /* UIViewExtension.swift in Sources */, DD0C0C6D2C48606200DBADDF /* CarbMetric.swift in Sources */, + DDC7E5422DBD8A1600EB1127 /* AlarmEditorFields.swift in Sources */, + DDC7E5432DBD8A1600EB1127 /* SoundFile.swift in Sources */, + DDC7E5442DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift in Sources */, + DDC7E5452DBD8A1600EB1127 /* HighBgAlarmEditor.swift in Sources */, + DDC7E5462DBD8A1600EB1127 /* LowBgAlarmEditor.swift in Sources */, + DDC7E5472DBD8A1600EB1127 /* AlarmEditor.swift in Sources */, DDA9ACA82D6A66E200E6F1A9 /* ContactColorOption.swift in Sources */, + DDC7E5382DBD887400EB1127 /* isOnPhoneCall.swift in Sources */, DD7E19882ACDA5DA00DBD158 /* Notes.swift in Sources */, FCEF87AC24A141A700AE6FA0 /* Localizer.swift in Sources */, FC1BDD3224A2585C001B652C /* DataStructs.swift in Sources */, @@ -1658,7 +1703,6 @@ DD1A97142D4294A5000DDC11 /* AdvancedSettingsView.swift in Sources */, DD9ACA0A2D33095600415D8A /* MinAgoTask.swift in Sources */, DD9ED0CC2D35526E000D2A63 /* SearchBar.swift in Sources */, - DD4AFB3F2DB55EA700BB593F /* AlarmContext.swift in Sources */, DDD10EFF2C510C3C00D76A8E /* ObservableUserDefaultsValue.swift in Sources */, DD58171A2D299EF80041FB98 /* DexcomHeartbeatBluetoothDevice.swift in Sources */, DDFF3D872D14280500BF9D9E /* BackgroundRefreshSettingsViewModel.swift in Sources */, diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index 8898e7e2e..66f16e66e 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -8,6 +8,7 @@ import Foundation import HealthKit +import UserNotifications enum PlaySoundOption: String, CaseIterable, Codable { case always, day, night, never @@ -93,9 +94,95 @@ struct Alarm: Identifiable, Codable, Equatable { return false } - func trigger() { - // TODO: play sound / update UI / schedule snooze etc. - print("🔔 Alarm “\(name)” triggered! Playing \(soundFile.displayName)") + /// Function for when the alarm is triggered. + /// If this alarm, all alarms is disabled or snoozed, then should not be called. This or all alarmd could be muted, then this function will just generate a notification. + func trigger(config : AlarmConfiguration, now: Date) { + LogManager.shared.log(category: .alarm, message: "Alarm triggered: \(type.rawValue)") + + var playSound: Bool = true + + // Global mute + if let until = config.muteUntil, until > now { + playSound = false + } + + // Mute during calls + if !config.audioDuringCalls && isOnPhoneCall() { + playSound = false + } + + // Mute this alarm day or night or always + let cal = Calendar.current + let today = cal.startOfDay(for: now) + let dayStart = cal.date(bySettingHour: config.dayStart.hour, + minute: config.dayStart.minute, + second: 0, + of: today)! + let nightStart = cal.date(bySettingHour: config.nightStart.hour, + minute: config.nightStart.minute, + second: 0, + of: today)! + + let isNight: Bool + if nightStart >= dayStart { + isNight = (now >= nightStart) || (now < dayStart) + } else { + isNight = (now >= nightStart) && (now < dayStart) + } + let isDay = !isNight + + switch playSoundOption { + case .always: + break + case .never: + playSound = false + case .day where !isDay: + playSound = false + case .night where !isNight: + playSound = false + default: + break + } + + let shouldRepeat: Bool = { + switch repeatSoundOption { + case .always: return true + case .never: return false + case .day: return isDay + case .night: return isNight + } + }() + + let content = UNMutableNotificationContent() + content.title = type.rawValue + content.subtitle += Observable.shared.bgText.value + " " + content.subtitle += Observable.shared.directionText.value + " " + content.subtitle += Observable.shared.deltaText.value + content.categoryIdentifier = "category" + // This is needed to trigger vibrate on watch and phone + // TODO: + // See if we can use .Critcal + // See if we should use this method instead of direct sound player + content.sound = .default + + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger) + UNUserNotificationCenter.current().add(request, withCompletionHandler: nil) + + let action = UNNotificationAction(identifier: "snooze", title: "Snooze", options: []) + let category = UNNotificationCategory(identifier: "category", actions: [action], intentIdentifiers: [], options: []) + UNUserNotificationCenter.current().setNotificationCategories([category]) + + /* TODO när vi gör bg alarm sätt timestamp/datum för denna readings tid så vi inte larmar på samma igen, se isBGBased + if snooozedBGReadingTime != nil { + UserDefaultsRepository.snoozedBGReadingTime.value = snooozedBGReadingTime + } + */ + + if playSound { + AlarmSound.setSoundFile(str: self.soundFile.rawValue) + AlarmSound.play(repeating: shouldRepeat) + } } init(type: AlarmType) { diff --git a/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift b/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift index 4034d06cb..2a341ae52 100644 --- a/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift @@ -17,19 +17,17 @@ protocol AlarmCondition { extension AlarmCondition { /// applies every global & per-alarm guard exactly once - func shouldFire(alarm: Alarm, data: AlarmData, context: AlarmContext) -> Bool { + func shouldFire(alarm: Alarm, data: AlarmData, now: Date, config: AlarmConfiguration) -> Bool { // master on/off guard alarm.isEnabled else { return false } - // global mute - if let until = context.config.muteUntil, until > context.now { return false } // per-alarm snooze - if let snooze = alarm.snoozedUntil, snooze > context.now { return false } + if let snooze = alarm.snoozedUntil, snooze > now { return false } // time-of-day guard - let comps = Calendar.current.dateComponents([.hour, .minute], from: context.now) + let comps = Calendar.current.dateComponents([.hour, .minute], from: now) let nowMin = (comps.hour! * 60) + comps.minute! - let dStart = context.config.dayStart.minutesSinceMidnight - let nStart = context.config.nightStart.minutesSinceMidnight + let dStart = config.dayStart.minutesSinceMidnight + let nStart = config.nightStart.minutesSinceMidnight let isNight = (nowMin < dStart) || (nowMin >= nStart) switch alarm.activeOption { diff --git a/LoopFollow/Alarm/AlarmContext.swift b/LoopFollow/Alarm/AlarmContext.swift deleted file mode 100644 index 27cb93fae..000000000 --- a/LoopFollow/Alarm/AlarmContext.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// AlarmContext.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-04-20. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// - - -import Foundation - -struct AlarmContext { - let now: Date - let config: AlarmConfiguration -} diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index dcc1d0cd0..d1fd0f5dd 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -27,8 +27,9 @@ class AlarmManager { evaluators = dict } + //TODO: Somehow we need to silent the current alarm if the current one is no longer active. func checkAlarms(data: AlarmData) { - let context = AlarmContext(now: Date(), config: config) + let now = Date() let alarms = Storage.shared.alarms.value let sorted = alarms.sorted { lhs, rhs in @@ -45,13 +46,48 @@ class AlarmManager { // Tertiary: fallback to insertion order return false } + var skipType: AlarmType? = nil for alarm in sorted { + // If there is already an active (snoozed) alarm of this type, skip to next [type] + if alarm.type == skipType { + continue + } + + // If the alarm itself is snoozed, skip lower‑priority alarms of the same type. + if let until = alarm.snoozedUntil, until > now { + skipType = alarm.type + continue + } + + // Evaluate the alarm condition. guard let checker = evaluators[alarm.type], - checker.shouldFire(alarm: alarm, data: data, context: context) - else { continue } - alarm.trigger() + checker.shouldFire(alarm: alarm, data: data, now: now, config: config) + else { + continue + } + + // Fire the alarm and break the loop; we only allow one alarm per evaluation tick. + + //TODO: a few things affecting the snoozed screen + Storage.shared.currentAlarm.value = alarm.id + //tabBarController?.selectedIndex = 2 + + alarm.trigger(config: config, now: now) break } } + + func snoozeCurrentAlarm(by minutes: Int) { + //guard var alarm = currentAlarm else { return } + //alarm.snoozedUntil = Date().addingTimeInterval(TimeInterval(minutes * 60)) + + // write it back to your Storage + /* + Storage.shared.alarms.value = Storage.shared.alarms.value + .map { $0.id == alarm.id ? alarm : $0 } +*/ + // clear so you can’t snooze twice + //currentAlarm = nil + } } diff --git a/LoopFollow/Alarm/AlarmType.swift b/LoopFollow/Alarm/AlarmType.swift index dc10eee90..7050c49cc 100644 --- a/LoopFollow/Alarm/AlarmType.swift +++ b/LoopFollow/Alarm/AlarmType.swift @@ -23,7 +23,7 @@ enum AlarmType: String, CaseIterable, Codable { case missedBolus = "Missed Bolus Alert" case sensorChange = "Sensor Change Alert" case pumpChange = "Pump Change Alert" - case pump = "Low Insulin Alert" + case pump = "Pump Insulin Alert" case battery = "Low Battery" case batteryDrop = "Battery Drop" case recBolus = "Rec. Bolus" @@ -94,3 +94,16 @@ enum TimeUnit { } } } + +extension AlarmType { + /// `true` for alarms whose primary trigger is a blood-glucose value + /// or its rate of change. + var isBGBased: Bool { + switch self { + case .low, .high, .fastDrop, .fastRise, .missedReading: + return true + default: + return false + } + } +} diff --git a/LoopFollow/Controllers/AlarmSound.swift b/LoopFollow/Controllers/AlarmSound.swift index bb19107c3..94a4abc94 100644 --- a/LoopFollow/Controllers/AlarmSound.swift +++ b/LoopFollow/Controllers/AlarmSound.swift @@ -112,26 +112,26 @@ class AlarmSound { //self.audioPlayer!.volume = (self.muted || (UserDefaultsRepository.fadeInTimeInterval.value > 0)) ? 0.0 : 1.0 if !self.audioPlayer!.prepareToPlay() { - NSLog("AlarmSound - audio player failed preparing to play") + LogManager.shared.log(category: .general, message: "AlarmSound - audio player failed preparing to play") } if self.audioPlayer!.play() { if !self.isPlaying { - NSLog("AlarmSound - not playing after calling play") - NSLog("AlarmSound - rate value: \(self.audioPlayer!.rate)") + LogManager.shared.log(category: .general, message: "AlarmSound - not playing after calling play") + LogManager.shared.log(category: .general, message: "AlarmSound - rate value: \(self.audioPlayer!.rate)") } } else { - NSLog("AlarmSound - audio player failed to play") + LogManager.shared.log(category: .general, message: "AlarmSound - audio player failed to play") } } catch let error { - NSLog("AlarmSound - unable to play sound; error: \(error)") + LogManager.shared.log(category: .general, message: "AlarmSound - unable to play sound; error: \(error)") } } - static func play(overrideVolume: Bool, numLoops: Int) { + static func play(repeating: Bool) { guard !self.isPlaying else { return } @@ -145,9 +145,8 @@ class AlarmSound { try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category(rawValue: convertFromAVAudioSessionCategory(AVAudioSession.Category.playback))) try AVAudioSession.sharedInstance().setActive(true) - // Play endless loops - self.audioPlayer!.numberOfLoops = numLoops - + self.audioPlayer!.numberOfLoops = repeating ? -1 : 0 + // Store existing volume if self.systemOutputVolumeBeforeOverride == nil { self.systemOutputVolumeBeforeOverride = AVAudioSession.sharedInstance().outputVolume @@ -157,16 +156,16 @@ class AlarmSound { //self.audioPlayer!.volume = (self.muted || (UserDefaultsRepository.fadeInTimeInterval.value > 0)) ? 0.0 : 1.0 if !self.audioPlayer!.prepareToPlay() { - NSLog("AlarmSound - audio player failed preparing to play") + LogManager.shared.log(category: .general, message: "AlarmSound - audio player failed preparing to play") } if self.audioPlayer!.play() { if !self.isPlaying { - NSLog("AlarmSound - not playing after calling play") - NSLog("AlarmSound - rate value: \(self.audioPlayer!.rate)") + LogManager.shared.log(category: .general, message: "AlarmSound - not playing after calling play") + LogManager.shared.log(category: .general, message: "AlarmSound - rate value: \(self.audioPlayer!.rate)") } } else { - NSLog("AlarmSound - audio player failed to play") + LogManager.shared.log(category: .general, message: "AlarmSound - audio player failed to play") } @@ -175,13 +174,11 @@ class AlarmSound { // self.audioPlayer!.setVolume(1.0, fadeDuration: UserDefaultsRepository.fadeInTimeInterval.value) //} - if overrideVolume { - MPVolumeView.setVolume(UserDefaultsRepository.forcedOutputVolume.value) + if Storage.shared.alarmConfiguration.value.overrideSystemOutputVolume { + MPVolumeView.setVolume(Storage.shared.alarmConfiguration.value.forcedOutputVolume) } - - } catch let error { - NSLog("AlarmSound - unable to play sound; error: \(error)") + LogManager.shared.log(category: .general, message: "AlarmSound - unable to play sound; error: \(error)") } } @@ -208,16 +205,16 @@ class AlarmSound { if !self.audioPlayer!.prepareToPlay() { - NSLog("Terminate AlarmSound - audio player failed preparing to play") + LogManager.shared.log(category: .general, message: "Terminate AlarmSound - audio player failed preparing to play") } if self.audioPlayer!.play() { if !self.isPlaying { - NSLog("Terminate AlarmSound - not playing after calling play") - NSLog("Terminate AlarmSound - rate value: \(self.audioPlayer!.rate)") + LogManager.shared.log(category: .general, message: "Terminate AlarmSound - not playing after calling play") + LogManager.shared.log(category: .general, message: "Terminate AlarmSound - rate value: \(self.audioPlayer!.rate)") } } else { - NSLog("Terminate AlarmSound - audio player failed to play") + LogManager.shared.log(category: .general, message: "Terminate AlarmSound - audio player failed to play") } @@ -225,7 +222,7 @@ class AlarmSound { } catch let error { - NSLog("Terminate AlarmSound - unable to play sound; error: \(error)") + LogManager.shared.log(category: .general, message: "Terminate AlarmSound - unable to play sound; error: \(error)") } } diff --git a/LoopFollow/Controllers/Alarms.swift b/LoopFollow/Controllers/Alarms.swift deleted file mode 100644 index ec07603c7..000000000 --- a/LoopFollow/Controllers/Alarms.swift +++ /dev/null @@ -1,1157 +0,0 @@ -// -// Alarms.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/16/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// - -import Foundation -import AVFoundation -import CallKit - -extension MainViewController { - func checkAlarms(bgs: [ShareGlucoseData]) { - // Don't check or fire alarms within 1 minute of prior alarm - if checkAlarmTimer.isValid { return } - - let date = Date() - let now = date.timeIntervalSince1970 - let currentBG = bgs[bgs.count - 1].sgv - - var skipZero = false - if UserDefaultsRepository.alertIgnoreZero.value && currentBG == 0 { - skipZero = true - } - - var deltas: [Int] = [] - if bgs.count > 3 { - deltas.append(bgs[bgs.count - 1].sgv - bgs[bgs.count - 2].sgv) - deltas.append(bgs[bgs.count - 2].sgv - bgs[bgs.count - 3].sgv) - deltas.append(bgs[bgs.count - 3].sgv - bgs[bgs.count - 4].sgv) - } else if bgs.count > 2 { - deltas.append(bgs[bgs.count - 1].sgv - bgs[bgs.count - 2].sgv) - deltas.append(bgs[bgs.count - 2].sgv - bgs[bgs.count - 3].sgv) - // Set remainder to match the last delta we have - deltas.append(bgs[bgs.count - 2].sgv - bgs[bgs.count - 3].sgv) - } else if bgs.count > 1 { - deltas.append(bgs[bgs.count - 1].sgv - bgs[bgs.count - 2].sgv) - // Set remainder to match the last delta we have - deltas.append(bgs[bgs.count - 1].sgv - bgs[bgs.count - 2].sgv) - deltas.append(bgs[bgs.count - 1].sgv - bgs[bgs.count - 2].sgv) - } else { - // We only have 1 reading, set all to 0. - deltas.append(0) - deltas.append(0) - deltas.append(0) - } - - - let currentBGTime = bgs[bgs.count - 1].date - var numLoops = 0 - var playSound = true - checkQuietHours() - clearOldSnoozes() - - // Exit if all is snoozed - // still send persistent notification with all snoozed - if UserDefaultsRepository.alertSnoozeAllIsSnoozed.value { - persistentNotification(bgTime: currentBGTime) - return - } - - // Check IOB - if UserDefaultsRepository.alertIOB.value && !UserDefaultsRepository.alertIOBIsSnoozed.value { - var bolusCount = 0 - var totalBoluses = 0.0 - let bolusTimeAgo = dateTimeUtils.getNowTimeIntervalUTC() - Double(UserDefaultsRepository.alertIOBBolusesWithin.value * 60) - if UserDefaultsRepository.alertIOBBolusesWithin.value > 0 { - for i in 0..= bolusTimeAgo && bolusData[i].value >= UserDefaultsRepository.alertIOBAt.value { - bolusCount += 1 - totalBoluses += bolusData[i].value - } - } - } - if bolusCount >= UserDefaultsRepository.alertIOBNumber.value || - totalBoluses >= Double(UserDefaultsRepository.alertIOBMaxBoluses.value) || - (latestIOB?.value ?? 0) >= Double(UserDefaultsRepository.alertIOBMaxBoluses.value) { - AlarmSound.whichAlarm = "IOB Alert" - //determine if it is day or night and what should happen - if UserDefaultsRepository.nightTime.value { - if UserDefaultsRepository.alertIOBNightTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertIOBNightTimeAudible.value { playSound = false } - } else { - if UserDefaultsRepository.alertIOBDayTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertIOBDayTimeAudible.value { playSound = false } - } - triggerAlarm(sound: UserDefaultsRepository.alertIOBSound.value, snooozedBGReadingTime: nil, overrideVolume: UserDefaultsRepository.overrideSystemOutputVolume.value, numLoops: numLoops, snoozeTime: UserDefaultsRepository.alertIOBSnoozeHours.value, snoozeIncrement: 1, audio: playSound) - return - } - } - - // Check COB - if UserDefaultsRepository.alertCOB.value && !UserDefaultsRepository.alertCOBIsSnoozed.value { - let alertAt = Double(UserDefaultsRepository.alertCOBAt.value) - if (latestCOB?.value ?? 0) >= alertAt { - AlarmSound.whichAlarm = "COB Alert" - //determine if it is day or night and what should happen - if UserDefaultsRepository.nightTime.value { - if UserDefaultsRepository.alertCOBNightTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertCOBNightTimeAudible.value { playSound = false } - } else { - if UserDefaultsRepository.alertCOBDayTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertCOBDayTimeAudible.value { playSound = false } - } - triggerAlarm(sound: UserDefaultsRepository.alertCOBSound.value, snooozedBGReadingTime: nil, overrideVolume: UserDefaultsRepository.overrideSystemOutputVolume.value, numLoops: numLoops, snoozeTime: UserDefaultsRepository.alertCOBSnoozeHours.value, snoozeIncrement: 1, audio: playSound) - return - } - } - - - // BG Based Alarms - // Check to make sure it is a current reading and has not already triggered alarm from this reading - if now - currentBGTime <= (5*60) && currentBGTime > UserDefaultsRepository.snoozedBGReadingTime.value as! TimeInterval { - - // trigger temporary alert first - if UserDefaultsRepository.alertTemporaryActive.value { - if UserDefaultsRepository.alertTemporaryBelow.value { - if Float(currentBG) < UserDefaultsRepository.alertTemporaryBG.value { - UserDefaultsRepository.alertTemporaryActive.value = false - AlarmSound.whichAlarm = "Temporary Alert" - if UserDefaultsRepository.alertTemporaryBGRepeat.value { numLoops = -1 } - triggerAlarm(sound: UserDefaultsRepository.alertTemporarySound.value, snooozedBGReadingTime: currentBGTime, overrideVolume: UserDefaultsRepository.overrideSystemOutputVolume.value, numLoops: numLoops) - return - } - } else{ - if Float(currentBG) > UserDefaultsRepository.alertTemporaryBG.value { - tabBarController?.selectedIndex = 2 - AlarmSound.whichAlarm = "Temporary Alert" - if UserDefaultsRepository.alertTemporaryBGRepeat.value { numLoops = -1 } - triggerAlarm(sound: UserDefaultsRepository.alertTemporarySound.value, snooozedBGReadingTime: currentBGTime, overrideVolume: UserDefaultsRepository.overrideSystemOutputVolume.value, numLoops: numLoops) - return - } - } - } - - // Check Urgent Low - - let predictiveNumReadings = Int(UserDefaultsRepository.alertUrgentLowPredictiveMinutes.value / 5) - var predictiveTrigger = false - if !predictionData.isEmpty { - for i in 0.. i { - if Float(predictionData[i].sgv) <= UserDefaultsRepository.alertUrgentLowBG.value { - predictiveTrigger = true - } - } - } - } - if UserDefaultsRepository.alertUrgentLowActive.value && !UserDefaultsRepository.alertUrgentLowIsSnoozed.value && - (Float(currentBG) <= UserDefaultsRepository.alertUrgentLowBG.value || predictiveTrigger) - && skipZero == false { - // Separating this makes it so the low or drop alerts won't trigger if they already snoozed the urgent low - if !UserDefaultsRepository.alertUrgentLowIsSnoozed.value { - - if predictiveTrigger { - AlarmSound.whichAlarm = "Predicted Urgent Low Alert" - } else { - AlarmSound.whichAlarm = "Urgent Low Alert" - } - - //determine if it is day or night and what should happen - if UserDefaultsRepository.nightTime.value { - if UserDefaultsRepository.alertUrgentLowNightTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertUrgentLowNightTimeAudible.value { playSound = false } - } else { - if UserDefaultsRepository.alertUrgentLowDayTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertUrgentLowDayTimeAudible.value { playSound = false } - } - triggerAlarm(sound: UserDefaultsRepository.alertUrgentLowSound.value, snooozedBGReadingTime: currentBGTime, overrideVolume: UserDefaultsRepository.overrideSystemOutputVolume.value, numLoops: numLoops, snoozeTime: UserDefaultsRepository.alertUrgentLowSnooze.value, audio: playSound) - return - } else { - return - } - } - - // Check Low, protect index out of bounds - let persistentLowReadings = min(Int(UserDefaultsRepository.alertLowPersistent.value / 5), bgData.count - 1) - let persistentLowBG = bgData[bgData.count - 1 - persistentLowReadings].sgv - let persistentLowTriggerImmediatelyBG = UserDefaultsRepository.alertLowBG.value - UserDefaultsRepository.alertLowPersistenceMax.value - - if UserDefaultsRepository.alertLowActive.value && - !UserDefaultsRepository.alertUrgentLowIsSnoozed.value && - !UserDefaultsRepository.alertLowIsSnoozed.value && - skipZero == false && - (Float(currentBG) <= UserDefaultsRepository.alertLowBG.value && - (Float(persistentLowBG) <= UserDefaultsRepository.alertLowBG.value || Float(currentBG) <= persistentLowTriggerImmediatelyBG) - ) - { - - - AlarmSound.whichAlarm = "Low Alert" - - - - //determine if it is day or night and what should happen - if UserDefaultsRepository.nightTime.value { - if UserDefaultsRepository.alertLowNightTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertLowNightTimeAudible.value { playSound = false } - print ("It is NightTime and playSound = ", playSound) - } else { - if UserDefaultsRepository.alertLowDayTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertLowDayTimeAudible.value { playSound = false } - //print ("It is DayTime and playSound = ", playSound) - } - - triggerAlarm(sound: UserDefaultsRepository.alertLowSound.value, snooozedBGReadingTime: currentBGTime, overrideVolume: UserDefaultsRepository.overrideSystemOutputVolume.value, numLoops: numLoops, snoozeTime: UserDefaultsRepository.alertLowSnooze.value, audio: playSound) - return - } - - // Check Urgent High - if UserDefaultsRepository.alertUrgentHighActive.value && !UserDefaultsRepository.alertUrgentHighIsSnoozed.value && - Float(currentBG) >= UserDefaultsRepository.alertUrgentHighBG.value { - // Separating this makes it so the high or rise alerts won't trigger if they already snoozed the urgent high - if !UserDefaultsRepository.alertUrgentHighIsSnoozed.value { - AlarmSound.whichAlarm = "Urgent High Alert" - //determine if it is day or night and what should happen - if UserDefaultsRepository.nightTime.value { - if UserDefaultsRepository.alertUrgentHighNightTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertUrgentHighNightTimeAudible.value { playSound = false } - } else { - if UserDefaultsRepository.alertUrgentHighDayTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertUrgentHighDayTimeAudible.value { playSound = false } - } - triggerAlarm(sound: UserDefaultsRepository.alertUrgentHighSound.value, snooozedBGReadingTime: currentBGTime, overrideVolume: UserDefaultsRepository.overrideSystemOutputVolume.value, numLoops: numLoops, snoozeTime: UserDefaultsRepository.alertUrgentHighSnooze.value, audio: playSound) - return - } else { - return - } - - } - - // Check High, protect index out of bounds - let persistentHighReadings = min(Int(UserDefaultsRepository.alertHighPersistent.value / 5), bgData.count-1) - let persistentHighBG = bgData[bgData.count - 1 - persistentHighReadings].sgv - if UserDefaultsRepository.alertHighActive.value && - !UserDefaultsRepository.alertHighIsSnoozed.value && - Float(currentBG) >= UserDefaultsRepository.alertHighBG.value && - Float(persistentHighBG) >= UserDefaultsRepository.alertHighBG.value && - !UserDefaultsRepository.alertHighIsSnoozed.value { - AlarmSound.whichAlarm = "High Alert" - //determine if it is day or night and what should happen - if UserDefaultsRepository.nightTime.value { - if UserDefaultsRepository.alertHighNightTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertHighNightTimeAudible.value { playSound = false } - print ("It is NightTime and playSound = ", playSound) - } else { - if UserDefaultsRepository.alertHighDayTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertHighDayTimeAudible.value { playSound = false } - //print ("It is DayTime and playSound = ", playSound) - } - triggerAlarm(sound: UserDefaultsRepository.alertHighSound.value, snooozedBGReadingTime: currentBGTime, overrideVolume: UserDefaultsRepository.overrideSystemOutputVolume.value, numLoops: numLoops, snoozeTime: UserDefaultsRepository.alertHighSnooze.value, audio: playSound) - return - } - - - - // Check Fast Drop - if UserDefaultsRepository.alertFastDropActive.value && !UserDefaultsRepository.alertFastDropIsSnoozed.value && skipZero == false { - // make sure limit is off or BG is below value - if (!UserDefaultsRepository.alertFastDropUseLimit.value) || (UserDefaultsRepository.alertFastDropUseLimit.value && Float(currentBG) < UserDefaultsRepository.alertFastDropBelowBG.value) { - let compare = 0 - UserDefaultsRepository.alertFastDropDelta.value - - //check last 2/3/4 readings - if (UserDefaultsRepository.alertFastDropReadings.value == 2 && Float(deltas[0]) <= compare) - || (UserDefaultsRepository.alertFastDropReadings.value == 3 && Float(deltas[0]) <= compare && Float(deltas[1]) <= compare) - || (UserDefaultsRepository.alertFastDropReadings.value == 4 && Float(deltas[0]) <= compare && Float(deltas[1]) <= compare && Float(deltas[2]) <= compare) { - AlarmSound.whichAlarm = "Fast Drop Alert" - //determine if it is day or night and what should happen - if UserDefaultsRepository.nightTime.value { - if UserDefaultsRepository.alertFastDropNightTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertFastDropNightTimeAudible.value { playSound = false } - } else { - if UserDefaultsRepository.alertFastDropDayTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertFastDropDayTimeAudible.value { playSound = false } - } - triggerAlarm(sound: UserDefaultsRepository.alertFastDropSound.value, snooozedBGReadingTime: currentBGTime, overrideVolume: UserDefaultsRepository.overrideSystemOutputVolume.value, numLoops: numLoops, snoozeTime: UserDefaultsRepository.alertFastDropSnooze.value, audio: playSound) - return - } - } - } - - // Check Fast Rise - if UserDefaultsRepository.alertFastRiseActive.value && !UserDefaultsRepository.alertFastRiseIsSnoozed.value { - // make sure limit is off or BG is above value - if (!UserDefaultsRepository.alertFastRiseUseLimit.value) || (UserDefaultsRepository.alertFastRiseUseLimit.value && Float(currentBG) > UserDefaultsRepository.alertFastRiseAboveBG.value) { - let compare = UserDefaultsRepository.alertFastDropDelta.value - - //check last 2/3/4 readings - if (UserDefaultsRepository.alertFastRiseReadings.value == 2 && Float(deltas[0]) >= compare) - || (UserDefaultsRepository.alertFastRiseReadings.value == 3 && Float(deltas[0]) >= compare && Float(deltas[1]) >= compare) - || (UserDefaultsRepository.alertFastRiseReadings.value == 4 && Float(deltas[0]) >= compare && Float(deltas[1]) >= compare && Float(deltas[2]) >= compare) { - AlarmSound.whichAlarm = "Fast Rise Alert" - //determine if it is day or night and what should happen - if UserDefaultsRepository.nightTime.value { - if UserDefaultsRepository.alertFastRiseNightTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertFastRiseNightTimeAudible.value { playSound = false } - } else { - if UserDefaultsRepository.alertFastRiseDayTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertFastRiseDayTimeAudible.value { playSound = false } - } - triggerAlarm(sound: UserDefaultsRepository.alertFastRiseSound.value, snooozedBGReadingTime: currentBGTime, overrideVolume: UserDefaultsRepository.overrideSystemOutputVolume.value, numLoops: numLoops, snoozeTime: UserDefaultsRepository.alertFastRiseSnooze.value, audio: playSound) - return - } - } - } - - - - } - - // These only get checked and fire if a BG reading doesn't fire - - //check for missed reading alert - if UserDefaultsRepository.alertMissedReadingActive.value && !UserDefaultsRepository.alertMissedReadingIsSnoozed.value && (Double(now - currentBGTime) >= Double(UserDefaultsRepository.alertMissedReading.value * 60)) { - AlarmSound.whichAlarm = "Missed Reading Alert" - //determine if it is day or night and what should happen - if UserDefaultsRepository.nightTime.value { - if UserDefaultsRepository.alertMissedReadingNightTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertMissedReadingNightTimeAudible.value { playSound = false } - } else { - if UserDefaultsRepository.alertMissedReadingDayTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertMissedReadingDayTimeAudible.value { playSound = false } - } - triggerAlarm(sound: UserDefaultsRepository.alertMissedReadingSound.value, snooozedBGReadingTime: nil, overrideVolume: UserDefaultsRepository.overrideSystemOutputVolume.value, numLoops: numLoops, snoozeTime: UserDefaultsRepository.alertMissedReadingSnooze.value, audio: playSound) - return - } - - //check for not looping alert - if IsNightscoutEnabled() { - LogManager.shared.log(category: .alarm, message: "Checking NotLooping LastLoopTime was \(UserDefaultsRepository.alertLastLoopTime.value) that gives a diff of: \(Double(dateTimeUtils.getNowTimeIntervalUTC() - UserDefaultsRepository.alertLastLoopTime.value))", isDebug: true) - if UserDefaultsRepository.alertNotLoopingActive.value - && !UserDefaultsRepository.alertNotLoopingIsSnoozed.value - && (Double(dateTimeUtils.getNowTimeIntervalUTC() - UserDefaultsRepository.alertLastLoopTime.value) >= Double(UserDefaultsRepository.alertNotLooping.value * 60)) - && UserDefaultsRepository.alertLastLoopTime.value > 0 { - - if (UserDefaultsRepository.alertNotLoopingUseLimits.value - && ( - (Float(currentBG) >= UserDefaultsRepository.alertNotLoopingUpperLimit.value - || Float(currentBG) <= UserDefaultsRepository.alertNotLoopingLowerLimit.value) || - // Ignore Limits if BG reading is older than non looping time - (Double(now - currentBGTime) >= Double(UserDefaultsRepository.alertNotLooping.value * 60)) - ) || - !UserDefaultsRepository.alertNotLoopingUseLimits.value) { - AlarmSound.whichAlarm = "Not Looping Alert" - //determine if it is day or night and what should happen - if UserDefaultsRepository.nightTime.value { - if UserDefaultsRepository.alertNotLoopingNightTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertNotLoopingNightTimeAudible.value { playSound = false } - } else { - if UserDefaultsRepository.alertNotLoopingDayTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertNotLoopingDayTimeAudible.value { playSound = false } - } - triggerAlarm(sound: UserDefaultsRepository.alertNotLoopingSound.value, snooozedBGReadingTime: nil, overrideVolume: UserDefaultsRepository.overrideSystemOutputVolume.value, numLoops: numLoops, snoozeTime: UserDefaultsRepository.alertNotLoopingSnooze.value, audio: playSound) - LogManager.shared.log(category: .alarm, message: "!!!Not Looping!!!") - return - } - } - - // check for missed bolus - Only checks within 1 hour of carb entry - // Only continue if alert is active, not snooozed, we have carb data, and bg is over the ignore limit - if UserDefaultsRepository.alertMissedBolusActive.value - && !UserDefaultsRepository.alertMissedBolusIsSnoozed.value - && carbData.count > 0 - && Float(currentBG) > UserDefaultsRepository.alertMissedBolusLowGramsBG.value { - - // Grab the latest carb entry - let lastCarb = carbData[carbData.count - 1].value - let lastCarbTime = carbData[carbData.count - 1].date - let now = dateTimeUtils.getNowTimeIntervalUTC() - - //Make sure carb entry is newer than 1 hour, has reached the time length, and is over the ignore limit - if lastCarbTime > (now - (60 * 60)) - && lastCarbTime < (now - Double((UserDefaultsRepository.alertMissedBolus.value * 60))) - && lastCarb > Double(UserDefaultsRepository.alertMissedBolusLowGrams.value) { - - // There is a current carb but no boluses data at all - if bolusData.count < 1 { - AlarmSound.whichAlarm = "Missed Bolus Alert" - //determine if it is day or night and what should happen - if UserDefaultsRepository.nightTime.value { - if UserDefaultsRepository.alertMissedBolusNightTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertMissedBolusNightTimeAudible.value { playSound = false } - } else { - if UserDefaultsRepository.alertMissedBolusDayTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertMissedBolusDayTimeAudible.value { playSound = false } - } - triggerAlarm(sound: UserDefaultsRepository.alertMissedBolusSound.value, snooozedBGReadingTime: nil, overrideVolume: UserDefaultsRepository.overrideSystemOutputVolume.value, numLoops: numLoops, snoozeTime: UserDefaultsRepository.alertMissedBolusSnooze.value, audio: playSound) - return - } - - // Get the latest bolus over the small bolus exclusion - // Start with 0.0 bolus assuming there isn't one to cause a trigger and only add one if found - var lastBolus = 0.0 - var i = 1 - // check the boluses in reverse order setting it only if the time is after the carb time minus prebolus time. - // This will make the loop stop at the most recent bolus that is over the minimum value or continue through all boluses - while lastBolus < UserDefaultsRepository.alertMissedBolusIgnoreBolus.value && i <= bolusData.count { - // Set the bolus if it's after the carb time minus prebolus time - if (bolusData[bolusData.count - i].date >= lastCarbTime - Double(UserDefaultsRepository.alertMissedBolusPrebolus.value * 60)) { - lastBolus = bolusData[bolusData.count - i].value - } - i += 1 - } - - // This will trigger is no boluses were set above - if (lastBolus == 0.0) { - AlarmSound.whichAlarm = "Missed Bolus Alert" - //determine if it is day or night and what should happen - if UserDefaultsRepository.nightTime.value { - if UserDefaultsRepository.alertMissedBolusNightTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertMissedBolusNightTimeAudible.value { playSound = false } - } else { - if UserDefaultsRepository.alertMissedBolusDayTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertMissedBolusDayTimeAudible.value { playSound = false } - } - triggerAlarm(sound: UserDefaultsRepository.alertMissedBolusSound.value, snooozedBGReadingTime: nil, overrideVolume: UserDefaultsRepository.overrideSystemOutputVolume.value, numLoops: numLoops, snoozeTime: UserDefaultsRepository.alertMissedBolusSnooze.value, audio: playSound) - return - } - - } - - } - - // Check Sage - if UserDefaultsRepository.alertSAGEActive.value && !UserDefaultsRepository.alertSAGEIsSnoozed.value { - let insertTime = Double(UserDefaultsRepository.alertSageInsertTime.value) - let alertDistance = Double(UserDefaultsRepository.alertSAGE.value * 60 * 60) - let delta = now - insertTime - let tenDays = 10 * 24 * 60 * 60 - if Double(tenDays) - Double(delta) <= alertDistance { - AlarmSound.whichAlarm = "Sensor Change Alert" - //determine if it is day or night and what should happen - if UserDefaultsRepository.nightTime.value { - if UserDefaultsRepository.alertSAGENightTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertSAGENightTimeAudible.value { playSound = false } - } else { - if UserDefaultsRepository.alertSAGEDayTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertSAGEDayTimeAudible.value { playSound = false } - } - triggerAlarm(sound: UserDefaultsRepository.alertSAGESound.value, snooozedBGReadingTime: nil, overrideVolume: UserDefaultsRepository.overrideSystemOutputVolume.value, numLoops: numLoops, snoozeTime: UserDefaultsRepository.alertSAGESnooze.value, snoozeIncrement: 1, audio: playSound) - return - } - } - - // Check Cage - if UserDefaultsRepository.alertCAGEActive.value && !UserDefaultsRepository.alertCAGEIsSnoozed.value { - let insertTime = Double(UserDefaultsRepository.alertCageInsertTime.value) - let alertDistance = Double(UserDefaultsRepository.alertCAGE.value * 60 * 60) - let delta = now - insertTime - let tenDays = 3 * 24 * 60 * 60 - if Double(tenDays) - Double(delta) <= alertDistance { - AlarmSound.whichAlarm = "Pump Change Alert" - //determine if it is day or night and what should happen - if UserDefaultsRepository.nightTime.value { - if UserDefaultsRepository.alertCAGENightTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertCAGENightTimeAudible.value { playSound = false } - } else { - if UserDefaultsRepository.alertCAGEDayTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertCAGEDayTimeAudible.value { playSound = false } - } - triggerAlarm(sound: UserDefaultsRepository.alertCAGESound.value, snooozedBGReadingTime: nil, overrideVolume: UserDefaultsRepository.overrideSystemOutputVolume.value, numLoops: numLoops, snoozeTime: UserDefaultsRepository.alertCAGESnooze.value, snoozeIncrement: 1, audio: playSound) - return - } - } - - // Check Pump - if UserDefaultsRepository.alertPump.value && !UserDefaultsRepository.alertPumpIsSnoozed.value { - let alertAt = Double(UserDefaultsRepository.alertPumpAt.value) - if latestPumpVolume <= alertAt { - AlarmSound.whichAlarm = "Low Insulin Alert" - //determine if it is day or night and what should happen - if UserDefaultsRepository.nightTime.value { - if UserDefaultsRepository.alertPumpNightTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertPumpNightTimeAudible.value { playSound = false } - } else { - if UserDefaultsRepository.alertPumpDayTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertPumpDayTimeAudible.value { playSound = false } - } - triggerAlarm(sound: UserDefaultsRepository.alertPumpSound.value, snooozedBGReadingTime: nil, overrideVolume: UserDefaultsRepository.overrideSystemOutputVolume.value, numLoops: numLoops, snoozeTime: UserDefaultsRepository.alertPumpSnoozeHours.value, snoozeIncrement: 1, audio: playSound) - return - } - } - } - - if UserDefaultsRepository.alertBatteryActive.value && !UserDefaultsRepository.alertBatteryIsSnoozed.value { - let currentBatteryLevel = UserDefaultsRepository.deviceBatteryLevel.value - let alertAtBatteryLevel = Double(UserDefaultsRepository.alertBatteryLevel.value) - - if currentBatteryLevel <= alertAtBatteryLevel { - AlarmSound.whichAlarm = "Low Battery" - - if UserDefaultsRepository.alertBatteryRepeat.value { numLoops = -1 } - triggerAlarm(sound: UserDefaultsRepository.alertBatterySound.value, snooozedBGReadingTime: nil, overrideVolume: UserDefaultsRepository.overrideSystemOutputVolume.value, numLoops: numLoops, snoozeTime: UserDefaultsRepository.alertBatterySnoozeHours.value, snoozeIncrement: 1, audio: true) - return - } - } - - // Check for rapid battery drop - if UserDefaultsRepository.alertBatteryDropActive.value && !UserDefaultsRepository.alertBatteryDropIsSnoozed.value { - let targetDate = Calendar.current.date(byAdding: .minute, value: Int(-UserDefaultsRepository.alertBatteryDropPeriod.value), to: Date())! - let currentBatteryLevel = UserDefaultsRepository.deviceBatteryLevel.value as Double - let dropPercentage = Double(UserDefaultsRepository.alertBatteryDropPercentage.value) - - // find the closest matching entry to the user defined timeframe - // this allows flexibility for ingress data matching as it can come at different intervals - if let previousBatteryLevel = deviceBatteryData.min(by: { - abs($0.timestamp.timeIntervalSince(targetDate)) < abs($1.timestamp.timeIntervalSince(targetDate)) - }) { - // ignore a drop with a previous level of 100 as it will trigger a false alarm - if (previousBatteryLevel.batteryLevel < 100) { - if (previousBatteryLevel.batteryLevel - currentBatteryLevel) >= dropPercentage { - AlarmSound.whichAlarm = "Battery Drop" - - if UserDefaultsRepository.alertBatteryDropRepeat.value { numLoops = -1 } - triggerAlarm(sound: UserDefaultsRepository.alertBatteryDropSound.value, snooozedBGReadingTime: nil, overrideVolume: UserDefaultsRepository.overrideSystemOutputVolume.value, numLoops: numLoops, snoozeTime: UserDefaultsRepository.alertBatteryDropSnoozeHours.value, snoozeIncrement: 1, audio: true) - return - } - } - } - } - - if UserDefaultsRepository.alertRecBolusActive.value, - !UserDefaultsRepository.alertRecBolusIsSnoozed.value - { - let currentRecBolus = UserDefaultsRepository.deviceRecBolus.value - let alertAtRecBolus = UserDefaultsRepository.alertRecBolusLevel.value - - if currentRecBolus >= alertAtRecBolus { - if currentRecBolus > (Observable.shared.lastRecBolusTriggered.value ?? 0) { - AlarmSound.whichAlarm = "Rec. Bolus" - - if UserDefaultsRepository.alertRecBolusRepeat.value { - numLoops = -1 - } - - triggerAlarm( - sound: UserDefaultsRepository.alertRecBolusSound.value, - snooozedBGReadingTime: nil, - overrideVolume: UserDefaultsRepository.overrideSystemOutputVolume.value, - numLoops: numLoops, - snoozeTime: UserDefaultsRepository.alertRecBolusSnooze.value, - snoozeIncrement: 5, - audio: true - ) - - Observable.shared.lastRecBolusTriggered.value = currentRecBolus - return - } - } else { - Observable.shared.lastRecBolusTriggered.value = nil - } - } - - // still send persistent notification if no alarms trigger and persistent notification is on - persistentNotification(bgTime: currentBGTime) - } - - func checkOverrideAlarms() - { - if UserDefaultsRepository.alertSnoozeAllIsSnoozed.value { return } - - let recentOverride = overrideGraphData.last - let recentStart: TimeInterval = recentOverride?.date ?? 0 - let recentEnd: TimeInterval = recentOverride?.endDate ?? 0 - let now = dateTimeUtils.getNowTimeIntervalUTC() - - var triggerStart = false - var triggerEnd = false - - // Trigger newly started override - if now - recentStart > 0 && now - recentStart <= (15 * 60) && recentStart > lastOverrideAlarm { - triggerStart = true - } else if now - recentEnd > 0 && now - recentEnd <= (15 * 60) && recentEnd > lastOverrideAlarm { - triggerEnd = true - } else { - return - } - - let overrideName = recentOverride?.reason as! String - - var numLoops = 0 - var playSound = true - if UserDefaultsRepository.alertOverrideStart.value && !UserDefaultsRepository.alertOverrideStartIsSnoozed.value && triggerStart { - AlarmSound.whichAlarm = overrideName + " Override Started" - //determine if it is day or night and what should happen - if UserDefaultsRepository.nightTime.value { - if UserDefaultsRepository.alertOverrideStartNightTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertOverrideStartNightTimeAudible.value { playSound = false } - } else { - if UserDefaultsRepository.alertOverrideStartDayTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertOverrideStartDayTimeAudible.value { playSound = false } - } - triggerOneTimeAlarm(sound: UserDefaultsRepository.alertOverrideStartSound.value, overrideVolume: UserDefaultsRepository.overrideSystemOutputVolume.value, numLoops: numLoops, audio: playSound) - lastOverrideStartTime = recentStart - lastOverrideAlarm = now - } else if UserDefaultsRepository.alertOverrideEnd.value && !UserDefaultsRepository.alertOverrideEndIsSnoozed.value && triggerEnd { - AlarmSound.whichAlarm = overrideName + " Override Ended" - //determine if it is day or night and what should happen - if UserDefaultsRepository.nightTime.value { - if UserDefaultsRepository.alertOverrideEndNightTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertOverrideEndNightTimeAudible.value { playSound = false } - } else { - if UserDefaultsRepository.alertOverrideEndDayTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertOverrideEndDayTimeAudible.value { playSound = false } - } - triggerOneTimeAlarm(sound: UserDefaultsRepository.alertOverrideEndSound.value, overrideVolume: UserDefaultsRepository.overrideSystemOutputVolume.value, numLoops: numLoops, audio: playSound) - lastOverrideEndTime = recentEnd - lastOverrideAlarm = now - } - } - - func checkTempTargetAlarms() { - if UserDefaultsRepository.alertSnoozeAllIsSnoozed.value { return } - - let recentTempTarget = tempTargetGraphData.last - let recentStart: TimeInterval = recentTempTarget?.date ?? 0 - let recentEnd: TimeInterval = recentTempTarget?.endDate ?? 0 - let now = dateTimeUtils.getNowTimeIntervalUTC() - - var triggerStart = false - var triggerEnd = false - - // Trigger newly started temp target - if now - recentStart > 0 && now - recentStart <= (15 * 60) && recentStart > lastTempTargetAlarm { - triggerStart = true - } else if now - recentEnd > 0 && now - recentEnd <= (15 * 60) && recentEnd > lastTempTargetAlarm { - triggerEnd = true - } else { - return - } - - var numLoops = 0 - var playSound = true - - // Check Temp Target Start Alarm - if UserDefaultsRepository.alertTempTargetStart.value && !UserDefaultsRepository.alertTempTargetStartIsSnoozed.value && triggerStart { - AlarmSound.whichAlarm = "Temp Target Started" - // Determine if it is day or night and what should happen - if UserDefaultsRepository.nightTime.value { - if UserDefaultsRepository.alertTempTargetStartNightTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertTempTargetStartNightTimeAudible.value { playSound = false } - } else { - if UserDefaultsRepository.alertTempTargetStartDayTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertTempTargetStartDayTimeAudible.value { playSound = false } - } - triggerOneTimeAlarm(sound: UserDefaultsRepository.alertTempTargetStartSound.value, overrideVolume: UserDefaultsRepository.overrideSystemOutputVolume.value, numLoops: numLoops, audio: playSound) - lastTempTargetStartTime = recentStart - lastTempTargetAlarm = now - } - - // Check Temp Target End Alarm - else if UserDefaultsRepository.alertTempTargetEnd.value && !UserDefaultsRepository.alertTempTargetEndIsSnoozed.value && triggerEnd { - AlarmSound.whichAlarm = "Temp Target Ended" - // Determine if it is day or night and what should happen - if UserDefaultsRepository.nightTime.value { - if UserDefaultsRepository.alertTempTargetEndNightTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertTempTargetEndNightTimeAudible.value { playSound = false } - } else { - if UserDefaultsRepository.alertTempTargetEndDayTime.value { numLoops = -1 } - if !UserDefaultsRepository.alertTempTargetEndDayTimeAudible.value { playSound = false } - } - triggerOneTimeAlarm(sound: UserDefaultsRepository.alertTempTargetEndSound.value, overrideVolume: UserDefaultsRepository.overrideSystemOutputVolume.value, numLoops: numLoops, audio: playSound) - lastTempTargetEndTime = recentEnd - lastTempTargetAlarm = now - } - } - - func triggerOneTimeAlarm(sound: String, overrideVolume: Bool, numLoops: Int, audio: Bool = true) - { - var audioDuringCall = true - if !UserDefaultsRepository.alertAudioDuringPhone.value && isOnPhoneCall() { audioDuringCall = false } - - guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } - snoozer.updateDisplayWhenTriggered(bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: Observable.shared.directionText.value, deltaVal: Observable.shared.deltaText.value, minAgoVal: Observable.shared.minAgoText.value, alertLabelVal: AlarmSound.whichAlarm) - if audio && !UserDefaultsRepository.alertMuteAllIsMuted.value && audioDuringCall{ - AlarmSound.setSoundFile(str: sound) - AlarmSound.play(overrideVolume: overrideVolume, numLoops: numLoops) - startAlarmPlayingTimer() - } - } - - func triggerAlarm(sound: String, snooozedBGReadingTime: TimeInterval?, overrideVolume: Bool, numLoops: Int, snoozeTime: Int = 0, snoozeIncrement: Int = 5, audio: Bool = true) - { - LogManager.shared.log(category: .alarm, message: "Alarm triggered: \(AlarmSound.whichAlarm)") - - var audioDuringCall = true - if !UserDefaultsRepository.alertAudioDuringPhone.value && isOnPhoneCall() { audioDuringCall = false } - - guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } - snoozer.updateDisplayWhenTriggered(bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: Observable.shared.directionText.value, deltaVal: Observable.shared.deltaText.value, minAgoVal: Observable.shared.minAgoText.value, alertLabelVal: AlarmSound.whichAlarm) - snoozer.SnoozeButton.isHidden = false - snoozer.AlertLabel.isHidden = false - snoozer.clockLabel.isHidden = true - snoozer.debugTextView.isHidden = true - snoozer.snoozeForMinuteLabel.text = String(snoozeTime) - snoozer.snoozeForMinuteStepper.value = Double(snoozeTime) - snoozer.snoozeForMinuteStepper.stepValue = Double(snoozeIncrement) - if snoozeTime != 0 { - snoozer.snoozeForMinuteStepper.isHidden = false - snoozer.snoozeForMinuteLabel.isHidden = false - } - - tabBarController?.selectedIndex = 2 - if snooozedBGReadingTime != nil { - UserDefaultsRepository.snoozedBGReadingTime.value = snooozedBGReadingTime - } - if audio && !UserDefaultsRepository.alertMuteAllIsMuted.value && audioDuringCall { - AlarmSound.setSoundFile(str: sound) - AlarmSound.play(overrideVolume: overrideVolume, numLoops: numLoops) - } - let bgSeconds = bgData.last!.date - let now = Date().timeIntervalSince1970 - let secondsAgo = now - bgSeconds - var timerLength = 290 - secondsAgo - if timerLength < 10 { timerLength = 290} - startAlarmPlayingTimer(time: timerLength) - } - - func stopAlarmAtNextReading(){ - - AlarmSound.whichAlarm = "none" - guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } - snoozer.updateDisplayWhenTriggered(bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: Observable.shared.directionText.value, deltaVal: Observable.shared.deltaText.value, minAgoVal: Observable.shared.minAgoText.value, alertLabelVal: AlarmSound.whichAlarm) - snoozer.SnoozeButton.isHidden = true - snoozer.AlertLabel.isHidden = true - if AlarmSound.isPlaying { - AlarmSound.stop() - } - } - - func clearOldSnoozes(){ - let date = Date() - guard let alarms = ViewControllerManager.shared.alarmViewController else { return } - - if date > UserDefaultsRepository.alertSnoozeAllTime.value ?? date { - UserDefaultsRepository.alertSnoozeAllTime.setNil(key: "alertSnoozeAllTime") - UserDefaultsRepository.alertSnoozeAllIsSnoozed.value = false - alarms.reloadSnoozeTime(key: "alertSnoozeAllTime", setNil: true) - alarms.reloadIsSnoozed(key: "alertSnoozeAllIsSnoozed", value: false) - } - - if date > UserDefaultsRepository.alertMuteAllTime.value ?? date { - UserDefaultsRepository.alertMuteAllTime.setNil(key: "alertMuteAllTime") - UserDefaultsRepository.alertMuteAllIsMuted.value = false - alarms.reloadMuteTime(key: "alertMuteAllTime", setNil: true) - alarms.reloadIsMuted(key: "alertMuteAllIsMuted", value: false) - } - - if date > UserDefaultsRepository.alertUrgentLowSnoozedTime.value ?? date { - UserDefaultsRepository.alertUrgentLowSnoozedTime.setNil(key: "alertUrgentLowSnoozedTime") - UserDefaultsRepository.alertUrgentLowIsSnoozed.value = false - alarms.reloadSnoozeTime(key: "alertUrgentLowSnoozedTime", setNil: true) - alarms.reloadIsSnoozed(key: "alertUrgentLowIsSnoozed", value: false) - - } - if date > UserDefaultsRepository.alertLowSnoozedTime.value ?? date { - UserDefaultsRepository.alertLowSnoozedTime.setNil(key: "alertLowSnoozedTime") - UserDefaultsRepository.alertLowIsSnoozed.value = false - alarms.reloadSnoozeTime(key: "alertLowSnoozedTime", setNil: true) - alarms.reloadIsSnoozed(key: "alertLowIsSnoozed", value: false) - - } - if date > UserDefaultsRepository.alertHighSnoozedTime.value ?? date { - UserDefaultsRepository.alertHighSnoozedTime.setNil(key: "alertHighSnoozedTime") - UserDefaultsRepository.alertHighIsSnoozed.value = false - alarms.reloadSnoozeTime(key: "alertHighSnoozedTime", setNil: true) - alarms.reloadIsSnoozed(key: "alertHighIsSnoozed", value: false) - - } - if date > UserDefaultsRepository.alertUrgentHighSnoozedTime.value ?? date { - UserDefaultsRepository.alertUrgentHighSnoozedTime.setNil(key: "alertUrgentHighSnoozedTime") - UserDefaultsRepository.alertUrgentHighIsSnoozed.value = false - alarms.reloadSnoozeTime(key: "alertUrgentHighSnoozedTime", setNil: true) - alarms.reloadIsSnoozed(key: "alertUrgentHighIsSnoozed", value: false) - - } - if date > UserDefaultsRepository.alertFastDropSnoozedTime.value ?? date { - UserDefaultsRepository.alertFastDropSnoozedTime.setNil(key: "alertFastDropSnoozedTime") - UserDefaultsRepository.alertFastDropIsSnoozed.value = false - alarms.reloadSnoozeTime(key: "alertFastDropSnoozedTime", setNil: true) - alarms.reloadIsSnoozed(key: "alertFastDropIsSnoozed", value: false) - - } - if date > UserDefaultsRepository.alertFastRiseSnoozedTime.value ?? date { - UserDefaultsRepository.alertFastRiseSnoozedTime.setNil(key: "alertFastRiseSnoozedTime") - UserDefaultsRepository.alertFastRiseIsSnoozed.value = false - alarms.reloadSnoozeTime(key: "alertFastRiseSnoozedTime", setNil: true) - alarms.reloadIsSnoozed(key: "alertFastRiseIsSnoozed", value: false) - - } - if date > UserDefaultsRepository.alertMissedReadingSnoozedTime.value ?? date { - UserDefaultsRepository.alertMissedReadingSnoozedTime.setNil(key: "alertMissedReadingSnoozedTime") - UserDefaultsRepository.alertMissedReadingIsSnoozed.value = false - alarms.reloadSnoozeTime(key: "alertMissedReadingSnoozedTime", setNil: true) - alarms.reloadIsSnoozed(key: "alertMissedReadingIsSnoozed", value: false) - - } - if date > UserDefaultsRepository.alertNotLoopingSnoozedTime.value ?? date { - UserDefaultsRepository.alertNotLoopingSnoozedTime.setNil(key: "alertNotLoopingSnoozedTime") - UserDefaultsRepository.alertNotLoopingIsSnoozed.value = false - alarms.reloadSnoozeTime(key: "alertNotLoopingSnoozedTime", setNil: true) - alarms.reloadIsSnoozed(key: "alertNotLoopingIsSnoozed", value: false) - - - } - if date > UserDefaultsRepository.alertMissedBolusSnoozedTime.value ?? date { - UserDefaultsRepository.alertMissedBolusSnoozedTime.setNil(key: "alertMissedBolusSnoozedTime") - UserDefaultsRepository.alertMissedBolusIsSnoozed.value = false - alarms.reloadSnoozeTime(key: "alertMissedBolusSnoozedTime", setNil: true) - alarms.reloadIsSnoozed(key: "alertMissedBolusIsSnoozed", value: false) - - } - if date > UserDefaultsRepository.alertSAGESnoozedTime.value ?? date { - UserDefaultsRepository.alertSAGESnoozedTime.setNil(key: "alertSAGESnoozedTime") - UserDefaultsRepository.alertSAGEIsSnoozed.value = false - alarms.reloadSnoozeTime(key: "alertSAGESnoozedTime", setNil: true) - alarms.reloadIsSnoozed(key: "alertSAGEIsSnoozed", value: false) - - } - if date > UserDefaultsRepository.alertCAGESnoozedTime.value ?? date { - UserDefaultsRepository.alertCAGESnoozedTime.setNil(key: "alertCAGESnoozedTime") - UserDefaultsRepository.alertCAGEIsSnoozed.value = false - alarms.reloadSnoozeTime(key: "alertCAGESnoozedTime", setNil: true) - alarms.reloadIsSnoozed(key: "alertCAGEIsSnoozed", value: false) - - } - if date > UserDefaultsRepository.alertOverrideStartSnoozedTime.value ?? date { - UserDefaultsRepository.alertOverrideStartSnoozedTime.setNil(key: "alertOverrideStartSnoozedTime") - UserDefaultsRepository.alertOverrideStartIsSnoozed.value = false - alarms.reloadSnoozeTime(key: "alertOverrideStartSnoozedTime", setNil: true) - alarms.reloadIsSnoozed(key: "alertOverrideStartIsSnoozed", value: false) - } - if date > UserDefaultsRepository.alertOverrideEndSnoozedTime.value ?? date { - UserDefaultsRepository.alertOverrideEndSnoozedTime.setNil(key: "alertOverrideEndSnoozedTime") - UserDefaultsRepository.alertOverrideEndIsSnoozed.value = false - alarms.reloadSnoozeTime(key: "alertOverrideEndSnoozedTime", setNil: true) - alarms.reloadIsSnoozed(key: "alertOverrideEndIsSnoozed", value: false) - - } - - if date > UserDefaultsRepository.alertPumpSnoozedTime.value ?? date { - UserDefaultsRepository.alertPumpSnoozedTime.setNil(key: "alertPumpSnoozedTime") - UserDefaultsRepository.alertPumpIsSnoozed.value = false - alarms.reloadSnoozeTime(key: "alertPumpSnoozedTime", setNil: true) - alarms.reloadIsSnoozed(key: "alertPumpIsSnoozed", value: false) - - } - - if date > UserDefaultsRepository.alertIOBSnoozedTime.value ?? date { - UserDefaultsRepository.alertIOBSnoozedTime.setNil(key: "alertIOBSnoozedTime") - UserDefaultsRepository.alertIOBIsSnoozed.value = false - alarms.reloadSnoozeTime(key: "alertIOBSnoozedTime", setNil: true) - alarms.reloadIsSnoozed(key: "alertIOBIsSnoozed", value: false) - - } - - if date > UserDefaultsRepository.alertCOBSnoozedTime.value ?? date { - UserDefaultsRepository.alertCOBSnoozedTime.setNil(key: "alertCOBSnoozedTime") - UserDefaultsRepository.alertCOBIsSnoozed.value = false - alarms.reloadSnoozeTime(key: "alertCOBSnoozedTime", setNil: true) - alarms.reloadIsSnoozed(key: "alertCOBIsSnoozed", value: false) - - } - - if date > UserDefaultsRepository.alertBatterySnoozedTime.value ?? date { - UserDefaultsRepository.alertBatterySnoozedTime.setNil(key: "alertBatterySnoozedTime") - UserDefaultsRepository.alertBatteryIsSnoozed.value = false - alarms.reloadSnoozeTime(key: "alertBatterySnoozedTime", setNil: true) - alarms.reloadIsSnoozed(key: "alertBatteryIsSnoozed", value: false) - } - - if date > UserDefaultsRepository.alertBatteryDropSnoozedTime.value ?? date { - UserDefaultsRepository.alertBatteryDropSnoozedTime.setNil(key: "alertBatteryDropSnoozedTime") - UserDefaultsRepository.alertBatteryDropIsSnoozed.value = false - alarms.reloadSnoozeTime(key: "alertBatteryDropSnoozedTime", setNil: true) - alarms.reloadIsSnoozed(key: "alertBatteryDropIsSnoozed", value: false) - } - - if date > UserDefaultsRepository.alertRecBolusSnoozedTime.value ?? date { - UserDefaultsRepository.alertRecBolusSnoozedTime.setNil(key: "alertRecBolusSnoozedTime") - UserDefaultsRepository.alertRecBolusIsSnoozed.value = false - alarms.reloadSnoozeTime(key: "alertRecBolusSnoozedTime", setNil: true) - alarms.reloadIsSnoozed(key: "alertRecBolusIsSnoozed", value: false) - } - if date > UserDefaultsRepository.alertTempTargetStartSnoozedTime.value ?? date { - UserDefaultsRepository.alertTempTargetStartSnoozedTime.setNil(key: "alertTempTargetStartSnoozedTime") - UserDefaultsRepository.alertTempTargetStartIsSnoozed.value = false - alarms.reloadSnoozeTime(key: "alertTempTargetStartSnoozedTime", setNil: true) - alarms.reloadIsSnoozed(key: "alertTempTargetStartIsSnoozed", value: false) - } - if date > UserDefaultsRepository.alertTempTargetEndSnoozedTime.value ?? date { - UserDefaultsRepository.alertTempTargetEndSnoozedTime.setNil(key: "alertTempTargetEndSnoozedTime") - UserDefaultsRepository.alertTempTargetEndIsSnoozed.value = false - alarms.reloadSnoozeTime(key: "alertTempTargetEndSnoozedTime", setNil: true) - alarms.reloadIsSnoozed(key: "alertTempTargetEndIsSnoozed", value: false) - } - } - - func checkQuietHours() { - if UserDefaultsRepository.quietHourStart.value == nil || UserDefaultsRepository.quietHourEnd.value == nil { return } - - let today = Date() - let todayCalendar = Calendar.current - let hour = todayCalendar.component(.hour, from: today) - let minute = todayCalendar.component(.minute, from: today) - let todayMinutes = (60 * hour) + minute - - let start = UserDefaultsRepository.quietHourStart.value - let startCalendar = Calendar.current - let startHour = startCalendar.component(.hour, from: start!) - let startMinute = startCalendar.component(.minute, from: start!) - let startMinutes = (60 * startHour) + startMinute - - let end = UserDefaultsRepository.quietHourEnd.value - let endCalendar = Calendar.current - let endHour = endCalendar.component(.hour, from: end!) - let endMinute = endCalendar.component(.minute, from: end!) - let endMinutes = (60 * endHour) + endMinute - - if todayMinutes >= startMinutes { - let tomorrow = Date().addingTimeInterval(86400) - let tomorrowCalendar = Calendar.current - let end = UserDefaultsRepository.quietHourEnd.value - let endCalendar = Calendar.current - - var components = DateComponents() - components.month = tomorrowCalendar.component(.month, from: tomorrow) - components.day = tomorrowCalendar.component(.day, from: tomorrow) - components.year = tomorrowCalendar.component(.year, from: tomorrow) - components.hour = endCalendar.component(.hour, from: end!) - components.minute = endCalendar.component(.minute, from: end!) - components.second = endCalendar.component(.second, from: end!) - let snoozeCalendar = Calendar.current - let snoozeTime = snoozeCalendar.date(from: components) - - UserDefaultsRepository.nightTime.value = true - guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } - snoozer.setPresnoozeNight(snoozeTime: snoozeTime!) - } else if todayMinutes < endMinutes { - let today = Date() - let todayCalendar = Calendar.current - let end = UserDefaultsRepository.quietHourEnd.value - let endCalendar = Calendar.current - - var components = DateComponents() - components.month = todayCalendar.component(.month, from: today) - components.day = todayCalendar.component(.day, from: today) - components.year = todayCalendar.component(.year, from: today) - components.hour = endCalendar.component(.hour, from: end!) - components.minute = endCalendar.component(.minute, from: end!) - components.second = endCalendar.component(.second, from: end!) - let snoozeCalendar = Calendar.current - let snoozeTime = snoozeCalendar.date(from: components) - - UserDefaultsRepository.nightTime.value = true - guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } - snoozer.setPresnoozeNight(snoozeTime: snoozeTime!) - } else { - let today = Date() - let todayCalendar = Calendar.current - let end = UserDefaultsRepository.quietHourStart.value - let endCalendar = Calendar.current - - var components = DateComponents() - components.month = todayCalendar.component(.month, from: today) - components.day = todayCalendar.component(.day, from: today) - components.year = todayCalendar.component(.year, from: today) - components.hour = endCalendar.component(.hour, from: end!) - components.minute = endCalendar.component(.minute, from: end!) - components.second = endCalendar.component(.second, from: end!) - let snoozeCalendar = Calendar.current - let snoozeTime = snoozeCalendar.date(from: components) - - UserDefaultsRepository.nightTime.value = false - guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } - snoozer.setPreSnoozeDay(snoozeTime: snoozeTime!) - } - - } - - func evaluateSpeakConditions(currentValue: Int, previousValue: Int) { - if !UserDefaultsRepository.speakBG.value { - return - } - - let always = UserDefaultsRepository.speakBGAlways.value - let lowThreshold = UserDefaultsRepository.speakLowBGLimit.value - let fastDropDelta = UserDefaultsRepository.speakFastDropDelta.value - let highThreshold = UserDefaultsRepository.speakHighBGLimit.value - let speakLowBG = UserDefaultsRepository.speakLowBG.value - let speakProactiveLowBG = UserDefaultsRepository.speakProactiveLowBG.value - let speakHighBG = UserDefaultsRepository.speakHighBG.value - - // Speak always - if always { - speakBG(currentValue: currentValue, previousValue: previousValue) - LogManager.shared.log(category: .general, message: "Speaking because 'Always' is enabled.", isDebug: true) - - return - } - - // Speak if low or last value was low - if speakLowBG { - if currentValue <= Int(lowThreshold) || previousValue <= Int(lowThreshold) { - speakBG(currentValue: currentValue, previousValue: previousValue) - LogManager.shared.log(category: .general, message: "Speaking because of 'Low' condition.", isDebug: true) - return - } - } - - // Speak predictive low if... - // * low or last value was low - // * next predictive value is low - // * fast drop occurs below high - if speakProactiveLowBG { - let predictiveTrigger = !predictionData.isEmpty && Float(predictionData.first!.sgv) <= lowThreshold - - if predictiveTrigger || - currentValue <= Int(lowThreshold) || previousValue <= Int(lowThreshold) || - ((currentValue <= Int(highThreshold) && (previousValue - currentValue) >= Int(fastDropDelta))) { - speakBG(currentValue: currentValue, previousValue: previousValue) - LogManager.shared.log(category: .general, message: "Speaking because of 'Proactive Low' condition. Predictive trigger: \(predictiveTrigger)", isDebug: true) - return - } - } - - //Speak if high or if last value was high - if speakHighBG { - if currentValue >= Int(highThreshold) || previousValue >= Int(highThreshold) { - speakBG(currentValue: currentValue, previousValue: previousValue) - LogManager.shared.log(category: .general, message: "Speaking because of 'High' condition.", isDebug: true) - return - } - } - - LogManager.shared.log(category: .general, message: "No condition met for speaking.", isDebug: true) - } - - struct AnnouncementTexts { - var stable: String - var increase: String - var decrease: String - var currentBGIs: String - - static func forLanguage(_ language: String) -> AnnouncementTexts { - switch language { - case "it": - return AnnouncementTexts( - stable: "ed è stabile", - increase: "ed è salita di", - decrease: "ed è scesa di", - currentBGIs: "Glicemia attuale è" - ) - case "sk": - return AnnouncementTexts( - stable: "a je stabilná", - increase: "a stúpla o", - decrease: "a klesla o", - currentBGIs: "Aktuálna glykémia je" - ) - case "sv": - return AnnouncementTexts( - stable: "och det är stabilt", - increase: "och det har ökat med", - decrease: "och det har minskat med", - currentBGIs: "Blodsockret är" - ) - case "en": fallthrough - default: - return AnnouncementTexts( - stable: "and it is stable", - increase: "and it is up", - decrease: "and it is down", - currentBGIs: "Glucose is" - ) - } - } - } - - struct LanguageVoiceMapping { - static let voiceLanguageMap: [String: String] = [ - "en": "en-US", - "it": "it-IT", - "sk": "sk-SK", - "sv": "sv-SE" - ] - - static func voiceLanguageCode(forAppLanguage appLanguage: String) -> String { - return voiceLanguageMap[appLanguage, default: "en-US"] - } - } - - - // Speaks the current blood glucose value and the change from the previous value. - // Repeated calls to the function within 30 seconds are prevented. - func speakBG(currentValue: Int, previousValue: Int) { - let audioSession = AVAudioSession.sharedInstance() - do { - try audioSession.setCategory(.playback, mode: .default) - try audioSession.setActive(true) - } catch { - LogManager.shared.log(category: .alarm, message: "speakBG, Failed to set up audio session: \(error)") - } - - // Get the current time - let currentTime = Date() - - // Check if speakBG was called less than 30 seconds ago. If so, prevent repeated announcements and return. - // If `lastSpeechTime` is `nil` (i.e., this is the first time `speakBG` is being called), use `Date.distantPast` as the default - // value to ensure that the `guard` statement passes and the announcement is made. - guard currentTime.timeIntervalSince(lastSpeechTime ?? .distantPast) >= 30 else { - LogManager.shared.log(category: .general, message: "Repeated calls to speakBG detected!", isDebug: true) - return - } - - // Update the last speech time - self.lastSpeechTime = currentTime - - let bloodGlucoseDifference = currentValue - previousValue - - let preferredLanguage = UserDefaultsRepository.speakLanguage.value - let voiceLanguageCode = LanguageVoiceMapping.voiceLanguageCode(forAppLanguage: preferredLanguage) - - let texts = AnnouncementTexts.forLanguage(preferredLanguage) - - let negligibleThreshold = 3 - let localizedCurrentValue = Localizer.toDisplayUnits(String(currentValue)).replacingOccurrences(of: ",", with: ".") - let announcementText: String - - if abs(bloodGlucoseDifference) <= negligibleThreshold { - announcementText = "\(texts.currentBGIs) \(localizedCurrentValue) \(texts.stable)" - } else { - let directionText = bloodGlucoseDifference < 0 ? texts.decrease : texts.increase - let absoluteDifference = Localizer.toDisplayUnits(String(abs(bloodGlucoseDifference))).replacingOccurrences(of: ",", with: ".") - announcementText = "\(texts.currentBGIs) \(localizedCurrentValue) \(directionText) \(absoluteDifference)" - } - - let speechUtterance = AVSpeechUtterance(string: announcementText) - speechUtterance.voice = AVSpeechSynthesisVoice(language: voiceLanguageCode) - - speechSynthesizer.speak(speechUtterance) - } - - func isOnPhoneCall() -> Bool { - /* - Returns true if the user is currently on a phone call - */ - for call in CXCallObserver().calls { - if call.hasEnded == false { - return true - } - } - return false - } - -} diff --git a/LoopFollow/Controllers/SpeakBG.swift b/LoopFollow/Controllers/SpeakBG.swift new file mode 100644 index 000000000..293a939b6 --- /dev/null +++ b/LoopFollow/Controllers/SpeakBG.swift @@ -0,0 +1,169 @@ +import Foundation +import AVFoundation +import CallKit + +extension MainViewController { + func evaluateSpeakConditions(currentValue: Int, previousValue: Int) { + if !UserDefaultsRepository.speakBG.value { + return + } + + let always = UserDefaultsRepository.speakBGAlways.value + let lowThreshold = UserDefaultsRepository.speakLowBGLimit.value + let fastDropDelta = UserDefaultsRepository.speakFastDropDelta.value + let highThreshold = UserDefaultsRepository.speakHighBGLimit.value + let speakLowBG = UserDefaultsRepository.speakLowBG.value + let speakProactiveLowBG = UserDefaultsRepository.speakProactiveLowBG.value + let speakHighBG = UserDefaultsRepository.speakHighBG.value + + // Speak always + if always { + speakBG(currentValue: currentValue, previousValue: previousValue) + LogManager.shared.log(category: .general, message: "Speaking because 'Always' is enabled.", isDebug: true) + + return + } + + // Speak if low or last value was low + if speakLowBG { + if currentValue <= Int(lowThreshold) || previousValue <= Int(lowThreshold) { + speakBG(currentValue: currentValue, previousValue: previousValue) + LogManager.shared.log(category: .general, message: "Speaking because of 'Low' condition.", isDebug: true) + return + } + } + + // Speak predictive low if... + // * low or last value was low + // * next predictive value is low + // * fast drop occurs below high + if speakProactiveLowBG { + let predictiveTrigger = !predictionData.isEmpty && Float(predictionData.first!.sgv) <= lowThreshold + + if predictiveTrigger || + currentValue <= Int(lowThreshold) || previousValue <= Int(lowThreshold) || + ((currentValue <= Int(highThreshold) && (previousValue - currentValue) >= Int(fastDropDelta))) { + speakBG(currentValue: currentValue, previousValue: previousValue) + LogManager.shared.log(category: .general, message: "Speaking because of 'Proactive Low' condition. Predictive trigger: \(predictiveTrigger)", isDebug: true) + return + } + } + + //Speak if high or if last value was high + if speakHighBG { + if currentValue >= Int(highThreshold) || previousValue >= Int(highThreshold) { + speakBG(currentValue: currentValue, previousValue: previousValue) + LogManager.shared.log(category: .general, message: "Speaking because of 'High' condition.", isDebug: true) + return + } + } + + LogManager.shared.log(category: .general, message: "No condition met for speaking.", isDebug: true) + } + + struct AnnouncementTexts { + var stable: String + var increase: String + var decrease: String + var currentBGIs: String + + static func forLanguage(_ language: String) -> AnnouncementTexts { + switch language { + case "it": + return AnnouncementTexts( + stable: "ed è stabile", + increase: "ed è salita di", + decrease: "ed è scesa di", + currentBGIs: "Glicemia attuale è" + ) + case "sk": + return AnnouncementTexts( + stable: "a je stabilná", + increase: "a stúpla o", + decrease: "a klesla o", + currentBGIs: "Aktuálna glykémia je" + ) + case "sv": + return AnnouncementTexts( + stable: "och det är stabilt", + increase: "och det har ökat med", + decrease: "och det har minskat med", + currentBGIs: "Blodsockret är" + ) + case "en": fallthrough + default: + return AnnouncementTexts( + stable: "and it is stable", + increase: "and it is up", + decrease: "and it is down", + currentBGIs: "Glucose is" + ) + } + } + } + + struct LanguageVoiceMapping { + static let voiceLanguageMap: [String: String] = [ + "en": "en-US", + "it": "it-IT", + "sk": "sk-SK", + "sv": "sv-SE" + ] + + static func voiceLanguageCode(forAppLanguage appLanguage: String) -> String { + return voiceLanguageMap[appLanguage, default: "en-US"] + } + } + + + // Speaks the current blood glucose value and the change from the previous value. + // Repeated calls to the function within 30 seconds are prevented. + func speakBG(currentValue: Int, previousValue: Int) { + let audioSession = AVAudioSession.sharedInstance() + do { + try audioSession.setCategory(.playback, mode: .default) + try audioSession.setActive(true) + } catch { + LogManager.shared.log(category: .alarm, message: "speakBG, Failed to set up audio session: \(error)") + } + + // Get the current time + let currentTime = Date() + + // Check if speakBG was called less than 30 seconds ago. If so, prevent repeated announcements and return. + // If `lastSpeechTime` is `nil` (i.e., this is the first time `speakBG` is being called), use `Date.distantPast` as the default + // value to ensure that the `guard` statement passes and the announcement is made. + guard currentTime.timeIntervalSince(lastSpeechTime ?? .distantPast) >= 30 else { + LogManager.shared.log(category: .general, message: "Repeated calls to speakBG detected!", isDebug: true) + return + } + + // Update the last speech time + self.lastSpeechTime = currentTime + + let bloodGlucoseDifference = currentValue - previousValue + + let preferredLanguage = UserDefaultsRepository.speakLanguage.value + let voiceLanguageCode = LanguageVoiceMapping.voiceLanguageCode(forAppLanguage: preferredLanguage) + + let texts = AnnouncementTexts.forLanguage(preferredLanguage) + + let negligibleThreshold = 3 + let localizedCurrentValue = Localizer.toDisplayUnits(String(currentValue)).replacingOccurrences(of: ",", with: ".") + let announcementText: String + + if abs(bloodGlucoseDifference) <= negligibleThreshold { + announcementText = "\(texts.currentBGIs) \(localizedCurrentValue) \(texts.stable)" + } else { + let directionText = bloodGlucoseDifference < 0 ? texts.decrease : texts.increase + let absoluteDifference = Localizer.toDisplayUnits(String(abs(bloodGlucoseDifference))).replacingOccurrences(of: ",", with: ".") + announcementText = "\(texts.currentBGIs) \(localizedCurrentValue) \(directionText) \(absoluteDifference)" + } + + let speechUtterance = AVSpeechUtterance(string: announcementText) + speechUtterance.voice = AVSpeechSynthesisVoice(language: voiceLanguageCode) + + speechSynthesizer.speak(speechUtterance) + } + +} diff --git a/LoopFollow/Controllers/Timers.swift b/LoopFollow/Controllers/Timers.swift index 38ccb68e5..67705e57f 100644 --- a/LoopFollow/Controllers/Timers.swift +++ b/LoopFollow/Controllers/Timers.swift @@ -10,51 +10,15 @@ import Foundation import UIKit extension MainViewController { - - // Runs a 60 second timer when an alarm is snoozed - // Prevents the alarm from triggering again while saving the snooze time to settings - // End function needs nothing done func startGraphNowTimer(time: TimeInterval = 60) { - graphNowTimer = Timer.scheduledTimer(timeInterval: time, - target: self, - selector: #selector(MainViewController.graphNowTimerDidEnd(_:)), - userInfo: nil, - repeats: true) + target: self, + selector: #selector(MainViewController.graphNowTimerDidEnd(_:)), + userInfo: nil, + repeats: true) } - + @objc func graphNowTimerDidEnd(_ timer:Timer) { createVerticalLines() } - - // Runs a 60 second timer when an alarm is snoozed - // Prevents the alarm from triggering again while saving the snooze time to settings - // End function needs nothing done - func startCheckAlarmTimer(time: TimeInterval = 60) { - - checkAlarmTimer = Timer.scheduledTimer(timeInterval: time, - target: self, - selector: #selector(MainViewController.checkAlarmTimerDidEnd(_:)), - userInfo: nil, - repeats: false) - } - - @objc func checkAlarmTimerDidEnd(_ timer:Timer) { - } - - // Cancel and reset the playing alarm if it has not been snoozed after 4 min 50 seconds. - // This allows the next BG reading to either start the timer going or not fire if the situation has been resolved - func startAlarmPlayingTimer(time: TimeInterval = 290) { - let alarmPlayingTimer = Timer.scheduledTimer(timeInterval: time, - target: self, - selector: #selector(MainViewController.alarmPlayingTimerDidEnd(_:)), - userInfo: nil, - repeats: false) - } - - @objc func alarmPlayingTimerDidEnd(_ timer:Timer) { - if AlarmSound.isPlaying { - stopAlarmAtNextReading() - } - } } diff --git a/LoopFollow/Helpers/isOnPhoneCall.swift b/LoopFollow/Helpers/isOnPhoneCall.swift new file mode 100644 index 000000000..d6ab27414 --- /dev/null +++ b/LoopFollow/Helpers/isOnPhoneCall.swift @@ -0,0 +1,16 @@ +// +// isOnPhoneCall.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-04-26. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import Foundation +import CallKit + +private let callObserver = CXCallObserver() + +func isOnPhoneCall() -> Bool { + return callObserver.calls.contains { !$0.hasEnded } +} diff --git a/LoopFollow/Storage/ObservableUserDefaultsValue.swift b/LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift similarity index 100% rename from LoopFollow/Storage/ObservableUserDefaultsValue.swift rename to LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift diff --git a/LoopFollow/Storage/ObservableValue.swift b/LoopFollow/Storage/Framework/ObservableValue.swift similarity index 100% rename from LoopFollow/Storage/ObservableValue.swift rename to LoopFollow/Storage/Framework/ObservableValue.swift diff --git a/LoopFollow/Storage/SecureStorageValue.swift b/LoopFollow/Storage/Framework/SecureStorageValue.swift similarity index 100% rename from LoopFollow/Storage/SecureStorageValue.swift rename to LoopFollow/Storage/Framework/SecureStorageValue.swift diff --git a/LoopFollow/Storage/StorageValue.swift b/LoopFollow/Storage/Framework/StorageValue.swift similarity index 100% rename from LoopFollow/Storage/StorageValue.swift rename to LoopFollow/Storage/Framework/StorageValue.swift diff --git a/LoopFollow/Storage/UserData.xcdatamodeld/UserData.xcdatamodel/contents b/LoopFollow/Storage/Framework/UserData.xcdatamodeld/UserData.xcdatamodel/contents similarity index 100% rename from LoopFollow/Storage/UserData.xcdatamodeld/UserData.xcdatamodel/contents rename to LoopFollow/Storage/Framework/UserData.xcdatamodeld/UserData.xcdatamodel/contents diff --git a/LoopFollow/Storage/UserDefaultsValue.swift b/LoopFollow/Storage/Framework/UserDefaultsValue.swift similarity index 100% rename from LoopFollow/Storage/UserDefaultsValue.swift rename to LoopFollow/Storage/Framework/UserDefaultsValue.swift diff --git a/LoopFollow/Storage/UserDefaultsValueGroups.swift b/LoopFollow/Storage/Framework/UserDefaultsValueGroups.swift similarity index 100% rename from LoopFollow/Storage/UserDefaultsValueGroups.swift rename to LoopFollow/Storage/Framework/UserDefaultsValueGroups.swift diff --git a/LoopFollow/Storage/Observable.swift b/LoopFollow/Storage/Observable.swift index 82e49053f..222b3b69a 100644 --- a/LoopFollow/Storage/Observable.swift +++ b/LoopFollow/Storage/Observable.swift @@ -10,6 +10,10 @@ import Foundation import HealthKit import SwiftUI +/* + Observable in memory storage + */ + class Observable { static let shared = Observable() diff --git a/LoopFollow/Storage/ObservableUserDefaults.swift b/LoopFollow/Storage/ObservableUserDefaults.swift index 7c98c6501..a4b999307 100644 --- a/LoopFollow/Storage/ObservableUserDefaults.swift +++ b/LoopFollow/Storage/ObservableUserDefaults.swift @@ -9,6 +9,10 @@ import Foundation import Combine +/* + Legacy storage, we are moving away from this + */ + class ObservableUserDefaults { static let shared = ObservableUserDefaults() diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index 771f1ce46..9eacd4059 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -9,6 +9,10 @@ import Foundation import HealthKit +/* + Observable persistant storage + */ + class Storage { var remoteType = StorageValue(key: "remoteType", defaultValue: .nightscout) var deviceToken = StorageValue(key: "deviceToken", defaultValue: "") @@ -51,8 +55,8 @@ class Storage { key: "alarmConfiguration", defaultValue: .default ) + var currentAlarm = StorageValue(key: "currentAlarm", defaultValue: nil) static let shared = Storage() - private init() { } } diff --git a/LoopFollow/Storage/UserDefaults.swift b/LoopFollow/Storage/UserDefaults.swift index a7e306957..cf6026c56 100644 --- a/LoopFollow/Storage/UserDefaults.swift +++ b/LoopFollow/Storage/UserDefaults.swift @@ -14,6 +14,10 @@ import Foundation import UIKit import HealthKit +/* + Legacy storage, we are moving away from this + */ + class UserDefaultsRepository { static let infoSort = UserDefaultsValue<[Int]>(key: "infoSort", default: InfoType.allCases.map { $0.sortOrder }) static let infoVisible = UserDefaultsValue<[Bool]>(key: "infoVisible", default: InfoType.allCases.map { $0.defaultVisible }) diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index 991fbb0e1..7353bb3c1 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -7,9 +7,10 @@ // import Foundation - +//TODO: Nu körs ju alarm var 60 sekund... men man vill nog ha det direkt efter bg-värdet kommer in etc. +//TODO: Men ändå kanske inte för nära ett tidigare alarm, men det kanske vi inte hanterar här.... extension MainViewController { - func scheduleAlarmTask(initialDelay: TimeInterval = 30) { + func scheduleAlarmTask(initialDelay: TimeInterval = 60) { let firstRun = Date().addingTimeInterval(initialDelay) TaskScheduler.shared.scheduleTask(id: .alarmCheck, nextRun: firstRun) { [weak self] in guard let self = self else { return } @@ -19,6 +20,8 @@ extension MainViewController { func alarmTaskAction() { DispatchQueue.main.async { + //TODO: Fyll på med mer alarmData + //TODO: gör det möjligt att köra med fejkad data. let alarmData = AlarmData( expireDate: Storage.shared.expirationDate.value ) @@ -26,18 +29,8 @@ extension MainViewController { LogManager.shared.log(category: .alarm, message: "Checking alarms based on \(alarmData)", isDebug: true) AlarmManager.shared.checkAlarms(data: alarmData) - /* - if self.bgData.count > 0 { - self.checkAlarms(bgs: self.bgData) - } - if self.overrideGraphData.count > 0 { - self.checkOverrideAlarms() - } - if self.tempTargetGraphData.count > 0 { - self.checkTempTargetAlarms() - }*/ - TaskScheduler.shared.rescheduleTask(id: .alarmCheck, to: Date().addingTimeInterval(30)) + TaskScheduler.shared.rescheduleTask(id: .alarmCheck, to: Date().addingTimeInterval(60)) } } } diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index c62669184..a599c7f83 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -60,10 +60,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele var backgroundTask = BackgroundTask() - // Check Alarms Timer - // Don't check within 1 minute of alarm triggering to give the snoozer time to save data - var checkAlarmTimer = Timer() - var checkAlarmInterval: TimeInterval = 60.0 var graphNowTimer = Timer() var lastCalendarWriteAttemptTime: TimeInterval = 0 @@ -671,9 +667,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele // General Notifications func sendGeneralNotification(_ sender: Any, title: String, subtitle: String, body: String, timer: TimeInterval) { - - UNUserNotificationCenter.current().delegate = self - let content = UNMutableNotificationContent() content.title = title content.subtitle = subtitle diff --git a/LoopFollow/ViewControllers/SnoozeViewController.swift b/LoopFollow/ViewControllers/SnoozeViewController.swift deleted file mode 100644 index 652fe1179..000000000 --- a/LoopFollow/ViewControllers/SnoozeViewController.swift +++ /dev/null @@ -1,498 +0,0 @@ -// -// SecondViewController.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/1/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// - -import UIKit -import UserNotifications - -//TODO -class SnoozeViewController: UIViewController, UNUserNotificationCenterDelegate { - var appStateController: AppStateController? - var snoozeTabItem: UITabBarItem = UITabBarItem() - var mainTabItem: UITabBarItem = UITabBarItem() - var clockTimer: Timer = Timer() - - - - @IBOutlet weak var SnoozeButton: UIButton! - - @IBOutlet weak var BGLabel: UILabel! - @IBOutlet weak var DirectionLabel: UILabel! - @IBOutlet weak var DeltaLabel: UILabel! - @IBOutlet weak var MinAgoLabel: UILabel! - @IBOutlet weak var AlertLabel: UILabel! - @IBOutlet weak var clockLabel: UILabel! - @IBOutlet weak var snoozeForMinuteLabel: UILabel! - @IBOutlet weak var snoozeForMinuteStepper: UIStepper! - @IBOutlet weak var debugTextView: UITextView! - - @IBAction func SnoozeButton(_ sender: Any) { - AlarmSound.stop() - - guard let mainVC = self.tabBarController!.viewControllers?[0] as? MainViewController else { return } - mainVC.startCheckAlarmTimer(time: mainVC.checkAlarmInterval) - - let tabBarControllerItems = self.tabBarController?.tabBar.items - if let arrayOfTabBarItems = tabBarControllerItems as! AnyObject as? NSArray{ - snoozeTabItem = arrayOfTabBarItems[2] as! UITabBarItem - - } - - - setSnoozeTime() - AlertLabel.isHidden = true - SnoozeButton.isHidden = true - clockLabel.isHidden = false - snoozeForMinuteStepper.isHidden = true - snoozeForMinuteLabel.isHidden = true - - } - - @IBAction func snoozeForMinuteValChanged(_ sender: UIStepper) { - snoozeForMinuteLabel.text = Int(sender.value).description - } - - // Update Time - func startClockTimer(time: TimeInterval) { - clockTimer = Timer.scheduledTimer(timeInterval: time, - target: self, - selector: #selector(clockTimerDidEnd(_:)), - userInfo: nil, - repeats: true) - } - - // Update Time Ended - @objc func clockTimerDidEnd(_ timer:Timer) { - let formatter = DateFormatter() - if dateTimeUtils.is24Hour() { - formatter.setLocalizedDateFormatFromTemplate("HH:mm") - } else { - formatter.setLocalizedDateFormatFromTemplate("hh:mm a") - } - - clockLabel.text = formatter.string(from: Date()) - } - - func updateDisplayWhenTriggered(bgVal: String, directionVal: String, deltaVal: String, minAgoVal: String, alertLabelVal: String){ - loadViewIfNeeded() - BGLabel.text = bgVal - DirectionLabel.text = directionVal - DeltaLabel.text = deltaVal - MinAgoLabel.text = minAgoVal - AlertLabel.text = alertLabelVal - if alertLabelVal == "none" { return } - sendNotification(self, bgVal: bgVal, directionVal: directionVal, deltaVal: deltaVal, minAgoVal: minAgoVal, alertLabelVal: alertLabelVal) - } - - func sendNotification(_ sender: Any, bgVal: String, directionVal: String, deltaVal: String, minAgoVal: String, alertLabelVal: String) { - - UNUserNotificationCenter.current().delegate = self - - let content = UNMutableNotificationContent() - content.title = alertLabelVal - content.subtitle += bgVal + " " - content.subtitle += directionVal + " " - content.subtitle += deltaVal - content.categoryIdentifier = "category" - // This is needed to trigger vibrate on watch and phone - // TODO: - // See if we can use .Critcal - // See if we should use this method instead of direct sound player - content.sound = .default - - let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) - let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger) - UNUserNotificationCenter.current().add(request, withCompletionHandler: nil) - - let action = UNNotificationAction(identifier: "snooze", title: "Snooze", options: []) - let category = UNNotificationCategory(identifier: "category", actions: [action], intentIdentifiers: [], options: []) - UNUserNotificationCenter.current().setNotificationCategories([category]) - } - - func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - if response.actionIdentifier == "snooze" { - SnoozeButton(self) - } - } - - func setSnoozeTime() { - guard let alarms = ViewControllerManager.shared.alarmViewController else { return } - - let snoozeDuration = TimeInterval(snoozeForMinuteStepper.value * 60) - let longSnoozeDuration = TimeInterval(snoozeForMinuteStepper.value * 60 * 60) - let currentDate = Date() - - switch AlarmSound.whichAlarm { - case "Temporary Alert": - UserDefaultsRepository.alertTemporaryActive.value = false - alarms.reloadIsSnoozed(key: "alertTemporaryActive", value: false) - - case "Urgent Low Alert": - UserDefaultsRepository.alertUrgentLowIsSnoozed.value = true - UserDefaultsRepository.alertUrgentLowSnoozedTime.value = currentDate.addingTimeInterval(snoozeDuration) - alarms.reloadIsSnoozed(key: "alertUrgentLowIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertUrgentLowSnoozedTime", setNil: false, value: currentDate.addingTimeInterval(snoozeDuration)) - - case "Low Alert": - UserDefaultsRepository.alertLowIsSnoozed.value = true - UserDefaultsRepository.alertLowSnoozedTime.value = currentDate.addingTimeInterval(snoozeDuration) - alarms.reloadIsSnoozed(key: "alertLowIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertLowSnoozedTime", setNil: false, value: currentDate.addingTimeInterval(snoozeDuration)) - - case "Predicted Urgent Low Alert": - UserDefaultsRepository.alertUrgentLowIsSnoozed.value = true - UserDefaultsRepository.alertUrgentLowSnoozedTime.value = currentDate.addingTimeInterval(snoozeDuration) - alarms.reloadIsSnoozed(key: "alertUrgentLowIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertUrgentLowSnoozedTime", setNil: false, value: currentDate.addingTimeInterval(snoozeDuration)) - - case "High Alert": - UserDefaultsRepository.alertHighIsSnoozed.value = true - UserDefaultsRepository.alertHighSnoozedTime.value = currentDate.addingTimeInterval(snoozeDuration) - alarms.reloadIsSnoozed(key: "alertHighIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertHighSnoozedTime", setNil: false, value: currentDate.addingTimeInterval(snoozeDuration)) - - case "Urgent High Alert": - UserDefaultsRepository.alertUrgentHighIsSnoozed.value = true - UserDefaultsRepository.alertUrgentHighSnoozedTime.value = currentDate.addingTimeInterval(snoozeDuration) - alarms.reloadIsSnoozed(key: "alertUrgentHighIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertUrgentHighSnoozedTime", setNil: false, value: currentDate.addingTimeInterval(snoozeDuration)) - - case "Fast Drop Alert": - UserDefaultsRepository.alertFastDropIsSnoozed.value = true - UserDefaultsRepository.alertFastDropSnoozedTime.value = currentDate.addingTimeInterval(snoozeDuration) - alarms.reloadIsSnoozed(key: "alertFastDropIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertFastDropSnoozedTime", setNil: false, value: currentDate.addingTimeInterval(snoozeDuration)) - - case "Fast Rise Alert": - UserDefaultsRepository.alertFastRiseIsSnoozed.value = true - UserDefaultsRepository.alertFastRiseSnoozedTime.value = currentDate.addingTimeInterval(snoozeDuration) - alarms.reloadIsSnoozed(key: "alertFastRiseIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertFastRiseSnoozedTime", setNil: false, value: currentDate.addingTimeInterval(snoozeDuration)) - - case "Missed Reading Alert": - UserDefaultsRepository.alertMissedReadingIsSnoozed.value = true - UserDefaultsRepository.alertMissedReadingSnoozedTime.value = currentDate.addingTimeInterval(snoozeDuration) - alarms.reloadIsSnoozed(key: "alertMissedReadingIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertMissedReadingSnoozedTime", setNil: false, value: currentDate.addingTimeInterval(snoozeDuration)) - - case "Sensor Change Alert": - UserDefaultsRepository.alertSAGEIsSnoozed.value = true - UserDefaultsRepository.alertSAGESnoozedTime.value = currentDate.addingTimeInterval(longSnoozeDuration) - alarms.reloadIsSnoozed(key: "alertSAGEIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertSAGESnoozedTime", setNil: false, value: currentDate.addingTimeInterval(longSnoozeDuration)) - - case "Pump Change Alert": - UserDefaultsRepository.alertCAGEIsSnoozed.value = true - UserDefaultsRepository.alertCAGESnoozedTime.value = currentDate.addingTimeInterval(longSnoozeDuration) - alarms.reloadIsSnoozed(key: "alertCAGEIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertCAGESnoozedTime", setNil: false, value: currentDate.addingTimeInterval(longSnoozeDuration)) - - case "Not Looping Alert": - UserDefaultsRepository.alertNotLoopingIsSnoozed.value = true - UserDefaultsRepository.alertNotLoopingSnoozedTime.value = currentDate.addingTimeInterval(snoozeDuration) - alarms.reloadIsSnoozed(key: "alertNotLoopingIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertNotLoopingSnoozedTime", setNil: false, value: currentDate.addingTimeInterval(snoozeDuration)) - - case "Missed Bolus Alert": - UserDefaultsRepository.alertMissedBolusIsSnoozed.value = true - UserDefaultsRepository.alertMissedBolusSnoozedTime.value = currentDate.addingTimeInterval(snoozeDuration) - alarms.reloadIsSnoozed(key: "alertMissedBolusIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertMissedBolusSnoozedTime", setNil: false, value: currentDate.addingTimeInterval(snoozeDuration)) - - case "Low Insulin Alert": - UserDefaultsRepository.alertPumpIsSnoozed.value = true - UserDefaultsRepository.alertPumpSnoozedTime.value = currentDate.addingTimeInterval(longSnoozeDuration) - alarms.reloadIsSnoozed(key: "alertPumpIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertPumpSnoozedTime", setNil: false, value: currentDate.addingTimeInterval(longSnoozeDuration)) - - case "IOB Alert": - UserDefaultsRepository.alertIOBIsSnoozed.value = true - UserDefaultsRepository.alertIOBSnoozedTime.value = currentDate.addingTimeInterval(longSnoozeDuration) - alarms.reloadIsSnoozed(key: "alertIOBIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertIOBSnoozedTime", setNil: false, value: currentDate.addingTimeInterval(longSnoozeDuration)) - - case "COB Alert": - UserDefaultsRepository.alertCOBIsSnoozed.value = true - UserDefaultsRepository.alertCOBSnoozedTime.value = currentDate.addingTimeInterval(longSnoozeDuration) - alarms.reloadIsSnoozed(key: "alertCOBIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertCOBSnoozedTime", setNil: false, value: currentDate.addingTimeInterval(longSnoozeDuration)) - - case "Low Battery": - UserDefaultsRepository.alertBatteryIsSnoozed.value = true - UserDefaultsRepository.alertBatterySnoozedTime.value = currentDate.addingTimeInterval(longSnoozeDuration) - alarms.reloadIsSnoozed(key: "alertBatteryIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertBatterySnoozedTime", setNil: false, value: currentDate.addingTimeInterval(longSnoozeDuration)) - - case "Battery Drop": - UserDefaultsRepository.alertBatteryDropIsSnoozed.value = true - UserDefaultsRepository.alertBatteryDropSnoozedTime.value = currentDate.addingTimeInterval(longSnoozeDuration) - alarms.reloadIsSnoozed(key: "alertBatteryDropIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertBatteryDropSnoozedTime", setNil: false, value: currentDate.addingTimeInterval(longSnoozeDuration)) - - case "Rec. Bolus": - UserDefaultsRepository.alertRecBolusIsSnoozed.value = true - UserDefaultsRepository.alertRecBolusSnoozedTime.value = currentDate.addingTimeInterval(snoozeDuration) - alarms.reloadIsSnoozed(key: "alertRecBolusIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertRecBolusSnoozedTime", setNil: false, value: currentDate.addingTimeInterval(snoozeDuration)) - - case "Temp Target Start": - UserDefaultsRepository.alertTempTargetStartIsSnoozed.value = true - UserDefaultsRepository.alertTempTargetStartSnoozedTime.value = currentDate.addingTimeInterval(snoozeDuration) - alarms.reloadIsSnoozed(key: "alertTempTargetStartIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertTempTargetStartSnoozedTime", setNil: false, value: currentDate.addingTimeInterval(snoozeDuration)) - - case "Temp Target End": - UserDefaultsRepository.alertTempTargetEndIsSnoozed.value = true - UserDefaultsRepository.alertTempTargetEndSnoozedTime.value = currentDate.addingTimeInterval(snoozeDuration) - alarms.reloadIsSnoozed(key: "alertTempTargetEndIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertTempTargetEndSnoozedTime", setNil: false, value: currentDate.addingTimeInterval(snoozeDuration)) - - default: - LogManager.shared.log(category: .alarm, message: "Unhandled alarm: \(AlarmSound.whichAlarm)") - } - } - - func setPresnoozeNight(snoozeTime: Date) { - guard let alarms = ViewControllerManager.shared.alarmViewController else { return } - - if UserDefaultsRepository.alertUrgentLowAutosnoozeNight.value { - UserDefaultsRepository.alertUrgentLowIsSnoozed.value = true - UserDefaultsRepository.alertUrgentLowSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertUrgentLowIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertUrgentLowSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertLowAutosnoozeNight.value { - UserDefaultsRepository.alertLowIsSnoozed.value = true - UserDefaultsRepository.alertLowSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertLowIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertLowSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertHighAutosnoozeNight.value { - UserDefaultsRepository.alertHighIsSnoozed.value = true - UserDefaultsRepository.alertHighSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertHighIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertHighSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertUrgentHighAutosnoozeNight.value { - UserDefaultsRepository.alertUrgentHighIsSnoozed.value = true - UserDefaultsRepository.alertUrgentHighSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertUrgentHighIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertUrgentHighSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertMissedReadingAutosnoozeNight.value { - UserDefaultsRepository.alertMissedReadingIsSnoozed.value = true - UserDefaultsRepository.alertMissedReadingSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertMissedReadingIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertMissedReadingSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertFastDropAutosnoozeNight.value { - UserDefaultsRepository.alertFastDropIsSnoozed.value = true - UserDefaultsRepository.alertFastDropSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertFastDropIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertFastDropSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertFastRiseAutosnoozeNight.value { - UserDefaultsRepository.alertFastRiseIsSnoozed.value = true - UserDefaultsRepository.alertFastRiseSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertFastRiseIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertFastRiseSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertNotLoopingAutosnoozeNight.value { - UserDefaultsRepository.alertNotLoopingIsSnoozed.value = true - UserDefaultsRepository.alertNotLoopingSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertNotLoopingIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertNotLoopingSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertMissedBolusAutosnoozeNight.value { - UserDefaultsRepository.alertMissedBolusIsSnoozed.value = true - UserDefaultsRepository.alertMissedBolusSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertMissedBolusIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertMissedBolusSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertOverrideStartAutosnoozeNight.value { - UserDefaultsRepository.alertOverrideStartIsSnoozed.value = true - UserDefaultsRepository.alertOverrideStartSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertOverrideStartIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertOverrideStartSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertOverrideEndAutosnoozeNight.value { - UserDefaultsRepository.alertOverrideEndIsSnoozed.value = true - UserDefaultsRepository.alertOverrideEndSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertOverrideEndIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertOverrideEndSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertCAGEAutosnoozeNight.value { - UserDefaultsRepository.alertCAGEIsSnoozed.value = true - UserDefaultsRepository.alertCAGESnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertCAGEIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertCAGESnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertSAGEAutosnoozeNight.value { - UserDefaultsRepository.alertSAGEIsSnoozed.value = true - UserDefaultsRepository.alertSAGESnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertSAGEIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertSAGESnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertPumpAutosnoozeNight.value { - UserDefaultsRepository.alertPumpIsSnoozed.value = true - UserDefaultsRepository.alertPumpSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertPumpIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertPumpSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertIOBAutosnoozeNight.value { - UserDefaultsRepository.alertIOBIsSnoozed.value = true - UserDefaultsRepository.alertIOBSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertIOBIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertIOBSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertCOBAutosnoozeNight.value { - UserDefaultsRepository.alertCOBIsSnoozed.value = true - UserDefaultsRepository.alertCOBSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertCOBIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertCOBSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertTempTargetStartAutosnoozeNight.value { - UserDefaultsRepository.alertTempTargetStartIsSnoozed.value = true - UserDefaultsRepository.alertTempTargetStartSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertTempTargetStartIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertTempTargetStartSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertTempTargetEndAutosnoozeNight.value { - UserDefaultsRepository.alertTempTargetEndIsSnoozed.value = true - UserDefaultsRepository.alertTempTargetEndSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertTempTargetEndIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertTempTargetEndSnoozedTime", setNil: false, value: snoozeTime) - } - } - - func setPreSnoozeDay(snoozeTime: Date) { - guard let alarms = ViewControllerManager.shared.alarmViewController else { return } - - if UserDefaultsRepository.alertUrgentLowAutosnoozeDay.value { - UserDefaultsRepository.alertUrgentLowIsSnoozed.value = true - UserDefaultsRepository.alertUrgentLowSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertUrgentLowIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertUrgentLowSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertLowAutosnoozeDay.value { - UserDefaultsRepository.alertLowIsSnoozed.value = true - UserDefaultsRepository.alertLowSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertLowIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertLowSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertHighAutosnoozeDay.value { - UserDefaultsRepository.alertHighIsSnoozed.value = true - UserDefaultsRepository.alertHighSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertHighIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertHighSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertUrgentHighAutosnoozeDay.value { - UserDefaultsRepository.alertUrgentHighIsSnoozed.value = true - UserDefaultsRepository.alertUrgentHighSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertUrgentHighIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertUrgentHighSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertMissedReadingAutosnoozeDay.value { - UserDefaultsRepository.alertMissedReadingIsSnoozed.value = true - UserDefaultsRepository.alertMissedReadingSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertMissedReadingIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertMissedReadingSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertFastDropAutosnoozeDay.value { - UserDefaultsRepository.alertFastDropIsSnoozed.value = true - UserDefaultsRepository.alertFastDropSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertFastDropIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertFastDropSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertFastRiseAutosnoozeDay.value { - UserDefaultsRepository.alertFastRiseIsSnoozed.value = true - UserDefaultsRepository.alertFastRiseSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertFastRiseIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertFastRiseSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertNotLoopingAutosnoozeDay.value { - UserDefaultsRepository.alertNotLoopingIsSnoozed.value = true - UserDefaultsRepository.alertNotLoopingSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertNotLoopingIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertNotLoopingSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertMissedBolusAutosnoozeDay.value { - UserDefaultsRepository.alertMissedBolusIsSnoozed.value = true - UserDefaultsRepository.alertMissedBolusSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertMissedBolusIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertMissedBolusSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertOverrideStartAutosnoozeDay.value { - UserDefaultsRepository.alertOverrideStartIsSnoozed.value = true - UserDefaultsRepository.alertOverrideStartSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertOverrideStartIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertOverrideStartSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertOverrideEndAutosnoozeDay.value { - UserDefaultsRepository.alertOverrideEndIsSnoozed.value = true - UserDefaultsRepository.alertOverrideEndSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertOverrideEndIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertOverrideEndSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertCAGEAutosnoozeDay.value { - UserDefaultsRepository.alertCAGEIsSnoozed.value = true - UserDefaultsRepository.alertCAGESnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertCAGEIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertCAGESnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertSAGEAutosnoozeDay.value { - UserDefaultsRepository.alertSAGEIsSnoozed.value = true - UserDefaultsRepository.alertSAGESnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertSAGEIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertSAGESnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertPumpAutosnoozeDay.value { - UserDefaultsRepository.alertPumpIsSnoozed.value = true - UserDefaultsRepository.alertPumpSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertPumpIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertPumpSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertIOBAutosnoozeDay.value { - UserDefaultsRepository.alertIOBIsSnoozed.value = true - UserDefaultsRepository.alertIOBSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertIOBIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertIOBSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertCOBAutosnoozeDay.value { - UserDefaultsRepository.alertCOBIsSnoozed.value = true - UserDefaultsRepository.alertCOBSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertCOBIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertCOBSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertTempTargetStartAutosnoozeDay.value { - UserDefaultsRepository.alertTempTargetStartIsSnoozed.value = true - UserDefaultsRepository.alertTempTargetStartSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertTempTargetStartIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertTempTargetStartSnoozedTime", setNil: false, value: snoozeTime) - } - if UserDefaultsRepository.alertTempTargetEndAutosnoozeDay.value { - UserDefaultsRepository.alertTempTargetEndIsSnoozed.value = true - UserDefaultsRepository.alertTempTargetEndSnoozedTime.value = snoozeTime - alarms.reloadIsSnoozed(key: "alertTempTargetEndIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertTempTargetEndSnoozedTime", setNil: false, value: snoozeTime) - } - } - - override func viewDidLoad() { - super.viewDidLoad() - if UserDefaultsRepository.forceDarkMode.value { - overrideUserInterfaceStyle = .dark - } - SnoozeButton.layer.cornerRadius = 5 - SnoozeButton.contentEdgeInsets = UIEdgeInsets(top: 10,left: 10,bottom: 10,right: 10) - clockLabel.text = "" - startClockTimer(time: 1) - } - - - -} diff --git a/LoopFollowTests/AlwaysTrueCondition.swift b/LoopFollowTests/AlwaysTrueCondition.swift index d15f00dd0..1cf5dd1d6 100644 --- a/LoopFollowTests/AlwaysTrueCondition.swift +++ b/LoopFollowTests/AlwaysTrueCondition.swift @@ -8,7 +8,7 @@ import XCTest @testable import LoopFollow - +/* struct AlwaysTrueCondition: AlarmCondition { static let type: AlarmType = .low init() {} @@ -75,3 +75,4 @@ final class CommonAlarmGuardsTests: XCTestCase { XCTAssertTrue(cond.shouldFire(alarm: alarm, data: data, context: context)) } } +*/ From 7b64fe48453d81b204d9b58be61d6227641e771b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 3 May 2025 21:16:01 +0200 Subject: [PATCH 009/138] Focus to the snooze tab --- LoopFollow/Alarm/AlarmManager.swift | 20 +++--- LoopFollow/Controllers/AlarmSound.swift | 42 ++++++------ LoopFollow/Storage/Observable.swift | 2 + LoopFollow/Storage/Storage.swift | 1 - LoopFollow/Task/AlarmTask.swift | 2 +- LoopFollow/Task/TaskScheduler.swift | 4 +- .../ViewControllers/MainViewController.swift | 9 +++ LoopFollowTests/AlwaysTrueCondition.swift | 68 ------------------- 8 files changed, 45 insertions(+), 103 deletions(-) diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index d1fd0f5dd..22dd6d9f3 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -12,16 +12,13 @@ class AlarmManager { static let shared = AlarmManager() private let evaluators: [AlarmType: AlarmCondition] - private let config: AlarmConfiguration private init( - config: AlarmConfiguration = .default, conditionTypes: [AlarmCondition.Type] = [ BuildExpireCondition.self - // …add your other condition types here + // TODO: add other condition types here ] ) { - self.config = config var dict = [AlarmType: AlarmCondition]() conditionTypes.forEach { dict[$0.type] = $0.init() } evaluators = dict @@ -62,18 +59,21 @@ class AlarmManager { // Evaluate the alarm condition. guard let checker = evaluators[alarm.type], - checker.shouldFire(alarm: alarm, data: data, now: now, config: config) + checker + .shouldFire( + alarm: alarm, + data: data, + now: now, + config: Storage.shared.alarmConfiguration.value + ) else { continue } // Fire the alarm and break the loop; we only allow one alarm per evaluation tick. + Observable.shared.currentAlarm.value = alarm.id - //TODO: a few things affecting the snoozed screen - Storage.shared.currentAlarm.value = alarm.id - //tabBarController?.selectedIndex = 2 - - alarm.trigger(config: config, now: now) + alarm.trigger(config: Storage.shared.alarmConfiguration.value, now: now) break } } diff --git a/LoopFollow/Controllers/AlarmSound.swift b/LoopFollow/Controllers/AlarmSound.swift index 94a4abc94..38ab115be 100644 --- a/LoopFollow/Controllers/AlarmSound.swift +++ b/LoopFollow/Controllers/AlarmSound.swift @@ -112,21 +112,21 @@ class AlarmSound { //self.audioPlayer!.volume = (self.muted || (UserDefaultsRepository.fadeInTimeInterval.value > 0)) ? 0.0 : 1.0 if !self.audioPlayer!.prepareToPlay() { - LogManager.shared.log(category: .general, message: "AlarmSound - audio player failed preparing to play") + LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed preparing to play") } if self.audioPlayer!.play() { if !self.isPlaying { - LogManager.shared.log(category: .general, message: "AlarmSound - not playing after calling play") - LogManager.shared.log(category: .general, message: "AlarmSound - rate value: \(self.audioPlayer!.rate)") + LogManager.shared.log(category: .alarm, message: "AlarmSound - not playing after calling play") + LogManager.shared.log(category: .alarm, message: "AlarmSound - rate value: \(self.audioPlayer!.rate)") } } else { - LogManager.shared.log(category: .general, message: "AlarmSound - audio player failed to play") + LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed to play") } } catch let error { - LogManager.shared.log(category: .general, message: "AlarmSound - unable to play sound; error: \(error)") + LogManager.shared.log(category: .alarm, message: "AlarmSound - unable to play sound; error: \(error)") } } @@ -156,16 +156,16 @@ class AlarmSound { //self.audioPlayer!.volume = (self.muted || (UserDefaultsRepository.fadeInTimeInterval.value > 0)) ? 0.0 : 1.0 if !self.audioPlayer!.prepareToPlay() { - LogManager.shared.log(category: .general, message: "AlarmSound - audio player failed preparing to play") + LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed preparing to play") } if self.audioPlayer!.play() { if !self.isPlaying { - LogManager.shared.log(category: .general, message: "AlarmSound - not playing after calling play") - LogManager.shared.log(category: .general, message: "AlarmSound - rate value: \(self.audioPlayer!.rate)") + LogManager.shared.log(category: .alarm, message: "AlarmSound - not playing after calling play") + LogManager.shared.log(category: .alarm, message: "AlarmSound - rate value: \(self.audioPlayer!.rate)") } } else { - LogManager.shared.log(category: .general, message: "AlarmSound - audio player failed to play") + LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed to play") } @@ -178,7 +178,7 @@ class AlarmSound { MPVolumeView.setVolume(Storage.shared.alarmConfiguration.value.forcedOutputVolume) } } catch let error { - LogManager.shared.log(category: .general, message: "AlarmSound - unable to play sound; error: \(error)") + LogManager.shared.log(category: .alarm, message: "AlarmSound - unable to play sound; error: \(error)") } } @@ -205,16 +205,16 @@ class AlarmSound { if !self.audioPlayer!.prepareToPlay() { - LogManager.shared.log(category: .general, message: "Terminate AlarmSound - audio player failed preparing to play") + LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - audio player failed preparing to play") } if self.audioPlayer!.play() { if !self.isPlaying { - LogManager.shared.log(category: .general, message: "Terminate AlarmSound - not playing after calling play") - LogManager.shared.log(category: .general, message: "Terminate AlarmSound - rate value: \(self.audioPlayer!.rate)") + LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - not playing after calling play") + LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - rate value: \(self.audioPlayer!.rate)") } } else { - LogManager.shared.log(category: .general, message: "Terminate AlarmSound - audio player failed to play") + LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - audio player failed to play") } @@ -222,7 +222,7 @@ class AlarmSound { } catch let error { - LogManager.shared.log(category: .general, message: "Terminate AlarmSound - unable to play sound; error: \(error)") + LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - unable to play sound; error: \(error)") } } @@ -249,7 +249,7 @@ class AlarmSound { try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: .mixWithOthers) try AVAudioSession.sharedInstance().setActive(true) } catch { - LogManager.shared.log(category: .general, message: "Enable audio error: \(error)") + LogManager.shared.log(category: .alarm, message: "Enable audio error: \(error)") } } } @@ -258,15 +258,15 @@ class AudioPlayerDelegate: NSObject, AVAudioPlayerDelegate { /* audioPlayerDidFinishPlaying:successfully: is called when a sound has finished playing. This method is NOT called if the player is stopped due to an interruption. */ func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { - LogManager.shared.log(category: .general, message: "AlarmRule - audioPlayerDidFinishPlaying (\(flag))", isDebug: true) + LogManager.shared.log(category: .alarm, message: "AlarmRule - audioPlayerDidFinishPlaying (\(flag))", isDebug: true) } /* if an error occurs while decoding it will be reported to the delegate. */ func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) { if let error = error { - LogManager.shared.log(category: .general, message: "AlarmRule - audioPlayerDecodeErrorDidOccur: \(error)") + LogManager.shared.log(category: .alarm, message: "AlarmRule - audioPlayerDecodeErrorDidOccur: \(error)") } else { - LogManager.shared.log(category: .general, message: "AlarmRule - audioPlayerDecodeErrorDidOccur") + LogManager.shared.log(category: .alarm, message: "AlarmRule - audioPlayerDecodeErrorDidOccur") } } @@ -274,14 +274,14 @@ class AudioPlayerDelegate: NSObject, AVAudioPlayerDelegate { /* audioPlayerBeginInterruption: is called when the audio session has been interrupted while the player was playing. The player will have been paused. */ func audioPlayerBeginInterruption(_ player: AVAudioPlayer) { - LogManager.shared.log(category: .general, message: "AlarmRule - audioPlayerBeginInterruption") + LogManager.shared.log(category: .alarm, message: "AlarmRule - audioPlayerBeginInterruption") } /* audioPlayerEndInterruption:withOptions: is called when the audio session interruption has ended and this player had been interrupted while playing. */ /* Currently the only flag is AVAudioSessionInterruptionFlags_ShouldResume. */ func audioPlayerEndInterruption(_ player: AVAudioPlayer, withOptions flags: Int) { - LogManager.shared.log(category: .general, message: "AlarmRule - audioPlayerEndInterruption withOptions: \(flags)") + LogManager.shared.log(category: .alarm, message: "AlarmRule - audioPlayerEndInterruption withOptions: \(flags)") } } diff --git a/LoopFollow/Storage/Observable.swift b/LoopFollow/Storage/Observable.swift index 222b3b69a..ec1462c3e 100644 --- a/LoopFollow/Storage/Observable.swift +++ b/LoopFollow/Storage/Observable.swift @@ -28,5 +28,7 @@ class Observable { var directionText = ObservableValue(default: "-") var deltaText = ObservableValue(default: "+0") + var currentAlarm = ObservableValue(default: nil) + private init() {} } diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index 9eacd4059..dd7230524 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -55,7 +55,6 @@ class Storage { key: "alarmConfiguration", defaultValue: .default ) - var currentAlarm = StorageValue(key: "currentAlarm", defaultValue: nil) static let shared = Storage() private init() { } diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index 7353bb3c1..2cbe63eca 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -23,7 +23,7 @@ extension MainViewController { //TODO: Fyll på med mer alarmData //TODO: gör det möjligt att köra med fejkad data. let alarmData = AlarmData( - expireDate: Storage.shared.expirationDate.value + expireDate: .distantPast // Storage.shared.expirationDate.value ) LogManager.shared.log(category: .alarm, message: "Checking alarms based on \(alarmData)", isDebug: true) diff --git a/LoopFollow/Task/TaskScheduler.swift b/LoopFollow/Task/TaskScheduler.swift index ebcc689b1..2da36c045 100644 --- a/LoopFollow/Task/TaskScheduler.swift +++ b/LoopFollow/Task/TaskScheduler.swift @@ -48,7 +48,7 @@ class TaskScheduler { func rescheduleTask(id: TaskID, to newRunDate: Date) { let timeString = self.formatTime(newRunDate) - LogManager.shared.log(category: .taskScheduler, message: "Reschedule Task \(id): next run = \(timeString)", isDebug: true) + //LogManager.shared.log(category: .taskScheduler, message: "Reschedule Task \(id): next run = \(timeString)", isDebug: true) queue.async { guard var existingTask = self.tasks[id] else { return } @@ -118,7 +118,7 @@ class TaskScheduler { updatedTask.nextRun = .distantFuture tasks[taskID] = updatedTask - LogManager.shared.log(category: .taskScheduler, message: "Executing task \(taskID)", isDebug: true) + //LogManager.shared.log(category: .taskScheduler, message: "Executing Task \(taskID)", isDebug: true) DispatchQueue.main.async { task.action() diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index a599c7f83..ad4124ca8 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -235,6 +235,15 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele self?.DeltaText.text = newValue } .store(in: &cancellables) + + /// When an alarm is triggered, go to the snoozer tab + Observable.shared.currentAlarm.$value + .receive(on: DispatchQueue.main) + .compactMap { $0 } /// Ignore nil + .sink { [weak self] _ in + self?.tabBarController?.selectedIndex = 2 + } + .store(in: &cancellables) } deinit { diff --git a/LoopFollowTests/AlwaysTrueCondition.swift b/LoopFollowTests/AlwaysTrueCondition.swift index 1cf5dd1d6..96c231309 100644 --- a/LoopFollowTests/AlwaysTrueCondition.swift +++ b/LoopFollowTests/AlwaysTrueCondition.swift @@ -8,71 +8,3 @@ import XCTest @testable import LoopFollow -/* -struct AlwaysTrueCondition: AlarmCondition { - static let type: AlarmType = .low - init() {} - func evaluate(alarm: Alarm, data: AlarmData) -> Bool { true } -} - -final class CommonAlarmGuardsTests: XCTestCase { - var now: Date! - var config: AlarmConfiguration! - var context: AlarmContext! - var alarm: Alarm! - var data: AlarmData! - var cond: AlwaysTrueCondition! - - override func setUp() { - super.setUp() - now = Date() - config = .default - context = AlarmContext(now: now, config: config) - - alarm = Alarm(type: .low) - alarm.name = "test" - alarm.isEnabled = true - alarm.snoozedUntil = nil - alarm.playSoundOption = .always - alarm.activeOption = .always - alarm.snoozeDuration = 0 - - data = AlarmData(expireDate: nil) - cond = AlwaysTrueCondition() - } - - func testMuteUntil() { - config.muteUntil = now.addingTimeInterval(60) - context = AlarmContext(now: now, config: config) - XCTAssertFalse(cond.shouldFire(alarm: alarm, data: data, context: context)) - } - - func testDisabledAlarm() { - alarm.isEnabled = false - XCTAssertFalse(cond.shouldFire(alarm: alarm, data: data, context: context)) - } - - func testSnoozedAlarm() { - alarm.snoozedUntil = now.addingTimeInterval(60) - XCTAssertFalse(cond.shouldFire(alarm: alarm, data: data, context: context)) - } - - func testRespectsNightFlag() { - let night = Calendar.current.date(bySettingHour: 23, minute: 0, second: 0, of: now)! - alarm.activeOption = .day // alarm should be inactive at night - context = AlarmContext(now: night, config: config) - XCTAssertFalse(cond.shouldFire(alarm: alarm, data: data, context: context)) - } - - func testRespectsDayFlag() { - let day = Calendar.current.date(bySettingHour: 9, minute: 0, second: 0, of: now)! - alarm.activeOption = .night // alarm should be inactive during day - context = AlarmContext(now: day, config: config) - XCTAssertFalse(cond.shouldFire(alarm: alarm, data: data, context: context)) - } - - func testAllGuardsPass() { - XCTAssertTrue(cond.shouldFire(alarm: alarm, data: data, context: context)) - } -} -*/ From 70fee8e0510a963212486cba91e975daa81d778e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 3 May 2025 21:27:14 +0200 Subject: [PATCH 010/138] No more AlarmViewController.swift --- LoopFollow.xcodeproj/project.pbxproj | 8 - LoopFollow/Application/AppDelegate.swift | 3 - LoopFollow/Application/SceneDelegate.swift | 8 - LoopFollow/Controllers/Nightscout/SAge.swift | 3 - .../Controllers/ViewControllerManager.swift | 26 - .../ViewControllers/AlarmViewController.swift | 3735 ----------------- .../SettingsViewController.swift | 11 - 7 files changed, 3794 deletions(-) delete mode 100644 LoopFollow/Controllers/ViewControllerManager.swift delete mode 100644 LoopFollow/ViewControllers/AlarmViewController.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 7dba17289..0ec25b515 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -143,7 +143,6 @@ DDF2C0102BEFA991007A20E6 /* GitHubService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF2C00F2BEFA991007A20E6 /* GitHubService.swift */; }; DDF2C0122BEFB733007A20E6 /* AppVersionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF2C0112BEFB733007A20E6 /* AppVersionManager.swift */; }; DDF2C0142BEFD468007A20E6 /* blacklisted-versions.json in Resources */ = {isa = PBXBuildFile; fileRef = DDF2C0132BEFD468007A20E6 /* blacklisted-versions.json */; }; - DDF699942C555B310058A8D9 /* ViewControllerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF699932C555B310058A8D9 /* ViewControllerManager.swift */; }; DDF699962C5582290058A8D9 /* TextFieldWithToolBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF699952C5582290058A8D9 /* TextFieldWithToolBar.swift */; }; DDF699992C5AA3060058A8D9 /* TempTargetPresetManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF699982C5AA3060058A8D9 /* TempTargetPresetManager.swift */; }; DDF6999B2C5AA32E0058A8D9 /* TempTargetPreset.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6999A2C5AA32E0058A8D9 /* TempTargetPreset.swift */; }; @@ -289,7 +288,6 @@ FC9788292485969C00A7906C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FC9788272485969C00A7906C /* LaunchScreen.storyboard */; }; FCA2DDE62501095000254A8C /* Timers.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCA2DDE52501095000254A8C /* Timers.swift */; }; FCC0FAC224922A22003E610E /* DictionaryKeyPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC0FAC124922A22003E610E /* DictionaryKeyPath.swift */; }; - FCC68850248935D800A0279D /* AlarmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC6884F248935D800A0279D /* AlarmViewController.swift */; }; FCC6885C2489559400A0279D /* blank.wav in Resources */ = {isa = PBXBuildFile; fileRef = FCC6885B2489559400A0279D /* blank.wav */; }; FCC6885E24896A6C00A0279D /* silence.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = FCC6885D24896A6C00A0279D /* silence.mp3 */; }; FCC6886524898EEE00A0279D /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC6886424898EEE00A0279D /* UserDefaults.swift */; }; @@ -456,7 +454,6 @@ DDF2C00F2BEFA991007A20E6 /* GitHubService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHubService.swift; sourceTree = ""; }; DDF2C0112BEFB733007A20E6 /* AppVersionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersionManager.swift; sourceTree = ""; }; DDF2C0132BEFD468007A20E6 /* blacklisted-versions.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "blacklisted-versions.json"; sourceTree = ""; }; - DDF699932C555B310058A8D9 /* ViewControllerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewControllerManager.swift; sourceTree = ""; }; DDF699952C5582290058A8D9 /* TextFieldWithToolBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldWithToolBar.swift; sourceTree = ""; }; DDF699982C5AA3060058A8D9 /* TempTargetPresetManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetPresetManager.swift; sourceTree = ""; }; DDF6999A2C5AA32E0058A8D9 /* TempTargetPreset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetPreset.swift; sourceTree = ""; }; @@ -604,7 +601,6 @@ FC9788282485969C00A7906C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; FCA2DDE52501095000254A8C /* Timers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timers.swift; sourceTree = ""; }; FCC0FAC124922A22003E610E /* DictionaryKeyPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryKeyPath.swift; sourceTree = ""; }; - FCC6884F248935D800A0279D /* AlarmViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmViewController.swift; sourceTree = ""; }; FCC688592489554800A0279D /* BackgroundTaskAudio.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundTaskAudio.swift; sourceTree = ""; }; FCC6885B2489559400A0279D /* blank.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = blank.wav; sourceTree = ""; }; FCC6885D24896A6C00A0279D /* silence.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = silence.mp3; sourceTree = ""; }; @@ -1052,7 +1048,6 @@ FC1BDD2C24A23204001B652C /* StatsView.swift */, FCA2DDE52501095000254A8C /* Timers.swift */, DD608A0B2C27415C00F91132 /* BackgroundAlertManager.swift */, - DDF699932C555B310058A8D9 /* ViewControllerManager.swift */, ); path = Controllers; sourceTree = ""; @@ -1293,7 +1288,6 @@ FC97881D2485969B00A7906C /* NightScoutViewController.swift */, FCD49B6B24AA536E007879DC /* DebugViewController.swift */, FCFEECA1248857A600402A7F /* SettingsViewController.swift */, - FCC6884F248935D800A0279D /* AlarmViewController.swift */, DDCF979524C1443C002C9752 /* GeneralSettingsViewController.swift */, DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */, DDCF979924C14DB4002C9752 /* WatchSettingsViewController.swift */, @@ -1607,7 +1601,6 @@ buildActionMask = 2147483647; files = ( DD5334292C6166A500062F9D /* InfoDisplaySettingsView.swift in Sources */, - FCC68850248935D800A0279D /* AlarmViewController.swift in Sources */, DD48781E2C7DAF2F0048F05C /* PushNotificationManager.swift in Sources */, DD2C2E562D3C3917006413A5 /* DexcomSettingsView.swift in Sources */, DD4AFB672DB68C5500BB593F /* UUID+Identifiable.swift in Sources */, @@ -1622,7 +1615,6 @@ DDCF9A822D85FD15004DF4DD /* AlarmType.swift in Sources */, DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */, FCC6886724898F8000A0279D /* UserDefaultsValue.swift in Sources */, - DDF699942C555B310058A8D9 /* ViewControllerManager.swift in Sources */, DDCF979E24C2382A002C9752 /* AppStateController.swift in Sources */, DDEF503F2D32754F00999A5D /* ProfileTask.swift in Sources */, DD5334212C60EBEE00062F9D /* InsulinCartridgeChange.swift in Sources */, diff --git a/LoopFollow/Application/AppDelegate.swift b/LoopFollow/Application/AppDelegate.swift index e067d7d2e..2377d3561 100644 --- a/LoopFollow/Application/AppDelegate.swift +++ b/LoopFollow/Application/AppDelegate.swift @@ -43,9 +43,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { UNUserNotificationCenter.current().delegate = self - // Ensure ViewControllerManager is initialized - _ = ViewControllerManager.shared - _ = BLEManager.shared return true diff --git a/LoopFollow/Application/SceneDelegate.swift b/LoopFollow/Application/SceneDelegate.swift index 24c5d03b5..8a7b4955d 100644 --- a/LoopFollow/Application/SceneDelegate.swift +++ b/LoopFollow/Application/SceneDelegate.swift @@ -41,14 +41,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { if let vc = viewControllers[i] as? SettingsViewController { vc.appStateController = appStateController } - if let vc = viewControllers[i] as? AlarmViewController { - vc.appStateController = appStateController - } - /* TODO - if let vc = viewControllers[i] as? SnoozeViewController { - vc.appStateController = appStateController - } - */ if let vc = viewControllers[i] as? debugViewController { vc.appStateController = appStateController } diff --git a/LoopFollow/Controllers/Nightscout/SAge.swift b/LoopFollow/Controllers/Nightscout/SAge.swift index 4f07a0ef2..3488667ae 100644 --- a/LoopFollow/Controllers/Nightscout/SAge.swift +++ b/LoopFollow/Controllers/Nightscout/SAge.swift @@ -53,9 +53,6 @@ extension MainViewController { let snoozeTime = Date(timeIntervalSince1970: UserDefaultsRepository.alertSageInsertTime.value + 7200) UserDefaultsRepository.alertSnoozeAllTime.value = snoozeTime UserDefaultsRepository.alertSnoozeAllIsSnoozed.value = true - guard let alarms = ViewControllerManager.shared.alarmViewController else { return } - alarms.reloadIsSnoozed(key: "alertSnoozeAllIsSnoozed", value: true) - alarms.reloadSnoozeTime(key: "alertSnoozeAllTime", setNil: false, value: snoozeTime) } if let sageTime = formatter.date(from: (lastSageString as! String))?.timeIntervalSince1970 { diff --git a/LoopFollow/Controllers/ViewControllerManager.swift b/LoopFollow/Controllers/ViewControllerManager.swift deleted file mode 100644 index 61e201596..000000000 --- a/LoopFollow/Controllers/ViewControllerManager.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// ViewControllerManager.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-07-27. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// - -import Foundation -import UIKit - -class ViewControllerManager { - - static let shared = ViewControllerManager() - - var alarmViewController: AlarmViewController? - - private init() { - instantiateAlarmViewController() - } - - private func instantiateAlarmViewController() { - let storyboard = UIStoryboard(name: "Main", bundle: nil) - self.alarmViewController = storyboard.instantiateViewController(withIdentifier: "AlarmViewController") as? AlarmViewController - } -} diff --git a/LoopFollow/ViewControllers/AlarmViewController.swift b/LoopFollow/ViewControllers/AlarmViewController.swift deleted file mode 100644 index 6c8c7db15..000000000 --- a/LoopFollow/ViewControllers/AlarmViewController.swift +++ /dev/null @@ -1,3735 +0,0 @@ -// -// AlarmViewController.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/3/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// -// -// -// -// - - - - - -import UIKit -import Eureka - -class AlarmViewController: FormViewController { - var appStateController: AppStateController? - - var soundFiles: [String] = [ - "Alarm_Buzzer", - "Alarm_Clock", - "Alert_Tone_Busy", - "Alert_Tone_Ringtone_1", - "Alert_Tone_Ringtone_2", - "Alien_Siren", - "Ambulance", - "Analog_Watch_Alarm", - "Big_Clock_Ticking", - "Burglar_Alarm_Siren_1", - "Burglar_Alarm_Siren_2", - "Cartoon_Ascend_Climb_Sneaky", - "Cartoon_Ascend_Then_Descend", - "Cartoon_Bounce_To_Ceiling", - "Cartoon_Dreamy_Glissando_Harp", - "Cartoon_Fail_Strings_Trumpet", - "Cartoon_Machine_Clumsy_Loop", - "Cartoon_Siren", - "Cartoon_Tip_Toe_Sneaky_Walk", - "Cartoon_Uh_Oh", - "Cartoon_Villain_Horns", - "Cell_Phone_Ring_Tone", - "Chimes_Glassy", - "Computer_Magic", - "CSFX-2_Alarm", - "Cuckoo_Clock", - "Dhol_Shuffleloop", - "Discreet", - "Early_Sunrise", - "Emergency_Alarm_Carbon_Monoxide", - "Emergency_Alarm_Siren", - "Emergency_Alarm", - "Ending_Reached", - "Fly", - "Ghost_Hover", - "Good_Morning", - "Hell_Yeah_Somewhat_Calmer", - "In_A_Hurry", - "Indeed", - "Insistently", - "Jingle_All_The_Way", - "Laser_Shoot", - "Machine_Charge", - "Magical_Twinkle", - "Marching_Heavy_Footed_Fat_Elephants", - "Marimba_Descend", - "Marimba_Flutter_or_Shake", - "Martian_Gun", - "Martian_Scanner", - "Metallic", - "Nightguard", - "Not_Kiddin", - "Open_Your_Eyes_And_See", - "Orchestral_Horns", - "Oringz", - "Pager_Beeps", - "Remembers_Me_Of_Asia", - "Rise_And_Shine", - "Rush", - "Sci-Fi_Air_Raid_Alarm", - "Sci-Fi_Alarm_Loop_1", - "Sci-Fi_Alarm_Loop_2", - "Sci-Fi_Alarm_Loop_3", - "Sci-Fi_Alarm_Loop_4", - "Sci-Fi_Alarm", - "Sci-Fi_Computer_Console_Alarm", - "Sci-Fi_Console_Alarm", - "Sci-Fi_Eerie_Alarm", - "Sci-Fi_Engine_Shut_Down", - "Sci-Fi_Incoming_Message_Alert", - "Sci-Fi_Spaceship_Message", - "Sci-Fi_Spaceship_Warm_Up", - "Sci-Fi_Warning", - "Signature_Corporate", - "Siri_Alert_Calibration_Needed", - "Siri_Alert_Device_Muted", - "Siri_Alert_Glucose_Dropping_Fast", - "Siri_Alert_Glucose_Rising_Fast", - "Siri_Alert_High_Glucose", - "Siri_Alert_Low_Glucose", - "Siri_Alert_Missed_Readings", - "Siri_Alert_Transmitter_Battery_Low", - "Siri_Alert_Urgent_High_Glucose", - "Siri_Alert_Urgent_Low_Glucose", - "Siri_Calibration_Needed", - "Siri_Device_Muted", - "Siri_Glucose_Dropping_Fast", - "Siri_Glucose_Rising_Fast", - "Siri_High_Glucose", - "Siri_Low_Glucose", - "Siri_Missed_Readings", - "Siri_Transmitter_Battery_Low", - "Siri_Urgent_High_Glucose", - "Siri_Urgent_Low_Glucose", - "Soft_Marimba_Pad_Positive", - "Soft_Warm_Airy_Optimistic", - "Soft_Warm_Airy_Reassuring", - "Store_Door_Chime", - "Sunny", - "Thunder_Sound_FX", - "Time_Has_Come", - "Tornado_Siren", - "Two_Turtle_Doves", - "Unpaved", - "Wake_Up_Will_You", - "Win_Gain", - "Wrong_Answer" - ] - - var alertRepeatOptions: [String] = [ - "Never", - "Always", - "At night", - "During the day" - ] - - var alertPlaySoundOptions: [String] = [ - "Always", - "At night", - "During the day", - "Never" - ] - - var alertAutosnoozeOptions: [String] = [ - "Never", - "At night", - "During the day" - ] - - - func timeBasedSettings (pickerValue: String) -> (dayTime:Bool, nightTime:Bool) { - var dayTime = false - var nightTime = false - - if pickerValue.contains("Always") { - dayTime = true - nightTime = true - } else if pickerValue.contains("Never") { - dayTime = false - nightTime = false - }else{ - if pickerValue.contains("night"){ - nightTime = true - } - if pickerValue.contains("day"){ - dayTime = true - } - } - return (dayTime, nightTime) - } - - func timeBasedSettingsNever (pickerValue: String) -> (dayTime:Bool, nightTime:Bool) { - var dayTime = false - var nightTime = false - - if pickerValue.contains("Never") { - dayTime = true - nightTime = true - }else{ - if pickerValue.contains("night"){ - nightTime = true - } - if pickerValue.contains("day"){ - dayTime = true - } - } - return (dayTime, nightTime) - } - - func reloadSnoozeTime(key: String, setNil: Bool, value: Date = Date()) { - - if let row = form.rowBy(tag: key) as? DateTimeInlineRow { - if setNil { - row.value = nil - } else { - row.value = value - } - - row.reload() - } - - } - - func reloadIsSnoozed(key: String, value: Bool) { - - if let row = form.rowBy(tag: key) as? SwitchRow { - row.value = value - row.reload() - } - } - - func reloadMuteTime(key: String, setNil: Bool, value: Date = Date()) { - - if let row = form.rowBy(tag: key) as? DateTimeInlineRow { - if setNil { - row.value = nil - } else { - row.value = value - } - - row.reload() - } - - } - - func reloadIsMuted(key: String, value: Bool) { - - if let row = form.rowBy(tag: key) as? SwitchRow { - row.value = value - row.reload() - } - } - - // static let shared = AlarmViewController() - - @IBAction func unwindToAlarms(sender: UIStoryboardSegue) - { - } - - override func viewDidLoad() { - super.viewDidLoad() - if UserDefaultsRepository.forceDarkMode.value { - overrideUserInterfaceStyle = .dark - } - - - form - +++ ButtonRow() { - $0.title = "DONE" - }.onCellSelection { (row, arg) in - self.dismiss(animated: true, completion: nil) - } - +++ Section("Select Alert") - <<< SegmentedRow("bgAlerts"){ row in - row.title = "" - row.options = ["Urgent Low", "Low", "High", "Urgent High"] - }.onChange { [weak self] row in - guard let value = row.value else { return } - let otherRow = self?.form.rowBy(tag: "bgExtraAlerts") as! SegmentedRow - otherRow.value = nil - otherRow.reload() - let otherRow2 = self?.form.rowBy(tag: "otherAlerts") as! SegmentedRow - otherRow2.value = nil - otherRow2.reload() - let otherRow3 = self?.form.rowBy(tag: "otherAlerts2") as! SegmentedRow - otherRow3.value = nil - otherRow3.reload() - let otherRow4 = self?.form.rowBy(tag: "otherAlerts3") as! SegmentedRow - otherRow4.value = nil - otherRow4.reload() - let otherRow5 = self?.form.rowBy(tag: "otherAlerts4") as! SegmentedRow - otherRow5.value = nil - otherRow5.reload() - row.value = value - } - <<< SegmentedRow("bgExtraAlerts"){ row in - row.title = "" - row.options = ["No Readings", "Fast Drop", "Fast Rise", "Temporary"] - }.onChange { [weak self] row in - guard let value = row.value else { return } - let otherRow = self?.form.rowBy(tag: "bgAlerts") as! SegmentedRow - otherRow.value = nil - otherRow.reload() - let otherRow2 = self?.form.rowBy(tag: "otherAlerts") as! SegmentedRow - otherRow2.value = nil - otherRow2.reload() - let otherRow3 = self?.form.rowBy(tag: "otherAlerts2") as! SegmentedRow - otherRow3.value = nil - otherRow3.reload() - let otherRow4 = self?.form.rowBy(tag: "otherAlerts3") as! SegmentedRow - otherRow4.value = nil - otherRow4.reload() - let otherRow5 = self?.form.rowBy(tag: "otherAlerts4") as! SegmentedRow - otherRow5.value = nil - otherRow5.reload() - row.value = value - } - <<< SegmentedRow("otherAlerts"){ row in - row.title = "" - row.options = ["Not Looping", "Missed Bolus", "SAGE", "CAGE"] - if !IsNightscoutEnabled() { - row.hidden = true - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - let otherRow = self?.form.rowBy(tag: "bgExtraAlerts") as! SegmentedRow - otherRow.value = nil - otherRow.reload() - let otherRow2 = self?.form.rowBy(tag: "bgAlerts") as! SegmentedRow - otherRow2.value = nil - otherRow2.reload() - let otherRow3 = self?.form.rowBy(tag: "otherAlerts2") as! SegmentedRow - otherRow3.value = nil - otherRow3.reload() - let otherRow4 = self?.form.rowBy(tag: "otherAlerts3") as! SegmentedRow - otherRow4.value = nil - otherRow4.reload() - let otherRow5 = self?.form.rowBy(tag: "otherAlerts4") as! SegmentedRow - otherRow5.value = nil - otherRow5.reload() - row.value = value - } - <<< SegmentedRow("otherAlerts2"){ row in - row.title = "" - row.options = ["Override Start", "Override End", "Pump"] - if !IsNightscoutEnabled() { - row.hidden = true - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - let otherRow = self?.form.rowBy(tag: "bgExtraAlerts") as! SegmentedRow - otherRow.value = nil - otherRow.reload() - let otherRow2 = self?.form.rowBy(tag: "bgAlerts") as! SegmentedRow - otherRow2.value = nil - otherRow2.reload() - let otherRow3 = self?.form.rowBy(tag: "otherAlerts") as! SegmentedRow - otherRow3.value = nil - otherRow3.reload() - let otherRow4 = self?.form.rowBy(tag: "otherAlerts3") as! SegmentedRow - otherRow4.value = nil - otherRow4.reload() - let otherRow5 = self?.form.rowBy(tag: "otherAlerts4") as! SegmentedRow - otherRow5.value = nil - otherRow5.reload() - row.value = value - } - - <<< SegmentedRow("otherAlerts3"){ row in - row.title = "" - row.options = ["IOB", "COB", "Battery", "Battery Drop"] - if !IsNightscoutEnabled() { - row.hidden = true - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - let otherRow = self?.form.rowBy(tag: "bgExtraAlerts") as! SegmentedRow - otherRow.value = nil - otherRow.reload() - let otherRow2 = self?.form.rowBy(tag: "bgAlerts") as! SegmentedRow - otherRow2.value = nil - otherRow2.reload() - let otherRow3 = self?.form.rowBy(tag: "otherAlerts") as! SegmentedRow - otherRow3.value = nil - otherRow3.reload() - let otherRow4 = self?.form.rowBy(tag: "otherAlerts2") as! SegmentedRow - otherRow4.value = nil - otherRow4.reload() - let otherRow5 = self?.form.rowBy(tag: "otherAlerts4") as! SegmentedRow - otherRow5.value = nil - otherRow5.reload() - row.value = value - } - - <<< SegmentedRow("otherAlerts4"){ row in - row.title = "" - row.options = ["Rec. Bolus", "Temp Target Start", "Temp Target End"] - if !IsNightscoutEnabled() { - row.hidden = true - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - let otherRow = self?.form.rowBy(tag: "bgExtraAlerts") as! SegmentedRow - otherRow.value = nil - otherRow.reload() - let otherRow2 = self?.form.rowBy(tag: "bgAlerts") as! SegmentedRow - otherRow2.value = nil - otherRow2.reload() - let otherRow3 = self?.form.rowBy(tag: "otherAlerts") as! SegmentedRow - otherRow3.value = nil - otherRow3.reload() - let otherRow4 = self?.form.rowBy(tag: "otherAlerts2") as! SegmentedRow - otherRow4.value = nil - otherRow4.reload() - let otherRow5 = self?.form.rowBy(tag: "otherAlerts3") as! SegmentedRow - otherRow5.value = nil - otherRow5.reload() - row.value = value - } - - buildUrgentLow() - buildLow() - buildHigh() - buildUrgentHigh() - - - - buildFastDropAlert() - buildFastRiseAlert() - buildMissedReadings() - - - - buildNotLooping() - buildMissedBolus() - buildSage() - buildCage() - - buildTemporaryAlert() - - buildOverrideStart() - buildOverrideEnd() - buildPump() - - buildIOB() - buildCOB() - buildBatteryAlarm() - buildBatteryDropAlarm() - buildRecBolus() - buildTempTargetStart() - buildTempTargetEnd() - - buildSnoozeAll() - buildAlarmSettings() - } - - override func viewDidAppear(_ animated: Bool) { - showHideNSDetails() - } - - func showHideNSDetails() { - var isHidden = false - var isEnabled = true - if !IsNightscoutEnabled() { - isHidden = true - isEnabled = false - } - - if let row1 = form.rowBy(tag: "otherAlerts") as? SegmentedRow { - row1.hidden = .function(["hide"], {form in - return isHidden - }) - row1.evaluateHidden() - } - if let row2 = form.rowBy(tag: "otherAlerts2") as? SegmentedRow { - row2.hidden = .function(["hide"], {form in - return isHidden - }) - row2.evaluateHidden() - } - if let row3 = form.rowBy(tag: "otherAlerts3") as? SegmentedRow { - row3.hidden = .function(["hide"], {form in - return isHidden - }) - row3.evaluateHidden() - } - if let row4 = form.rowBy(tag: "alertUrgentLowPredictiveMinutes") as? Section { - row4.hidden = .function(["hide"], {form in - return isHidden - }) - row4.evaluateHidden() - UserDefaultsRepository.alertUrgentLowPredictiveMinutes.value = 0 - } - if let row5 = form.rowBy(tag: "alertAutoSnoozeCGMStart") as? SwitchRow { - row5.hidden = .function(["hide"], {form in - return isHidden - }) - row5.evaluateHidden() - } - - - - if IsNightscoutEnabled() { - isEnabled = true - } - - guard let nightscoutTab = self.tabBarController?.tabBar.items![3] else { return } - nightscoutTab.isEnabled = isEnabled - - } - - func buildSnoozeAll(){ - form - +++ Section(header: "Snooze & Mute Options", footer: "Snooze and Mute All Sounds: Snooze All turns everything off, Mute All turns off phone sounds but leaves vibration and iOS notifications on") - <<< DateTimeInlineRow("alertSnoozeAllTime") { row in - row.title = "Snooze All Until" - - if (UserDefaultsRepository.alertSnoozeAllTime.value != nil) { - row.value = UserDefaultsRepository.alertSnoozeAllTime.value - } - row.minuteInterval = 5 - row.noValueDisplayText = "Not Snoozed" - } - .onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertSnoozeAllTime.value = value - UserDefaultsRepository.alertSnoozeAllIsSnoozed.value = true - let otherRow = self?.form.rowBy(tag: "alertSnoozeAllIsSnoozed") as! SwitchRow - otherRow.value = true - otherRow.reload() - } - .onExpandInlineRow { [weak self] cell, row, inlineRow in - inlineRow.cellUpdate() { cell, row in - cell.datePicker.datePickerMode = .dateAndTime - cell.datePicker.preferredDatePickerStyle = .wheels - } - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - - <<< SwitchRow("alertSnoozeAllIsSnoozed"){ row in - row.title = "All Alerts Snoozed" - row.value = UserDefaultsRepository.alertSnoozeAllIsSnoozed.value - row.hidden = "$alertSnoozeAllTime == nil" - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertSnoozeAllIsSnoozed.value = value - if !value { - UserDefaultsRepository.alertSnoozeAllTime.setNil(key: "alertSnoozeAllTime") - let otherRow = self?.form.rowBy(tag: "alertSnoozeAllTime") as! DateTimeInlineRow - otherRow.value = nil - otherRow.reload() - } - } - - <<< DateTimeInlineRow("alertMuteAllTime") { row in - row.title = "Mute All Until" - - if (UserDefaultsRepository.alertMuteAllTime.value != nil) { - row.value = UserDefaultsRepository.alertMuteAllTime.value - } - row.minuteInterval = 5 - row.noValueDisplayText = "Not Muted" - } - .onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertMuteAllTime.value = value - UserDefaultsRepository.alertMuteAllIsMuted.value = true - let otherRow = self?.form.rowBy(tag: "alertMuteAllIsMuted") as! SwitchRow - otherRow.value = true - otherRow.reload() - } - .onExpandInlineRow { [weak self] cell, row, inlineRow in - inlineRow.cellUpdate() { cell, row in - cell.datePicker.datePickerMode = .dateAndTime - cell.datePicker.preferredDatePickerStyle = .wheels - } - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - <<< SwitchRow("alertMuteAllIsMuted"){ row in - row.title = "All Sounds Muted" - row.value = UserDefaultsRepository.alertMuteAllIsMuted.value - row.hidden = "$alertMuteAllTime == nil" - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertMuteAllIsMuted.value = value - if !value { - UserDefaultsRepository.alertMuteAllTime.setNil(key: "alertMuteAllTime") - let otherRow = self?.form.rowBy(tag: "alertMuteAllTime") as! DateTimeInlineRow - otherRow.value = nil - otherRow.reload() - } - } - } - - func buildTemporaryAlert(){ - form - - - +++ Section(header: "Temporary Alert", footer: "Temporary Alert will trigger once and disable. Disabling Alert Below BG will trigger it as a high alert above the BG.") { row in - row.hidden = "$bgExtraAlerts != 'Temporary'" - } - <<< SwitchRow("alertTemporaryActive"){ row in - row.title = "Active" - row.value = UserDefaultsRepository.alertTemporaryActive.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertTemporaryActive.value = value - } - <<< SwitchRow("alertTemporaryBelow"){ row in - row.title = "Alert Below BG" - row.value = UserDefaultsRepository.alertTemporaryBelow.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertTemporaryBelow.value = value - } - <<< StepperRow("alertTemporaryBG") { row in - row.title = "BG" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 40 - row.cell.stepper.maximumValue = 400 - row.value = Double(UserDefaultsRepository.alertTemporaryBG.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return Localizer.toDisplayUnits(String(value)) - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertTemporaryBG.value = Float(value) - } - <<< PickerInputRow("alertTemporarySound") { row in - row.title = "Sound" - row.options = soundFiles - row.value = UserDefaultsRepository.alertTemporarySound.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertTemporarySound.value = value - AlarmSound.setSoundFile(str: value) - AlarmSound.stop() - AlarmSound.playTest() - } - <<< SwitchRow("alertTemporaryRepeat"){ row in - row.title = "Repeat Sound" - row.value = UserDefaultsRepository.alertTemporaryBGRepeat.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertTemporaryBGRepeat.value = value - } - } - - func buildUrgentLow(){ - form - +++ Section(header: "Urgent Low Alert", footer: "Alerts when BG drops below value") { row in - row.hidden = "$bgAlerts != 'Urgent Low'" - } - <<< SwitchRow("alertUrgentLowActive"){ row in - row.title = "Active" - row.value = UserDefaultsRepository.alertUrgentLowActive.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertUrgentLowActive.value = value - } - <<< StepperRow("alertUrgentLowBG") { row in - row.title = "BG" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 40 - row.cell.stepper.maximumValue = 80 - row.value = Double(UserDefaultsRepository.alertUrgentLowBG.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return Localizer.toDisplayUnits(String(value)) - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertUrgentLowBG.value = Float(value) - } - <<< StepperRow("alertUrgentLowPredictiveMinutes") { row in - row.title = "Predictive (Minutes)" - row.cell.stepper.stepValue = 5 - row.cell.stepper.minimumValue = 0 - row.cell.stepper.maximumValue = 60 - row.value = Double(UserDefaultsRepository.alertUrgentLowPredictiveMinutes.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertUrgentLowPredictiveMinutes.value = Int(value) - } - <<< StepperRow("alertUrgentLowSnooze") { row in - row.title = "Default Snooze" - row.cell.stepper.stepValue = 5 - row.cell.stepper.minimumValue = 5 - row.cell.stepper.maximumValue = 15 - row.value = Double(UserDefaultsRepository.alertUrgentLowSnooze.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertUrgentLowSnooze.value = Int(value) - } - <<< PickerInputRow("alertUrgentLowSound") { row in - row.title = "Sound" - row.options = soundFiles - row.value = UserDefaultsRepository.alertUrgentLowSound.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertUrgentLowSound.value = value //changed - AlarmSound.setSoundFile(str: value) - AlarmSound.stop() - AlarmSound.playTest() - } - - <<< PickerInputRow("alertUrgentLowPlaySound") { row in - row.title = "Play Sound" - row.options = alertPlaySoundOptions - row.value = UserDefaultsRepository.alertUrgentLowAudible.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertUrgentLowAudible.value = value - let alertVol = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertUrgentLowDayTimeAudible.value = alertVol.dayTime - UserDefaultsRepository.alertUrgentLowNightTimeAudible.value = alertVol.nightTime - } - <<< PickerInputRow("alertUrgentLowRepeat") { row in - row.title = "Repeat Sound" - row.options = alertRepeatOptions - row.value = UserDefaultsRepository.alertUrgentLowRepeat.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertUrgentLowRepeat.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertUrgentLowDayTime.value = alertTimes.dayTime - UserDefaultsRepository.alertUrgentLowNightTime.value = alertTimes.nightTime - } - <<< PickerInputRow("alertUrgentLowAutoSnooze") { row in - row.title = "Pre-Snooze" - row.options = alertAutosnoozeOptions - row.value = UserDefaultsRepository.alertUrgentLowAutosnooze.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertUrgentLowAutosnooze.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertUrgentLowAutosnoozeDay.value = alertTimes.dayTime - UserDefaultsRepository.alertUrgentLowAutosnoozeNight.value = alertTimes.nightTime - } - <<< DateTimeInlineRow("alertUrgentLowSnoozedTime") { row in - row.title = "Snoozed Until" - - if (UserDefaultsRepository.alertUrgentLowSnoozedTime.value != nil) { - row.value = UserDefaultsRepository.alertUrgentLowSnoozedTime.value - } - row.minuteInterval = 5 - row.noValueDisplayText = "Not Snoozed" - } - .onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertUrgentLowSnoozedTime.value = value - UserDefaultsRepository.alertUrgentLowIsSnoozed.value = true - let otherRow = self?.form.rowBy(tag: "alertUrgentLowIsSnoozed") as! SwitchRow - otherRow.value = true - otherRow.reload() - } - .onExpandInlineRow { [weak self] cell, row, inlineRow in - inlineRow.cellUpdate() { cell, row in - cell.datePicker.datePickerMode = .dateAndTime - cell.datePicker.preferredDatePickerStyle = .wheels - } - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - - <<< SwitchRow("alertUrgentLowIsSnoozed"){ row in - row.title = "Is Snoozed" - row.value = UserDefaultsRepository.alertUrgentLowIsSnoozed.value - row.hidden = "$alertUrgentLowSnoozedTime == nil" - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertUrgentLowIsSnoozed.value = value - if !value { - UserDefaultsRepository.alertUrgentLowSnoozedTime.setNil(key: "alertUrgentLowSnoozedTime") - let otherRow = self?.form.rowBy(tag: "alertUrgentLowSnoozedTime") as! DateTimeInlineRow - otherRow.value = nil - otherRow.reload() - } - } - } - - func buildLow(){ - form - +++ Section(header: "Low Alert", footer: "Alerts when BG drops below value. Persitent for minutes will allow the alert to be ignored within the Delta value to prevent alerts that Loop self-corrected the drop. Predictive minutes looks forward to Loop's prediction and will trigger an alert if a low is predicted within that time frame. Predictive uses the minimum persistence Delta value for the trigger.") { row in - row.hidden = "$bgAlerts != 'Low'" - } - <<< SwitchRow("alertLowActive"){ row in - row.title = "Active" - row.value = UserDefaultsRepository.alertLowActive.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertLowActive.value = value - } - <<< StepperRow("alertLowBG") { row in - row.title = "BG" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 40 - row.cell.stepper.maximumValue = 150 - row.value = Double(UserDefaultsRepository.alertLowBG.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return Localizer.toDisplayUnits(String(value)) - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertLowBG.value = Float(value) - } - <<< StepperRow("alertLowPersistent") { row in - row.title = "Persistent For (Minutes)" - row.cell.stepper.stepValue = 5 - row.cell.stepper.minimumValue = 0 - row.cell.stepper.maximumValue = 240 - row.value = Double(UserDefaultsRepository.alertLowPersistent.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertLowPersistent.value = Int(value) - } - <<< StepperRow("alertLowPersistenceMax") { row in - row.title = "Ignore Persistence (-Delta)" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 0 - row.cell.stepper.maximumValue = 20 - row.value = Double(UserDefaultsRepository.alertLowPersistenceMax.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return Localizer.toDisplayUnits(String(value)) - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertLowPersistenceMax.value = Float(value) - } - - - <<< StepperRow("alertLowSnooze") { row in - row.title = "Snooze" - row.cell.stepper.stepValue = 5 - row.cell.stepper.minimumValue = 5 - row.cell.stepper.maximumValue = 30 - row.value = Double(UserDefaultsRepository.alertLowSnooze.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertLowSnooze.value = Int(value) - } - <<< PickerInputRow("alertLowSound") { row in - row.title = "Sound" - row.options = soundFiles - row.value = UserDefaultsRepository.alertLowSound.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertLowSound.value = value - AlarmSound.setSoundFile(str: value) - AlarmSound.stop() - AlarmSound.playTest() - } - <<< PickerInputRow("alertPlaySound") { row in - row.title = "Play Sound" - row.options = alertPlaySoundOptions - row.value = UserDefaultsRepository.alertLowAudible.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertLowAudible.value = value - let alertVol = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertLowDayTimeAudible.value = alertVol.dayTime - UserDefaultsRepository.alertLowNightTimeAudible.value = alertVol.nightTime - } - <<< PickerInputRow("alertLowRepeat") { row in - row.title = "Repeat Sound" - row.options = alertRepeatOptions - row.value = UserDefaultsRepository.alertLowRepeat.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertLowRepeat.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertLowDayTime.value = alertTimes.dayTime - UserDefaultsRepository.alertLowNightTime.value = alertTimes.nightTime - } - - <<< PickerInputRow("alertLowAutoSnooze") { row in - row.title = "Pre-Snooze" - row.options = alertAutosnoozeOptions - row.value = UserDefaultsRepository.alertLowAutosnooze.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertLowAutosnooze.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertLowAutosnoozeDay.value = alertTimes.dayTime - UserDefaultsRepository.alertLowAutosnoozeNight.value = alertTimes.nightTime - } - - <<< DateTimeInlineRow("alertLowSnoozedTime") { row in - row.title = "Snoozed Until" - if (UserDefaultsRepository.alertLowSnoozedTime.value != nil) { - row.value = UserDefaultsRepository.alertLowSnoozedTime.value - } - row.minuteInterval = 5 - row.noValueDisplayText = "Not Snoozed" - } - .onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertLowSnoozedTime.value = value - UserDefaultsRepository.alertLowIsSnoozed.value = true - let otherRow = self?.form.rowBy(tag: "alertLowIsSnoozed") as! SwitchRow - otherRow.value = true - otherRow.reload() - } - .onExpandInlineRow { [weak self] cell, row, inlineRow in - inlineRow.cellUpdate() { cell, row in - cell.datePicker.datePickerMode = .dateAndTime - cell.datePicker.preferredDatePickerStyle = .wheels - } - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - <<< SwitchRow("alertLowIsSnoozed"){ row in - row.title = "Is Snoozed" - row.value = UserDefaultsRepository.alertLowIsSnoozed.value - row.hidden = "$alertLowSnoozedTime == nil" - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertLowIsSnoozed.value = value - if !value { - UserDefaultsRepository.alertLowSnoozedTime.setNil(key: "alertLowSnoozedTime") - let otherRow = self?.form.rowBy(tag: "alertLowSnoozedTime") as! DateTimeInlineRow - otherRow.value = nil - otherRow.reload() - } - } - } - - func buildHigh(){ - form - +++ Section(header: "High Alert", footer: "Alerts when BG rises above value. If Persistence is set greater than 0, it will not alert until BG has been high for that many minutes.") { row in - row.hidden = "$bgAlerts != 'High'" - } - - <<< SwitchRow("alertHighActive"){ row in - row.title = "Active" - row.value = UserDefaultsRepository.alertHighActive.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertHighActive.value = value - } - - <<< StepperRow("alertHighBG") { row in - row.title = "BG" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 120 - row.cell.stepper.maximumValue = 300 - row.value = Double(UserDefaultsRepository.alertHighBG.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return Localizer.toDisplayUnits(String(value)) - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertHighBG.value = Float(value) - } - <<< StepperRow("alertHighPersistent") { row in - row.title = "Persistent For" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 0 - row.cell.stepper.maximumValue = 120 - row.value = Double(UserDefaultsRepository.alertHighPersistent.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertHighPersistent.value = Int(value) - } - <<< StepperRow("alertHighSnooze") { row in - row.title = "Snooze" - row.cell.stepper.stepValue = 5 - row.cell.stepper.minimumValue = 10 - row.cell.stepper.maximumValue = 120 - row.value = Double(UserDefaultsRepository.alertHighSnooze.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertHighSnooze.value = Int(value) - } - <<< PickerInputRow("alertHighSound") { row in - row.title = "Sound" - row.options = soundFiles - row.value = UserDefaultsRepository.alertHighSound.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertHighSound.value = value - AlarmSound.setSoundFile(str: value) - AlarmSound.stop() - AlarmSound.playTest() - } - <<< PickerInputRow("alertHighPlaySound") { row in - row.title = "Play Sound" - row.options = alertPlaySoundOptions - row.value = UserDefaultsRepository.alertHighAudible.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertHighAudible.value = value - let alertVol = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertHighDayTimeAudible.value = alertVol.dayTime - UserDefaultsRepository.alertHighNightTimeAudible.value = alertVol.nightTime - } - <<< PickerInputRow("alertHighRepeat") { row in - row.title = "Repeat Sound" - row.options = alertRepeatOptions - row.value = UserDefaultsRepository.alertHighRepeat.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertHighRepeat.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertHighDayTime.value = alertTimes.dayTime - UserDefaultsRepository.alertHighNightTime.value = alertTimes.nightTime - } - - <<< PickerInputRow("alertHightAutoSnooze") { row in - row.title = "Pre-Snooze" - row.options = alertAutosnoozeOptions - row.value = UserDefaultsRepository.alertHighAutosnooze.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertHighAutosnooze.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertHighAutosnoozeDay.value = alertTimes.dayTime - UserDefaultsRepository.alertHighAutosnoozeNight.value = alertTimes.nightTime - } - - <<< DateTimeInlineRow("alertHighSnoozedTime") { row in - row.title = "Snoozed Until" - if (UserDefaultsRepository.alertHighSnoozedTime.value != nil) { - row.value = UserDefaultsRepository.alertHighSnoozedTime.value - } - row.minuteInterval = 5 - row.noValueDisplayText = "Not Snoozed" - } - .onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertHighSnoozedTime.value = value - UserDefaultsRepository.alertHighIsSnoozed.value = true - let otherRow = self?.form.rowBy(tag: "alertHighIsSnoozed") as! SwitchRow - otherRow.value = true - otherRow.reload() - } - .onExpandInlineRow { [weak self] cell, row, inlineRow in - inlineRow.cellUpdate() { cell, row in - cell.datePicker.datePickerMode = .dateAndTime - cell.datePicker.preferredDatePickerStyle = .wheels - } - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - <<< SwitchRow("alertHighIsSnoozed"){ row in - row.title = "Is Snoozed" - row.value = UserDefaultsRepository.alertHighIsSnoozed.value - row.hidden = "$alertHighSnoozedTime == nil" - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertHighIsSnoozed.value = value - if !value { - UserDefaultsRepository.alertHighSnoozedTime.setNil(key: "alertHighSnoozedTime") - let otherRow = self?.form.rowBy(tag: "alertHighSnoozedTime") as! DateTimeInlineRow - otherRow.value = nil - otherRow.reload() - } - } - } - - func buildUrgentHigh(){ - form - +++ Section(header: "Urgent High Alert", footer: "Alerts when BG rises above value.") { row in - row.hidden = "$bgAlerts != 'Urgent High'" - } - <<< SwitchRow("alertUrgentHighActive"){ row in - row.title = "Active" - row.value = UserDefaultsRepository.alertUrgentHighActive.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertUrgentHighActive.value = value - } - <<< StepperRow("alertUrgentHighBG") { row in - row.title = "BG" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 120 - row.cell.stepper.maximumValue = 350 - row.value = Double(UserDefaultsRepository.alertUrgentHighBG.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return Localizer.toDisplayUnits(String(value)) - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertUrgentHighBG.value = Float(value) - } - <<< StepperRow("alertUrgentHighSnooze") { row in - row.title = "Snooze" - row.cell.stepper.stepValue = 5 - row.cell.stepper.minimumValue = 10 - row.cell.stepper.maximumValue = 120 - row.value = Double(UserDefaultsRepository.alertUrgentHighSnooze.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertUrgentHighSnooze.value = Int(value) - } - <<< PickerInputRow("alertUrgentHighSound") { row in - row.title = "Sound" - row.options = soundFiles - row.value = UserDefaultsRepository.alertUrgentHighSound.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertUrgentHighSound.value = value - AlarmSound.setSoundFile(str: value) - AlarmSound.stop() - AlarmSound.playTest() - } - <<< PickerInputRow("alertUrgentHighPlaySound") { row in - row.title = "Play Sound" - row.options = alertPlaySoundOptions - row.value = UserDefaultsRepository.alertUrgentHighAudible.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertUrgentHighAudible.value = value - let alertVol = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertUrgentHighDayTimeAudible.value = alertVol.dayTime - UserDefaultsRepository.alertUrgentHighNightTimeAudible.value = alertVol.nightTime - } - <<< PickerInputRow("alertUrgentHighRepeat") { row in - row.title = "Repeat Sound" - row.options = alertRepeatOptions - row.value = UserDefaultsRepository.alertUrgentHighRepeat.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertUrgentHighRepeat.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertUrgentHighDayTime.value = alertTimes.dayTime - UserDefaultsRepository.alertUrgentHighNightTime.value = alertTimes.nightTime - } - <<< PickerInputRow("alertUrgentHighAutoSnooze") { row in - row.title = "Pre-Snooze" - row.options = alertAutosnoozeOptions - row.value = UserDefaultsRepository.alertUrgentHighAutosnooze.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertUrgentHighAutosnooze.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertUrgentHighAutosnoozeDay.value = alertTimes.dayTime - UserDefaultsRepository.alertUrgentHighAutosnoozeNight.value = alertTimes.nightTime - } - - <<< DateTimeInlineRow("alertUrgentHighSnoozedTime") { row in - row.title = "Snoozed Until" - if (UserDefaultsRepository.alertUrgentHighSnoozedTime.value != nil) { - row.value = UserDefaultsRepository.alertUrgentHighSnoozedTime.value - } - row.minuteInterval = 5 - row.noValueDisplayText = "Not Snoozed" - } - .onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertUrgentHighSnoozedTime.value = value - UserDefaultsRepository.alertUrgentHighIsSnoozed.value = true - let otherRow = self?.form.rowBy(tag: "alertUrgentHighIsSnoozed") as! SwitchRow - otherRow.value = true - otherRow.reload() - } - .onExpandInlineRow { [weak self] cell, row, inlineRow in - inlineRow.cellUpdate() { cell, row in - cell.datePicker.datePickerMode = .dateAndTime - cell.datePicker.preferredDatePickerStyle = .wheels - } - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - <<< SwitchRow("alertUrgentHighIsSnoozed"){ row in - row.title = "Is Snoozed" - row.value = UserDefaultsRepository.alertUrgentHighIsSnoozed.value - row.hidden = "$alertUrgentHighSnoozedTime == nil" - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertUrgentHighIsSnoozed.value = value - if !value { - UserDefaultsRepository.alertUrgentHighSnoozedTime.setNil(key: "alertUrgentHighSnoozedTime") - let otherRow = self?.form.rowBy(tag: "alertUrgentHighSnoozedTime") as! DateTimeInlineRow - otherRow.value = nil - otherRow.reload() - } - } - } - - func buildFastDropAlert(){ - form - +++ Section(header: "Fast Drop Alert", footer: "Alert when BG is dropping fast over consecutive readings. Optional: only alert when dropping below a specific BG") { row in - row.hidden = "$bgExtraAlerts != 'Fast Drop'" - } - <<< SwitchRow("alertFastDropActive"){ row in - row.title = "Active" - row.value = UserDefaultsRepository.alertFastDropActive.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertFastDropActive.value = value - } - <<< StepperRow("alertFastDropDelta") { row in - row.title = "Delta" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 3 - row.cell.stepper.maximumValue = 20 - row.value = Double(UserDefaultsRepository.alertFastDropDelta.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return Localizer.toDisplayUnits(String(value)) - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertFastDropDelta.value = Float(value) - } - <<< StepperRow("alertFastDropReadings") { row in - row.title = "# Readings" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 2 - row.cell.stepper.maximumValue = 4 - row.value = Double(UserDefaultsRepository.alertFastDropReadings.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertFastDropReadings.value = Int(value) - } - <<< SwitchRow("alertFastDropUseLimit"){ row in - row.title = "Use BG Limit" - row.value = UserDefaultsRepository.alertFastDropUseLimit.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertFastDropUseLimit.value = value - } - - <<< StepperRow("alertFastDropBelowBG") { row in - row.title = "Dropping Below BG" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 40 - row.cell.stepper.maximumValue = 300 - row.value = Double(UserDefaultsRepository.alertFastDropBelowBG.value) - row.hidden = "$alertFastDropUseLimit == false" - row.displayValueFor = { value in - guard let value = value else { return nil } - return Localizer.toDisplayUnits(String(value)) - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertFastDropBelowBG.value = Float(value) - } - <<< StepperRow("alertFastDropSnooze") { row in - row.title = "Snooze" - row.cell.stepper.stepValue = 5 - row.cell.stepper.minimumValue = 5 - row.cell.stepper.maximumValue = 60 - row.value = Double(UserDefaultsRepository.alertFastDropSnooze.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertFastDropSnooze.value = Int(value) - } - <<< PickerInputRow("alertFastDropSound") { row in - row.title = "Sound" - row.options = soundFiles - row.value = UserDefaultsRepository.alertFastDropSound.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertFastDropSound.value = value - AlarmSound.setSoundFile(str: value) - AlarmSound.stop() - AlarmSound.playTest() - } - <<< PickerInputRow("alertFastDropPlaySound") { row in - row.title = "Play Sound" - row.options = alertPlaySoundOptions - row.value = UserDefaultsRepository.alertFastDropAudible.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertFastDropAudible.value = value - let alertVol = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertFastDropDayTimeAudible.value = alertVol.dayTime - UserDefaultsRepository.alertFastDropNightTimeAudible.value = alertVol.nightTime - } - <<< PickerInputRow("alertFastDropRepeat") { row in - row.title = "Repeat Sound" - row.options = alertRepeatOptions - row.value = UserDefaultsRepository.alertFastDropRepeat.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertFastDropRepeat.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertFastDropDayTime.value = alertTimes.dayTime - UserDefaultsRepository.alertFastDropNightTime.value = alertTimes.nightTime - } - - <<< PickerInputRow("alertFastDropAutoSnooze") { row in - row.title = "Pre-Snooze" - row.options = alertAutosnoozeOptions - row.value = UserDefaultsRepository.alertFastDropAutosnooze.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertFastDropAutosnooze.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertFastDropAutosnoozeDay.value = alertTimes.dayTime - UserDefaultsRepository.alertFastDropAutosnoozeNight.value = alertTimes.nightTime - } - - <<< DateTimeInlineRow("alertFastDropSnoozedTime") { row in - row.title = "Snoozed Until" - if (UserDefaultsRepository.alertFastDropSnoozedTime.value != nil) { - row.value = UserDefaultsRepository.alertFastDropSnoozedTime.value - } - row.minuteInterval = 5 - row.noValueDisplayText = "Not Snoozed" - } - .onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertFastDropSnoozedTime.value = value - UserDefaultsRepository.alertFastDropIsSnoozed.value = true - let otherRow = self?.form.rowBy(tag: "alertFastDropIsSnoozed") as! SwitchRow - otherRow.value = true - otherRow.reload() - } - .onExpandInlineRow { [weak self] cell, row, inlineRow in - inlineRow.cellUpdate() { cell, row in - cell.datePicker.datePickerMode = .dateAndTime - cell.datePicker.preferredDatePickerStyle = .wheels - } - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - <<< SwitchRow("alertFastDropIsSnoozed"){ row in - row.title = "Is Snoozed" - row.value = UserDefaultsRepository.alertFastDropIsSnoozed.value - row.hidden = "$alertFastDropSnoozedTime == nil" - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertFastDropIsSnoozed.value = value - if !value { - UserDefaultsRepository.alertFastDropSnoozedTime.setNil(key: "alertFastDropSnoozedTime") - let otherRow = self?.form.rowBy(tag: "alertFastDropSnoozedTime") as! DateTimeInlineRow - otherRow.value = nil - otherRow.reload() - } - } - } - - func buildFastRiseAlert(){ - form - +++ Section(header: "Fast Rise Alert", footer: "Alert when BG is rising fast over consecutive readings. Optional: only alert when rising above a specific BG") { row in - row.hidden = "$bgExtraAlerts != 'Fast Rise'" - } - <<< SwitchRow("alertFastRiseActive"){ row in - row.title = "Active" - row.value = UserDefaultsRepository.alertFastRiseActive.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertFastRiseActive.value = value - } - <<< StepperRow("alertFastRiseDelta") { row in - row.title = "Delta" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 3 - row.cell.stepper.maximumValue = 20 - row.value = Double(UserDefaultsRepository.alertFastRiseDelta.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return Localizer.toDisplayUnits(String(value)) - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertFastRiseDelta.value = Float(value) - } - <<< StepperRow("alertFastRiseReadings") { row in - row.title = "# Readings" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 2 - row.cell.stepper.maximumValue = 4 - row.value = Double(UserDefaultsRepository.alertFastRiseReadings.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertFastRiseReadings.value = Int(value) - } - <<< SwitchRow("alertFastRiseUseLimit"){ row in - row.title = "Use BG Limit" - row.value = UserDefaultsRepository.alertFastRiseUseLimit.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertFastRiseUseLimit.value = value - } - - <<< StepperRow("alertFastRiseAboveBG") { row in - row.title = "Rising Above BG" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 40 - row.cell.stepper.maximumValue = 300 - row.value = Double(UserDefaultsRepository.alertFastRiseAboveBG.value) - row.hidden = "$alertFastRiseUseLimit == false" - row.displayValueFor = { value in - guard let value = value else { return nil } - return Localizer.toDisplayUnits(String(value)) - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertFastRiseAboveBG.value = Float(value) - } - <<< StepperRow("alertFastRiseSnooze") { row in - row.title = "Snooze" - row.cell.stepper.stepValue = 5 - row.cell.stepper.minimumValue = 5 - row.cell.stepper.maximumValue = 60 - row.value = Double(UserDefaultsRepository.alertFastRiseSnooze.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertFastRiseSnooze.value = Int(value) - } - <<< PickerInputRow("alertFastRiseSound") { row in - row.title = "Sound" - row.options = soundFiles - row.value = UserDefaultsRepository.alertFastRiseSound.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertFastRiseSound.value = value - AlarmSound.setSoundFile(str: value) - AlarmSound.stop() - AlarmSound.playTest() - } - <<< PickerInputRow("alertFastRisePlaySound") { row in - row.title = "Play Sound" - row.options = alertPlaySoundOptions - row.value = UserDefaultsRepository.alertFastRiseAudible.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertFastRiseAudible.value = value - let alertVol = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertFastRiseDayTimeAudible.value = alertVol.dayTime - UserDefaultsRepository.alertFastRiseNightTimeAudible.value = alertVol.nightTime - } - <<< PickerInputRow("alertFastRiseRepeat") { row in - row.title = "Repeat Sound" - row.options = alertRepeatOptions - row.value = UserDefaultsRepository.alertFastRiseRepeat.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertFastRiseRepeat.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertFastRiseDayTime.value = alertTimes.dayTime - UserDefaultsRepository.alertFastRiseNightTime.value = alertTimes.nightTime - } - - <<< PickerInputRow("alertFastRiseAutoSnooze") { row in - row.title = "Pre-Snooze" - row.options = alertAutosnoozeOptions - row.value = UserDefaultsRepository.alertFastRiseAutosnooze.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertFastRiseAutosnooze.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertFastRiseAutosnoozeDay.value = alertTimes.dayTime - UserDefaultsRepository.alertFastRiseAutosnoozeNight.value = alertTimes.nightTime - } - <<< DateTimeInlineRow("alertFastRiseSnoozedTime") { row in - row.title = "Snoozed Until" - if (UserDefaultsRepository.alertFastRiseSnoozedTime.value != nil) { - row.value = UserDefaultsRepository.alertFastRiseSnoozedTime.value - } - row.minuteInterval = 5 - row.noValueDisplayText = "Not Snoozed" - } - .onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertFastRiseSnoozedTime.value = value - UserDefaultsRepository.alertFastRiseIsSnoozed.value = true - let otherRow = self?.form.rowBy(tag: "alertFastRiseIsSnoozed") as! SwitchRow - otherRow.value = true - otherRow.reload() - } - .onExpandInlineRow { [weak self] cell, row, inlineRow in - inlineRow.cellUpdate() { cell, row in - cell.datePicker.datePickerMode = .dateAndTime - cell.datePicker.preferredDatePickerStyle = .wheels - } - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - <<< SwitchRow("alertFastRiseIsSnoozed"){ row in - row.title = "Is Snoozed" - row.value = UserDefaultsRepository.alertFastRiseIsSnoozed.value - row.hidden = "$alertFastRiseSnoozedTime == nil" - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertFastRiseIsSnoozed.value = value - if !value { - UserDefaultsRepository.alertFastRiseSnoozedTime.setNil(key: "alertFastRiseSnoozedTime") - let otherRow = self?.form.rowBy(tag: "alertFastRiseSnoozedTime") as! DateTimeInlineRow - otherRow.value = nil - otherRow.reload() - } - } - } - - func buildMissedReadings(){ - form - +++ Section(header: "No Readings", footer: "Alert when there have been no BG readings for X minutes") { row in - row.hidden = "$bgExtraAlerts != 'No Readings'" - } - - <<< SwitchRow("alertMissedReadingActive"){ row in - row.title = "Active" - row.value = UserDefaultsRepository.alertMissedReadingActive.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertMissedReadingActive.value = value - } - - <<< StepperRow("alertMissedReading") { row in - row.title = "Time" - row.cell.stepper.stepValue = 5 - row.cell.stepper.minimumValue = 11 - row.cell.stepper.maximumValue = 121 - row.value = Double(UserDefaultsRepository.alertMissedReading.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertMissedReading.value = Int(value) - } - <<< StepperRow("alertMissedReadingSnooze") { row in - row.title = "Snooze" - row.cell.stepper.stepValue = 5 - row.cell.stepper.minimumValue = 10 - row.cell.stepper.maximumValue = 180 - row.value = Double(UserDefaultsRepository.alertMissedReadingSnooze.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertMissedReadingSnooze.value = Int(value) - } - <<< PickerInputRow("alertMissedReadingSound") { row in - row.title = "Sound" - row.options = soundFiles - row.value = UserDefaultsRepository.alertMissedReadingSound.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertMissedReadingSound.value = value - AlarmSound.setSoundFile(str: value) - AlarmSound.stop() - AlarmSound.playTest() - } - <<< PickerInputRow("alertMissedReadingPlaySound") { row in - row.title = "PlaySound" - row.options = alertPlaySoundOptions - row.value = UserDefaultsRepository.alertMissedReadingAudible.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertMissedReadingAudible.value = value - let alertVol = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertMissedReadingDayTimeAudible.value = alertVol.dayTime - UserDefaultsRepository.alertMissedReadingNightTimeAudible.value = alertVol.nightTime - } - <<< PickerInputRow("alertMissedReadingRepeat") { row in - row.title = "Repeat Sound" - row.options = alertRepeatOptions - row.value = UserDefaultsRepository.alertMissedReadingRepeat.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertMissedReadingRepeat.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertMissedReadingDayTime.value = alertTimes.dayTime - UserDefaultsRepository.alertMissedReadingNightTime.value = alertTimes.nightTime - } - - <<< PickerInputRow("alertMissedReadingAutoSnooze") { row in - row.title = "Pre-Snooze" - row.options = alertAutosnoozeOptions - row.value = UserDefaultsRepository.alertMissedReadingAutosnooze.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertMissedReadingAutosnooze.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertMissedReadingAutosnoozeDay.value = alertTimes.dayTime - UserDefaultsRepository.alertMissedReadingAutosnoozeNight.value = alertTimes.nightTime - } - <<< DateTimeInlineRow("alertMissedReadingSnoozedTime") { row in - row.title = "Snoozed Until" - if (UserDefaultsRepository.alertMissedReadingSnoozedTime.value != nil) { - row.value = UserDefaultsRepository.alertMissedReadingSnoozedTime.value - } - row.minuteInterval = 5 - row.noValueDisplayText = "Not Snoozed" - } - .onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertMissedReadingSnoozedTime.value = value - UserDefaultsRepository.alertMissedReadingIsSnoozed.value = true - let otherRow = self?.form.rowBy(tag: "alertMissedReadingIsSnoozed") as! SwitchRow - otherRow.value = true - otherRow.reload() - } - .onExpandInlineRow { [weak self] cell, row, inlineRow in - inlineRow.cellUpdate() { cell, row in - cell.datePicker.datePickerMode = .dateAndTime - cell.datePicker.preferredDatePickerStyle = .wheels - } - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - <<< SwitchRow("alertMissedReadingIsSnoozed"){ row in - row.title = "Is Snoozed" - row.value = UserDefaultsRepository.alertMissedReadingIsSnoozed.value - row.hidden = "$alertMissedReadingSnoozedTime == nil" - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertMissedReadingIsSnoozed.value = value - if !value { - UserDefaultsRepository.alertMissedReadingSnoozedTime.setNil(key: "alertMissedReadingSnoozedTime") - let otherRow = self?.form.rowBy(tag: "alertMissedReadingSnoozedTime") as! DateTimeInlineRow - otherRow.value = nil - otherRow.reload() - } - } - } - - func buildNotLooping(){ - form - +++ Section(header: "Not Looping", footer: "Alert when Loop has not completed a successful Loop for X minutes") { row in - row.hidden = "$otherAlerts != 'Not Looping'" - } - <<< SwitchRow("alertNotLoopingActive"){ row in - row.title = "Active" - row.value = UserDefaultsRepository.alertNotLoopingActive.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertNotLoopingActive.value = value - } - <<< StepperRow("alertNotLooping") { row in - row.title = "Time" - row.cell.stepper.stepValue = 5 - row.cell.stepper.minimumValue = 16 - row.cell.stepper.maximumValue = 61 - row.value = Double(UserDefaultsRepository.alertNotLooping.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertNotLooping.value = Int(value) - } - - <<< SwitchRow("alertNotLoopingUseLimits"){ row in - row.title = "Use BG Limits" - row.value = UserDefaultsRepository.alertNotLoopingUseLimits.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertNotLoopingUseLimits.value = value - } - <<< StepperRow("alertNotLoopingLowerLimit") { row in - row.title = "If Below BG" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 50 - row.cell.stepper.maximumValue = 200 - row.value = Double(UserDefaultsRepository.alertNotLoopingLowerLimit.value) - row.hidden = "$alertNotLoopingUseLimits == false" - row.displayValueFor = { value in - guard let value = value else { return nil } - return Localizer.toDisplayUnits(String(value)) - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertNotLoopingLowerLimit.value = Float(value) - } - <<< StepperRow("alertNotLoopingUpperLimit") { row in - row.title = "If Above BG" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 100 - row.cell.stepper.maximumValue = 300 - row.value = Double(UserDefaultsRepository.alertNotLoopingUpperLimit.value) - row.hidden = "$alertNotLoopingUseLimits == false" - row.displayValueFor = { value in - guard let value = value else { return nil } - return Localizer.toDisplayUnits(String(value)) - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertNotLoopingUpperLimit.value = Float(value) - } - <<< StepperRow("alertNotLoopingSnooze") { row in - row.title = "Snooze" - row.cell.stepper.stepValue = 5 - row.cell.stepper.minimumValue = 10 - row.cell.stepper.maximumValue = 120 - row.value = Double(UserDefaultsRepository.alertNotLoopingSnooze.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertNotLoopingSnooze.value = Int(value) - } - <<< PickerInputRow("alertNotLoopingSound") { row in - row.title = "Sound" - row.options = soundFiles - row.value = UserDefaultsRepository.alertNotLoopingSound.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertNotLoopingSound.value = value - AlarmSound.setSoundFile(str: value) - AlarmSound.stop() - AlarmSound.playTest() - } - <<< PickerInputRow("alertNotLoopingPlaySound") { row in - row.title = "PlaySound" - row.options = alertPlaySoundOptions - row.value = UserDefaultsRepository.alertNotLoopingAudible.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertNotLoopingAudible.value = value - let alertVol = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertNotLoopingDayTimeAudible.value = alertVol.dayTime - UserDefaultsRepository.alertNotLoopingNightTimeAudible.value = alertVol.nightTime - } - <<< PickerInputRow("alertNotLoopingRepeat") { row in - row.title = "Repeat Sound" - row.options = alertRepeatOptions - row.value = UserDefaultsRepository.alertNotLoopingRepeat.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertNotLoopingRepeat.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertNotLoopingDayTime.value = alertTimes.dayTime - UserDefaultsRepository.alertNotLoopingNightTime.value = alertTimes.nightTime - } - <<< PickerInputRow("alertNotLoopingAutoSnooze") { row in - row.title = "Pre-Snooze" - row.options = alertAutosnoozeOptions - row.value = UserDefaultsRepository.alertNotLoopingAutosnooze.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertNotLoopingAutosnooze.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertNotLoopingAutosnoozeDay.value = alertTimes.dayTime - UserDefaultsRepository.alertNotLoopingAutosnoozeNight.value = alertTimes.nightTime - } - <<< DateTimeInlineRow("alertNotLoopingSnoozedTime") { row in - row.title = "Snoozed Until" - if (UserDefaultsRepository.alertNotLoopingSnoozedTime.value != nil) { - row.value = UserDefaultsRepository.alertNotLoopingSnoozedTime.value - } - row.minuteInterval = 5 - row.noValueDisplayText = "Not Snoozed" - } - .onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertNotLoopingSnoozedTime.value = value - UserDefaultsRepository.alertNotLoopingIsSnoozed.value = true - let otherRow = self?.form.rowBy(tag: "alertNotLoopingIsSnoozed") as! SwitchRow - otherRow.value = true - otherRow.reload() - } - .onExpandInlineRow { [weak self] cell, row, inlineRow in - inlineRow.cellUpdate() { cell, row in - cell.datePicker.datePickerMode = .dateAndTime - cell.datePicker.preferredDatePickerStyle = .wheels - } - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - <<< SwitchRow("alertNotLoopingIsSnoozed"){ row in - row.title = "Is Snoozed" - row.value = UserDefaultsRepository.alertNotLoopingIsSnoozed.value - row.hidden = "$alertNotLoopingSnoozedTime == nil" - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertNotLoopingIsSnoozed.value = value - if !value { - UserDefaultsRepository.alertNotLoopingSnoozedTime.setNil(key: "alertNotLoopingSnoozedTime") - let otherRow = self?.form.rowBy(tag: "alertNotLoopingSnoozedTime") as! DateTimeInlineRow - otherRow.value = nil - otherRow.reload() - } - } - } - - func buildMissedBolus(){ - form - +++ Section(header: "Missed Bolus", footer: "Alert after X minutes when carbs are entered with no Bolus. Options to Ignore low treatment carbs under a certain BG, ignore small boluses, and consider boluses within a certain amount of time before the carbs as a prebolus.") { row in - row.hidden = "$otherAlerts != 'Missed Bolus'" - } - <<< SwitchRow("alertMissedBolusActive"){ row in - row.title = "Active" - row.value = UserDefaultsRepository.alertMissedBolusActive.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertMissedBolusActive.value = value - } - <<< StepperRow("alertMissedBolus") { row in - row.title = "Time" - row.cell.stepper.stepValue = 5 - row.cell.stepper.minimumValue = 5 - row.cell.stepper.maximumValue = 60 - row.value = Double(UserDefaultsRepository.alertMissedBolus.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertMissedBolus.value = Int(value) - } - <<< StepperRow("alertMissedBolusPrebolus") { row in - row.title = "Prebolus Max Time" - row.cell.stepper.stepValue = 5 - row.cell.stepper.minimumValue = 5 - row.cell.stepper.maximumValue = 45 - row.value = Double(UserDefaultsRepository.alertMissedBolusPrebolus.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertMissedBolusPrebolus.value = Int(value) - } - <<< StepperRow("alertMissedBolusIgnoreBolus") { row in - row.title = "Ignore Bolus <=" - row.cell.stepper.stepValue = 0.05 - row.cell.stepper.minimumValue = 0.05 - row.cell.stepper.maximumValue = 2 - row.value = Double(UserDefaultsRepository.alertMissedBolusIgnoreBolus.value) - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertMissedBolusIgnoreBolus.value = value - } - - - <<< StepperRow("alertMissedBolusLowGrams") { row in - row.title = "Ignore Under Grams" - row.tag = "missedBolusLowGrams" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 0 - row.cell.stepper.maximumValue = 15 - row.value = Double(UserDefaultsRepository.alertMissedBolusLowGrams.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertMissedBolusLowGrams.value = Int(value) - } - <<< StepperRow("alertMissedBolusLowGramsBG") { row in - row.title = "Ignore Under BG" - row.tag = "missedBolusLowGramsBG" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 40 - row.cell.stepper.maximumValue = 100 - row.value = Double(UserDefaultsRepository.alertMissedBolusLowGramsBG.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return Localizer.toDisplayUnits(String(value)) - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertMissedBolusLowGramsBG.value = Float(value) - } - - <<< StepperRow("alertMissedBolusSnooze") { row in - row.title = "Snooze" - row.cell.stepper.stepValue = 5 - row.cell.stepper.minimumValue = 5 - row.cell.stepper.maximumValue = 60 - row.value = Double(UserDefaultsRepository.alertMissedBolusSnooze.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertMissedBolusSnooze.value = Int(value) - } - <<< PickerInputRow("alertMissedBolusSound") { row in - row.title = "Sound" - row.options = soundFiles - row.value = UserDefaultsRepository.alertMissedBolusSound.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertMissedBolusSound.value = value - AlarmSound.setSoundFile(str: value) - AlarmSound.stop() - AlarmSound.playTest() - } - <<< PickerInputRow("alertMissedBolusPlaySound") { row in - row.title = "PlaySound" - row.options = alertPlaySoundOptions - row.value = UserDefaultsRepository.alertMissedBolusAudible.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertMissedBolusAudible.value = value - let alertVol = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertMissedBolusDayTimeAudible.value = alertVol.dayTime - UserDefaultsRepository.alertMissedBolusNightTimeAudible.value = alertVol.nightTime - } - //<<< SwitchRow("alertMissedBolusQuiet"){ row in - //row.title = "Mute at night" - //row.value = UserDefaultsRepository.alertMissedBolusQuiet.value - //}.onChange { [weak self] row in - // guard let value = row.value else { return } - // UserDefaultsRepository.alertMissedBolusQuiet.value = value - //} - <<< PickerInputRow("alertMissedBolusRepeat") { row in - row.title = "Repeat Sound" - row.options = alertRepeatOptions - row.value = UserDefaultsRepository.alertMissedBolusRepeat.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertMissedBolusRepeat.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertMissedBolusDayTime.value = alertTimes.dayTime - UserDefaultsRepository.alertMissedBolusNightTime.value = alertTimes.nightTime - } - <<< PickerInputRow("alertMissedBolusAutoSnooze") { row in - row.title = "Pre-Snooze" - row.options = alertAutosnoozeOptions - row.value = UserDefaultsRepository.alertMissedBolusAutosnooze.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertMissedBolusAutosnooze.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertMissedBolusAutosnoozeDay.value = alertTimes.dayTime - UserDefaultsRepository.alertMissedBolusAutosnoozeNight.value = alertTimes.nightTime - } - <<< DateTimeInlineRow("alertMissedBolusSnoozedTime") { row in - row.title = "Snoozed Until" - if (UserDefaultsRepository.alertMissedBolusSnoozedTime.value != nil) { - row.value = UserDefaultsRepository.alertMissedBolusSnoozedTime.value - } - row.minuteInterval = 5 - row.noValueDisplayText = "Not Snoozed" - } - .onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertMissedBolusSnoozedTime.value = value - UserDefaultsRepository.alertMissedBolusIsSnoozed.value = true - let otherRow = self?.form.rowBy(tag: "alertMissedBolusIsSnoozed") as! SwitchRow - otherRow.value = true - otherRow.reload() - } - .onExpandInlineRow { [weak self] cell, row, inlineRow in - inlineRow.cellUpdate() { cell, row in - cell.datePicker.datePickerMode = .dateAndTime - cell.datePicker.preferredDatePickerStyle = .wheels - } - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - <<< SwitchRow("alertMissedBolusIsSnoozed"){ row in - row.title = "Is Snoozed" - row.value = UserDefaultsRepository.alertMissedBolusIsSnoozed.value - row.hidden = "$alertMissedBolusSnoozedTime == nil" - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertMissedBolusIsSnoozed.value = value - if !value { - UserDefaultsRepository.alertMissedBolusSnoozedTime.setNil(key: "alertMissedBolusSnoozedTime") - let otherRow = self?.form.rowBy(tag: "alertMissedBolusSnoozedTime") as! DateTimeInlineRow - otherRow.value = nil - otherRow.reload() - } - } - } - - - - func buildSage(){ - form - +++ Section(header: "Sensor Change Reminder", footer: "Alert for 10 Day Sensor Change. Values are in Hours.") { row in - row.hidden = "$otherAlerts != 'SAGE'" - } - - <<< SwitchRow("alertSAGEActive"){ row in - row.title = "Active" - row.value = UserDefaultsRepository.alertSAGEActive.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertSAGEActive.value = value - } - - <<< StepperRow("alertSAGE") { row in - row.title = "Time" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 1 - row.cell.stepper.maximumValue = 24 - row.value = Double(UserDefaultsRepository.alertSAGE.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertSAGE.value = Int(value) - } - <<< StepperRow("alertSAGESnooze") { row in - row.title = "Snooze" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 1 - row.cell.stepper.maximumValue = 24 - row.value = Double(UserDefaultsRepository.alertSAGESnooze.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertSAGESnooze.value = Int(value) - } - - <<< PickerInputRow("alertSAGESound") { row in - row.title = "Sound" - row.options = soundFiles - row.value = UserDefaultsRepository.alertSAGESound.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertSAGESound.value = value - AlarmSound.setSoundFile(str: value) - AlarmSound.stop() - AlarmSound.playTest() - } - //<<< SwitchRow("alertSAGEQuiet"){ row in - //row.title = "Mute at night" - //row.value = UserDefaultsRepository.alertSAGEQuiet.value - //}.onChange { [weak self] row in - // guard let value = row.value else { return } - // UserDefaultsRepository.alertSAGEQuiet.value = value - //} - <<< PickerInputRow("alertSAGEPlaySound") { row in - row.title = "Play Sound" - row.options = alertPlaySoundOptions - row.value = UserDefaultsRepository.alertSAGEAudible.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertSAGEAudible.value = value - let alertVol = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertSAGEDayTimeAudible.value = alertVol.dayTime - UserDefaultsRepository.alertSAGENightTimeAudible.value = alertVol.nightTime - } - <<< PickerInputRow("alertSAGERepeat") { row in - row.title = "Repeat Sound" - row.options = alertRepeatOptions - row.value = UserDefaultsRepository.alertSAGERepeat.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertSAGERepeat.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertSAGEDayTime.value = alertTimes.dayTime - UserDefaultsRepository.alertSAGENightTime.value = alertTimes.nightTime - } - <<< PickerInputRow("alertSAGEAutoSnooze") { row in - row.title = "Pre-Snooze" - row.options = alertAutosnoozeOptions - row.value = UserDefaultsRepository.alertSAGEAutosnooze.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertSAGEAutosnooze.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertSAGEAutosnoozeDay.value = alertTimes.dayTime - UserDefaultsRepository.alertSAGEAutosnoozeNight.value = alertTimes.nightTime - } - <<< DateTimeInlineRow("alertSAGESnoozedTime") { row in - row.title = "Snoozed Until" - if (UserDefaultsRepository.alertSAGESnoozedTime.value != nil) { - row.value = UserDefaultsRepository.alertSAGESnoozedTime.value - } - row.minuteInterval = 5 - row.noValueDisplayText = "Not Snoozed" - } - .onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertSAGESnoozedTime.value = value - UserDefaultsRepository.alertSAGEIsSnoozed.value = true - let otherRow = self?.form.rowBy(tag: "alertSAGEIsSnoozed") as! SwitchRow - otherRow.value = true - otherRow.reload() - } - .onExpandInlineRow { [weak self] cell, row, inlineRow in - inlineRow.cellUpdate() { cell, row in - cell.datePicker.datePickerMode = .dateAndTime - cell.datePicker.preferredDatePickerStyle = .wheels - } - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - <<< SwitchRow("alertSAGEIsSnoozed"){ row in - row.title = "Is Snoozed" - row.value = UserDefaultsRepository.alertSAGEIsSnoozed.value - row.hidden = "$alertSAGESnoozedTime == nil" - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertSAGEIsSnoozed.value = value - if !value { - UserDefaultsRepository.alertSAGESnoozedTime.setNil(key: "alertSAGESnoozedTime") - let otherRow = self?.form.rowBy(tag: "alertSAGESnoozedTime") as! DateTimeInlineRow - otherRow.value = nil - otherRow.reload() - } - } - } - - func buildCage(){ - form - +++ Section(header: "Pump/Canula Change Reminder", footer: "Alert for Canula Change. Values are in Hours.") { row in - row.hidden = "$otherAlerts != 'CAGE'" - } - <<< SwitchRow("alertCAGEActive"){ row in - row.title = "Active" - row.value = UserDefaultsRepository.alertCAGEActive.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertCAGEActive.value = value - } - - <<< StepperRow("alertCAGE") { row in - row.title = "Time" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 1 - row.cell.stepper.maximumValue = 24 - row.value = Double(UserDefaultsRepository.alertCAGE.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertCAGE.value = Int(value) - } - <<< StepperRow("alertCAGESnooze") { row in - row.title = "Snooze" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 1 - row.cell.stepper.maximumValue = 24 - row.value = Double(UserDefaultsRepository.alertCAGESnooze.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertCAGESnooze.value = Int(value) - } - <<< PickerInputRow("alertCAGESound") { row in - row.title = "Sound" - row.options = soundFiles - row.value = UserDefaultsRepository.alertCAGESound.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertCAGESound.value = value - AlarmSound.setSoundFile(str: value) - AlarmSound.stop() - AlarmSound.playTest() - } - <<< PickerInputRow("alertCAGEPlaySound") { row in - row.title = "Play Sound" - row.options = alertPlaySoundOptions - row.value = UserDefaultsRepository.alertCAGEAudible.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertCAGEAudible.value = value - let alertVol = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertCAGEDayTimeAudible.value = alertVol.dayTime - UserDefaultsRepository.alertCAGENightTimeAudible.value = alertVol.nightTime - } - //<<< SwitchRow("alertCAGEQuiet"){ row in - //row.title = "Mute at night" - //row.value = UserDefaultsRepository.alertCAGEQuiet.value - //}.onChange { [weak self] row in - // guard let value = row.value else { return } - // UserDefaultsRepository.alertCAGEQuiet.value = value - //} - <<< PickerInputRow("alertCAGERepeat") { row in - row.title = "Repeat Sound" - row.options = alertRepeatOptions - row.value = UserDefaultsRepository.alertCAGERepeat.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertCAGERepeat.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertCAGEDayTime.value = alertTimes.dayTime - UserDefaultsRepository.alertCAGENightTime.value = alertTimes.nightTime - } - <<< PickerInputRow("alertCAGEAutoSnooze") { row in - row.title = "Pre-Snooze" - row.options = alertAutosnoozeOptions - row.value = UserDefaultsRepository.alertCAGEAutosnooze.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertCAGEAutosnooze.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertCAGEAutosnoozeDay.value = alertTimes.dayTime - UserDefaultsRepository.alertCAGEAutosnoozeNight.value = alertTimes.nightTime - } - <<< DateTimeInlineRow("alertCAGESnoozedTime") { row in - row.title = "Snoozed Until" - if (UserDefaultsRepository.alertCAGESnoozedTime.value != nil) { - row.value = UserDefaultsRepository.alertCAGESnoozedTime.value - } - row.minuteInterval = 5 - row.noValueDisplayText = "Not Snoozed" - } - .onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertCAGESnoozedTime.value = value - UserDefaultsRepository.alertCAGEIsSnoozed.value = true - let otherRow = self?.form.rowBy(tag: "alertCAGEIsSnoozed") as! SwitchRow - otherRow.value = true - otherRow.reload() - } - .onExpandInlineRow { [weak self] cell, row, inlineRow in - inlineRow.cellUpdate() { cell, row in - cell.datePicker.datePickerMode = .dateAndTime - cell.datePicker.preferredDatePickerStyle = .wheels - } - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - <<< SwitchRow("alertCAGEIsSnoozed"){ row in - row.title = "Is Snoozed" - row.value = UserDefaultsRepository.alertCAGEIsSnoozed.value - row.hidden = "$alertCAGESnoozedTime == nil" - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertCAGEIsSnoozed.value = value - if !value { - UserDefaultsRepository.alertCAGESnoozedTime.setNil(key: "alertCAGESnoozedTime") - let otherRow = self?.form.rowBy(tag: "alertCAGESnoozedTime") as! DateTimeInlineRow - otherRow.value = nil - otherRow.reload() - } - } - } - - func buildOverrideStart(){ - form - +++ Section(header: "Override Started Alert", footer: "Alert will trigger without repeat once when override is activated. There is no need to snooze this alert") { row in - row.hidden = "$otherAlerts2 != 'Override Start'" - } - <<< SwitchRow("alertOverrideStart"){ row in - row.title = "Active" - row.value = UserDefaultsRepository.alertOverrideStart.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertOverrideStart.value = value - } - - <<< PickerInputRow("alertOverrideStartSound") { row in - row.title = "Sound" - row.options = soundFiles - row.value = UserDefaultsRepository.alertOverrideStartSound.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertOverrideStartSound.value = value - AlarmSound.setSoundFile(str: value) - AlarmSound.stop() - AlarmSound.playTest() - } - <<< PickerInputRow("alertOverrideStartPlaySound") { row in - row.title = "Play Sound" - row.options = alertPlaySoundOptions - row.value = UserDefaultsRepository.alertOverrideStartAudible.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertOverrideStartAudible.value = value - let alertVol = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertOverrideStartDayTimeAudible.value = alertVol.dayTime - UserDefaultsRepository.alertOverrideStartNightTimeAudible.value = alertVol.nightTime - } - //<<< SwitchRow("alertOverrideStartQuiet"){ row in - //row.title = "Mute at night" - //row.value = UserDefaultsRepository.alertOverrideStartQuiet.value - //}.onChange { [weak self] row in - // guard let value = row.value else { return } - // UserDefaultsRepository.alertOverrideStartQuiet.value = value - //} - <<< PickerInputRow("alertOverrideStartRepeat") { row in - row.title = "Repeat Sound" - row.options = alertRepeatOptions - row.value = UserDefaultsRepository.alertOverrideStartRepeat.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertOverrideStartRepeat.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertOverrideStartDayTime.value = alertTimes.dayTime - UserDefaultsRepository.alertOverrideStartNightTime.value = alertTimes.nightTime - - } - <<< PickerInputRow("alertOverrideStartAutoSnooze") { row in - row.title = "Pre-Snooze" - row.options = alertAutosnoozeOptions - row.value = UserDefaultsRepository.alertOverrideStartAutosnooze.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertOverrideStartAutosnooze.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertOverrideStartAutosnoozeDay.value = alertTimes.dayTime - UserDefaultsRepository.alertOverrideStartAutosnoozeNight.value = alertTimes.nightTime - } - <<< DateTimeInlineRow("alertOverrideStartSnoozedTime") { row in - row.title = "Snoozed Until" - if (UserDefaultsRepository.alertOverrideStartSnoozedTime.value != nil) { - row.value = UserDefaultsRepository.alertOverrideStartSnoozedTime.value - } - row.minuteInterval = 5 - row.noValueDisplayText = "Not Snoozed" - } - .onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertOverrideStartSnoozedTime.value = value - UserDefaultsRepository.alertOverrideStartIsSnoozed.value = true - let otherRow = self?.form.rowBy(tag: "alertOverrideStartIsSnoozed") as! SwitchRow - otherRow.value = true - otherRow.reload() - } - .onExpandInlineRow { [weak self] cell, row, inlineRow in - inlineRow.cellUpdate() { cell, row in - cell.datePicker.datePickerMode = .dateAndTime - cell.datePicker.preferredDatePickerStyle = .wheels - } - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - <<< SwitchRow("alertOverrideStartIsSnoozed"){ row in - row.title = "Is Snoozed" - row.value = UserDefaultsRepository.alertOverrideStartIsSnoozed.value - row.hidden = "$alertOverrideStartSnoozedTime == nil" - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertOverrideStartIsSnoozed.value = value - if !value { - UserDefaultsRepository.alertOverrideStartSnoozedTime.setNil(key: "alertOverrideStartSnoozedTime") - let otherRow = self?.form.rowBy(tag: "alertOverrideStartSnoozedTime") as! DateTimeInlineRow - otherRow.value = nil - otherRow.reload() - } - } - - } - - func buildOverrideEnd(){ - form - +++ Section(header: "Override Ended Alert", footer: "Alert will trigger without repeat once when an override is turned off. There is no need to snooze this alert") { row in - row.hidden = "$otherAlerts2 != 'Override End'" - } - <<< SwitchRow("alertOverrideEnd"){ row in - row.title = "Active" - row.value = UserDefaultsRepository.alertOverrideEnd.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertOverrideEnd.value = value - } - - <<< PickerInputRow("alertOverrideEndSound") { row in - row.title = "Sound" - row.options = soundFiles - row.value = UserDefaultsRepository.alertOverrideEndSound.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertOverrideEndSound.value = value - AlarmSound.setSoundFile(str: value) - AlarmSound.stop() - AlarmSound.playTest() - } - <<< PickerInputRow("alertOverrideEndPlaySound") { row in - row.title = "Play Sound" - row.options = alertPlaySoundOptions - row.value = UserDefaultsRepository.alertOverrideEndAudible.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertOverrideEndAudible.value = value - let alertVol = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertOverrideEndDayTimeAudible.value = alertVol.dayTime - UserDefaultsRepository.alertOverrideEndNightTimeAudible.value = alertVol.nightTime - } - //<<< SwitchRow("alertOverrideEndQuiet"){ row in - //row.title = "Mute at night" - //row.value = UserDefaultsRepository.alertOverrideEndQuiet.value - //}.onChange { [weak self] row in - // guard let value = row.value else { return } - // UserDefaultsRepository.alertOverrideEndQuiet.value = value - //} - <<< PickerInputRow("alertOverrideEndRepeat") { row in - row.title = "Repeat Sound" - row.options = alertRepeatOptions - row.value = UserDefaultsRepository.alertOverrideEndRepeat.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertOverrideEndRepeat.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertOverrideEndDayTime.value = alertTimes.dayTime - UserDefaultsRepository.alertOverrideEndNightTime.value = alertTimes.nightTime - } - <<< PickerInputRow("alertOverrideEndAutoSnooze") { row in - row.title = "Pre-Snooze" - row.options = alertAutosnoozeOptions - row.value = UserDefaultsRepository.alertOverrideEndAutosnooze.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertOverrideEndAutosnooze.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertOverrideEndAutosnoozeDay.value = alertTimes.dayTime - UserDefaultsRepository.alertOverrideEndAutosnoozeNight.value = alertTimes.nightTime - } - <<< DateTimeInlineRow("alertOverrideEndSnoozedTime") { row in - row.title = "Snoozed Until" - if (UserDefaultsRepository.alertOverrideEndSnoozedTime.value != nil) { - row.value = UserDefaultsRepository.alertOverrideEndSnoozedTime.value - } - row.minuteInterval = 5 - row.noValueDisplayText = "Not Snoozed" - } - .onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertOverrideEndSnoozedTime.value = value - UserDefaultsRepository.alertOverrideEndIsSnoozed.value = true - let otherRow = self?.form.rowBy(tag: "alertOverrideEndIsSnoozed") as! SwitchRow - otherRow.value = true - otherRow.reload() - } - .onExpandInlineRow { [weak self] cell, row, inlineRow in - inlineRow.cellUpdate() { cell, row in - cell.datePicker.datePickerMode = .dateAndTime - cell.datePicker.preferredDatePickerStyle = .wheels - } - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - <<< SwitchRow("alertOverrideEndIsSnoozed"){ row in - row.title = "Is Snoozed" - row.value = UserDefaultsRepository.alertOverrideEndIsSnoozed.value - row.hidden = "$alertOverrideEndSnoozedTime == nil" - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertOverrideEndIsSnoozed.value = value - if !value { - UserDefaultsRepository.alertOverrideEndSnoozedTime.setNil(key: "alertOverrideEndSnoozedTime") - let otherRow = self?.form.rowBy(tag: "alertOverrideEndSnoozedTime") as! DateTimeInlineRow - otherRow.value = nil - otherRow.reload() - } - } - - } - - func buildPump() { - form - +++ Section(header: "Pump", footer: "Alert will trigger when pump reservoir is below value") { row in - row.hidden = "$otherAlerts2 != 'Pump'" - } - <<< SwitchRow("alertPump"){ row in - row.title = "Active" - row.value = UserDefaultsRepository.alertPump.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertPump.value = value - } - - <<< StepperRow("alertPumpAt") { row in - row.title = "Units Remaining" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 1 - row.cell.stepper.maximumValue = 49 - row.value = Double(UserDefaultsRepository.alertPumpAt.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertPumpAt.value = Int(value) - } - - <<< StepperRow("alertPumpSnoozeHours") { row in - row.title = "Snooze Hours" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 1 - row.cell.stepper.maximumValue = 24 - row.value = Double(UserDefaultsRepository.alertPumpSnoozeHours.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertPumpSnoozeHours.value = Int(value) - } - <<< PickerInputRow("alertPumpSound") { row in - row.title = "Sound" - row.options = soundFiles - row.value = UserDefaultsRepository.alertPumpSound.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertPumpSound.value = value - AlarmSound.setSoundFile(str: value) - AlarmSound.stop() - AlarmSound.playTest() - } - <<< PickerInputRow("alertPumpPlaySound") { row in - row.title = "Play Sound" - row.options = alertPlaySoundOptions - row.value = UserDefaultsRepository.alertPumpAudible.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertPumpAudible.value = value - let alertVol = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertPumpDayTimeAudible.value = alertVol.dayTime - UserDefaultsRepository.alertPumpNightTimeAudible.value = alertVol.nightTime - } - //<<< SwitchRow("alertPumpQuiet"){ row in - //row.title = "Mute at night" - //row.value = UserDefaultsRepository.alertPumpQuiet.value - //}.onChange { [weak self] row in - // guard let value = row.value else { return } - // UserDefaultsRepository.alertPumpQuiet.value = value - //} - <<< PickerInputRow("alertPumpRepeat") { row in - row.title = "Repeat Sound" - row.options = alertRepeatOptions - row.value = UserDefaultsRepository.alertPumpRepeat.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertPumpRepeat.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertPumpDayTime.value = alertTimes.dayTime - UserDefaultsRepository.alertPumpNightTime.value = alertTimes.nightTime - } - <<< PickerInputRow("alertPumpAutoSnooze") { row in - row.title = "Pre-Snooze" - row.options = alertAutosnoozeOptions - row.value = UserDefaultsRepository.alertPumpAutosnooze.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertPumpAutosnooze.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertPumpAutosnoozeDay.value = alertTimes.dayTime - UserDefaultsRepository.alertPumpAutosnoozeNight.value = alertTimes.nightTime - } - <<< DateTimeInlineRow("alertPumpSnoozedTime") { row in - row.title = "Snoozed Until" - if (UserDefaultsRepository.alertPumpSnoozedTime.value != nil) { - row.value = UserDefaultsRepository.alertPumpSnoozedTime.value - } - row.minuteInterval = 5 - row.noValueDisplayText = "Not Snoozed" - } - .onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertPumpSnoozedTime.value = value - UserDefaultsRepository.alertPumpIsSnoozed.value = true - let otherRow = self?.form.rowBy(tag: "alertPumpIsSnoozed") as! SwitchRow - otherRow.value = true - otherRow.reload() - } - .onExpandInlineRow { [weak self] cell, row, inlineRow in - inlineRow.cellUpdate() { cell, row in - cell.datePicker.datePickerMode = .dateAndTime - cell.datePicker.preferredDatePickerStyle = .wheels - } - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - <<< SwitchRow("alertPumpIsSnoozed"){ row in - row.title = "Is Snoozed" - row.value = UserDefaultsRepository.alertPumpIsSnoozed.value - row.hidden = "$alertPumpSnoozedTime == nil" - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertPumpIsSnoozed.value = value - if !value { - UserDefaultsRepository.alertPumpSnoozedTime.setNil(key: "alertPumpSnoozedTime") - let otherRow = self?.form.rowBy(tag: "alertPumpSnoozedTime") as! DateTimeInlineRow - otherRow.value = nil - otherRow.reload() - } - } - } - - func buildIOB() { - form - +++ Section(header: "IOB", footer: "Alert will trigger when multiple boluses exceed the values.") { row in - row.hidden = "$otherAlerts3 != 'IOB'" - } - <<< SwitchRow("alertIOB"){ row in - row.title = "Active" - row.value = UserDefaultsRepository.alertIOB.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertIOB.value = value - } - - <<< StepperRow("alertIOBAt") { row in - row.title = "Boluses >=" - row.cell.stepper.stepValue = 0.1 - row.cell.stepper.minimumValue = 0.1 - row.cell.stepper.maximumValue = 50 - row.value = Double(UserDefaultsRepository.alertIOBAt.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Double(round(10*value)/10))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertIOBAt.value = value - } - <<< StepperRow("alertIOBNumber") { row in - row.title = "Number of Boluses >=" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 1 - row.cell.stepper.maximumValue = 10 - row.value = Double(UserDefaultsRepository.alertIOBNumber.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertIOBNumber.value = Int(value) - } - <<< StepperRow("alertIOBBolusesWithin") { row in - row.title = "Within # Minutes" - row.cell.stepper.stepValue = 5 - row.cell.stepper.minimumValue = 5 - row.cell.stepper.maximumValue = 120 - row.value = Double(UserDefaultsRepository.alertIOBBolusesWithin.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertIOBBolusesWithin.value = Int(value) - } - - <<< StepperRow("alertIOBMaxBoluses") { row in - row.title = "Or Total IOB" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 1 - row.cell.stepper.maximumValue = 20 - row.value = Double(UserDefaultsRepository.alertIOBMaxBoluses.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertIOBMaxBoluses.value = Int(value) - } - - <<< StepperRow("alertIOBSnoozeHours") { row in - row.title = "Snooze Hours" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 1 - row.cell.stepper.maximumValue = 6 - row.value = Double(UserDefaultsRepository.alertIOBSnoozeHours.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertIOBSnoozeHours.value = Int(value) - } - <<< PickerInputRow("alertIOBSound") { row in - row.title = "Sound" - row.options = soundFiles - row.value = UserDefaultsRepository.alertIOBSound.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertIOBSound.value = value - AlarmSound.setSoundFile(str: value) - AlarmSound.stop() - AlarmSound.playTest() - } - <<< PickerInputRow("alertIOBPlaySound") { row in - row.title = "Play Sound" - row.options = alertPlaySoundOptions - row.value = UserDefaultsRepository.alertIOBAudible.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertIOBAudible.value = value - let alertVol = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertIOBDayTimeAudible.value = alertVol.dayTime - UserDefaultsRepository.alertIOBNightTimeAudible.value = alertVol.nightTime - } - <<< PickerInputRow("alertIOBRepeat") { row in - row.title = "Repeat Sound" - row.options = alertRepeatOptions - row.value = UserDefaultsRepository.alertIOBRepeat.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertIOBRepeat.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertIOBDayTime.value = alertTimes.dayTime - UserDefaultsRepository.alertIOBNightTime.value = alertTimes.nightTime - } - <<< PickerInputRow("alertIOBAutoSnooze") { row in - row.title = "Pre-Snooze" - row.options = alertAutosnoozeOptions - row.value = UserDefaultsRepository.alertIOBAutosnooze.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertIOBAutosnooze.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertIOBAutosnoozeDay.value = alertTimes.dayTime - UserDefaultsRepository.alertIOBAutosnoozeNight.value = alertTimes.nightTime - } - <<< DateTimeInlineRow("alertIOBSnoozedTime") { row in - row.title = "Snoozed Until" - if (UserDefaultsRepository.alertIOBSnoozedTime.value != nil) { - row.value = UserDefaultsRepository.alertIOBSnoozedTime.value - } - row.minuteInterval = 5 - row.noValueDisplayText = "Not Snoozed" - } - .onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertIOBSnoozedTime.value = value - UserDefaultsRepository.alertIOBIsSnoozed.value = true - let otherRow = self?.form.rowBy(tag: "alertIOBIsSnoozed") as! SwitchRow - otherRow.value = true - otherRow.reload() - } - .onExpandInlineRow { [weak self] cell, row, inlineRow in - inlineRow.cellUpdate() { cell, row in - cell.datePicker.datePickerMode = .dateAndTime - cell.datePicker.preferredDatePickerStyle = .wheels - } - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - <<< SwitchRow("alertIOBIsSnoozed"){ row in - row.title = "Is Snoozed" - row.value = UserDefaultsRepository.alertIOBIsSnoozed.value - row.hidden = "$alertIOBSnoozedTime == nil" - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertIOBIsSnoozed.value = value - if !value { - UserDefaultsRepository.alertIOBSnoozedTime.setNil(key: "alertIOBSnoozedTime") - let otherRow = self?.form.rowBy(tag: "alertIOBSnoozedTime") as! DateTimeInlineRow - otherRow.value = nil - otherRow.reload() - } - } - } - - func buildCOB() { - form - +++ Section(header: "COB", footer: "Alert will trigger when COB is above value") { row in - row.hidden = "$otherAlerts3 != 'COB'" - } - <<< SwitchRow("alertCOB"){ row in - row.title = "Active" - row.value = UserDefaultsRepository.alertCOB.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertCOB.value = value - } - - <<< StepperRow("alertCOBAt") { row in - row.title = "COB >=" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 1 - row.cell.stepper.maximumValue = 200 - row.value = Double(UserDefaultsRepository.alertCOBAt.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertCOBAt.value = Int(value) - } - - - <<< StepperRow("alertCOBSnoozeHours") { row in - row.title = "Snooze Hours" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 1 - row.cell.stepper.maximumValue = 6 - row.value = Double(UserDefaultsRepository.alertCOBSnoozeHours.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertCOBSnoozeHours.value = Int(value) - } - <<< PickerInputRow("alertCOBSound") { row in - row.title = "Sound" - row.options = soundFiles - row.value = UserDefaultsRepository.alertCOBSound.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertCOBSound.value = value - AlarmSound.setSoundFile(str: value) - AlarmSound.stop() - AlarmSound.playTest() - } - <<< PickerInputRow("alertCOBPlaySound") { row in - row.title = "Play Sound" - row.options = alertPlaySoundOptions - row.value = UserDefaultsRepository.alertCOBAudible.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertCOBAudible.value = value - let alertVol = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertCOBDayTimeAudible.value = alertVol.dayTime - UserDefaultsRepository.alertCOBNightTimeAudible.value = alertVol.nightTime - } - <<< PickerInputRow("alertCOBRepeat") { row in - row.title = "Repeat Sound" - row.options = alertRepeatOptions - row.value = UserDefaultsRepository.alertCOBRepeat.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertCOBRepeat.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertCOBDayTime.value = alertTimes.dayTime - UserDefaultsRepository.alertCOBNightTime.value = alertTimes.nightTime - } - <<< PickerInputRow("alertCOBAutoSnooze") { row in - row.title = "Pre-Snooze" - row.options = alertAutosnoozeOptions - row.value = UserDefaultsRepository.alertCOBAutosnooze.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertCOBAutosnooze.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertCOBAutosnoozeDay.value = alertTimes.dayTime - UserDefaultsRepository.alertCOBAutosnoozeNight.value = alertTimes.nightTime - } - <<< DateTimeInlineRow("alertCOBSnoozedTime") { row in - row.title = "Snoozed Until" - if (UserDefaultsRepository.alertCOBSnoozedTime.value != nil) { - row.value = UserDefaultsRepository.alertCOBSnoozedTime.value - } - row.minuteInterval = 5 - row.noValueDisplayText = "Not Snoozed" - } - .onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertCOBSnoozedTime.value = value - UserDefaultsRepository.alertCOBIsSnoozed.value = true - let otherRow = self?.form.rowBy(tag: "alertCOBIsSnoozed") as! SwitchRow - otherRow.value = true - otherRow.reload() - } - .onExpandInlineRow { [weak self] cell, row, inlineRow in - inlineRow.cellUpdate() { cell, row in - cell.datePicker.datePickerMode = .dateAndTime - cell.datePicker.preferredDatePickerStyle = .wheels - } - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - <<< SwitchRow("alertCOBIsSnoozed"){ row in - row.title = "Is Snoozed" - row.value = UserDefaultsRepository.alertCOBIsSnoozed.value - row.hidden = "$alertCOBSnoozedTime == nil" - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertCOBIsSnoozed.value = value - if !value { - UserDefaultsRepository.alertCOBSnoozedTime.setNil(key: "alertCOBSnoozedTime") - let otherRow = self?.form.rowBy(tag: "alertCOBSnoozedTime") as! DateTimeInlineRow - otherRow.value = nil - otherRow.reload() - } - } - } - - func buildBatteryAlarm(){ - form - +++ Section(header: "Battery Alarm", footer: "Activates a notification alert whenever the battery level drops below a user-defined threshold, allowing for proactive device charging and power management.") { row in - row.hidden = "$otherAlerts3 != 'Battery'" - } - <<< SwitchRow("alertBatteryActive"){ row in - row.title = "Active" - row.value = UserDefaultsRepository.alertBatteryActive.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertBatteryActive.value = value - } - <<< StepperRow("alertBatteryLevel") { row in - row.title = "Battery Level" - row.cell.stepper.stepValue = 5 - row.cell.stepper.minimumValue = 0 - row.cell.stepper.maximumValue = 100 - row.value = Double(UserDefaultsRepository.alertBatteryLevel.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))%" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertBatteryLevel.value = Int(value) - } - <<< StepperRow("alertBatterySnoozeHours") { row in - row.title = "Snooze Hours" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 1 - row.cell.stepper.maximumValue = 24 - row.value = Double(UserDefaultsRepository.alertBatterySnoozeHours.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertBatterySnoozeHours.value = Int(value) - } - <<< PickerInputRow("alertBatterySound") { row in - row.title = "Sound" - row.options = soundFiles - row.value = UserDefaultsRepository.alertBatterySound.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertBatterySound.value = value - AlarmSound.setSoundFile(str: value) - AlarmSound.stop() - AlarmSound.playTest() - } - <<< SwitchRow("alertBatteryRepeat"){ row in - row.title = "Repeat Sound" - row.value = UserDefaultsRepository.alertBatteryRepeat.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertBatteryRepeat.value = value - } - } - - func buildBatteryDropAlarm(){ - form - +++ Section(header: "Battery Drop Alarm", footer: "Activates a notification alert whenever the battery level drops quickly based on a user-defined percentage and time interval, allowing for proactive device charging and power management.") { row in - row.hidden = "$otherAlerts3 != 'Battery Drop'" - } - <<< SwitchRow("alertBatteryDropActive"){ row in - row.title = "Active" - row.value = UserDefaultsRepository.alertBatteryDropActive.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertBatteryDropActive.value = value - } - <<< StepperRow("alertBatteryDropPercentage") { row in - row.title = "Battery Drop" - row.cell.stepper.stepValue = 5 - row.cell.stepper.minimumValue = 5 - row.cell.stepper.maximumValue = 100 - row.value = Double(UserDefaultsRepository.alertBatteryDropPercentage.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))%" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertBatteryDropPercentage.value = Int(value) - } - <<< StepperRow("alertBatteryDropPeriod") { row in - row.title = "Period (minutes)" - row.cell.stepper.stepValue = 5 - row.cell.stepper.minimumValue = 5 - row.cell.stepper.maximumValue = 30 - row.value = Double(UserDefaultsRepository.alertBatteryDropPeriod.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertBatteryDropPeriod.value = Int(value) - } - <<< StepperRow("alertBatteryDropSnoozeHours") { row in - row.title = "Snooze Hours" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 1 - row.cell.stepper.maximumValue = 24 - row.value = Double(UserDefaultsRepository.alertBatterySnoozeHours.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertBatteryDropSnoozeHours.value = Int(value) - } - <<< PickerInputRow("alertBatteryDropSound") { row in - row.title = "Sound" - row.options = soundFiles - row.value = UserDefaultsRepository.alertBatteryDropSound.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertBatteryDropSound.value = value - AlarmSound.setSoundFile(str: value) - AlarmSound.stop() - AlarmSound.playTest() - } - <<< SwitchRow("alertBatteryDropRepeat"){ row in - row.title = "Repeat Sound" - row.value = UserDefaultsRepository.alertBatteryDropRepeat.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertBatteryDropRepeat.value = value - } - } - - func buildRecBolus(){ - form - +++ Section(header: "Rec. Bolus Alert", footer: "Activates a notification alert whenever recommended bolus is above a user-defined threshold, allowing for proactive manual bolusing.") { row in - row.hidden = "$otherAlerts4 != 'Rec. Bolus'" - } - <<< SwitchRow("alertRecBolusActive"){ row in - row.title = "Active" - row.value = UserDefaultsRepository.alertRecBolusActive.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertRecBolusActive.value = value - } - <<< StepperRow("alertRecBolusAt") { row in - row.title = "Rec. Bolus threshold" - row.cell.stepper.stepValue = 0.1 - row.cell.stepper.minimumValue = 0.1 - row.cell.stepper.maximumValue = 50 - row.value = Double(UserDefaultsRepository.alertRecBolusLevel.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Double(round(10*value)/10))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertRecBolusLevel.value = value - } - <<< StepperRow("alertRecBolusSnooze") { row in - row.title = "Default Snooze" - row.cell.stepper.stepValue = 5 - row.cell.stepper.minimumValue = 5 - row.cell.stepper.maximumValue = 60 - row.value = Double(UserDefaultsRepository.alertRecBolusSnooze.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertRecBolusSnooze.value = Int(value) - } - <<< PickerInputRow("alertRecBolusSound") { row in - row.title = "Sound" - row.options = soundFiles - row.value = UserDefaultsRepository.alertRecBolusSound.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertRecBolusSound.value = value - AlarmSound.setSoundFile(str: value) - AlarmSound.stop() - AlarmSound.playTest() - } - <<< SwitchRow("alertRecBolusRepeat"){ row in - row.title = "Repeat Sound" - row.value = UserDefaultsRepository.alertRecBolusRepeat.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertRecBolusRepeat.value = value - } - } - - func buildAlarmSettings() { - form - +++ Section(header: "Alarm Settings", footer: "") - - <<< SwitchRow("overrideSystemOutputVolume"){ row in - row.title = "Override System Volume" - row.value = UserDefaultsRepository.overrideSystemOutputVolume.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.overrideSystemOutputVolume.value = value - } - <<< StepperRow("forcedOutputVolume") { row in - row.title = "Volume Level" - row.cell.stepper.stepValue = 0.05 - row.cell.stepper.minimumValue = 0 - row.cell.stepper.maximumValue = 1 - row.value = Double(UserDefaultsRepository.forcedOutputVolume.value) - row.hidden = "$overrideSystemOutputVolume == false" - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value*100))%" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.forcedOutputVolume.value = Float(value) - } - <<< SwitchRow("alertAudioDuringPhone"){ row in - row.title = "Audio During Calls" - row.value = UserDefaultsRepository.alertAudioDuringPhone.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertAudioDuringPhone.value = value - } - <<< SwitchRow("alertIgnoreZero"){ row in - row.title = "Ignore Zero BG" - row.value = UserDefaultsRepository.alertIgnoreZero.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertIgnoreZero.value = value - } - <<< SwitchRow("alertAutoSnoozeCGMStart"){ row in - row.title = "Auto-Snooze CGM Start" - row.value = UserDefaultsRepository.alertAutoSnoozeCGMStart.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertAutoSnoozeCGMStart.value = value - } - - +++ Section(header: "Night Time Settings", footer: "Night time hours are used to differ how alerts are managed during the day and at night. For instance, automatically snooze, at night time, non-critical alerts that you do not wish to be awakened for such as a sensor change pre-alert.") { row in - row.tag = "quietHourSection" - } - <<< TimeInlineRow("quietHourStart") { row in - row.title = "Night Time Starts Today" - row.value = UserDefaultsRepository.quietHourStart.value - - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.quietHourStart.value = value - } - <<< TimeInlineRow("quietHourEnd") { row in - row.title = "Night Time Ends Tomorrow" - row.value = UserDefaultsRepository.quietHourEnd.value - - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.quietHourEnd.value = value - } - } - - func buildTempTargetStart() { - form - +++ Section(header: "Temp Target Started Alert", footer: "Alert will trigger without repeat once when Temp Target is activated. There is no need to snooze this alert") { row in - row.hidden = "$otherAlerts4 != 'Temp Target Start'" - } - <<< SwitchRow("alertTempTargetStart"){ row in - row.title = "Active" - row.value = UserDefaultsRepository.alertTempTargetStart.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertTempTargetStart.value = value - } - - <<< PickerInputRow("alertTempTargetStartSound") { row in - row.title = "Sound" - row.options = soundFiles - row.value = UserDefaultsRepository.alertTempTargetStartSound.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertTempTargetStartSound.value = value - AlarmSound.setSoundFile(str: value) - AlarmSound.stop() - AlarmSound.playTest() - } - <<< PickerInputRow("alertTempTargetStartPlaySound") { row in - row.title = "Play Sound" - row.options = alertPlaySoundOptions - row.value = UserDefaultsRepository.alertTempTargetStartAudible.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertTempTargetStartAudible.value = value - let alertVol = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertTempTargetStartDayTimeAudible.value = alertVol.dayTime - UserDefaultsRepository.alertTempTargetStartNightTimeAudible.value = alertVol.nightTime - } - <<< PickerInputRow("alertTempTargetStartRepeat") { row in - row.title = "Repeat Sound" - row.options = alertRepeatOptions - row.value = UserDefaultsRepository.alertTempTargetStartRepeat.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertTempTargetStartRepeat.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertTempTargetStartDayTime.value = alertTimes.dayTime - UserDefaultsRepository.alertTempTargetStartNightTime.value = alertTimes.nightTime - - } - <<< PickerInputRow("alertTempTargetStartAutoSnooze") { row in - row.title = "Pre-Snooze" - row.options = alertAutosnoozeOptions - row.value = UserDefaultsRepository.alertTempTargetStartAutosnooze.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertTempTargetStartAutosnooze.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertTempTargetStartAutosnoozeDay.value = alertTimes.dayTime - UserDefaultsRepository.alertTempTargetStartAutosnoozeNight.value = alertTimes.nightTime - } - <<< DateTimeInlineRow("alertTempTargetStartSnoozedTime") { row in - row.title = "Snoozed Until" - if (UserDefaultsRepository.alertTempTargetStartSnoozedTime.value != nil) { - row.value = UserDefaultsRepository.alertTempTargetStartSnoozedTime.value - } - row.minuteInterval = 5 - row.noValueDisplayText = "Not Snoozed" - } - .onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertTempTargetStartSnoozedTime.value = value - UserDefaultsRepository.alertTempTargetStartIsSnoozed.value = true - let otherRow = self?.form.rowBy(tag: "alertTempTargetStartIsSnoozed") as! SwitchRow - otherRow.value = true - otherRow.reload() - } - .onExpandInlineRow { [weak self] cell, row, inlineRow in - inlineRow.cellUpdate() { cell, row in - cell.datePicker.datePickerMode = .dateAndTime - cell.datePicker.preferredDatePickerStyle = .wheels - } - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - <<< SwitchRow("alertTempTargetStartIsSnoozed"){ row in - row.title = "Is Snoozed" - row.value = UserDefaultsRepository.alertTempTargetStartIsSnoozed.value - row.hidden = "$alertTempTargetStartSnoozedTime == nil" - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertTempTargetStartIsSnoozed.value = value - if !value { - UserDefaultsRepository.alertTempTargetStartSnoozedTime.setNil(key: "alertTempTargetStartSnoozedTime") - let otherRow = self?.form.rowBy(tag: "alertTempTargetStartSnoozedTime") as! DateTimeInlineRow - otherRow.value = nil - otherRow.reload() - } - } - - } - - func buildTempTargetEnd() { - form - +++ Section(header: "Temp Target Ended Alert", footer: "Alert will trigger without repeat once when Temp Target is turned off. There is no need to snooze this alert") { row in - row.hidden = "$otherAlerts4 != 'Temp Target End'" - } - <<< SwitchRow("alertTempTargetEnd"){ row in - row.title = "Active" - row.value = UserDefaultsRepository.alertTempTargetEnd.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertTempTargetEnd.value = value - } - - <<< PickerInputRow("alertTempTargetEndSound") { row in - row.title = "Sound" - row.options = soundFiles - row.value = UserDefaultsRepository.alertTempTargetEndSound.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertTempTargetEndSound.value = value - AlarmSound.setSoundFile(str: value) - AlarmSound.stop() - AlarmSound.playTest() - } - <<< PickerInputRow("alertTempTargetEndPlaySound") { row in - row.title = "Play Sound" - row.options = alertPlaySoundOptions - row.value = UserDefaultsRepository.alertTempTargetEndAudible.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertTempTargetEndAudible.value = value - let alertVol = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertTempTargetEndDayTimeAudible.value = alertVol.dayTime - UserDefaultsRepository.alertTempTargetEndNightTimeAudible.value = alertVol.nightTime - } - <<< PickerInputRow("alertTempTargetEndRepeat") { row in - row.title = "Repeat Sound" - row.options = alertRepeatOptions - row.value = UserDefaultsRepository.alertTempTargetEndRepeat.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertTempTargetEndRepeat.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertTempTargetEndDayTime.value = alertTimes.dayTime - UserDefaultsRepository.alertTempTargetEndNightTime.value = alertTimes.nightTime - - } - <<< PickerInputRow("alertTempTargetEndAutoSnooze") { row in - row.title = "Pre-Snooze" - row.options = alertAutosnoozeOptions - row.value = UserDefaultsRepository.alertTempTargetEndAutosnooze.value - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(String(value.replacingOccurrences(of: "_", with: " ")))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertTempTargetEndAutosnooze.value = value - let alertTimes = self!.timeBasedSettings(pickerValue: value) - UserDefaultsRepository.alertTempTargetEndAutosnoozeDay.value = alertTimes.dayTime - UserDefaultsRepository.alertTempTargetEndAutosnoozeNight.value = alertTimes.nightTime - } - <<< DateTimeInlineRow("alertTempTargetEndSnoozedTime") { row in - row.title = "Snoozed Until" - if (UserDefaultsRepository.alertTempTargetEndSnoozedTime.value != nil) { - row.value = UserDefaultsRepository.alertTempTargetEndSnoozedTime.value - } - row.minuteInterval = 5 - row.noValueDisplayText = "Not Snoozed" - } - .onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertTempTargetEndSnoozedTime.value = value - UserDefaultsRepository.alertTempTargetEndIsSnoozed.value = true - let otherRow = self?.form.rowBy(tag: "alertTempTargetEndIsSnoozed") as! SwitchRow - otherRow.value = true - otherRow.reload() - } - .onExpandInlineRow { [weak self] cell, row, inlineRow in - inlineRow.cellUpdate() { cell, row in - cell.datePicker.datePickerMode = .dateAndTime - cell.datePicker.preferredDatePickerStyle = .wheels - } - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - <<< SwitchRow("alertTempTargetEndIsSnoozed"){ row in - row.title = "Is Snoozed" - row.value = UserDefaultsRepository.alertTempTargetEndIsSnoozed.value - row.hidden = "$alertTempTargetEndSnoozedTime == nil" - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.alertTempTargetEndIsSnoozed.value = value - if !value { - UserDefaultsRepository.alertTempTargetEndSnoozedTime.setNil(key: "alertTempTargetEndSnoozedTime") - let otherRow = self?.form.rowBy(tag: "alertTempTargetEndSnoozedTime") as! DateTimeInlineRow - otherRow.value = nil - otherRow.reload() - } - } - - } -} - - diff --git a/LoopFollow/ViewControllers/SettingsViewController.swift b/LoopFollow/ViewControllers/SettingsViewController.swift index 1a6d4d9b6..0ce9ccd45 100644 --- a/LoopFollow/ViewControllers/SettingsViewController.swift +++ b/LoopFollow/ViewControllers/SettingsViewController.swift @@ -127,17 +127,6 @@ class SettingsViewController: FormViewController, NightscoutSettingsViewModelDel } ), onDismiss: nil) } - <<< ButtonRow("alarmsSettingstobedeleted") { - $0.title = "Alarms" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - guard let alarmVC = ViewControllerManager.shared.alarmViewController else { - fatalError("AlarmViewController should be pre-instantiated and available") - } - return alarmVC - }), onDismiss: nil) - } - <<< ButtonRow("alarmsList") { $0.title = "Alarms" From 345d1daa10f0b5fb028b4b0aec2a986b87b1e863 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 3 May 2025 21:28:43 +0200 Subject: [PATCH 011/138] No more DebugViewController.swift --- LoopFollow.xcodeproj/project.pbxproj | 4 --- LoopFollow/Application/SceneDelegate.swift | 3 --- .../ViewControllers/DebugViewController.swift | 25 ------------------- 3 files changed, 32 deletions(-) delete mode 100644 LoopFollow/ViewControllers/DebugViewController.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 0ec25b515..40fdb1111 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -297,7 +297,6 @@ FCC6886D2489909D00A0279D /* AnyConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC6886C2489909D00A0279D /* AnyConvertible.swift */; }; FCC6886F2489A53800A0279D /* AppConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC6886E2489A53800A0279D /* AppConstants.swift */; }; FCD2A27D24C9D044009F7B7B /* Globals.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCD2A27C24C9D044009F7B7B /* Globals.swift */; }; - FCD49B6C24AA536E007879DC /* DebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCD49B6B24AA536E007879DC /* DebugViewController.swift */; }; FCE537BC249A4D7D00F80BF8 /* carbBolusArrays.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCE537BB249A4D7D00F80BF8 /* carbBolusArrays.swift */; }; FCEF87AC24A141A700AE6FA0 /* Localizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCEF87AA24A1417900AE6FA0 /* Localizer.swift */; }; FCFEEC9E2486E68E00402A7F /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCFEEC9D2486E68E00402A7F /* WebKit.framework */; }; @@ -612,7 +611,6 @@ FCC6886E2489A53800A0279D /* AppConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppConstants.swift; sourceTree = ""; }; FCC688702489A57C00A0279D /* Loop Follow.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Loop Follow.entitlements"; sourceTree = ""; }; FCD2A27C24C9D044009F7B7B /* Globals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Globals.swift; sourceTree = ""; }; - FCD49B6B24AA536E007879DC /* DebugViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugViewController.swift; sourceTree = ""; }; FCE537BB249A4D7D00F80BF8 /* carbBolusArrays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = carbBolusArrays.swift; sourceTree = ""; }; FCE537C2249AAB2600F80BF8 /* NotificationCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NotificationCenter.framework; path = System/Library/Frameworks/NotificationCenter.framework; sourceTree = SDKROOT; }; FCEF87AA24A1417900AE6FA0 /* Localizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Localizer.swift; sourceTree = ""; }; @@ -1286,7 +1284,6 @@ children = ( FC97881B2485969B00A7906C /* MainViewController.swift */, FC97881D2485969B00A7906C /* NightScoutViewController.swift */, - FCD49B6B24AA536E007879DC /* DebugViewController.swift */, FCFEECA1248857A600402A7F /* SettingsViewController.swift */, DDCF979524C1443C002C9752 /* GeneralSettingsViewController.swift */, DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */, @@ -1737,7 +1734,6 @@ DD0C0C662C46E54C00DBADDF /* InfoDataSeparator.swift in Sources */, DD58171C2D299F940041FB98 /* BluetoothDevice.swift in Sources */, DD7E198A2ACDA62600DBD158 /* SensorStart.swift in Sources */, - FCD49B6C24AA536E007879DC /* DebugViewController.swift in Sources */, DD5334B02D1447C500CDD6EA /* BLEManager.swift in Sources */, DD4878032C7B297E0048F05C /* StorageValue.swift in Sources */, DD4878192C7C56D60048F05C /* TrioNightscoutRemoteController.swift in Sources */, diff --git a/LoopFollow/Application/SceneDelegate.swift b/LoopFollow/Application/SceneDelegate.swift index 8a7b4955d..1d87e90b7 100644 --- a/LoopFollow/Application/SceneDelegate.swift +++ b/LoopFollow/Application/SceneDelegate.swift @@ -41,9 +41,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { if let vc = viewControllers[i] as? SettingsViewController { vc.appStateController = appStateController } - if let vc = viewControllers[i] as? debugViewController { - vc.appStateController = appStateController - } } // Register the SceneDelegate as an observer for the "toggleSpeakBG" notification, which will be triggered when the user toggles the "Speak BG" feature in General Settings. This helps ensure that the Quick Action is updated according to the current setting. diff --git a/LoopFollow/ViewControllers/DebugViewController.swift b/LoopFollow/ViewControllers/DebugViewController.swift deleted file mode 100644 index 45e9593e9..000000000 --- a/LoopFollow/ViewControllers/DebugViewController.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// DebugViewController.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/29/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// - -import Foundation -import UIKit - -class debugViewController: UIViewController { - - - var appStateController: AppStateController? - - override func viewDidLoad() { - super.viewDidLoad() - - } - - - - -} From e096c9a74824c8e37406df1e75714ce5a8bb32eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sun, 4 May 2025 13:35:43 +0200 Subject: [PATCH 012/138] Working on snoozer --- LoopFollow.xcodeproj/project.pbxproj | 4 + LoopFollow/Alarm/AlarmManager.swift | 23 ++-- LoopFollow/Application/AppDelegate.swift | 5 + .../Extensions/ShareClientExtension.swift | 1 - LoopFollow/Helpers/Mobileprovision.swift | 1 - LoopFollow/Snoozer/SnoozerView.swift | 121 ++++++++---------- .../Snoozer/SnoozerViewController.swift | 7 +- LoopFollow/Snoozer/SnoozerViewModel.swift | 42 ++++++ .../ViewControllers/MainViewController.swift | 6 - 9 files changed, 119 insertions(+), 91 deletions(-) create mode 100644 LoopFollow/Snoozer/SnoozerViewModel.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 40fdb1111..1411e1af0 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -116,6 +116,7 @@ DDC7E5452DBD8A1600EB1127 /* HighBgAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E53D2DBD8A1600EB1127 /* HighBgAlarmEditor.swift */; }; DDC7E5462DBD8A1600EB1127 /* LowBgAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E53E2DBD8A1600EB1127 /* LowBgAlarmEditor.swift */; }; DDC7E5472DBD8A1600EB1127 /* AlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5402DBD8A1600EB1127 /* AlarmEditor.swift */; }; + DDC7E5CF2DC77C2000EB1127 /* SnoozerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5CE2DC77C2000EB1127 /* SnoozerViewModel.swift */; }; DDCF979424C0D380002C9752 /* UIViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979324C0D380002C9752 /* UIViewExtension.swift */; }; DDCF979624C1443C002C9752 /* GeneralSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979524C1443C002C9752 /* GeneralSettingsViewController.swift */; }; DDCF979824C1489C002C9752 /* GraphSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */; }; @@ -426,6 +427,7 @@ DDC7E53D2DBD8A1600EB1127 /* HighBgAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighBgAlarmEditor.swift; sourceTree = ""; }; DDC7E53E2DBD8A1600EB1127 /* LowBgAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LowBgAlarmEditor.swift; sourceTree = ""; }; DDC7E5402DBD8A1600EB1127 /* AlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmEditor.swift; sourceTree = ""; }; + DDC7E5CE2DC77C2000EB1127 /* SnoozerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozerViewModel.swift; sourceTree = ""; }; DDCF979324C0D380002C9752 /* UIViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtension.swift; sourceTree = ""; }; DDCF979524C1443C002C9752 /* GeneralSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsViewController.swift; sourceTree = ""; }; DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphSettingsViewController.swift; sourceTree = ""; }; @@ -882,6 +884,7 @@ DDC7E5142DBCE1B900EB1127 /* Snoozer */ = { isa = PBXGroup; children = ( + DDC7E5CE2DC77C2000EB1127 /* SnoozerViewModel.swift */, DDC7E5122DBCE1B900EB1127 /* SnoozerView.swift */, DDC7E5132DBCE1B900EB1127 /* SnoozerViewController.swift */, ); @@ -1754,6 +1757,7 @@ DDCF9A802D85FD0B004DF4DD /* Alarm.swift in Sources */, DDD10F012C510C6B00D76A8E /* ObservableUserDefaults.swift in Sources */, DD16AF0D2C98485400FB655A /* SecureStorageValue.swift in Sources */, + DDC7E5CF2DC77C2000EB1127 /* SnoozerViewModel.swift in Sources */, DD2C2E4F2D3B8AF1006413A5 /* NightscoutSettingsView.swift in Sources */, DD1A97162D4294B3000DDC11 /* AdvancedSettingsViewModel.swift in Sources */, FCA2DDE62501095000254A8C /* Timers.swift in Sources */, diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index 22dd6d9f3..9752252cb 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -78,16 +78,19 @@ class AlarmManager { } } - func snoozeCurrentAlarm(by minutes: Int) { - //guard var alarm = currentAlarm else { return } - //alarm.snoozedUntil = Date().addingTimeInterval(TimeInterval(minutes * 60)) + //TODO: Handle default snooze for notofication snoze + //TODO: Check interval type handling + func performSnooze(_ minutes: Int? = nil) { + if let alarmID = Observable.shared.currentAlarm.value { + var alarms = Storage.shared.alarms.value + if let idx = alarms.firstIndex(where: { $0.id == alarmID }) { + alarms[idx].snoozedUntil = Date().addingTimeInterval( + TimeInterval((minutes ?? 5) * 60)) // fix default value + } + Storage.shared.alarms.value = alarms - // write it back to your Storage - /* - Storage.shared.alarms.value = Storage.shared.alarms.value - .map { $0.id == alarm.id ? alarm : $0 } -*/ - // clear so you can’t snooze twice - //currentAlarm = nil + AlarmSound.stop() + Observable.shared.currentAlarm.value = nil + } } } diff --git a/LoopFollow/Application/AppDelegate.swift b/LoopFollow/Application/AppDelegate.swift index 2377d3561..e3e825b98 100644 --- a/LoopFollow/Application/AppDelegate.swift +++ b/LoopFollow/Application/AppDelegate.swift @@ -134,6 +134,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { window.rootViewController?.present(MainViewController(), animated: true, completion: nil) } } + + if response.actionIdentifier == "snooze" { + AlarmManager.shared.performSnooze() + } + completionHandler() } } diff --git a/LoopFollow/Extensions/ShareClientExtension.swift b/LoopFollow/Extensions/ShareClientExtension.swift index 33d158d25..c5ce2925a 100644 --- a/LoopFollow/Extensions/ShareClientExtension.swift +++ b/LoopFollow/Extensions/ShareClientExtension.swift @@ -61,7 +61,6 @@ private var TrendTable: [String] = [ "RATE OUT OF RANGE" // 9 ] -// TODO: probably better to make this an inherited class rather than an extension extension ShareClient { public func fetchData(_ entries: Int, callback: @escaping (ShareError?, [ShareGlucoseData]?) -> Void) { diff --git a/LoopFollow/Helpers/Mobileprovision.swift b/LoopFollow/Helpers/Mobileprovision.swift index 7752a5f7f..c5c64d929 100644 --- a/LoopFollow/Helpers/Mobileprovision.swift +++ b/LoopFollow/Helpers/Mobileprovision.swift @@ -99,7 +99,6 @@ extension MobileProvision { let provision = try decoder.decode(MobileProvision.self, from: plist) return provision } catch { - // TODO: log / handle error return nil } } diff --git a/LoopFollow/Snoozer/SnoozerView.swift b/LoopFollow/Snoozer/SnoozerView.swift index 040c94ca3..c241f7160 100644 --- a/LoopFollow/Snoozer/SnoozerView.swift +++ b/LoopFollow/Snoozer/SnoozerView.swift @@ -9,6 +9,8 @@ import SwiftUI struct SnoozerView: View { + @StateObject private var vm = SnoozerViewModel() + @ObservedObject var minAgoText = Observable.shared.minAgoText @ObservedObject var bgText = Observable.shared.bgText @ObservedObject var bgTextColor = Observable.shared.bgTextColor @@ -16,37 +18,26 @@ struct SnoozerView: View { @ObservedObject var deltaText = Observable.shared.deltaText @ObservedObject var bgStale = Observable.shared.bgStale - @Binding var snoozeMinutes: Int - var onSnooze: () -> Void - var body: some View { GeometryReader { geo in - ZStack { - Color.black - .edgesIgnoringSafeArea(.all) - - Group { - if geo.size.width > geo.size.height { - // Landscape: two columns - HStack(spacing: 0) { - leftColumn - rightColumn - } - } else { - // Portrait: single column - VStack(spacing: 0) { - leftColumn - rightColumn - } - } - } - .frame(width: geo.size.width, height: geo.size.height) - } + Color.black.ignoresSafeArea() + .overlay(contentColumn(size: geo.size)) + .animation(.easeInOut, value: vm.activeAlarm != nil) + } + } + + + // MARK: – Layout helper + @ViewBuilder private func contentColumn(size: CGSize) -> some View { + if size.width > size.height { // landscape + HStack(spacing: 0) { leftPanel ; rightPanel } + } else { // portrait + VStack(spacing: 0) { leftPanel ; rightPanel } } } // MARK: - Left Column (BG / Direction / Delta / Age) - private var leftColumn: some View { + private var leftPanel: some View { VStack(spacing: 0) { Text(bgText.value) .font(.system(size: 220, weight: .black)) @@ -81,62 +72,58 @@ struct SnoozerView: View { .padding(.horizontal, 16) } - // MARK: - Right Column (Clock/Alert + Snooze Controls) - private var rightColumn: some View { + // MARK: – Right (Clock + Snooze) + private var rightPanel: some View { VStack(spacing: 0) { Spacer() - VStack(spacing: 8) { - TimelineView(.periodic(from: .now, by: 1)) { context in - Text(context.date, format: - Date.FormatStyle(date: .omitted, time: .shortened) - ) - .font(.system(size: 70)) - .minimumScaleFactor(0.5) + TimelineView(.periodic(from: .now, by: 1)) { ctx in + Text(ctx.date, style: .time) + .font(.system(size: 70, weight: .regular)) .foregroundColor(.white) - .frame(height: 78) - } - - /* - if let alarm = alarmText.value, !alarm.isEmpty { - Text(alarm) - .font(.system(size: 40, weight: .semibold)) - .foregroundColor(.red) - .minimumScaleFactor(0.5) - .lineLimit(1) - .frame(height: 48) - } - */ + } + .frame(height: 80) + + if let alarm = vm.activeAlarm { + Text(alarm.name) + .font(.system(size: 40, weight: .semibold)) + .foregroundColor(.red) + .minimumScaleFactor(0.5) + .padding(.top, 4) + + Spacer(minLength: 32) + + snoozeControls(alarm: alarm) } Spacer() + } + .padding(.horizontal, 24) + } - // Snooze controls - HStack(spacing: 12) { + // MARK: – Snooze UI + @ViewBuilder private func snoozeControls(alarm: Alarm) -> some View { + VStack(spacing: 16) { + HStack { Text("Snooze for") - .font(.system(size: 20)) - .foregroundColor(.white) - Text("\(snoozeMinutes) min") - .font(.system(size: 20, weight: .semibold)) - .foregroundColor(.white) Spacer() - Stepper("", value: $snoozeMinutes, in: 1...60) - .labelsHidden() + Text("\(vm.snoozeMins) \(vm.timeUnitLabel)") } - .padding(.horizontal, 32) - .frame(height: 44) + .font(.title3) + .foregroundColor(.white) + + Stepper("", value: $vm.snoozeMins, + in: 1...120, + step: alarm.type.timeUnit == .minute ? 5 : 1) + .labelsHidden() - Button(action: onSnooze) { + Button(action: vm.snoozeTapped) { Text("Snooze") - .font(.system(size: 24, weight: .bold)) - .frame(maxWidth: .infinity, minHeight: 56) + .bold() + .frame(maxWidth: .infinity, minHeight: 50) + .background(Color.white.opacity(0.15)) + .cornerRadius(10) } - .background(Color(white: 0.15)) - .foregroundColor(.white) - .cornerRadius(8) - .padding(.horizontal, 32) - .padding(.bottom, 32) } - .padding(.top, 16) } } diff --git a/LoopFollow/Snoozer/SnoozerViewController.swift b/LoopFollow/Snoozer/SnoozerViewController.swift index 6ee9a6cc7..805f99360 100644 --- a/LoopFollow/Snoozer/SnoozerViewController.swift +++ b/LoopFollow/Snoozer/SnoozerViewController.swift @@ -20,12 +20,7 @@ class SnoozerViewController: UIViewController { super.viewDidLoad() view.backgroundColor = .black - let snoozerView = SnoozerView(snoozeMinutes: $snoozeMinutes, - onSnooze: { - // Trigger snooze logic here (e.g., update UserDefaultsRepository, stop alarm, etc.) - print("Snoozed for \(self.snoozeMinutes) minutes") - }, - ) + let snoozerView = SnoozerView() let hosting = UIHostingController(rootView: snoozerView) self.hostingController = hosting diff --git a/LoopFollow/Snoozer/SnoozerViewModel.swift b/LoopFollow/Snoozer/SnoozerViewModel.swift new file mode 100644 index 000000000..3198e78e3 --- /dev/null +++ b/LoopFollow/Snoozer/SnoozerViewModel.swift @@ -0,0 +1,42 @@ +// +// SnoozerViewModel.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-04. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import Foundation +import Combine + +final class SnoozerViewModel: ObservableObject { + @Published var activeAlarm: Alarm? + @Published var snoozeMins: Int = 5 + @Published var timeUnitLabel: String = "minutes" + + private var bag = Set() + + init() { + Observable.shared.currentAlarm.$value + .compactMap { $0 } // drop nils + .map { id in + Storage.shared.alarms.value.first { $0.id == id } + } + .receive(on: DispatchQueue.main) + .sink { [weak self] alarm in + self?.activeAlarm = alarm // may be nil + if let a = alarm { + self?.snoozeMins = a.snoozeDuration + self?.timeUnitLabel = a.type.timeUnit.label + } + } + .store(in: &bag) + } + + func snoozeTapped() { + guard let alarm = activeAlarm else { return } + AlarmManager.shared.performSnooze( + snoozeMins * Int(alarm.type.timeUnit.seconds) / 60 + ) + } +} diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index ad4124ca8..a7e673fb3 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -171,11 +171,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele self.tabBarController?.overrideUserInterfaceStyle = .dark } - // Load the snoozer tab - //TODO: - //guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } - //snoozer.loadViewIfNeeded() - // Trigger foreground and background functions let notificationCenter = NotificationCenter.default notificationCenter.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) @@ -305,7 +300,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele UIApplication.shared.isIdleTimerDisabled = UserDefaultsRepository.screenlockSwitchState.value; // check the app state - // TODO: move to a function ? if let appState = self.appStateController { if appState.chartSettingsChanged { From dead9c38bd68e8f0b1bb40c3c031cca579c0bad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sun, 4 May 2025 18:13:42 +0200 Subject: [PATCH 013/138] Snoozer view part 1 --- LoopFollow/Snoozer/SnoozerView.swift | 149 +++++++++++++--------- LoopFollow/Snoozer/SnoozerViewModel.swift | 12 +- LoopFollow/Task/AlarmTask.swift | 2 +- 3 files changed, 99 insertions(+), 64 deletions(-) diff --git a/LoopFollow/Snoozer/SnoozerView.swift b/LoopFollow/Snoozer/SnoozerView.swift index c241f7160..4cdb52760 100644 --- a/LoopFollow/Snoozer/SnoozerView.swift +++ b/LoopFollow/Snoozer/SnoozerView.swift @@ -20,24 +20,32 @@ struct SnoozerView: View { var body: some View { GeometryReader { geo in - Color.black.ignoresSafeArea() - .overlay(contentColumn(size: geo.size)) - .animation(.easeInOut, value: vm.activeAlarm != nil) - } - } - - - // MARK: – Layout helper - @ViewBuilder private func contentColumn(size: CGSize) -> some View { - if size.width > size.height { // landscape - HStack(spacing: 0) { leftPanel ; rightPanel } - } else { // portrait - VStack(spacing: 0) { leftPanel ; rightPanel } + ZStack { + Color.black + .edgesIgnoringSafeArea(.all) + + Group { + if geo.size.width > geo.size.height { + // Landscape: two columns + HStack(spacing: 0) { + leftColumn + rightColumn + } + } else { + // Portrait: single column + VStack(spacing: 0) { + leftColumn + rightColumn + } + } + } + .frame(width: geo.size.width, height: geo.size.height) + } } } // MARK: - Left Column (BG / Direction / Delta / Age) - private var leftPanel: some View { + private var leftColumn: some View { VStack(spacing: 0) { Text(bgText.value) .font(.system(size: 220, weight: .black)) @@ -72,58 +80,85 @@ struct SnoozerView: View { .padding(.horizontal, 16) } - // MARK: – Right (Clock + Snooze) - private var rightPanel: some View { + // MARK: - Right Column (Clock/Alert + Snooze Controls) + private var rightColumn: some View { VStack(spacing: 0) { Spacer() - TimelineView(.periodic(from: .now, by: 1)) { ctx in - Text(ctx.date, style: .time) - .font(.system(size: 70, weight: .regular)) - .foregroundColor(.white) - } - .frame(height: 80) - if let alarm = vm.activeAlarm { - Text(alarm.name) - .font(.system(size: 40, weight: .semibold)) - .foregroundColor(.red) + VStack(spacing: 16) { + Text(alarm.name) + .font(.system(size: 30, weight: .semibold)) + .foregroundColor(.white) + .lineLimit(1) + .minimumScaleFactor(0.5) + .padding(.top, 20) + Divider() + + // snooze controls + HStack { + VStack(alignment: .leading, spacing: 4) { + Text("Snooze for") + .font(.headline) + Text("\(vm.snoozeMins) \(vm.timeUnitLabel)") + .font(.title3).bold() + } + Spacer() + // stepper or wheel picker style for days/hours/minutes + Stepper("", value: $vm.snoozeMins, + in: 1...(alarm.type.timeUnit == .day ? 30 : + alarm.type.timeUnit == .hour ? 24 : 60), + step: alarm.type.timeUnit == .minute ? 5 : 1) + .labelsHidden() + } + .padding(.horizontal, 24) + + Button(action: vm.snoozeTapped) { + Text("Snooze") + .font(.title2).bold() + .frame(maxWidth: .infinity, minHeight: 50) + .background(Color.accentColor) + .foregroundColor(.white) + .cornerRadius(12) + } + .padding(.horizontal, 24) + .padding(.bottom, 20) + } + .background(.ultraThinMaterial) + .cornerRadius(20, corners: [.topLeft, .topRight]) + .transition(.move(edge: .bottom).combined(with: .opacity)) + .animation(.spring(), value: vm.activeAlarm != nil) + } else { + TimelineView(.periodic(from: .now, by: 1)) { context in + Text(context.date, format: + Date.FormatStyle(date: .omitted, time: .shortened) + ) + .font(.system(size: 70)) .minimumScaleFactor(0.5) - .padding(.top, 4) - - Spacer(minLength: 32) - - snoozeControls(alarm: alarm) + .foregroundColor(.white) + .frame(height: 78) + } + Spacer() } - - Spacer() } - .padding(.horizontal, 24) } +} - // MARK: – Snooze UI - @ViewBuilder private func snoozeControls(alarm: Alarm) -> some View { - VStack(spacing: 16) { - HStack { - Text("Snooze for") - Spacer() - Text("\(vm.snoozeMins) \(vm.timeUnitLabel)") - } - .font(.title3) - .foregroundColor(.white) - - Stepper("", value: $vm.snoozeMins, - in: 1...120, - step: alarm.type.timeUnit == .minute ? 5 : 1) - .labelsHidden() +fileprivate extension View { + func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View { + clipShape( RoundedCorner(radius: radius, corners: corners) ) + } +} - Button(action: vm.snoozeTapped) { - Text("Snooze") - .bold() - .frame(maxWidth: .infinity, minHeight: 50) - .background(Color.white.opacity(0.15)) - .cornerRadius(10) - } - } +fileprivate struct RoundedCorner: Shape { + var radius: CGFloat = .infinity + var corners: UIRectCorner = .allCorners + func path(in rect: CGRect) -> Path { + let path = UIBezierPath( + roundedRect: rect, + byRoundingCorners: corners, + cornerRadii: CGSize(width: radius, height: radius) + ) + return Path(path.cgPath) } } diff --git a/LoopFollow/Snoozer/SnoozerViewModel.swift b/LoopFollow/Snoozer/SnoozerViewModel.swift index 3198e78e3..a1f3002ee 100644 --- a/LoopFollow/Snoozer/SnoozerViewModel.swift +++ b/LoopFollow/Snoozer/SnoozerViewModel.swift @@ -14,23 +14,23 @@ final class SnoozerViewModel: ObservableObject { @Published var snoozeMins: Int = 5 @Published var timeUnitLabel: String = "minutes" - private var bag = Set() + private var cancellables = Set() init() { Observable.shared.currentAlarm.$value - .compactMap { $0 } // drop nils - .map { id in - Storage.shared.alarms.value.first { $0.id == id } + .map { id -> Alarm? in + guard let id = id else { return nil } + return Storage.shared.alarms.value.first { $0.id == id } } .receive(on: DispatchQueue.main) .sink { [weak self] alarm in - self?.activeAlarm = alarm // may be nil + self?.activeAlarm = alarm if let a = alarm { self?.snoozeMins = a.snoozeDuration self?.timeUnitLabel = a.type.timeUnit.label } } - .store(in: &bag) + .store(in: &cancellables) } func snoozeTapped() { diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index 2cbe63eca..515c580b8 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -10,7 +10,7 @@ import Foundation //TODO: Nu körs ju alarm var 60 sekund... men man vill nog ha det direkt efter bg-värdet kommer in etc. //TODO: Men ändå kanske inte för nära ett tidigare alarm, men det kanske vi inte hanterar här.... extension MainViewController { - func scheduleAlarmTask(initialDelay: TimeInterval = 60) { + func scheduleAlarmTask(initialDelay: TimeInterval = 1) { let firstRun = Date().addingTimeInterval(initialDelay) TaskScheduler.shared.scheduleTask(id: .alarmCheck, nextRun: firstRun) { [weak self] in guard let self = self else { return } From 6a868069480f5e6d308ca60012bd6e75bde1eafd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sun, 4 May 2025 19:53:39 +0200 Subject: [PATCH 014/138] Snoozer unit --- LoopFollow/Alarm/AlarmManager.swift | 18 ++++++++---------- LoopFollow/Snoozer/SnoozerView.swift | 5 ++--- LoopFollow/Snoozer/SnoozerViewModel.swift | 9 +++------ 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index 9752252cb..249a0b30a 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -78,17 +78,15 @@ class AlarmManager { } } - //TODO: Handle default snooze for notofication snoze - //TODO: Check interval type handling - func performSnooze(_ minutes: Int? = nil) { - if let alarmID = Observable.shared.currentAlarm.value { - var alarms = Storage.shared.alarms.value - if let idx = alarms.firstIndex(where: { $0.id == alarmID }) { - alarms[idx].snoozedUntil = Date().addingTimeInterval( - TimeInterval((minutes ?? 5) * 60)) // fix default value - } + func performSnooze(_ snoozeUnits: Int? = nil) { + guard let alarmID = Observable.shared.currentAlarm.value else { return } + var alarms = Storage.shared.alarms.value + if let idx = alarms.firstIndex(where: { $0.id == alarmID }) { + let alarm = alarms[idx] + let units = snoozeUnits ?? alarm.snoozeDuration + let snoozeSeconds = Double(units) * alarm.type.timeUnit.seconds + alarms[idx].snoozedUntil = Date().addingTimeInterval(snoozeSeconds) Storage.shared.alarms.value = alarms - AlarmSound.stop() Observable.shared.currentAlarm.value = nil } diff --git a/LoopFollow/Snoozer/SnoozerView.swift b/LoopFollow/Snoozer/SnoozerView.swift index 4cdb52760..5451895c4 100644 --- a/LoopFollow/Snoozer/SnoozerView.swift +++ b/LoopFollow/Snoozer/SnoozerView.swift @@ -100,12 +100,11 @@ struct SnoozerView: View { VStack(alignment: .leading, spacing: 4) { Text("Snooze for") .font(.headline) - Text("\(vm.snoozeMins) \(vm.timeUnitLabel)") + Text("\(vm.snoozeUnits) \(vm.timeUnitLabel)") .font(.title3).bold() } Spacer() - // stepper or wheel picker style for days/hours/minutes - Stepper("", value: $vm.snoozeMins, + Stepper("", value: $vm.snoozeUnits, in: 1...(alarm.type.timeUnit == .day ? 30 : alarm.type.timeUnit == .hour ? 24 : 60), step: alarm.type.timeUnit == .minute ? 5 : 1) diff --git a/LoopFollow/Snoozer/SnoozerViewModel.swift b/LoopFollow/Snoozer/SnoozerViewModel.swift index a1f3002ee..51cf6d3f7 100644 --- a/LoopFollow/Snoozer/SnoozerViewModel.swift +++ b/LoopFollow/Snoozer/SnoozerViewModel.swift @@ -11,7 +11,7 @@ import Combine final class SnoozerViewModel: ObservableObject { @Published var activeAlarm: Alarm? - @Published var snoozeMins: Int = 5 + @Published var snoozeUnits: Int = 5 @Published var timeUnitLabel: String = "minutes" private var cancellables = Set() @@ -26,7 +26,7 @@ final class SnoozerViewModel: ObservableObject { .sink { [weak self] alarm in self?.activeAlarm = alarm if let a = alarm { - self?.snoozeMins = a.snoozeDuration + self?.snoozeUnits = a.snoozeDuration self?.timeUnitLabel = a.type.timeUnit.label } } @@ -34,9 +34,6 @@ final class SnoozerViewModel: ObservableObject { } func snoozeTapped() { - guard let alarm = activeAlarm else { return } - AlarmManager.shared.performSnooze( - snoozeMins * Int(alarm.type.timeUnit.seconds) / 60 - ) + AlarmManager.shared.performSnooze(snoozeUnits) } } From ef100e66baa6080894fe62d55ca6e442dc9df5c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 5 May 2025 12:56:00 +0200 Subject: [PATCH 015/138] Alarm handling --- LoopFollow/Alarm/Alarm.swift | 2 ++ LoopFollow/Alarm/AlarmManager.swift | 26 ++++++++++++++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index 66f16e66e..8de081b78 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -153,6 +153,8 @@ struct Alarm: Identifiable, Codable, Equatable { } }() + UNUserNotificationCenter.current().removeAllPendingNotificationRequests() + let content = UNMutableNotificationContent() content.title = type.rawValue content.subtitle += Observable.shared.bgText.value + " " diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index 249a0b30a..0306f9c23 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -7,6 +7,7 @@ // import Foundation +import UserNotifications class AlarmManager { static let shared = AlarmManager() @@ -24,7 +25,6 @@ class AlarmManager { evaluators = dict } - //TODO: Somehow we need to silent the current alarm if the current one is no longer active. func checkAlarms(data: AlarmData) { let now = Date() let alarms = Storage.shared.alarms.value @@ -51,7 +51,8 @@ class AlarmManager { continue } - // If the alarm itself is snoozed, skip lower‑priority alarms of the same type. + // If the alarm itself is snoozed skip it, and skip lower‑priority alarms of the same type. + // We still want other types af alarm to go off, so we continue here without breaking if let until = alarm.snoozedUntil, until > now { skipType = alarm.type continue @@ -67,9 +68,21 @@ class AlarmManager { config: Storage.shared.alarmConfiguration.value ) else { + // If this alarm is active, but no longer fulfill the requirements, stop it. + // Continue evaluating other alarams + if Observable.shared.currentAlarm.value == alarm.id { + stopAlarm() + } + continue } + // If this alarm is active, and still fulfill the requirements, let it be active + // Break the loop, nothing else to do + if Observable.shared.currentAlarm.value == alarm.id { + break + } + // Fire the alarm and break the loop; we only allow one alarm per evaluation tick. Observable.shared.currentAlarm.value = alarm.id @@ -87,8 +100,13 @@ class AlarmManager { let snoozeSeconds = Double(units) * alarm.type.timeUnit.seconds alarms[idx].snoozedUntil = Date().addingTimeInterval(snoozeSeconds) Storage.shared.alarms.value = alarms - AlarmSound.stop() - Observable.shared.currentAlarm.value = nil + stopAlarm() } } + + func stopAlarm() { + AlarmSound.stop() + Observable.shared.currentAlarm.value = nil + UNUserNotificationCenter.current().removeAllPendingNotificationRequests() + } } From 8c7783ba9c06302378abbabd5dcd1c81040af6f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 5 May 2025 16:26:19 +0200 Subject: [PATCH 016/138] Testdata for alarms --- LoopFollow/Alarm/Alarm.swift | 1 - LoopFollow/Alarm/AlarmData.swift | 2 +- LoopFollow/Task/AlarmTask.swift | 42 +++++++++++++++++++++++++---- LoopFollow/Task/TaskScheduler.swift | 3 +++ 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index 8de081b78..2ecb7f414 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -162,7 +162,6 @@ struct Alarm: Identifiable, Codable, Equatable { content.subtitle += Observable.shared.deltaText.value content.categoryIdentifier = "category" // This is needed to trigger vibrate on watch and phone - // TODO: // See if we can use .Critcal // See if we should use this method instead of direct sound player content.sound = .default diff --git a/LoopFollow/Alarm/AlarmData.swift b/LoopFollow/Alarm/AlarmData.swift index ecc1ab9fd..55814d1a9 100644 --- a/LoopFollow/Alarm/AlarmData.swift +++ b/LoopFollow/Alarm/AlarmData.swift @@ -8,7 +8,7 @@ import Foundation -struct AlarmData { +struct AlarmData : Encodable, Decodable{ // let bgReadings: [ShareGlucoseData] // let iob: Double? // let cob: Double? diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index 515c580b8..9d203cde7 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -7,8 +7,7 @@ // import Foundation -//TODO: Nu körs ju alarm var 60 sekund... men man vill nog ha det direkt efter bg-värdet kommer in etc. -//TODO: Men ändå kanske inte för nära ett tidigare alarm, men det kanske vi inte hanterar här.... + extension MainViewController { func scheduleAlarmTask(initialDelay: TimeInterval = 1) { let firstRun = Date().addingTimeInterval(initialDelay) @@ -20,12 +19,14 @@ extension MainViewController { func alarmTaskAction() { DispatchQueue.main.async { - //TODO: Fyll på med mer alarmData - //TODO: gör det möjligt att köra med fejkad data. - let alarmData = AlarmData( + var alarmData = AlarmData( expireDate: .distantPast // Storage.shared.expirationDate.value ) + self.saveLatestAlarmDataToFile(alarmData) + + alarmData = self.loadTestAlarmData() ?? alarmData + LogManager.shared.log(category: .alarm, message: "Checking alarms based on \(alarmData)", isDebug: true) AlarmManager.shared.checkAlarms(data: alarmData) @@ -33,4 +34,35 @@ extension MainViewController { TaskScheduler.shared.rescheduleTask(id: .alarmCheck, to: Date().addingTimeInterval(60)) } } + + func saveLatestAlarmDataToFile(_ alarmData: AlarmData) { + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + do { + let data = try encoder.encode(alarmData) + let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("latestAlarmData.json") + try data.write(to: url) + } catch { + LogManager.shared.log(category: .alarm, message: "Failed to save latest AlarmData: \(error)", isDebug: true) + } + } + + func loadTestAlarmData() -> AlarmData? { + let fileManager = FileManager.default + let url = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("testAlarmData.json") + + if fileManager.fileExists(atPath: url.path) { + do { + let data = try Data(contentsOf: url) + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + let alarmData = try decoder.decode(AlarmData.self, from: data) + LogManager.shared.log(category: .alarm, message: "Loaded test AlarmData from \(url.path)", isDebug: true) + return alarmData + } catch { + LogManager.shared.log(category: .alarm, message: "Failed to load test AlarmData: \(error)", isDebug: true) + } + } + return nil + } } diff --git a/LoopFollow/Task/TaskScheduler.swift b/LoopFollow/Task/TaskScheduler.swift index 2da36c045..39bc804c5 100644 --- a/LoopFollow/Task/TaskScheduler.swift +++ b/LoopFollow/Task/TaskScheduler.swift @@ -101,6 +101,9 @@ class TaskScheduler { continue } + // Skip alarm checks if data-fetching tasks (deviceStatus, treatments, fetchBG) are currently due or just executed. + // This ensures alarms are evaluated with the latest data, avoiding premature or incorrect triggers. + // If skipped, reschedule alarmCheck 5 seconds later to retry after data updates. if taskID == .alarmCheck { let shouldSkip = tasksToSkipAlarmCheck.contains { guard let checkTask = tasks[$0] else { return false } From 9990e12afe2c6f5fcf179a90ebabef646e3c83d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 6 May 2025 11:01:13 +0200 Subject: [PATCH 017/138] Debug helpers --- LoopFollow.xcodeproj/project.pbxproj | 4 ++ LoopFollow/Alarm/AlarmData.swift | 12 +++++- LoopFollow/Alarm/GlucoseValue.swift | 14 +++++++ .../Nightscout/ProfileManager.swift | 1 - LoopFollow/Storage/Observable.swift | 2 + LoopFollow/Task/AlarmTask.swift | 24 ++++++++---- .../ViewControllers/MainViewController.swift | 38 +++++++++++++++++++ 7 files changed, 85 insertions(+), 10 deletions(-) create mode 100644 LoopFollow/Alarm/GlucoseValue.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 1411e1af0..bc0501104 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -76,6 +76,7 @@ DD58171A2D299EF80041FB98 /* DexcomHeartbeatBluetoothDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5817192D299EF40041FB98 /* DexcomHeartbeatBluetoothDevice.swift */; }; DD58171C2D299F940041FB98 /* BluetoothDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD58171B2D299F8D0041FB98 /* BluetoothDevice.swift */; }; DD58171E2D299FCA0041FB98 /* BluetoothDeviceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD58171D2D299FC50041FB98 /* BluetoothDeviceDelegate.swift */; }; + DD5DA27C2DC930D6003D44FC /* GlucoseValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DA27B2DC930D6003D44FC /* GlucoseValue.swift */; }; DD608A082C1F584900F91132 /* DeviceStatusLoop.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD608A072C1F584900F91132 /* DeviceStatusLoop.swift */; }; DD608A0A2C23593900F91132 /* SMB.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD608A092C23593900F91132 /* SMB.swift */; }; DD608A0C2C27415C00F91132 /* BackgroundAlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD608A0B2C27415C00F91132 /* BackgroundAlertManager.swift */; }; @@ -386,6 +387,7 @@ DD5817192D299EF40041FB98 /* DexcomHeartbeatBluetoothDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DexcomHeartbeatBluetoothDevice.swift; sourceTree = ""; }; DD58171B2D299F8D0041FB98 /* BluetoothDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothDevice.swift; sourceTree = ""; }; DD58171D2D299FC50041FB98 /* BluetoothDeviceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothDeviceDelegate.swift; sourceTree = ""; }; + DD5DA27B2DC930D6003D44FC /* GlucoseValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseValue.swift; sourceTree = ""; }; DD608A072C1F584900F91132 /* DeviceStatusLoop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatusLoop.swift; sourceTree = ""; }; DD608A092C23593900F91132 /* SMB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMB.swift; sourceTree = ""; }; DD608A0B2C27415C00F91132 /* BackgroundAlertManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundAlertManager.swift; sourceTree = ""; }; @@ -935,6 +937,7 @@ DDCF9A7E2D85FCE6004DF4DD /* Alarm */ = { isa = PBXGroup; children = ( + DD5DA27B2DC930D6003D44FC /* GlucoseValue.swift */, DD4AFB602DB68BBC00BB593F /* AlarmListView.swift */, DDC7E5412DBD8A1600EB1127 /* AlarmEditing */, DD4AFB482DB576C200BB593F /* AlarmSettingsView.swift */, @@ -1722,6 +1725,7 @@ FCC6886B24898FD800A0279D /* ObservationToken.swift in Sources */, DD4AFB6B2DB6BF2A00BB593F /* Binding+Optional.swift in Sources */, DD608A082C1F584900F91132 /* DeviceStatusLoop.swift in Sources */, + DD5DA27C2DC930D6003D44FC /* GlucoseValue.swift in Sources */, DD9ACA062D32AF7900415D8A /* TreatmentsTask.swift in Sources */, DD98F54424BCEFEE0007425A /* ShareClientExtension.swift in Sources */, DD9ED0CE2D35587A000D2A63 /* LogEntry.swift in Sources */, diff --git a/LoopFollow/Alarm/AlarmData.swift b/LoopFollow/Alarm/AlarmData.swift index 55814d1a9..a1f1ada63 100644 --- a/LoopFollow/Alarm/AlarmData.swift +++ b/LoopFollow/Alarm/AlarmData.swift @@ -8,11 +8,18 @@ import Foundation +struct AlarmData: Codable { + let bgReadings: [GlucoseValue] + let predictionData: [GlucoseValue] + let expireDate: Date? +} + +/* struct AlarmData : Encodable, Decodable{ -// let bgReadings: [ShareGlucoseData] + let bgReadings: [ShareGlucoseData] // let iob: Double? // let cob: Double? -// let predictionData: [ShareGlucoseData] + let predictionData: [ShareGlucoseData] // let latestBoluses: [BolusEntry] // let batteryLevel: Double? // let latestCarbs: [CarbEntry] @@ -21,3 +28,4 @@ struct AlarmData : Encodable, Decodable{ // let pumpVolume: Double? let expireDate: Date? } +*/ diff --git a/LoopFollow/Alarm/GlucoseValue.swift b/LoopFollow/Alarm/GlucoseValue.swift new file mode 100644 index 000000000..a25e7a2bc --- /dev/null +++ b/LoopFollow/Alarm/GlucoseValue.swift @@ -0,0 +1,14 @@ +// +// GlucoseValue.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-05. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import Foundation + +struct GlucoseValue: Codable { + let sgv: Int + let date: Date +} diff --git a/LoopFollow/Controllers/Nightscout/ProfileManager.swift b/LoopFollow/Controllers/Nightscout/ProfileManager.swift index c31c4f9d5..60b54fa31 100644 --- a/LoopFollow/Controllers/Nightscout/ProfileManager.swift +++ b/LoopFollow/Controllers/Nightscout/ProfileManager.swift @@ -107,7 +107,6 @@ final class ProfileManager { Storage.shared.deviceToken.value = profileData.deviceToken ?? "" if let expirationDate = profileData.expirationDate { Storage.shared.expirationDate.value = NightscoutUtils.parseDate(expirationDate) - Storage.shared.expirationDate.value = NightscoutUtils.parseDate("2025-04-25T17:54:56.000Z") } else { Storage.shared.expirationDate.value = nil } diff --git a/LoopFollow/Storage/Observable.swift b/LoopFollow/Storage/Observable.swift index ec1462c3e..9c906108f 100644 --- a/LoopFollow/Storage/Observable.swift +++ b/LoopFollow/Storage/Observable.swift @@ -30,5 +30,7 @@ class Observable { var currentAlarm = ObservableValue(default: nil) + var debug = ObservableValue(default: false) + private init() {} } diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index 9d203cde7..ecf3352ae 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -19,17 +19,27 @@ extension MainViewController { func alarmTaskAction() { DispatchQueue.main.async { - var alarmData = AlarmData( - expireDate: .distantPast // Storage.shared.expirationDate.value + let alarmData = AlarmData( + bgReadings: self.bgData + .suffix(5) + .map { GlucoseValue(sgv: $0.sgv, date: Date(timeIntervalSince1970: $0.date)) }, + predictionData: self.predictionData + .prefix(5) + .map { GlucoseValue(sgv: $0.sgv, date: Date(timeIntervalSince1970: $0.date)) }, + expireDate: Storage.shared.expirationDate.value ) - self.saveLatestAlarmDataToFile(alarmData) - - alarmData = self.loadTestAlarmData() ?? alarmData + let finalAlarmData: AlarmData + if Observable.shared.debug.value { + self.saveLatestAlarmDataToFile(alarmData) + finalAlarmData = self.loadTestAlarmData() ?? alarmData + } else { + finalAlarmData = alarmData + } - LogManager.shared.log(category: .alarm, message: "Checking alarms based on \(alarmData)", isDebug: true) + LogManager.shared.log(category: .alarm, message: "Checking alarms based on \(finalAlarmData)", isDebug: true) - AlarmManager.shared.checkAlarms(data: alarmData) + AlarmManager.shared.checkAlarms(data: finalAlarmData) TaskScheduler.shared.rescheduleTask(id: .alarmCheck, to: Date().addingTimeInterval(60)) } diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index a7e673fb3..18ad0208c 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -125,6 +125,8 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele override func viewDidLoad() { super.viewDidLoad() + loadDebugData() + if ObservableUserDefaults.shared.device.value != "Trio" && Storage.shared.remoteType.value == .trc { Storage.shared.remoteType.value = .none } @@ -701,4 +703,40 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele func calculateMaxBgGraphValue() -> Float { return max(topBG, topPredictionBG) } + + func loadDebugData() { + struct DebugData: Codable { + let debug: Bool? + let url: String? + let token: String? + } + + let fileManager = FileManager.default + let url = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("debugData.json") + + if fileManager.fileExists(atPath: url.path) { + do { + let data = try Data(contentsOf: url) + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + + let debugData = try decoder.decode(DebugData.self, from: data) + LogManager.shared.log(category: .alarm, message: "Loaded DebugData from \(url.path)", isDebug: true) + + if let debug = debugData.debug { + Observable.shared.debug.value = debug + } + + if let url = debugData.url { + ObservableUserDefaults.shared.url.value = url + } + + if let token = debugData.token { + UserDefaultsRepository.token.value = token + } + } catch { + LogManager.shared.log(category: .alarm, message: "Failed to load DebugData: \(error)", isDebug: true) + } + } + } } From 002b328fb3a62f07e67ee92e7dd3171f27341753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 6 May 2025 20:49:54 +0200 Subject: [PATCH 018/138] bg picker --- LoopFollow.xcodeproj/project.pbxproj | 4 + .../AlarmThresholdPickerSection.swift | 92 +++++++++++++++++++ .../Editors/LowBgAlarmEditor.swift | 24 ++--- LoopFollow/Alarm/AlarmType.swift | 4 +- LoopFollow/Task/AlarmTask.swift | 4 +- 5 files changed, 110 insertions(+), 18 deletions(-) create mode 100644 LoopFollow/Alarm/AlarmEditing/Components/AlarmThresholdPickerSection.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index bc0501104..d45875def 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 3F1335F351590E573D8E6962 /* Pods_LoopFollow.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */; }; DD0247592DB2E89600FCADF6 /* AlarmCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */; }; DD0247712DB4337700FCADF6 /* BuildExpireCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */; }; + DD0650A92DCA8A10004D3B41 /* AlarmThresholdPickerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650A82DCA8A10004D3B41 /* AlarmThresholdPickerSection.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 */; }; @@ -322,6 +323,7 @@ DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmCondition.swift; sourceTree = ""; }; DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildExpireCondition.swift; sourceTree = ""; }; DD0247612DB2EB9A00FCADF6 /* LoopFollowTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LoopFollowTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + DD0650A82DCA8A10004D3B41 /* AlarmThresholdPickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmThresholdPickerSection.swift; sourceTree = ""; }; DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUtils.swift; sourceTree = ""; }; DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileManager.swift; sourceTree = ""; }; DD0C0C612C4175FD00DBADDF /* NSProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSProfile.swift; sourceTree = ""; }; @@ -896,6 +898,7 @@ DDC7E53B2DBD8A1600EB1127 /* Components */ = { isa = PBXGroup; children = ( + DD0650A82DCA8A10004D3B41 /* AlarmThresholdPickerSection.swift */, DDC7E5392DBD8A1600EB1127 /* AlarmEditorFields.swift */, DDC7E53A2DBD8A1600EB1127 /* SoundFile.swift */, ); @@ -1722,6 +1725,7 @@ DD0C0C6B2C48562000DBADDF /* InsulinMetric.swift in Sources */, DD493AD92ACF2171009A6922 /* Carbs.swift in Sources */, DD493AE92ACF2445009A6922 /* BGData.swift in Sources */, + DD0650A92DCA8A10004D3B41 /* AlarmThresholdPickerSection.swift in Sources */, FCC6886B24898FD800A0279D /* ObservationToken.swift in Sources */, DD4AFB6B2DB6BF2A00BB593F /* Binding+Optional.swift in Sources */, DD608A082C1F584900F91132 /* DeviceStatusLoop.swift in Sources */, diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmThresholdPickerSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmThresholdPickerSection.swift new file mode 100644 index 000000000..20eef7845 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmThresholdPickerSection.swift @@ -0,0 +1,92 @@ +// +// AlarmThresholdPickerSection.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-06. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import SwiftUI +import HealthKit + +struct AlarmThresholdRow: View { + // ── Public API ────────────────────────────────────────────────────────────── + let title: String + let range: ClosedRange + let step: Double // 1 for mg/dL, 0.1 for mmol/L + @Binding var value: Double // **stored in mg/dL** + + // ── Private state ────────────────────────────────────────────────────────── + @State private var showPicker = false + + // Preferred unit – mmol/L or mg/dL + private var unit: HKUnit { UserDefaultsRepository.getPreferredUnit() } + + private var displayValue: String { + format(value, in: unit) + } + + // Generate all selectable display values for the picker + private var pickerValues: [Double] { + stride(from: range.lowerBound, through: range.upperBound, by: step) + .map { $0 } + } + + var body: some View { + Section { + // Collapsed row + HStack { + Text(title) + Spacer() + Text(displayValue) + .foregroundColor(.secondary) + } + .contentShape(Rectangle()) + .onTapGesture { showPicker = true } + } + .sheet(isPresented: $showPicker) { + // Expanded wheel picker + NavigationStack { + VStack { + Picker("", selection: bindingForPicker) { + ForEach(pickerValues, id: \.self) { v in + Text(format(v, in: unit)) + .tag(v) + } + } + .pickerStyle(.wheel) + } + .navigationTitle(title) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Done") { showPicker = false } + } + } + } .presentationDetents([.fraction(0.35), .medium]) // 35 % or medium height + .presentationDragIndicator(.visible) // the little grab-bar + } + } + + // MARK: – Helpers + private func format(_ mgdl: Double, in unit: HKUnit) -> String { + if unit == .millimolesPerLiter { + let mmol = mgdl / 18.0 + return String(format: "%.1f mmol/L", mmol) + } else { + return String(format: "%.0f mg/dL", mgdl) + } + } + + // Bind the picker directly to `value`, snapping to the closest choice + private var bindingForPicker: Binding { + Binding( + get: { + // pick the closest representable value + pickerValues.min(by: { abs($0 - value) < abs($1 - value) }) ?? value + }, + set: { newVal in + value = newVal // stored in mg/dL + } + ) + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift index 8995c4052..a233c6855 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift @@ -13,23 +13,19 @@ struct LowBgAlarmEditor: View { @Binding var alarm: Alarm var body: some View { - Form {/* - AlarmNameField(alarm: $alarm) - EnabledToggle(alarm: $alarm) - ValueStepper( - title: "BG Below", + Form { + AlarmGeneralSection(alarm: $alarm) + + AlarmThresholdRow( + title: "BG", + range: 40...150, + step: UserDefaultsRepository.getPreferredUnit() == .millimolesPerLiter ? 18.0 * 0.1 : 1.0, value: Binding( - get: { Double(alarm.threshold ?? 0) }, + get: { Double(alarm.threshold ?? 80) }, set: { alarm.threshold = Float($0) } - ), - range: 0...500, step: 1, - formatter: { "\(Int($0))" } + ) ) - DayNightToggle(alarm: $alarm) - SoundPicker(alarm: $alarm) - SnoozeDatePicker(alarm: $alarm) - SnoozeDurationStepper(alarm: $alarm)*/ } - .navigationTitle("Low BG Alert") + .navigationTitle(alarm.type.rawValue) } } diff --git a/LoopFollow/Alarm/AlarmType.swift b/LoopFollow/Alarm/AlarmType.swift index 7050c49cc..fcb7f0f84 100644 --- a/LoopFollow/Alarm/AlarmType.swift +++ b/LoopFollow/Alarm/AlarmType.swift @@ -14,8 +14,8 @@ enum AlarmType: String, CaseIterable, Codable { case iob = "IOB Alert" case bolus = "Bolus Alert" case cob = "COB Alert" - case low = "Low Alert" - case high = "High Alert" + case low = "Low BG Alert" + case high = "High BG Alert" case fastDrop = "Fast Drop Alert" case fastRise = "Fast Rise Alert" case missedReading = "Missed Reading Alert" diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index ecf3352ae..c87786b25 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -22,10 +22,10 @@ extension MainViewController { let alarmData = AlarmData( bgReadings: self.bgData .suffix(5) - .map { GlucoseValue(sgv: $0.sgv, date: Date(timeIntervalSince1970: $0.date)) }, + .map { GlucoseValue(sgv: $0.sgv, date: Date(timeIntervalSince1970: $0.date)) }, /// These are oldest .. newest predictionData: self.predictionData .prefix(5) - .map { GlucoseValue(sgv: $0.sgv, date: Date(timeIntervalSince1970: $0.date)) }, + .map { GlucoseValue(sgv: $0.sgv, date: Date(timeIntervalSince1970: $0.date)) }, /// These are oldest .. newest, Predictions not currently available for Trio expireDate: Storage.shared.expirationDate.value ) From 570ae3370ea2ef3c88b6cb99d492df515c014d40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 7 May 2025 17:09:42 +0200 Subject: [PATCH 019/138] AlarmBGSection --- LoopFollow/Alarm/Alarm.swift | 10 +++--- .../AlarmThresholdPickerSection.swift | 32 +++++++------------ .../Editors/BuildExpireAlarmEditor.swift | 4 +-- .../Editors/LowBgAlarmEditor.swift | 7 ++-- LoopFollow/Alarm/AlarmManager.swift | 4 +-- LoopFollow/Helpers/Localizer.swift | 15 ++++----- 6 files changed, 30 insertions(+), 42 deletions(-) diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index 2ecb7f414..d315cd5da 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -34,14 +34,14 @@ struct Alarm: Identifiable, Codable, Equatable { /// Alarm threashold, it can be a bgvalue (in mg/Dl), or day for example /// Also used as bg limit for drop alarms for example - var threshold: Float? + var threshold: Double? /// If the alarm looks at predictions, this is how many predictions to include var predictiveReadings: Int? /// If the alarm acts on delta, the delta is stored here, it can be a delta bgvalue (in mg/Dl) /// If a delta alarm is only active below a bg, that bg is stored in threshold - var delta: Float? + var delta: Double? /// Number of consecutive 5‑min readings that must satisfy the alarm criteria var consecutiveReadings: Int? @@ -74,13 +74,13 @@ struct Alarm: Identifiable, Codable, Equatable { var missedBolusPrebolusWindow: Int? /// “Ignore Bolus <= X units” (don’t count any bolus smaller than or equal to this) - var missedBolusIgnoreSmallBolusUnits: Float? + var missedBolusIgnoreSmallBolusUnits: Double? /// “Ignore Under Grams” (if carb entry is under this many grams, skip the alert) - var missedBolusIgnoreUnderGrams: Float? + var missedBolusIgnoreUnderGrams: Double? /// “Ignore Under BG” (if current BG is below this, skip the alert) - var missedBolusIgnoreUnderBG: Float? + var missedBolusIgnoreUnderBG: Double? // ───────────────────────────────────────────────────────────── // Bolus‑Count fields ─ diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmThresholdPickerSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmThresholdPickerSection.swift index 20eef7845..c188f7615 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmThresholdPickerSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmThresholdPickerSection.swift @@ -9,12 +9,11 @@ import SwiftUI import HealthKit -struct AlarmThresholdRow: View { +struct AlarmBGSection: View { // ── Public API ────────────────────────────────────────────────────────────── let title: String let range: ClosedRange - let step: Double // 1 for mg/dL, 0.1 for mmol/L - @Binding var value: Double // **stored in mg/dL** + @Binding var value: Double // ── Private state ────────────────────────────────────────────────────────── @State private var showPicker = false @@ -23,12 +22,14 @@ struct AlarmThresholdRow: View { private var unit: HKUnit { UserDefaultsRepository.getPreferredUnit() } private var displayValue: String { - format(value, in: unit) + Localizer.formatQuantity(value) } // Generate all selectable display values for the picker private var pickerValues: [Double] { - stride(from: range.lowerBound, through: range.upperBound, by: step) + let step : Double = unit == .millimolesPerLiter ? 18.0 * 0.1 : 1.0 + + return stride(from: range.lowerBound, through: range.upperBound, by: step) .map { $0 } } @@ -45,12 +46,11 @@ struct AlarmThresholdRow: View { .onTapGesture { showPicker = true } } .sheet(isPresented: $showPicker) { - // Expanded wheel picker NavigationStack { VStack { Picker("", selection: bindingForPicker) { ForEach(pickerValues, id: \.self) { v in - Text(format(v, in: unit)) + Text(Localizer.formatQuantity(v)) .tag(v) } } @@ -62,22 +62,12 @@ struct AlarmThresholdRow: View { Button("Done") { showPicker = false } } } - } .presentationDetents([.fraction(0.35), .medium]) // 35 % or medium height - .presentationDragIndicator(.visible) // the little grab-bar - } - } - - // MARK: – Helpers - private func format(_ mgdl: Double, in unit: HKUnit) -> String { - if unit == .millimolesPerLiter { - let mmol = mgdl / 18.0 - return String(format: "%.1f mmol/L", mmol) - } else { - return String(format: "%.0f mg/dL", mgdl) + } + .presentationDetents([.fraction(0.35), .medium]) + .presentationDragIndicator(.visible) } } - // Bind the picker directly to `value`, snapping to the closest choice private var bindingForPicker: Binding { Binding( get: { @@ -85,7 +75,7 @@ struct AlarmThresholdRow: View { pickerValues.min(by: { abs($0 - value) < abs($1 - value) }) ?? value }, set: { newVal in - value = newVal // stored in mg/dL + value = newVal // stored in mg/dL } ) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift index 7b036c5d4..76e18bc60 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift @@ -21,8 +21,8 @@ struct BuildExpireAlarmEditor: View { step: 1, unitLabel: alarm.type.timeUnit.label, value: Binding( - get: { Double(alarm.threshold ?? 1) }, - set: { alarm.threshold = Float($0) } + get: { alarm.threshold ?? 1 }, + set: { alarm.threshold = $0 } ) ) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift index a233c6855..469ae46f5 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift @@ -16,13 +16,12 @@ struct LowBgAlarmEditor: View { Form { AlarmGeneralSection(alarm: $alarm) - AlarmThresholdRow( + AlarmBGSection( title: "BG", range: 40...150, - step: UserDefaultsRepository.getPreferredUnit() == .millimolesPerLiter ? 18.0 * 0.1 : 1.0, value: Binding( - get: { Double(alarm.threshold ?? 80) }, - set: { alarm.threshold = Float($0) } + get: { alarm.threshold ?? 80 }, + set: { alarm.threshold = $0 } ) ) } diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index 0306f9c23..919e6be5c 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -36,8 +36,8 @@ class AlarmManager { } // Secondary: threshold ordering if applicable if let asc = lhs.type.thresholdSortAscending { - let leftVal = lhs.threshold ?? (asc ? Float.infinity : -Float.infinity) - let rightVal = rhs.threshold ?? (asc ? Float.infinity : -Float.infinity) + let leftVal = lhs.threshold ?? (asc ? Double.infinity : -Double.infinity) + let rightVal = rhs.threshold ?? (asc ? Double.infinity : -Double.infinity) return asc ? leftVal < rightVal : leftVal > rightVal } // Tertiary: fallback to insertion order diff --git a/LoopFollow/Helpers/Localizer.swift b/LoopFollow/Helpers/Localizer.swift index 8e15f13c0..fbade35dd 100644 --- a/LoopFollow/Helpers/Localizer.swift +++ b/LoopFollow/Helpers/Localizer.swift @@ -22,15 +22,14 @@ class Localizer { } static func formatQuantity(_ quantity: HKQuantity) -> String { - let unitPreference = UserDefaultsRepository.units.value + let unit: HKUnit = UserDefaultsRepository.getPreferredUnit() + let value = quantity.doubleValue(for: unit) - if unitPreference == "mg/dL" { - let valueInMgdL = quantity.doubleValue(for: .milligramsPerDeciliter) - return formatToLocalizedString(valueInMgdL, maxFractionDigits: 0, minFractionDigits: 0) - } else { - let valueInMmolL = quantity.doubleValue(for: .millimolesPerLiter) - return formatToLocalizedString(valueInMmolL, maxFractionDigits: 1, minFractionDigits: 1) - } + return formatToLocalizedString(value, maxFractionDigits: unit.preferredFractionDigits, minFractionDigits: unit.preferredFractionDigits) + } + + static func formatQuantity(_ value: Double) -> String { + formatQuantity(HKQuantity(unit: .milligramsPerDeciliter, doubleValue: value)) } static func formatTimestampToLocalString(_ timestamp: TimeInterval) -> String { From fc3188fee4613d2f25dfe6b9f57c736b4cb7157c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 7 May 2025 20:01:36 +0200 Subject: [PATCH 020/138] Low bg alarm in progress --- LoopFollow.xcodeproj/project.pbxproj | 8 ++--- LoopFollow/Alarm/Alarm.swift | 4 +-- ...ckerSection.swift => AlarmBGSection.swift} | 33 ++++++++++++++----- .../Components/AlarmEditorFields.swift | 2 +- .../Editors/BuildExpireAlarmEditor.swift | 2 +- .../Editors/LowBgAlarmEditor.swift | 28 ++++++++++++++++ 6 files changed, 60 insertions(+), 17 deletions(-) rename LoopFollow/Alarm/AlarmEditing/Components/{AlarmThresholdPickerSection.swift => AlarmBGSection.swift} (73%) diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index d45875def..db6174ef6 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -10,7 +10,7 @@ 3F1335F351590E573D8E6962 /* Pods_LoopFollow.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */; }; DD0247592DB2E89600FCADF6 /* AlarmCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */; }; DD0247712DB4337700FCADF6 /* BuildExpireCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */; }; - DD0650A92DCA8A10004D3B41 /* AlarmThresholdPickerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650A82DCA8A10004D3B41 /* AlarmThresholdPickerSection.swift */; }; + DD0650A92DCA8A10004D3B41 /* AlarmBGSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650A82DCA8A10004D3B41 /* AlarmBGSection.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 */; }; @@ -323,7 +323,7 @@ DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmCondition.swift; sourceTree = ""; }; DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildExpireCondition.swift; sourceTree = ""; }; DD0247612DB2EB9A00FCADF6 /* LoopFollowTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LoopFollowTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - DD0650A82DCA8A10004D3B41 /* AlarmThresholdPickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmThresholdPickerSection.swift; sourceTree = ""; }; + DD0650A82DCA8A10004D3B41 /* AlarmBGSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmBGSection.swift; sourceTree = ""; }; DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUtils.swift; sourceTree = ""; }; DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileManager.swift; sourceTree = ""; }; DD0C0C612C4175FD00DBADDF /* NSProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSProfile.swift; sourceTree = ""; }; @@ -898,7 +898,7 @@ DDC7E53B2DBD8A1600EB1127 /* Components */ = { isa = PBXGroup; children = ( - DD0650A82DCA8A10004D3B41 /* AlarmThresholdPickerSection.swift */, + DD0650A82DCA8A10004D3B41 /* AlarmBGSection.swift */, DDC7E5392DBD8A1600EB1127 /* AlarmEditorFields.swift */, DDC7E53A2DBD8A1600EB1127 /* SoundFile.swift */, ); @@ -1725,7 +1725,7 @@ DD0C0C6B2C48562000DBADDF /* InsulinMetric.swift in Sources */, DD493AD92ACF2171009A6922 /* Carbs.swift in Sources */, DD493AE92ACF2445009A6922 /* BGData.swift in Sources */, - DD0650A92DCA8A10004D3B41 /* AlarmThresholdPickerSection.swift in Sources */, + DD0650A92DCA8A10004D3B41 /* AlarmBGSection.swift in Sources */, FCC6886B24898FD800A0279D /* ObservationToken.swift in Sources */, DD4AFB6B2DB6BF2A00BB593F /* Binding+Optional.swift in Sources */, DD608A082C1F584900F91132 /* DeviceStatusLoop.swift in Sources */, diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index d315cd5da..011a57d6a 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -36,8 +36,8 @@ struct Alarm: Identifiable, Codable, Equatable { /// Also used as bg limit for drop alarms for example var threshold: Double? - /// If the alarm looks at predictions, this is how many predictions to include - var predictiveReadings: Int? + /// If the alarm looks at predictions, this is how long into the future to look + var predictiveMinutes: Int? /// If the alarm acts on delta, the delta is stored here, it can be a delta bgvalue (in mg/Dl) /// If a delta alarm is only active below a bg, that bg is stored in threshold diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmThresholdPickerSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift similarity index 73% rename from LoopFollow/Alarm/AlarmEditing/Components/AlarmThresholdPickerSection.swift rename to LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift index c188f7615..84350bf1f 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmThresholdPickerSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift @@ -1,5 +1,5 @@ // -// AlarmThresholdPickerSection.swift +// AlarmBGSection.swift // LoopFollow // // Created by Jonas Björkert on 2025-05-06. @@ -10,22 +10,35 @@ import SwiftUI import HealthKit struct AlarmBGSection: View { - // ── Public API ────────────────────────────────────────────────────────────── + let header: String? + let footer: String? let title: String let range: ClosedRange @Binding var value: Double - // ── Private state ────────────────────────────────────────────────────────── @State private var showPicker = false - // Preferred unit – mmol/L or mg/dL + init( + header: String? = nil, + footer: String? = nil, + title: String, + range: ClosedRange, + value: Binding + ) { + self.header = header + self.footer = footer + self.title = title + self.range = range + self._value = value + } + private var unit: HKUnit { UserDefaultsRepository.getPreferredUnit() } private var displayValue: String { - Localizer.formatQuantity(value) + let formatted = Localizer.formatQuantity(value) + return "\(formatted) \(unit.localizedShortUnitString)" } - // Generate all selectable display values for the picker private var pickerValues: [Double] { let step : Double = unit == .millimolesPerLiter ? 18.0 * 0.1 : 1.0 @@ -34,8 +47,10 @@ struct AlarmBGSection: View { } var body: some View { - Section { - // Collapsed row + Section( + header: header.map(Text.init), + footer: footer.map(Text.init) + ) { HStack { Text(title) Spacer() @@ -50,7 +65,7 @@ struct AlarmBGSection: View { VStack { Picker("", selection: bindingForPicker) { ForEach(pickerValues, id: \.self) { v in - Text(Localizer.formatQuantity(v)) + Text("\(Localizer.formatQuantity(v)) \(unit.localizedShortUnitString)") .tag(v) } } diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift index 6c91caf02..611e49b45 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift @@ -24,7 +24,7 @@ struct AlarmGeneralSection: View { } } -struct AlarmThresholdSection: View { +struct AlarmStepperSection: View { let title: String let range: ClosedRange let step: Double diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift index 76e18bc60..72b3c7498 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift @@ -15,7 +15,7 @@ struct BuildExpireAlarmEditor: View { Form { AlarmGeneralSection(alarm: $alarm) - AlarmThresholdSection( + AlarmStepperSection( title: "Expires In", range: 1...14, step: 1, diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift index 469ae46f5..717bb290b 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift @@ -17,6 +17,7 @@ struct LowBgAlarmEditor: View { AlarmGeneralSection(alarm: $alarm) AlarmBGSection( + header: "Threshold", title: "BG", range: 40...150, value: Binding( @@ -24,6 +25,33 @@ struct LowBgAlarmEditor: View { set: { alarm.threshold = $0 } ) ) + + AlarmStepperSection( + title: "Predictive", + range: 0...60, + step: 5, + unitLabel: alarm.type.timeUnit.label, + value: Binding( + get: { Double(alarm.predictiveMinutes ?? 0) }, + set: { alarm.predictiveMinutes = Int($0) } + ) + ) + + AlarmStepperSection( + title: "Default Snooze", + range: 5...30, + 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) } From e8b920222423f0d47ebb03d82a1337dd37f95b96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Thu, 8 May 2025 07:09:29 +0200 Subject: [PATCH 021/138] Low bg alarm in progress --- LoopFollow/Alarm/Alarm.swift | 4 ++-- .../AlarmEditing/Components/AlarmEditorFields.swift | 2 +- .../AlarmEditing/Editors/LowBgAlarmEditor.swift | 12 ++++++++++++ LoopFollow/Task/AlarmTask.swift | 4 ++-- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index 011a57d6a..c07c3e635 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -43,8 +43,8 @@ struct Alarm: Identifiable, Codable, Equatable { /// If a delta alarm is only active below a bg, that bg is stored in threshold var delta: Double? - /// Number of consecutive 5‑min readings that must satisfy the alarm criteria - var consecutiveReadings: Int? + /// Number of minutes that must satisfy the alarm criteria + var persistentMinutes: Int? /// Size of window to observe values, for example battery drop of x within this number of minutes, var monitoringWindow: Int? diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift index 611e49b45..28ed031dd 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift @@ -34,7 +34,7 @@ struct AlarmStepperSection: View { var body: some View { Section( header: Text(title), - footer: Text("\(title) in \(Int(range.lowerBound))–\(Int(range.upperBound)) \(unitLabel)") + footer: Text("Set \(title), \(Int(range.lowerBound))–\(Int(range.upperBound)) \(unitLabel)") ) { Stepper( "\(title): \(Int(value)) \(unitLabel)", diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift index 717bb290b..ee24c56d6 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift @@ -26,8 +26,20 @@ struct LowBgAlarmEditor: View { ) ) + AlarmStepperSection( + title: "Persistent", + range: 0...120, + step: 5, + unitLabel: alarm.type.timeUnit.label, + value: Binding( + get: { Double(alarm.persistentMinutes ?? 0) }, + set: { alarm.persistentMinutes = Int($0) } + ) + ) + AlarmStepperSection( title: "Predictive", + //footer: include this manu minutes of prediction range: 0...60, step: 5, unitLabel: alarm.type.timeUnit.label, diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index c87786b25..88dad5257 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -21,10 +21,10 @@ extension MainViewController { DispatchQueue.main.async { let alarmData = AlarmData( bgReadings: self.bgData - .suffix(5) + .suffix(24) .map { GlucoseValue(sgv: $0.sgv, date: Date(timeIntervalSince1970: $0.date)) }, /// These are oldest .. newest predictionData: self.predictionData - .prefix(5) + .prefix(12) .map { GlucoseValue(sgv: $0.sgv, date: Date(timeIntervalSince1970: $0.date)) }, /// These are oldest .. newest, Predictions not currently available for Trio expireDate: Storage.shared.expirationDate.value ) From 0b0bfec5bf8cc745af21138c1a04b3f562550b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 9 May 2025 20:28:04 +0200 Subject: [PATCH 022/138] bg alarm work --- LoopFollow/Alarm/AlarmManager.swift | 26 ++++++++++++++++++++++++++ LoopFollow/Task/AlarmTask.swift | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index 919e6be5c..34fe941b5 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -13,6 +13,7 @@ class AlarmManager { static let shared = AlarmManager() private let evaluators: [AlarmType: AlarmCondition] + private var lastBGAlarmTime : Date? private init( conditionTypes: [AlarmCondition.Type] = [ @@ -45,12 +46,31 @@ class AlarmManager { } var skipType: AlarmType? = nil + let isLatestReadingRecent: Bool = { + guard let last = data.bgReadings.last else { return false } + return now.timeIntervalSince(last.date) <= 5 * 60 + }() + for alarm in sorted { // If there is already an active (snoozed) alarm of this type, skip to next [type] if alarm.type == skipType { continue } + // If the alarm is based on bg values, and the value isnt recent, skip to next + if alarm.type.isBGBased && !isLatestReadingRecent { + continue + } + + // If this is a bg-based alarm and we've already handled that same BG reading, + // skip until we see a newer one. + if alarm.type.isBGBased, + let lastHandled = lastBGAlarmTime, + let latestDate = data.bgReadings.last?.date, + !(latestDate > lastHandled) { + continue + } + // If the alarm itself is snoozed skip it, and skip lower‑priority alarms of the same type. // We still want other types af alarm to go off, so we continue here without breaking if let until = alarm.snoozedUntil, until > now { @@ -87,6 +107,12 @@ class AlarmManager { Observable.shared.currentAlarm.value = alarm.id alarm.trigger(config: Storage.shared.alarmConfiguration.value, now: now) + + // Store the latest bg time so we don't use it again + if alarm.type.isBGBased, + let latestDate = data.bgReadings.last?.date { + lastBGAlarmTime = latestDate + } break } } diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index 88dad5257..d7f4fc843 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -9,7 +9,7 @@ import Foundation extension MainViewController { - func scheduleAlarmTask(initialDelay: TimeInterval = 1) { + func scheduleAlarmTask(initialDelay: TimeInterval = 30) { let firstRun = Date().addingTimeInterval(initialDelay) TaskScheduler.shared.scheduleTask(id: .alarmCheck, nextRun: firstRun) { [weak self] in guard let self = self else { return } From 8a8e5f6c51076f606a6c54a4228f822ef42dd88e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 9 May 2025 21:36:55 +0200 Subject: [PATCH 023/138] LowBGCondition.swift --- LoopFollow.xcodeproj/project.pbxproj | 4 ++ .../Alarm/AlarmCondition/LowBGCondition.swift | 72 +++++++++++++++++++ LoopFollow/Alarm/AlarmManager.swift | 3 +- 3 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index db6174ef6..337dd818c 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ DD0247592DB2E89600FCADF6 /* AlarmCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */; }; DD0247712DB4337700FCADF6 /* BuildExpireCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */; }; DD0650A92DCA8A10004D3B41 /* AlarmBGSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650A82DCA8A10004D3B41 /* AlarmBGSection.swift */; }; + DD0650EB2DCE8385004D3B41 /* LowBGCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650EA2DCE8385004D3B41 /* LowBGCondition.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 */; }; @@ -324,6 +325,7 @@ DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildExpireCondition.swift; sourceTree = ""; }; DD0247612DB2EB9A00FCADF6 /* LoopFollowTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LoopFollowTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DD0650A82DCA8A10004D3B41 /* AlarmBGSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmBGSection.swift; sourceTree = ""; }; + DD0650EA2DCE8385004D3B41 /* LowBGCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LowBGCondition.swift; sourceTree = ""; }; DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUtils.swift; sourceTree = ""; }; DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileManager.swift; sourceTree = ""; }; DD0C0C612C4175FD00DBADDF /* NSProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSProfile.swift; sourceTree = ""; }; @@ -668,6 +670,7 @@ DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = { isa = PBXGroup; children = ( + DD0650EA2DCE8385004D3B41 /* LowBGCondition.swift */, DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */, DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */, ); @@ -1722,6 +1725,7 @@ DD4878082C7B30BF0048F05C /* RemoteSettingsView.swift in Sources */, DD4AFB3B2DB55CB600BB593F /* TimeOfDay.swift in Sources */, DDEF503C2D31BE2D00999A5D /* TaskScheduler.swift in Sources */, + DD0650EB2DCE8385004D3B41 /* LowBGCondition.swift in Sources */, DD0C0C6B2C48562000DBADDF /* InsulinMetric.swift in Sources */, DD493AD92ACF2171009A6922 /* Carbs.swift in Sources */, DD493AE92ACF2445009A6922 /* BGData.swift in Sources */, diff --git a/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift b/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift new file mode 100644 index 000000000..1d2c54790 --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift @@ -0,0 +1,72 @@ +// +// LowBGCondition.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-09. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import Foundation + +/// Fires when: +/// • every BG in `persistentMinutes` (if set) **and** the latest BG are ≤ `threshold`; **or** +/// • any predicted BG within `predictiveMinutes` is ≤ `threshold`. +struct LowBGCondition: AlarmCondition { + static let type: AlarmType = .low + init() {} + + func evaluate(alarm: Alarm, data: AlarmData) -> Bool { + // ──────────────────────────────── + // 0. sanity checks + // ──────────────────────────────── + guard let threshold = alarm.threshold else { return false } + guard let latest = data.bgReadings.last, latest.sgv > 0 else { return false } + + func isLow(_ g: GlucoseValue) -> Bool { + g.sgv > 0 && Double(g.sgv) <= threshold + } + + // ──────────────────────────────── + // 1. predictive low? + // ──────────────────────────────── + var predictiveTrigger = false + if let predictiveMinutes = alarm.predictiveMinutes, + predictiveMinutes > 0, + !data.predictionData.isEmpty { + + let lookAhead = min( + data.predictionData.count, + Int(ceil(Double(predictiveMinutes) / 5.0)) + ) + + for i in 0.. 0 { + + let window = Int(ceil(Double(persistentMinutes) / 5.0)) + + if data.bgReadings.count >= window { + let recent = data.bgReadings.suffix(window) + persistentOK = recent.allSatisfy(isLow) + } else { + // not enough samples to prove persistence ⇒ don’t alarm yet + persistentOK = false + } + } + + // ──────────────────────────────── + // 3. final decision + // ──────────────────────────────── + let currentLow = isLow(latest) + return (currentLow && persistentOK) || predictiveTrigger + } +} diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index 34fe941b5..875bbe1ec 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -17,7 +17,8 @@ class AlarmManager { private init( conditionTypes: [AlarmCondition.Type] = [ - BuildExpireCondition.self + BuildExpireCondition.self, + LowBGCondition.self // TODO: add other condition types here ] ) { From 1fa4c830e7de65ed737132b295b11f4185b2639c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 9 May 2025 22:08:58 +0200 Subject: [PATCH 024/138] High bg alarm --- LoopFollow.xcodeproj/project.pbxproj | 12 ++-- .../AlarmCondition/HighBGCondition.swift | 54 ++++++++++++++++++ .../Editors/HighBgAlarmEditor.swift | 56 +++++++++++++------ LoopFollow/Alarm/AlarmManager.swift | 3 +- 4 files changed, 103 insertions(+), 22 deletions(-) create mode 100644 LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 337dd818c..9a9a6bdd4 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -12,6 +12,8 @@ DD0247712DB4337700FCADF6 /* BuildExpireCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */; }; DD0650A92DCA8A10004D3B41 /* AlarmBGSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650A82DCA8A10004D3B41 /* AlarmBGSection.swift */; }; 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 */; }; 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 */; }; @@ -116,7 +118,6 @@ DDC7E5422DBD8A1600EB1127 /* AlarmEditorFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5392DBD8A1600EB1127 /* AlarmEditorFields.swift */; }; DDC7E5432DBD8A1600EB1127 /* SoundFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E53A2DBD8A1600EB1127 /* SoundFile.swift */; }; DDC7E5442DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E53C2DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift */; }; - DDC7E5452DBD8A1600EB1127 /* HighBgAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E53D2DBD8A1600EB1127 /* HighBgAlarmEditor.swift */; }; DDC7E5462DBD8A1600EB1127 /* LowBgAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E53E2DBD8A1600EB1127 /* LowBgAlarmEditor.swift */; }; DDC7E5472DBD8A1600EB1127 /* AlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5402DBD8A1600EB1127 /* AlarmEditor.swift */; }; DDC7E5CF2DC77C2000EB1127 /* SnoozerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5CE2DC77C2000EB1127 /* SnoozerViewModel.swift */; }; @@ -326,6 +327,8 @@ DD0247612DB2EB9A00FCADF6 /* LoopFollowTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LoopFollowTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DD0650A82DCA8A10004D3B41 /* AlarmBGSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmBGSection.swift; sourceTree = ""; }; DD0650EA2DCE8385004D3B41 /* LowBGCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LowBGCondition.swift; sourceTree = ""; }; + DD0650EC2DCE9371004D3B41 /* HighBgAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighBgAlarmEditor.swift; sourceTree = ""; }; + DD0650EE2DCE96FF004D3B41 /* HighBGCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighBGCondition.swift; sourceTree = ""; }; DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUtils.swift; sourceTree = ""; }; DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileManager.swift; sourceTree = ""; }; DD0C0C612C4175FD00DBADDF /* NSProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSProfile.swift; sourceTree = ""; }; @@ -430,7 +433,6 @@ DDC7E5392DBD8A1600EB1127 /* AlarmEditorFields.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmEditorFields.swift; sourceTree = ""; }; DDC7E53A2DBD8A1600EB1127 /* SoundFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundFile.swift; sourceTree = ""; }; DDC7E53C2DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildExpireAlarmEditor.swift; sourceTree = ""; }; - DDC7E53D2DBD8A1600EB1127 /* HighBgAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighBgAlarmEditor.swift; sourceTree = ""; }; DDC7E53E2DBD8A1600EB1127 /* LowBgAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LowBgAlarmEditor.swift; sourceTree = ""; }; DDC7E5402DBD8A1600EB1127 /* AlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmEditor.swift; sourceTree = ""; }; DDC7E5CE2DC77C2000EB1127 /* SnoozerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozerViewModel.swift; sourceTree = ""; }; @@ -673,6 +675,7 @@ DD0650EA2DCE8385004D3B41 /* LowBGCondition.swift */, DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */, DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */, + DD0650EE2DCE96FF004D3B41 /* HighBGCondition.swift */, ); path = AlarmCondition; sourceTree = ""; @@ -911,8 +914,8 @@ DDC7E53F2DBD8A1600EB1127 /* Editors */ = { isa = PBXGroup; children = ( + DD0650EC2DCE9371004D3B41 /* HighBgAlarmEditor.swift */, DDC7E53C2DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift */, - DDC7E53D2DBD8A1600EB1127 /* HighBgAlarmEditor.swift */, DDC7E53E2DBD8A1600EB1127 /* LowBgAlarmEditor.swift */, ); path = Editors; @@ -1633,6 +1636,7 @@ FC3AE7B5249E8E0E00AAE1E0 /* LoopFollow.xcdatamodeld in Sources */, DDC7E5152DBCFA7900EB1127 /* SnoozerViewController.swift in Sources */, DDDF6F492D479AF000884336 /* NoRemoteView.swift in Sources */, + DD0650ED2DCE9371004D3B41 /* HighBgAlarmEditor.swift in Sources */, FCC6886F2489A53800A0279D /* AppConstants.swift in Sources */, DD7E19842ACDA50C00DBD158 /* Overrides.swift in Sources */, DDF2C0102BEFA991007A20E6 /* GitHubService.swift in Sources */, @@ -1679,7 +1683,6 @@ DDC7E5422DBD8A1600EB1127 /* AlarmEditorFields.swift in Sources */, DDC7E5432DBD8A1600EB1127 /* SoundFile.swift in Sources */, DDC7E5442DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift in Sources */, - DDC7E5452DBD8A1600EB1127 /* HighBgAlarmEditor.swift in Sources */, DDC7E5462DBD8A1600EB1127 /* LowBgAlarmEditor.swift in Sources */, DDC7E5472DBD8A1600EB1127 /* AlarmEditor.swift in Sources */, DDA9ACA82D6A66E200E6F1A9 /* ContactColorOption.swift in Sources */, @@ -1741,6 +1744,7 @@ DD4AFB3D2DB55D2900BB593F /* AlarmConfiguration.swift in Sources */, DDF699962C5582290058A8D9 /* TextFieldWithToolBar.swift in Sources */, DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */, + DD0650EF2DCE96FF004D3B41 /* HighBGCondition.swift in Sources */, DDDF6F452D479AB100884336 /* LoopOverrideView.swift in Sources */, DD4878132C7B750D0048F05C /* TempTargetView.swift in Sources */, DD0C0C682C48529400DBADDF /* Metric.swift in Sources */, diff --git a/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift b/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift new file mode 100644 index 000000000..cc4a7bf8a --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift @@ -0,0 +1,54 @@ +// +// HighBGCondition.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-10. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import Foundation + +/// Fires when **every** BG in `persistentMinutes` (if set) **and** the latest BG +/// are ≥ `threshold`. +/// — No predictive branch for highs. +struct HighBGCondition: AlarmCondition { + static let type: AlarmType = .high + init() {} + + func evaluate(alarm: Alarm, data: AlarmData) -> Bool { + + // ──────────────────────────────── + // 0. sanity checks + // ──────────────────────────────── + guard let threshold = alarm.threshold else { return false } + guard let latest = data.bgReadings.last, latest.sgv > 0 else { return false } + + func isHigh(_ g: GlucoseValue) -> Bool { + g.sgv > 0 && Double(g.sgv) >= threshold + } + + // ──────────────────────────────── + // 1. persistent-window guard + // ──────────────────────────────── + var persistentOK = true + if let persistentMinutes = alarm.persistentMinutes, + persistentMinutes > 0 { + + let window = Int(ceil(Double(persistentMinutes) / 5.0)) + + if data.bgReadings.count >= window { + let recent = data.bgReadings.suffix(window) + persistentOK = recent.allSatisfy(isHigh) + } else { + // not enough samples yet ⇒ don’t alarm + persistentOK = false + } + } + + // ──────────────────────────────── + // 2. final decision + // ──────────────────────────────── + let currentHigh = isHigh(latest) + return currentHigh && persistentOK + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift index a22a16486..46544b089 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift @@ -2,34 +2,56 @@ // HighBgAlarmEditor.swift // LoopFollow // -// Created by Jonas Björkert on 2025-04-21. +// Created by Jonas Björkert on 2025-05-09. // Copyright © 2025 Jon Fawcett. All rights reserved. // - import SwiftUI struct HighBgAlarmEditor: View { @Binding var alarm: Alarm var body: some View { - Form {/* - AlarmNameField(alarm: $alarm) - EnabledToggle(alarm: $alarm) - ValueStepper( - title: "BG Above", + Form { + AlarmGeneralSection(alarm: $alarm) + + AlarmBGSection( + header: "Threshold", + title: "BG", + range: 120...350, + value: Binding( + get: { alarm.threshold ?? 120 }, + set: { alarm.threshold = $0 } + ) + ) + + AlarmStepperSection( + title: "Persistent", + range: 0...120, + step: 5, + unitLabel: alarm.type.timeUnit.label, value: Binding( - get: { Double(alarm.threshold ?? 0) }, - set: { alarm.threshold = Float($0) } - ), - range: 0...500, step: 1, - formatter: { "\(Int($0))" } + get: { Double(alarm.persistentMinutes ?? 0) }, + set: { alarm.persistentMinutes = Int($0) } + ) ) - DayNightToggle(alarm: $alarm) - SoundPicker(alarm: $alarm) - SnoozeDatePicker(alarm: $alarm) - SnoozeDurationStepper(alarm: $alarm)*/ + + AlarmStepperSection( + title: "Default Snooze", + range: 10...120, + 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("High BG Alert") + .navigationTitle(alarm.type.rawValue) } } diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index 875bbe1ec..d2e97c3dd 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -18,7 +18,8 @@ class AlarmManager { private init( conditionTypes: [AlarmCondition.Type] = [ BuildExpireCondition.self, - LowBGCondition.self + LowBGCondition.self, + HighBGCondition.self // TODO: add other condition types here ] ) { From 03fda1915cf2a01b19f68afce264879017bcf0b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 10 May 2025 21:55:40 +0200 Subject: [PATCH 025/138] Alarm editing gui improvements --- LoopFollow.xcodeproj/project.pbxproj | 16 ++++++ .../MissedReadingCondition.swift | 29 ++++++++++ .../Alarm/AlarmEditing/AlarmEditor.swift | 5 ++ .../Components/AlarmEditorFields.swift | 22 -------- .../Components/AlarmStepperSection.swift | 56 +++++++++++++++++++ .../AlarmEditing/Components/InfoBanner.swift | 47 ++++++++++++++++ .../Editors/BuildExpireAlarmEditor.swift | 49 ++++++++-------- .../Editors/HighBgAlarmEditor.swift | 14 ++++- .../Editors/LowBgAlarmEditor.swift | 11 +++- .../Editors/MissedReadingEditor.swift | 50 +++++++++++++++++ LoopFollow/Alarm/AlarmListView.swift | 2 + 11 files changed, 252 insertions(+), 49 deletions(-) create mode 100644 LoopFollow/Alarm/AlarmCondition/MissedReadingCondition.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 9a9a6bdd4..9983e1f46 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/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 = ""; }; DD0650EC2DCE9371004D3B41 /* HighBgAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighBgAlarmEditor.swift; sourceTree = ""; }; DD0650EE2DCE96FF004D3B41 /* HighBGCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighBGCondition.swift; sourceTree = ""; }; + DD0650F02DCE9A9E004D3B41 /* MissedReadingCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissedReadingCondition.swift; sourceTree = ""; }; + DD0650F22DCE9B3D004D3B41 /* MissedReadingEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissedReadingEditor.swift; sourceTree = ""; }; + DD0650F42DCF303F004D3B41 /* AlarmStepperSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmStepperSection.swift; sourceTree = ""; }; + DD0650F62DCFDA26004D3B41 /* InfoBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoBanner.swift; sourceTree = ""; }; DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUtils.swift; sourceTree = ""; }; DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileManager.swift; sourceTree = ""; }; DD0C0C612C4175FD00DBADDF /* NSProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSProfile.swift; sourceTree = ""; }; @@ -676,6 +684,7 @@ DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */, DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */, DD0650EE2DCE96FF004D3B41 /* HighBGCondition.swift */, + DD0650F02DCE9A9E004D3B41 /* MissedReadingCondition.swift */, ); path = AlarmCondition; sourceTree = ""; @@ -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 = ""; @@ -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 */, diff --git a/LoopFollow/Alarm/AlarmCondition/MissedReadingCondition.swift b/LoopFollow/Alarm/AlarmCondition/MissedReadingCondition.swift new file mode 100644 index 000000000..e415f4a08 --- /dev/null +++ b/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 + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index d0d84fcee..c25a2ab0d 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/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() diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift index 28ed031dd..d535e096a 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift @@ -24,28 +24,6 @@ struct AlarmGeneralSection: View { } } -struct AlarmStepperSection: View { - let title: String - let range: ClosedRange - 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 diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift new file mode 100644 index 000000000..fda3fd23d --- /dev/null +++ b/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 + let step: Double + let unitLabel: String + @Binding var value: Double + + init( + header: String? = nil, + footer: String? = nil, + title: String, + range: ClosedRange, + step: Double, + unitLabel: String, + value: Binding + ) { + 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 + ) + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift b/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift new file mode 100644 index 000000000..63db3284e --- /dev/null +++ b/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) + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift index 72b3c7498..49da21f88 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift +++ b/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) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift index 46544b089..ff66795d0 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift +++ b/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) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift index ee24c56d6..b65bb923d 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift +++ b/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) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift new file mode 100644 index 000000000..716c0609a --- /dev/null +++ b/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) + } +} diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index 5d930e9ab..a0a676c82 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/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"), From ebd2d68a8874282919eeeff1997b4c65c086cc04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sun, 11 May 2025 21:45:08 +0200 Subject: [PATCH 026/138] Changed picker style --- LoopFollow.xcodeproj/project.pbxproj | 4 + .../Alarm/AlarmEditing/AlarmEditor.swift | 2 + .../Components/AlarmBGSection.swift | 81 ++++------------ .../Components/AlarmEditorFields.swift | 8 +- .../Components/AlarmStepperSection.swift | 11 +-- .../Editors/FastDropAlarmEditor.swift | 92 +++++++++++++++++++ .../Editors/LowBgAlarmEditor.swift | 1 + 7 files changed, 122 insertions(+), 77 deletions(-) create mode 100644 LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 9983e1f46..d32ea9d19 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 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 */; }; + DD0650F92DCFE7BE004D3B41 /* FastDropAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650F82DCFE7BE004D3B41 /* FastDropAlarmEditor.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 */; }; @@ -337,6 +338,7 @@ DD0650F22DCE9B3D004D3B41 /* MissedReadingEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissedReadingEditor.swift; sourceTree = ""; }; DD0650F42DCF303F004D3B41 /* AlarmStepperSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmStepperSection.swift; sourceTree = ""; }; DD0650F62DCFDA26004D3B41 /* InfoBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoBanner.swift; sourceTree = ""; }; + DD0650F82DCFE7BE004D3B41 /* FastDropAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastDropAlarmEditor.swift; sourceTree = ""; }; DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUtils.swift; sourceTree = ""; }; DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileManager.swift; sourceTree = ""; }; DD0C0C612C4175FD00DBADDF /* NSProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSProfile.swift; sourceTree = ""; }; @@ -929,6 +931,7 @@ DDC7E53C2DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift */, DDC7E53E2DBD8A1600EB1127 /* LowBgAlarmEditor.swift */, DD0650F22DCE9B3D004D3B41 /* MissedReadingEditor.swift */, + DD0650F82DCFE7BE004D3B41 /* FastDropAlarmEditor.swift */, ); path = Editors; sourceTree = ""; @@ -1665,6 +1668,7 @@ FC16A97D24996747003D6245 /* SpeakBG.swift in Sources */, DDFD5C532CB167DA00D3FD68 /* TRCCommandType.swift in Sources */, DD16AF112C997B4600FB655A /* LoadingButtonView.swift in Sources */, + DD0650F92DCFE7BE004D3B41 /* FastDropAlarmEditor.swift in Sources */, DD5817172D2710E90041FB98 /* BLEDeviceSelectionView.swift in Sources */, FC16A97B249966A3003D6245 /* AlarmSound.swift in Sources */, DDBE3ABD2CB5A961006B37DC /* OverrideView.swift in Sources */, diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index c25a2ab0d..b20cdb030 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -22,6 +22,8 @@ struct AlarmEditor: View { LowBgAlarmEditor(alarm: $alarm) case .missedReading: MissedReadingEditor(alarm: $alarm) + case .fastDrop: + FastDropAlarmEditor(alarm: $alarm) // TODO: add other condition types here diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift index 84350bf1f..cc8ce6427 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift @@ -10,40 +10,21 @@ import SwiftUI import HealthKit struct AlarmBGSection: View { - let header: String? - let footer: String? - let title: String - let range: ClosedRange + let header: String? + let footer: String? + let title: String + let range: ClosedRange @Binding var value: Double - @State private var showPicker = false - - init( - header: String? = nil, - footer: String? = nil, - title: String, - range: ClosedRange, - value: Binding - ) { - self.header = header - self.footer = footer - self.title = title - self.range = range - self._value = value - } - private var unit: HKUnit { UserDefaultsRepository.getPreferredUnit() } private var displayValue: String { - let formatted = Localizer.formatQuantity(value) - return "\(formatted) \(unit.localizedShortUnitString)" + "\(Localizer.formatQuantity(value)) \(unit.localizedShortUnitString)" } - private var pickerValues: [Double] { - let step : Double = unit == .millimolesPerLiter ? 18.0 * 0.1 : 1.0 - - return stride(from: range.lowerBound, through: range.upperBound, by: step) - .map { $0 } + private var allValues: [Double] { + let step = unit == .millimolesPerLiter ? 18.0 * 0.1 : 1 + return Array(stride(from: range.lowerBound, through: range.upperBound, by: step)) } var body: some View { @@ -51,47 +32,17 @@ struct AlarmBGSection: View { header: header.map(Text.init), footer: footer.map(Text.init) ) { - HStack { - Text(title) - Spacer() - Text(displayValue) - .foregroundColor(.secondary) - } - .contentShape(Rectangle()) - .onTapGesture { showPicker = true } - } - .sheet(isPresented: $showPicker) { - NavigationStack { - VStack { - Picker("", selection: bindingForPicker) { - ForEach(pickerValues, id: \.self) { v in - Text("\(Localizer.formatQuantity(v)) \(unit.localizedShortUnitString)") - .tag(v) - } - } - .pickerStyle(.wheel) + Picker( + selection: $value, + label: HStack { + Text(title) } - .navigationTitle(title) - .toolbar { - ToolbarItem(placement: .confirmationAction) { - Button("Done") { showPicker = false } - } + ) { + ForEach(allValues, id: \.self) { v in + Text("\(Localizer.formatQuantity(v)) \(unit.localizedShortUnitString)") + .tag(v) } } - .presentationDetents([.fraction(0.35), .medium]) - .presentationDragIndicator(.visible) } } - - private var bindingForPicker: Binding { - Binding( - get: { - // pick the closest representable value - pickerValues.min(by: { abs($0 - value) < abs($1 - value) }) ?? value - }, - set: { newVal in - value = newVal // stored in mg/dL - } - ) - } } diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift index d535e096a..6467fc76e 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift @@ -102,17 +102,17 @@ struct AlarmAudioSection: View { } // ——— Play / Repeat Toggles ——— - VStack(alignment: .leading, spacing: 8) { + HStack() { Text("Play") .font(.subheadline) .foregroundColor(.secondary) - + Spacer() Picker("", selection: $alarm.playSoundOption) { ForEach(PlaySoundOption.allCases, id: \.self) { opt in Text(opt.rawValue.capitalized).tag(opt) } } - .pickerStyle(.segmented) + .pickerStyle(.menu) } VStack(alignment: .leading, spacing: 8) { @@ -125,7 +125,7 @@ struct AlarmAudioSection: View { Text(opt.rawValue.capitalized).tag(opt) } } - .pickerStyle(.segmented) + .pickerStyle(.menu) } } } diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift index fda3fd23d..0db9fde9d 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift @@ -14,7 +14,7 @@ struct AlarmStepperSection: View { let title: String let range: ClosedRange let step: Double - let unitLabel: String + let unitLabel: String? @Binding var value: Double init( @@ -23,7 +23,7 @@ struct AlarmStepperSection: View { title: String, range: ClosedRange, step: Double, - unitLabel: String, + unitLabel: String? = nil, value: Binding ) { self.header = header @@ -35,18 +35,13 @@ struct AlarmStepperSection: View { 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)", + "\(title): \(Int(value))\(unitLabel.map { " \($0)" } ?? "")", value: $value, in: range, step: step diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift new file mode 100644 index 000000000..73d30d8db --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift @@ -0,0 +1,92 @@ +// +// FastDropAlarmEditor.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-10. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import SwiftUI + +struct FastDropAlarmEditor: View { + @Binding var alarm: Alarm + + @State private var useLimit: Bool = false + + var body: some View { + Form { + InfoBanner( + text: "Alerts when glucose readings drop rapidly. For example, three straight readings each falling by at least the amount you set. Optionally limit alerts to only fire below a certain BG level." + ) + AlarmGeneralSection(alarm: $alarm) + + AlarmBGSection( + header: "Rate of Fall", + footer: "How much the bg must fall to count as a “fast” drop.", + title: "Drop per reading", + range: 3...20, + value: Binding( + get: { alarm.threshold ?? 4 }, + set: { alarm.threshold = $0 } + ) + ) + + //TODO: In the migration script, use 1 value less than stored since we are switching from readings to drops + AlarmStepperSection( + header: "Consecutive Drops", + footer: "Number of back-to-back drops—each meeting the rate above—required before an alert fires.", + title: "Drops in a row", + range: 1...3, + step: 1, + value: Binding( + get: { Double(alarm.monitoringWindow ?? 2) }, + set: { alarm.monitoringWindow = Int($0) } + ) + ) +/* + // ────────── BG LIMIT ─────────── + Section { + Toggle("Only alert when below BG limit", isOn: $useLimit) + .onAppear { + useLimit = (alarm.threshold != nil) + } + .onChange(of: useLimit) { newValue in + if !newValue { alarm.threshold = nil } + } + + AlarmBGSection( + header: nil, + footer: "Ignored unless the toggle above is enabled.", + title: "Dropping below", + range: 40...300, + value: Binding( + get: { alarm.threshold ?? 70 }, + set: { alarm.threshold = $0 } + ) + ) + .disabled(!useLimit) + .opacity(useLimit ? 1 : 0.35) + } // Section +*/ + // ────────── SNOOZE ──────────── + AlarmStepperSection( + header: "Default Snooze", + footer: "How long to silence this alert after you press Snooze.", + title: "Default Snooze", + range: 5...60, + step: 5, + unitLabel: alarm.type.timeUnit.label, + value: Binding( + get: { Double(alarm.snoozeDuration) }, + set: { alarm.snoozeDuration = Int($0) } + ) + ) + + // ────── SOUND / ACTIVE / UNTIL ────── + AlarmAudioSection(alarm: $alarm) + AlarmActiveSection(alarm: $alarm) + AlarmSnoozedUntilSection(alarm: $alarm) + } + .navigationTitle(alarm.type.rawValue) + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift index b65bb923d..c29d6b982 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift @@ -22,6 +22,7 @@ struct LowBgAlarmEditor: View { AlarmBGSection( header: "Threshold", + footer: "bla bla", title: "BG", range: 40...150, value: Binding( From 9738af95e005403db06c986d45106306db48f093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 12 May 2025 12:44:57 +0200 Subject: [PATCH 027/138] PickerStyle --- LoopFollow.xcodeproj/project.pbxproj | 8 ++ LoopFollow/Alarm/Alarm.swift | 24 +++-- .../Components/AlarmActiveSection.swift | 20 ++++ .../Components/AlarmAudioSection.swift | 94 ++++++++++++++++ .../Components/AlarmBGSection.swift | 14 +++ .../Components/AlarmEditorFields.swift | 100 ------------------ .../Editors/LowBgAlarmEditor.swift | 1 - 7 files changed, 154 insertions(+), 107 deletions(-) create mode 100644 LoopFollow/Alarm/AlarmEditing/Components/AlarmActiveSection.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index d32ea9d19..16eef9e6b 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -95,6 +95,8 @@ DD7E19862ACDA59700DBD158 /* BGCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E19852ACDA59700DBD158 /* BGCheck.swift */; }; DD7E19882ACDA5DA00DBD158 /* Notes.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E19872ACDA5DA00DBD158 /* Notes.swift */; }; DD7E198A2ACDA62600DBD158 /* SensorStart.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E19892ACDA62600DBD158 /* SensorStart.swift */; }; + DD7F4B9D2DD1EAE500D449E9 /* AlarmAudioSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4B9C2DD1EAE500D449E9 /* AlarmAudioSection.swift */; }; + DD7F4B9F2DD1F92700D449E9 /* AlarmActiveSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4B9E2DD1F92700D449E9 /* AlarmActiveSection.swift */; }; DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */; }; DD85E9952D739CFE001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */; }; DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */; }; @@ -414,6 +416,8 @@ DD7E19852ACDA59700DBD158 /* BGCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGCheck.swift; sourceTree = ""; }; DD7E19872ACDA5DA00DBD158 /* Notes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notes.swift; sourceTree = ""; }; DD7E19892ACDA62600DBD158 /* SensorStart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorStart.swift; sourceTree = ""; }; + DD7F4B9C2DD1EAE500D449E9 /* AlarmAudioSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmAudioSection.swift; sourceTree = ""; }; + DD7F4B9E2DD1F92700D449E9 /* AlarmActiveSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmActiveSection.swift; sourceTree = ""; }; DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EKEventStore+Extensions.swift"; sourceTree = ""; }; DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnipodDashHeartbeatBluetoothTransmitter.swift; sourceTree = ""; }; DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseConversion.swift; sourceTree = ""; }; @@ -915,6 +919,8 @@ DDC7E53B2DBD8A1600EB1127 /* Components */ = { isa = PBXGroup; children = ( + DD7F4B9E2DD1F92700D449E9 /* AlarmActiveSection.swift */, + DD7F4B9C2DD1EAE500D449E9 /* AlarmAudioSection.swift */, DD0650F62DCFDA26004D3B41 /* InfoBanner.swift */, DD0650F42DCF303F004D3B41 /* AlarmStepperSection.swift */, DD0650A82DCA8A10004D3B41 /* AlarmBGSection.swift */, @@ -1630,6 +1636,7 @@ DD5334292C6166A500062F9D /* InfoDisplaySettingsView.swift in Sources */, DD48781E2C7DAF2F0048F05C /* PushNotificationManager.swift in Sources */, DD2C2E562D3C3917006413A5 /* DexcomSettingsView.swift in Sources */, + DD7F4B9F2DD1F92700D449E9 /* AlarmActiveSection.swift in Sources */, DD4AFB672DB68C5500BB593F /* UUID+Identifiable.swift in Sources */, DD9ED0CA2D355257000D2A63 /* LogView.swift in Sources */, DD9ACA082D32F68B00415D8A /* BGTask.swift in Sources */, @@ -1645,6 +1652,7 @@ DDCF979E24C2382A002C9752 /* AppStateController.swift in Sources */, DDEF503F2D32754F00999A5D /* ProfileTask.swift in Sources */, DD5334212C60EBEE00062F9D /* InsulinCartridgeChange.swift in Sources */, + DD7F4B9D2DD1EAE500D449E9 /* AlarmAudioSection.swift in Sources */, FC97881E2485969B00A7906C /* NightScoutViewController.swift in Sources */, DD608A0A2C23593900F91132 /* SMB.swift in Sources */, DDCF979824C1489C002C9752 /* GraphSettingsViewController.swift in Sources */, diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index c07c3e635..afeadad5e 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -10,14 +10,26 @@ import Foundation import HealthKit import UserNotifications -enum PlaySoundOption: String, CaseIterable, Codable { - case always, day, night, never +protocol DayNightDisplayable { + var displayName: String { get } } -enum RepeatSoundOption: String, CaseIterable, Codable { - case always, day, night, never + +extension DayNightDisplayable where Self: RawRepresentable, Self.RawValue == String { + var displayName: String { + rawValue == "always" ? "Day & Night" : rawValue.capitalized + } +} + +enum PlaySoundOption: String, CaseIterable, Codable, DayNightDisplayable { + case always, day, night, never } -enum ActiveOption: String, CaseIterable, Codable { - case always, day, night + +enum RepeatSoundOption: String, CaseIterable, Codable, DayNightDisplayable { + case always, day, night, never +} + +enum ActiveOption: String, CaseIterable, Codable, DayNightDisplayable { + case always, day, night } struct Alarm: Identifiable, Codable, Equatable { diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmActiveSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmActiveSection.swift new file mode 100644 index 000000000..9ae0f6bc6 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmActiveSection.swift @@ -0,0 +1,20 @@ +// +// AlarmActiveSection.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-12. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import SwiftUI + +struct AlarmActiveSection: View { + @Binding var alarm: Alarm + + var body: some View { + Section(header: Text("Active During")) { + AlarmEnumMenuPicker(title: "Active", + selection: $alarm.activeOption) + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift new file mode 100644 index 000000000..559e91510 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift @@ -0,0 +1,94 @@ +// +// AlarmAudioSection.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-12. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + + +import SwiftUI + +struct AlarmAudioSection: View { + @Binding var alarm: Alarm + @State private var showingTonePicker = false + + var body: some View { + Section(header: Text("Alert Sound")) { + Button { + showingTonePicker = true + } label: { + HStack { + Text("Tone") + Spacer() + Text(alarm.soundFile.displayName) + .foregroundColor(.secondary) + Image(systemName: "chevron.right") + .foregroundColor(.secondary) + } + } + .buttonStyle(.plain) + .sheet(isPresented: $showingTonePicker) { + TonePickerSheet(selected: $alarm.soundFile) + } + + AlarmEnumMenuPicker(title: "Play", selection: $alarm.playSoundOption) + AlarmEnumMenuPicker(title: "Repeat", selection: $alarm.repeatSoundOption) + } + } +} + +struct AlarmEnumMenuPicker: View { + let title: String + @Binding var selection: E + + var body: some View { + HStack { + Text(title) + Spacer() + Picker("", selection: $selection) { + ForEach(Array(E.allCases), id: \.self) { option in + Text(option.displayName).tag(option) + } + } + } + } +} + +private struct TonePickerSheet: View { + @Binding var selected: SoundFile + @Environment(\.dismiss) private var dismiss + + var body: some View { + NavigationView { + List { + ForEach(SoundFile.allCases) { tone in + Button { + selected = tone + AlarmSound.setSoundFile(str: tone.rawValue) + AlarmSound.stop() + AlarmSound.playTest() + } label: { + HStack { + Text(tone.displayName) + if tone == selected { + Spacer() + Image(systemName: "checkmark") + .foregroundColor(.accentColor) + } + } + } + } + } + .navigationTitle("Choose Tone") + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Done") { + AlarmSound.stop() + dismiss() + } + } + } + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift index cc8ce6427..d8b427c56 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift @@ -16,6 +16,20 @@ struct AlarmBGSection: View { let range: ClosedRange @Binding var value: Double + init( + header: String? = nil, + footer: String? = nil, + title: String, + range: ClosedRange, + value: Binding + ) { + self.header = header + self.footer = footer + self.title = title + self.range = range + self._value = value + } + private var unit: HKUnit { UserDefaultsRepository.getPreferredUnit() } private var displayValue: String { diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift index 6467fc76e..c1f42ea93 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift @@ -46,106 +46,6 @@ struct AlarmSnoozeSection: View { } } -import SwiftUI - -struct AlarmAudioSection: View { - @Binding var alarm: Alarm - @State private var showingTonePicker = false - - var body: some View { - Section(header: Text("Alert Sound")) { - // ——— Tone Row ——— - Button { - showingTonePicker = true - } label: { - HStack { - Text("Tone") - Spacer() - Text(alarm.soundFile.displayName) - .foregroundColor(.secondary) - Image(systemName: "chevron.right") - .foregroundColor(.secondary) - } - } - .sheet(isPresented: $showingTonePicker) { - NavigationView { - List { - ForEach(SoundFile.allCases) { tone in - Button { - alarm.soundFile = tone - // play test tone - AlarmSound.setSoundFile(str: tone.rawValue) - AlarmSound.stop() - AlarmSound.playTest() - } label: { - HStack { - Text(tone.displayName) - if alarm.soundFile == tone { - Spacer() - Image(systemName: "checkmark") - .foregroundColor(.accentColor) - } - } - } - } - } - .navigationTitle("Choose Tone") - .toolbar { - ToolbarItem(placement: .confirmationAction) { - Button("Done") { - AlarmSound.stop() - showingTonePicker = false - } - } - } - } - } - - // ——— Play / Repeat Toggles ——— - HStack() { - Text("Play") - .font(.subheadline) - .foregroundColor(.secondary) - Spacer() - Picker("", selection: $alarm.playSoundOption) { - ForEach(PlaySoundOption.allCases, id: \.self) { opt in - Text(opt.rawValue.capitalized).tag(opt) - } - } - .pickerStyle(.menu) - } - - VStack(alignment: .leading, spacing: 8) { - Text("Repeat") - .font(.subheadline) - .foregroundColor(.secondary) - - Picker("", selection: $alarm.repeatSoundOption) { - ForEach(RepeatSoundOption.allCases, id: \.self) { opt in - Text(opt.rawValue.capitalized).tag(opt) - } - } - .pickerStyle(.menu) - } - } - } -} - -struct AlarmActiveSection: View { - @Binding var alarm: Alarm - - var body: some View { - Section(header: Text("Active During")) { - Picker("Active", selection: $alarm.activeOption) { - Text("Always").tag(ActiveOption.always) - Text("Day").tag(ActiveOption.day) - Text("Night").tag(ActiveOption.night) - } - .pickerStyle(.segmented) - } - } -} - struct AlarmSnoozedUntilSection: View { @Binding var alarm: Alarm diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift index c29d6b982..b65bb923d 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift @@ -22,7 +22,6 @@ struct LowBgAlarmEditor: View { AlarmBGSection( header: "Threshold", - footer: "bla bla", title: "BG", range: 40...150, value: Binding( From 1a9af37a96a1f24a23bad910b9d426cd9447e525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 12 May 2025 13:14:12 +0200 Subject: [PATCH 028/138] Tinted InfoBanner --- .../AlarmEditing/Components/InfoBanner.swift | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift b/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift index 63db3284e..ba7702653 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift @@ -6,42 +6,31 @@ // 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 tint: Color? = Color.blue.opacity(0.07) + 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() + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(tint ?? .clear) + .background(.thinMaterial) + ) .listRowInsets(EdgeInsets()) - .padding(.bottom, 4) } } From 6118f48f2db882ff515d3d8358976623af94157d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 12 May 2025 14:31:07 +0200 Subject: [PATCH 029/138] Alarm gui adjustments --- LoopFollow.xcodeproj/project.pbxproj | 12 ++- .../Components/AlarmEditorFields.swift | 89 ------------------- .../Components/AlarmGeneralSection.swift | 29 ++++++ .../Components/AlarmSnoozeSection.swift | 82 +++++++++++++++++ .../Editors/BuildExpireAlarmEditor.swift | 17 ++-- .../Editors/FastDropAlarmEditor.swift | 23 ++--- .../Editors/HighBgAlarmEditor.swift | 17 ++-- .../Editors/LowBgAlarmEditor.swift | 21 ++--- .../Editors/MissedReadingEditor.swift | 19 ++-- 9 files changed, 152 insertions(+), 157 deletions(-) delete mode 100644 LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Components/AlarmGeneralSection.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 16eef9e6b..1149c9f42 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -97,6 +97,7 @@ DD7E198A2ACDA62600DBD158 /* SensorStart.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E19892ACDA62600DBD158 /* SensorStart.swift */; }; DD7F4B9D2DD1EAE500D449E9 /* AlarmAudioSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4B9C2DD1EAE500D449E9 /* AlarmAudioSection.swift */; }; DD7F4B9F2DD1F92700D449E9 /* AlarmActiveSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4B9E2DD1F92700D449E9 /* AlarmActiveSection.swift */; }; + DD7F4BA12DD2193F00D449E9 /* AlarmSnoozeSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4BA02DD2193F00D449E9 /* AlarmSnoozeSection.swift */; }; DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */; }; DD85E9952D739CFE001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */; }; DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */; }; @@ -122,7 +123,7 @@ DDC7E5152DBCFA7900EB1127 /* SnoozerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5132DBCE1B900EB1127 /* SnoozerViewController.swift */; }; DDC7E5162DBCFA7F00EB1127 /* SnoozerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5122DBCE1B900EB1127 /* SnoozerView.swift */; }; DDC7E5382DBD887400EB1127 /* isOnPhoneCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5372DBD887400EB1127 /* isOnPhoneCall.swift */; }; - DDC7E5422DBD8A1600EB1127 /* AlarmEditorFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5392DBD8A1600EB1127 /* AlarmEditorFields.swift */; }; + DDC7E5422DBD8A1600EB1127 /* AlarmGeneralSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5392DBD8A1600EB1127 /* AlarmGeneralSection.swift */; }; DDC7E5432DBD8A1600EB1127 /* SoundFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E53A2DBD8A1600EB1127 /* SoundFile.swift */; }; DDC7E5442DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E53C2DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift */; }; DDC7E5462DBD8A1600EB1127 /* LowBgAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E53E2DBD8A1600EB1127 /* LowBgAlarmEditor.swift */; }; @@ -418,6 +419,7 @@ DD7E19892ACDA62600DBD158 /* SensorStart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorStart.swift; sourceTree = ""; }; DD7F4B9C2DD1EAE500D449E9 /* AlarmAudioSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmAudioSection.swift; sourceTree = ""; }; DD7F4B9E2DD1F92700D449E9 /* AlarmActiveSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmActiveSection.swift; sourceTree = ""; }; + DD7F4BA02DD2193F00D449E9 /* AlarmSnoozeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSnoozeSection.swift; sourceTree = ""; }; DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EKEventStore+Extensions.swift"; sourceTree = ""; }; DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnipodDashHeartbeatBluetoothTransmitter.swift; sourceTree = ""; }; DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseConversion.swift; sourceTree = ""; }; @@ -444,7 +446,7 @@ DDC7E5122DBCE1B900EB1127 /* SnoozerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozerView.swift; sourceTree = ""; }; DDC7E5132DBCE1B900EB1127 /* SnoozerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozerViewController.swift; sourceTree = ""; }; DDC7E5372DBD887400EB1127 /* isOnPhoneCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = isOnPhoneCall.swift; sourceTree = ""; }; - DDC7E5392DBD8A1600EB1127 /* AlarmEditorFields.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmEditorFields.swift; sourceTree = ""; }; + DDC7E5392DBD8A1600EB1127 /* AlarmGeneralSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmGeneralSection.swift; sourceTree = ""; }; DDC7E53A2DBD8A1600EB1127 /* SoundFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundFile.swift; sourceTree = ""; }; DDC7E53C2DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildExpireAlarmEditor.swift; sourceTree = ""; }; DDC7E53E2DBD8A1600EB1127 /* LowBgAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LowBgAlarmEditor.swift; sourceTree = ""; }; @@ -919,12 +921,13 @@ DDC7E53B2DBD8A1600EB1127 /* Components */ = { isa = PBXGroup; children = ( + DD7F4BA02DD2193F00D449E9 /* AlarmSnoozeSection.swift */, DD7F4B9E2DD1F92700D449E9 /* AlarmActiveSection.swift */, DD7F4B9C2DD1EAE500D449E9 /* AlarmAudioSection.swift */, DD0650F62DCFDA26004D3B41 /* InfoBanner.swift */, DD0650F42DCF303F004D3B41 /* AlarmStepperSection.swift */, DD0650A82DCA8A10004D3B41 /* AlarmBGSection.swift */, - DDC7E5392DBD8A1600EB1127 /* AlarmEditorFields.swift */, + DDC7E5392DBD8A1600EB1127 /* AlarmGeneralSection.swift */, DDC7E53A2DBD8A1600EB1127 /* SoundFile.swift */, ); path = Components; @@ -1700,6 +1703,7 @@ DD0650F72DCFDA26004D3B41 /* InfoBanner.swift in Sources */, DD4AFB612DB68BBC00BB593F /* AlarmListView.swift in Sources */, DD7B0D442D730A3B0063DCB6 /* CycleHelper.swift in Sources */, + DD7F4BA12DD2193F00D449E9 /* AlarmSnoozeSection.swift in Sources */, DD9ACA0C2D33BB8600415D8A /* CalendarTask.swift in Sources */, DD13BC792C3FE63A0062313B /* InfoManager.swift in Sources */, DD0C0C702C4AFFE800DBADDF /* RemoteViewController.swift in Sources */, @@ -1707,7 +1711,7 @@ FCFEECA02488157B00402A7F /* Chart.swift in Sources */, DDCF979424C0D380002C9752 /* UIViewExtension.swift in Sources */, DD0C0C6D2C48606200DBADDF /* CarbMetric.swift in Sources */, - DDC7E5422DBD8A1600EB1127 /* AlarmEditorFields.swift in Sources */, + DDC7E5422DBD8A1600EB1127 /* AlarmGeneralSection.swift in Sources */, DDC7E5432DBD8A1600EB1127 /* SoundFile.swift in Sources */, DDC7E5442DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift in Sources */, DDC7E5462DBD8A1600EB1127 /* LowBgAlarmEditor.swift in Sources */, diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift deleted file mode 100644 index c1f42ea93..000000000 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmEditorFields.swift +++ /dev/null @@ -1,89 +0,0 @@ -// -// AlarmEditorFields.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-04-21. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// - -import SwiftUI - -struct AlarmGeneralSection: View { - @Binding var alarm: Alarm - - var body: some View { - Section(header: Text("General")) { - HStack { - Text("Name") - TextField("Alarm Name", text: $alarm.name) - .multilineTextAlignment(.trailing) - .textFieldStyle(.plain) - } - Toggle("Enabled", isOn: $alarm.isEnabled) - } - } -} - -struct AlarmSnoozeSection: View { - let title: String - let range: ClosedRange - let step: Double - let unitLabel: String - @Binding var value: Double - - var body: some View { - Section( - header: Text(title), - footer: Text("How long to snooze after firing \(Int(range.lowerBound))–\(Int(range.upperBound)) \(unitLabel)") - ) { - Stepper( - "\(title): \(Int(value)) \(unitLabel)", - value: $value, - in: range, - step: step - ) - } - } -} - -struct AlarmSnoozedUntilSection: View { - @Binding var alarm: Alarm - - private var isSnoozed: Binding { - Binding( - get: { - if let until = alarm.snoozedUntil, until > Date() { - return true - } - return false - }, - set: { on in - if on { - // keep existing future snooze or set default ahead - if let until = alarm.snoozedUntil, until > Date() { - alarm.snoozedUntil = until - } else { - let secs = alarm.type.timeUnit.seconds - alarm.snoozedUntil = Date().addingTimeInterval(Double(alarm.snoozeDuration) * secs) - } - } else { - alarm.snoozedUntil = nil - } - } - ) - } - - var body: some View { - Section(header: Text("Snoozed Until")) { - Toggle("Snoozed", isOn: isSnoozed) - if isSnoozed.wrappedValue, let until = alarm.snoozedUntil { - DatePicker("Until", selection: Binding( - get: { until }, - set: { alarm.snoozedUntil = $0 } - ), displayedComponents: [.date, .hourAndMinute]) - } - } - } -} - - diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmGeneralSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmGeneralSection.swift new file mode 100644 index 000000000..f4c69f3c1 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmGeneralSection.swift @@ -0,0 +1,29 @@ +// +// AlarmGeneralSection.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-04-21. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import SwiftUI + +struct AlarmGeneralSection: View { + @Binding var alarm: Alarm + + var body: some View { + Section( + header: Text("General"), + footer: Text("Give each alarm a unique name—especially if you’ve added more than one of the same kind—so you can tell them apart at a glance.")) + { + HStack { + Text("Name") + TextField("Alarm Name", text: $alarm.name) + .multilineTextAlignment(.trailing) + .textFieldStyle(.plain) + .foregroundColor(.secondary) + } + Toggle("Enabled", isOn: $alarm.isEnabled) + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift new file mode 100644 index 000000000..e959ad3e4 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift @@ -0,0 +1,82 @@ +// +// AlarmSnoozeSection.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-12. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + + +import SwiftUI + +struct AlarmSnoozeSection: View { + @Binding var alarm: Alarm + let range: ClosedRange + let step: Int + + private var unitLabel: String { alarm.type.timeUnit.label } + + private var defaultSnoozeBinding: Binding { + Binding( + get: { alarm.snoozeDuration }, + set: { alarm.snoozeDuration = $0 } + ) + } + + private var isSnoozed: Binding { + Binding( + get: { + if let until = alarm.snoozedUntil, until > Date() { return true } + return false + }, + set: { on in + if on { + if alarm.snoozedUntil == nil || alarm.snoozedUntil! < Date() { + let secs = alarm.type.timeUnit.seconds + alarm.snoozedUntil = Date() + .addingTimeInterval(Double(alarm.snoozeDuration) * secs) + } + } else { + alarm.snoozedUntil = nil + } + } + ) + } + + var body: some View { + Section( + header: Text("SNOOZE"), + footer: Text( + "“Default Snooze” controls how long the alert stays quiet after " + + "you press Snooze. Toggle “Snoozed” to mute this alarm right now " + + "until the time below." + ) + ) { + Stepper( + value: defaultSnoozeBinding, + in: range, + step: step + ) { + HStack { + Text("Default Snooze:") + Spacer() + Text("\(alarm.snoozeDuration) \(unitLabel)") + .foregroundColor(.secondary) + } + } + + Toggle("Snoozed", isOn: isSnoozed) + + if isSnoozed.wrappedValue, let until = alarm.snoozedUntil { + DatePicker( + "Until", + selection: Binding( + get: { until }, + set: { alarm.snoozedUntil = $0 } + ), + displayedComponents: [.date, .hourAndMinute] + ) + } + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift index 49da21f88..db3d8c3f6 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift @@ -31,20 +31,13 @@ struct BuildExpireAlarmEditor: View { ) ) - 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) + AlarmSnoozeSection( + alarm: $alarm, + range: 1...14, + step: 1 + ) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift index 73d30d8db..505f736c3 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift @@ -68,24 +68,15 @@ struct FastDropAlarmEditor: View { .opacity(useLimit ? 1 : 0.35) } // Section */ - // ────────── SNOOZE ──────────── - AlarmStepperSection( - header: "Default Snooze", - footer: "How long to silence this alert after you press Snooze.", - title: "Default Snooze", - range: 5...60, - step: 5, - unitLabel: alarm.type.timeUnit.label, - value: Binding( - get: { Double(alarm.snoozeDuration) }, - set: { alarm.snoozeDuration = Int($0) } - ) - ) - - // ────── SOUND / ACTIVE / UNTIL ────── AlarmAudioSection(alarm: $alarm) + AlarmActiveSection(alarm: $alarm) - AlarmSnoozedUntilSection(alarm: $alarm) + + AlarmSnoozeSection( + alarm: $alarm, + range: 5...60, + step: 5 + ) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift index ff66795d0..c56d82125 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift @@ -45,20 +45,13 @@ struct HighBgAlarmEditor: View { ) ) - AlarmStepperSection( - title: "Default Snooze", - range: 10...120, - 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) + AlarmSnoozeSection( + alarm: $alarm, + range: 10...120, + step: 5 + ) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift index b65bb923d..1972ad653 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift @@ -31,6 +31,7 @@ struct LowBgAlarmEditor: View { ) AlarmStepperSection( + header: "PERSISTENCE", footer: "Glucose must stay below the threshold for this many minutes " + "before the alert sounds. Set 0 to alert immediately.", title: "Persistent", @@ -44,6 +45,7 @@ struct LowBgAlarmEditor: View { ) AlarmStepperSection( + header: "PREDICTION", 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.", @@ -57,20 +59,15 @@ struct LowBgAlarmEditor: View { ) ) - AlarmStepperSection( - title: "Default Snooze", - range: 5...30, - 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) + + AlarmSnoozeSection( + alarm: $alarm, + range: 5...30, + step: 5 + ) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift index 716c0609a..399ab4cdd 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift @@ -29,20 +29,15 @@ struct MissedReadingEditor: View { ) ) - 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) + + AlarmSnoozeSection( + alarm: $alarm, + range: 10...180, + step: 5 + ) } .navigationTitle(alarm.type.rawValue) From 1ec4a40342991484c308c9b4999e36aa963eca00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 12 May 2025 14:36:14 +0200 Subject: [PATCH 030/138] Alarm gui adjustments --- .../Components/AlarmStepperSection.swift | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift index 0db9fde9d..1099ff501 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift @@ -40,12 +40,14 @@ struct AlarmStepperSection: View { header: header.map(Text.init), footer: footer.map(Text.init) ) { - Stepper( - "\(title): \(Int(value))\(unitLabel.map { " \($0)" } ?? "")", - value: $value, - in: range, - step: step - ) + Stepper(value: $value, in: range, step: step) { + HStack { + Text(title) + Spacer() + Text("\(Int(value))\(unitLabel.map { " \($0)" } ?? "")") + .foregroundColor(.secondary) + } + } } } } From 97378e423cb5a34c0774e95ee3a30cc23f60b440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 12 May 2025 14:51:10 +0200 Subject: [PATCH 031/138] Swap place of Done/+ --- LoopFollow/Alarm/AlarmListView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index a0a676c82..fd700e10f 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -31,14 +31,14 @@ struct AlarmListView: View { } .navigationTitle("Alarms") .toolbar { - ToolbarItem(placement: .navigationBarLeading) { + ToolbarItem(placement: .navigationBarTrailing) { Button { showingTypePicker = true } label: { Image(systemName: "plus") } } - ToolbarItem(placement: .navigationBarTrailing) { + ToolbarItem(placement: .navigationBarLeading) { Button("Done") { presentationMode.wrappedValue.dismiss() } From ef140b41e3f28012ef14dbf301b0bd516b1b4ec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 12 May 2025 16:23:02 +0200 Subject: [PATCH 032/138] Alarm selection --- LoopFollow/Alarm/AlarmListView.swift | 206 ++++++++++++++++++++++----- 1 file changed, 168 insertions(+), 38 deletions(-) diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index fd700e10f..4e001e48d 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -6,74 +6,204 @@ // Copyright © 2025 Jon Fawcett. All rights reserved. // - import SwiftUI -/// Displays all configured alarms and allows adding a new one by selecting its type. +extension AlarmType { + enum Group: String, CaseIterable { + case glucose = "Glucose" + case insulin = "Insulin / Food" + case device = "Device / System" + case other = "Other" + } + + var group: Group { + switch self { + case .low, .high, .fastDrop, .fastRise, .missedReading: + return .glucose + case .iob, .bolus, .cob, .missedBolus, .recBolus: + return .insulin + case .battery, .batteryDrop, .pump, .pumpChange, + .sensorChange, .notLooping, .buildExpire: + return .device + default: + return .other + } + } + + var icon: String { + switch self { + case .low : return "arrow.down" + case .high : return "arrow.up" + case .fastDrop : return "arrow.down.to.line" + case .fastRise : return "arrow.up.to.line" + case .missedReading: return "wifi.slash" + + case .iob, .bolus: return "syringe" + case .cob : return "fork.knife" + case .missedBolus: return "exclamationmark.arrow.triangle.2.circlepath" + case .recBolus : return "bolt.horizontal" + + case .battery : return "battery.25" + case .batteryDrop: return "battery.100.bolt" + case .pump : return "drop" + case .pumpChange: return "arrow.triangle.2.circlepath" + case .sensorChange: return "sensor.tag.radiowaves.forward" + + case .notLooping: return "circle.slash" + case .buildExpire: return "calendar.badge.exclamationmark" + + case .overrideStart: return "play.circle" + case .overrideEnd : return "stop.circle" + case .tempTargetStart: return "flag" + case .tempTargetEnd : return "flag.slash" + } + } + + var blurb: String { + switch self { + case .low: return "Alerts when BG goes below a limit." + case .high: return "Alerts when BG rises above a limit." + case .fastDrop: return "Rapid downward BG trend." + case .fastRise: return "Rapid upward BG trend." + case .missedReading: return "No CGM data for X minutes." + + case .iob: return "High insulin-on-board." + case .bolus: return "Large individual bolus." + case .cob: return "High carbs-on-board." + case .missedBolus: return "Carbs without bolus." + case .recBolus: return "Recommended bolus issued." + + case .battery: return "Pump / phone battery low." + case .batteryDrop: return "Battery drops quickly." + case .pump: return "Reservoir level low." + case .pumpChange: return "Infusion-set change due." + case .sensorChange: return "Sensor change due." + case .notLooping: return "Loop hasn’t completed." + case .buildExpire: return "Follow-app build expiring." + + case .overrideStart: return "Override just started." + case .overrideEnd: return "Override ended." + case .tempTargetStart:return "Temp target started." + case .tempTargetEnd: return "Temp target ended." + } + } +} + +struct AddAlarmSheet: View { + let onSelect: (AlarmType) -> Void + @Environment(\.dismiss) private var dismiss + + private let columns = [ + GridItem(.adaptive(minimum: 110), spacing: 16) + ] + + var body: some View { + NavigationStack { + ScrollView { + LazyVGrid(columns: columns, spacing: 16) { + ForEach(AlarmType.Group.allCases, id: \.self) { group in + if AlarmType.allCases.contains(where: { $0.group == group }) { + Section(header: + Text(group.rawValue) + .font(.headline) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 4) + ) { + ForEach(AlarmType.allCases.filter { $0.group == group }, + id: \.self) { type in + AlarmTile(type: type) { + onSelect(type) + dismiss() + } + } + } + } + } + } + .padding() + } + .navigationTitle("Add Alarm") + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { dismiss() } + } + } + } + } +} + +private struct AlarmTile: View { + let type: AlarmType + let action: () -> Void + + var body: some View { + Button(action: action) { + VStack(spacing: 8) { + Image(systemName: type.icon) + .font(.title2) + .foregroundColor(.accentColor) + Text(type.rawValue) + .font(.subheadline) + .multilineTextAlignment(.center) + .lineLimit(2) + Text(type.blurb) + .font(.caption2) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .lineLimit(2) + } + .padding() + .frame(maxWidth: .infinity, minHeight: 110) + .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12)) + } + .buttonStyle(.plain) + } +} + struct AlarmListView: View { @ObservedObject private var store = Storage.shared.alarms - @Environment(\.presentationMode) var presentationMode - @State private var showingTypePicker = false + @Environment(\.dismiss) private var dismiss + + @State private var showAddSheet = false @State private var editingAlarmID: UUID? var body: some View { - NavigationView { + NavigationStack { 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)) } } - .onDelete { idxs in - store.value.remove(atOffsets: idxs) - } + .onDelete { store.value.remove(atOffsets: $0) } } .navigationTitle("Alarms") .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button { - showingTypePicker = true - } label: { - Image(systemName: "plus") - } - } ToolbarItem(placement: .navigationBarLeading) { - Button("Done") { - presentationMode.wrappedValue.dismiss() - } + Button("Done") { dismiss() } + } + ToolbarItem(placement: .navigationBarTrailing) { + Button { showAddSheet = true } label: { Image(systemName: "plus") } } } - // 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"), - buttons: AlarmType.allCases.map { type in - .default(Text(type.rawValue)) { - let newAlarm = Alarm(type: type) - store.value.append(newAlarm) - editingAlarmID = newAlarm.id - } - } + [.cancel()] - ) + .sheet(isPresented: $showAddSheet) { + AddAlarmSheet { type in + let new = Alarm(type: type) + store.value.append(new) + editingAlarmID = new.id + } } - // Step 2: when an ID is set, present the editor .sheet(item: $editingAlarmID) { id in if let idx = store.value.firstIndex(where: { $0.id == id }) { AlarmEditor(alarm: $store.value[idx]) - } else { - Text("Alarm not found") - .padding() } } } } - /// Find and return a binding to the given alarm in the store private func binding(for alarm: Alarm) -> Binding { guard let idx = store.value.firstIndex(where: { $0.id == alarm.id }) else { - fatalError("Alarm not found in store") + fatalError("Alarm not found") } return $store.value[idx] } From 6aa3f7816ca7fa83c98dc4f567c94921c9458c07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 12 May 2025 20:43:07 +0200 Subject: [PATCH 033/138] Alarm selection --- .../Alarm/AlarmEditing/AlarmEditor.swift | 34 +++++++-- .../Components/AlarmBGSection.swift | 11 ++- LoopFollow/Alarm/AlarmListView.swift | 70 +++++++++++++++---- 3 files changed, 95 insertions(+), 20 deletions(-) diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index b20cdb030..59ccd05af 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -5,14 +5,41 @@ // Created by Jonas Björkert on 2025-04-21. // Copyright © 2025 Jon Fawcett. All rights reserved. // - - import SwiftUI struct AlarmEditor: View { @Binding var alarm: Alarm + var isNew: Bool = false + var onDone: () -> Void = { } + var onCancel: () -> Void = { } + + @Environment(\.dismiss) private var dismiss var body: some View { + NavigationStack { + innerEditor() + .navigationBarTitleDisplayMode(.inline) + .toolbar { + if isNew { + ToolbarItem(placement: .confirmationAction) { + Button("Done") { + onDone() + dismiss() + } + } + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + onCancel() + dismiss() + } + } + } + } + } + } + + @ViewBuilder + private func innerEditor() -> some View { switch alarm.type { case .buildExpire: BuildExpireAlarmEditor(alarm: $alarm) @@ -25,8 +52,7 @@ struct AlarmEditor: View { case .fastDrop: FastDropAlarmEditor(alarm: $alarm) - // TODO: add other condition types here - + /* TODO: add other condition types here */ default: Text("No editor for \(alarm.type.rawValue)") .padding() diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift index d8b427c56..d86dfb53d 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift @@ -37,8 +37,15 @@ struct AlarmBGSection: View { } private var allValues: [Double] { - let step = unit == .millimolesPerLiter ? 18.0 * 0.1 : 1 - return Array(stride(from: range.lowerBound, through: range.upperBound, by: step)) + if unit == .millimolesPerLiter { + let stepMMOL = 0.1 + let lower = ceil((range.lowerBound / 18) / stepMMOL) * stepMMOL + let upper = floor((range.upperBound / 18) / stepMMOL) * stepMMOL + + return stride(from: lower, through: upper, by: stepMMOL).map { $0 * 18 } + } else { + return Array(stride(from: range.lowerBound, through: range.upperBound, by: 1)) + } } var body: some View { diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index 4e001e48d..7da7a4a5f 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -113,7 +113,7 @@ struct AddAlarmSheet: View { id: \.self) { type in AlarmTile(type: type) { onSelect(type) - dismiss() +// dismiss() } } } @@ -160,12 +160,26 @@ private struct AlarmTile: View { } } +private enum SheetInfo: Identifiable { + case picker + case editor(id: UUID, isNew: Bool) + + var id: UUID { + switch self { + case .picker: + return UUID(uuidString:"00000000-0000-0000-0000-000000000000")! + case .editor(let id, _): + return id + } + } +} + struct AlarmListView: View { @ObservedObject private var store = Storage.shared.alarms @Environment(\.dismiss) private var dismiss - @State private var showAddSheet = false - @State private var editingAlarmID: UUID? + @State private var sheetInfo: SheetInfo? + @State private var deleteAfterDismiss: UUID? var body: some View { NavigationStack { @@ -183,20 +197,48 @@ struct AlarmListView: View { Button("Done") { dismiss() } } ToolbarItem(placement: .navigationBarTrailing) { - Button { showAddSheet = true } label: { Image(systemName: "plus") } + Button { sheetInfo = .picker } label: { Image(systemName:"plus") } } } - .sheet(isPresented: $showAddSheet) { - AddAlarmSheet { type in - let new = Alarm(type: type) - store.value.append(new) - editingAlarmID = new.id - } + .sheet(item: $sheetInfo, + onDismiss: handleSheetDismiss) { info in + sheetContent(for: info) } - .sheet(item: $editingAlarmID) { id in - if let idx = store.value.firstIndex(where: { $0.id == id }) { - AlarmEditor(alarm: $store.value[idx]) - } + } + } + + private func handleSheetDismiss() { + if let id = deleteAfterDismiss, + let idx = store.value.firstIndex(where: { $0.id == id }) { + store.value.remove(at: idx) + } + deleteAfterDismiss = nil + } + + @ViewBuilder + private func sheetContent(for info: SheetInfo) -> some View { + switch info { + + case .picker: + AddAlarmSheet { type in + let new = Alarm(type: type) + store.value.append(new) + sheetInfo = .editor(id: new.id, isNew: true) + } + + case .editor(let id, let isNew): + if let idx = store.value.firstIndex(where: { $0.id == id }) { + AlarmEditor( + alarm: $store.value[idx], + isNew: isNew, + onDone: { sheetInfo = nil }, + onCancel: { + deleteAfterDismiss = id + sheetInfo = nil + } + ) + } else { + Text("Alarm not found").padding() } } } From d110b5d7a3e50891ad59b2f750a62007a4c7cb3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 12 May 2025 20:52:34 +0200 Subject: [PATCH 034/138] Alarm icon and text changes --- LoopFollow/Alarm/AlarmListView.swift | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index 7da7a4a5f..e38628a84 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -32,20 +32,20 @@ extension AlarmType { var icon: String { switch self { - case .low : return "arrow.down" - case .high : return "arrow.up" - case .fastDrop : return "arrow.down.to.line" - case .fastRise : return "arrow.up.to.line" + case .low : return "arrow.down.to.line" + case .high : return "arrow.up.to.line" + case .fastDrop : return "chevron.down.2" + case .fastRise : return "chevron.up.2" case .missedReading: return "wifi.slash" case .iob, .bolus: return "syringe" - case .cob : return "fork.knife" + case .cob : return "fork.knife" case .missedBolus: return "exclamationmark.arrow.triangle.2.circlepath" - case .recBolus : return "bolt.horizontal" + case .recBolus : return "bolt.horizontal" - case .battery : return "battery.25" + case .battery: return "battery.25" case .batteryDrop: return "battery.100.bolt" - case .pump : return "drop" + case .pump: return "drop" case .pumpChange: return "arrow.triangle.2.circlepath" case .sensorChange: return "sensor.tag.radiowaves.forward" @@ -53,9 +53,9 @@ extension AlarmType { case .buildExpire: return "calendar.badge.exclamationmark" case .overrideStart: return "play.circle" - case .overrideEnd : return "stop.circle" + case .overrideEnd: return "stop.circle" case .tempTargetStart: return "flag" - case .tempTargetEnd : return "flag.slash" + case .tempTargetEnd: return "flag.slash" } } @@ -73,13 +73,13 @@ extension AlarmType { case .missedBolus: return "Carbs without bolus." case .recBolus: return "Recommended bolus issued." - case .battery: return "Pump / phone battery low." + case .battery: return "Phone battery low." case .batteryDrop: return "Battery drops quickly." case .pump: return "Reservoir level low." - case .pumpChange: return "Infusion-set change due." + case .pumpChange: return "Pump change due." case .sensorChange: return "Sensor change due." case .notLooping: return "Loop hasn’t completed." - case .buildExpire: return "Follow-app build expiring." + case .buildExpire: return "Looping-app build expiring." case .overrideStart: return "Override just started." case .overrideEnd: return "Override ended." From 4952fa215bc01bcbcf5e7318132752de62fc8a02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 12 May 2025 20:53:48 +0200 Subject: [PATCH 035/138] Alarm icon and text changes --- LoopFollow/Alarm/Alarm.swift | 81 ++++++++++++++++++++++++++++ LoopFollow/Alarm/AlarmListView.swift | 81 ---------------------------- 2 files changed, 81 insertions(+), 81 deletions(-) diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index afeadad5e..fbff4738e 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -252,3 +252,84 @@ struct Alarm: Identifiable, Codable, Equatable { } } } + +extension AlarmType { + enum Group: String, CaseIterable { + case glucose = "Glucose" + case insulin = "Insulin / Food" + case device = "Device / System" + case other = "Override / Target" + } + + var group: Group { + switch self { + case .low, .high, .fastDrop, .fastRise, .missedReading: + return .glucose + case .iob, .bolus, .cob, .missedBolus, .recBolus: + return .insulin + case .battery, .batteryDrop, .pump, .pumpChange, + .sensorChange, .notLooping, .buildExpire: + return .device + default: + return .other + } + } + + var icon: String { + switch self { + case .low : return "arrow.down.to.line" + case .high : return "arrow.up.to.line" + case .fastDrop : return "chevron.down.2" + case .fastRise : return "chevron.up.2" + case .missedReading: return "wifi.slash" + + case .iob, .bolus: return "syringe" + case .cob : return "fork.knife" + case .missedBolus: return "exclamationmark.arrow.triangle.2.circlepath" + case .recBolus : return "bolt.horizontal" + + case .battery: return "battery.25" + case .batteryDrop: return "battery.100.bolt" + case .pump: return "drop" + case .pumpChange: return "arrow.triangle.2.circlepath" + case .sensorChange: return "sensor.tag.radiowaves.forward" + + case .notLooping: return "circle.slash" + case .buildExpire: return "calendar.badge.exclamationmark" + + case .overrideStart: return "play.circle" + case .overrideEnd: return "stop.circle" + case .tempTargetStart: return "flag" + case .tempTargetEnd: return "flag.slash" + } + } + + var blurb: String { + switch self { + case .low: return "Alerts when BG goes below a limit." + case .high: return "Alerts when BG rises above a limit." + case .fastDrop: return "Rapid downward BG trend." + case .fastRise: return "Rapid upward BG trend." + case .missedReading: return "No CGM data for X minutes." + + case .iob: return "High insulin-on-board." + case .bolus: return "Large individual bolus." + case .cob: return "High carbs-on-board." + case .missedBolus: return "Carbs without bolus." + case .recBolus: return "Recommended bolus issued." + + case .battery: return "Phone battery low." + case .batteryDrop: return "Battery drops quickly." + case .pump: return "Reservoir level low." + case .pumpChange: return "Pump change due." + case .sensorChange: return "Sensor change due." + case .notLooping: return "Loop hasn’t completed." + case .buildExpire: return "Looping-app build expiring." + + case .overrideStart: return "Override just started." + case .overrideEnd: return "Override ended." + case .tempTargetStart:return "Temp target started." + case .tempTargetEnd: return "Temp target ended." + } + } +} diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index e38628a84..666b218d1 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -8,87 +8,6 @@ import SwiftUI -extension AlarmType { - enum Group: String, CaseIterable { - case glucose = "Glucose" - case insulin = "Insulin / Food" - case device = "Device / System" - case other = "Other" - } - - var group: Group { - switch self { - case .low, .high, .fastDrop, .fastRise, .missedReading: - return .glucose - case .iob, .bolus, .cob, .missedBolus, .recBolus: - return .insulin - case .battery, .batteryDrop, .pump, .pumpChange, - .sensorChange, .notLooping, .buildExpire: - return .device - default: - return .other - } - } - - var icon: String { - switch self { - case .low : return "arrow.down.to.line" - case .high : return "arrow.up.to.line" - case .fastDrop : return "chevron.down.2" - case .fastRise : return "chevron.up.2" - case .missedReading: return "wifi.slash" - - case .iob, .bolus: return "syringe" - case .cob : return "fork.knife" - case .missedBolus: return "exclamationmark.arrow.triangle.2.circlepath" - case .recBolus : return "bolt.horizontal" - - case .battery: return "battery.25" - case .batteryDrop: return "battery.100.bolt" - case .pump: return "drop" - case .pumpChange: return "arrow.triangle.2.circlepath" - case .sensorChange: return "sensor.tag.radiowaves.forward" - - case .notLooping: return "circle.slash" - case .buildExpire: return "calendar.badge.exclamationmark" - - case .overrideStart: return "play.circle" - case .overrideEnd: return "stop.circle" - case .tempTargetStart: return "flag" - case .tempTargetEnd: return "flag.slash" - } - } - - var blurb: String { - switch self { - case .low: return "Alerts when BG goes below a limit." - case .high: return "Alerts when BG rises above a limit." - case .fastDrop: return "Rapid downward BG trend." - case .fastRise: return "Rapid upward BG trend." - case .missedReading: return "No CGM data for X minutes." - - case .iob: return "High insulin-on-board." - case .bolus: return "Large individual bolus." - case .cob: return "High carbs-on-board." - case .missedBolus: return "Carbs without bolus." - case .recBolus: return "Recommended bolus issued." - - case .battery: return "Phone battery low." - case .batteryDrop: return "Battery drops quickly." - case .pump: return "Reservoir level low." - case .pumpChange: return "Pump change due." - case .sensorChange: return "Sensor change due." - case .notLooping: return "Loop hasn’t completed." - case .buildExpire: return "Looping-app build expiring." - - case .overrideStart: return "Override just started." - case .overrideEnd: return "Override ended." - case .tempTargetStart:return "Temp target started." - case .tempTargetEnd: return "Temp target ended." - } - } -} - struct AddAlarmSheet: View { let onSelect: (AlarmType) -> Void @Environment(\.dismiss) private var dismiss From 53c0036e19ce72c5abc30af170066ce06d554808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 13 May 2025 08:19:48 +0200 Subject: [PATCH 036/138] Delta sorting --- .../Editors/FastDropAlarmEditor.swift | 3 ++ LoopFollow/Alarm/AlarmManager.swift | 30 ++++++++++++++----- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift index 505f736c3..aef02143d 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift @@ -43,6 +43,9 @@ struct FastDropAlarmEditor: View { set: { alarm.monitoringWindow = Int($0) } ) ) + + //TODO: Vi måste bestämma var denna bglimit lagras. + //Kanske införa en bgLimit för tydlighets skull /* // ────────── BG LIMIT ─────────── Section { diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index d2e97c3dd..7a5fa003e 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -19,7 +19,7 @@ class AlarmManager { conditionTypes: [AlarmCondition.Type] = [ BuildExpireCondition.self, LowBGCondition.self, - HighBGCondition.self + HighBGCondition.self, // TODO: add other condition types here ] ) { @@ -33,17 +33,33 @@ class AlarmManager { let alarms = Storage.shared.alarms.value let sorted = alarms.sorted { lhs, rhs in - // Primary: type priority + // 1) by type priority if lhs.type.priority != rhs.type.priority { return lhs.type.priority < rhs.type.priority } - // Secondary: threshold ordering if applicable + + // 2) by “main” value for that type if let asc = lhs.type.thresholdSortAscending { - let leftVal = lhs.threshold ?? (asc ? Double.infinity : -Double.infinity) - let rightVal = rhs.threshold ?? (asc ? Double.infinity : -Double.infinity) - return asc ? leftVal < rightVal : leftVal > rightVal + // pick the right field: + let leftVal: Double + let rightVal: Double + + switch lhs.type { + case .fastDrop, .fastRise: + // sort on the per-reading delta + leftVal = lhs.delta ?? (asc ? Double.infinity : -Double.infinity) + rightVal = rhs.delta ?? (asc ? Double.infinity : -Double.infinity) + + default: + // sort on the BG limit threshold + leftVal = lhs.threshold ?? (asc ? Double.infinity : -Double.infinity) + rightVal = rhs.threshold ?? (asc ? Double.infinity : -Double.infinity) + } + + return asc ? (leftVal < rightVal) : (leftVal > rightVal) } - // Tertiary: fallback to insertion order + + // 3) fallback return false } var skipType: AlarmType? = nil From 754e46affe34d0acd7fb8fea4f33d373cff73d5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 13 May 2025 08:30:47 +0200 Subject: [PATCH 037/138] More blue for InfoBanner --- .../AlarmEditing/Components/InfoBanner.swift | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift b/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift index ba7702653..d5d0d05fb 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift @@ -10,9 +10,14 @@ import SwiftUI struct InfoBanner: View { let text: String + var systemImage: String = "info.circle.fill" - var iconColour: Color = .accentColor - var tint: Color? = Color.blue.opacity(0.07) + + var iconColour: Color = .accentColor + + var tint: Color = Color.blue.opacity(0.20) + + var border: Color = Color.blue.opacity(0.40) var body: some View { HStack(alignment: .top, spacing: 12) { @@ -28,8 +33,11 @@ struct InfoBanner: View { .frame(maxWidth: .infinity, alignment: .leading) .background( RoundedRectangle(cornerRadius: 12, style: .continuous) - .fill(tint ?? .clear) - .background(.thinMaterial) + .fill(tint) + ) + .overlay( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .stroke(border, lineWidth: 1) ) .listRowInsets(EdgeInsets()) } From 9af524a1c3226b396aa8c31371046544374f3458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 13 May 2025 09:25:23 +0200 Subject: [PATCH 038/138] Icons icons icons --- .../AlarmEditing/Components/InfoBanner.swift | 17 ++++--- .../Editors/MissedReadingEditor.swift | 2 +- LoopFollow/Alarm/AlarmListView.swift | 45 ++++++++++++++++--- 3 files changed, 51 insertions(+), 13 deletions(-) diff --git a/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift b/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift index d5d0d05fb..4794f25a9 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift @@ -9,19 +9,24 @@ import SwiftUI struct InfoBanner: View { + /// Main explanatory text let text: String - var systemImage: String = "info.circle.fill" + /// Optional alarm type whose icon you’d like to show. + /// If `nil`, we fall back to the standard “info” symbol. + var alarmType: AlarmType? = nil - var iconColour: Color = .accentColor + /// Colour for the leading symbol + var iconColour: Color = .accentColor - var tint: Color = Color.blue.opacity(0.20) - - var border: Color = Color.blue.opacity(0.40) + /// Background + border tints + var tint : Color = Color.blue.opacity(0.20) + var border: Color = Color.blue.opacity(0.40) + // ────────── View ────────── var body: some View { HStack(alignment: .top, spacing: 12) { - Image(systemName: systemImage) + Image(systemName: alarmType?.icon ?? "info.circle.fill") .font(.title3) .foregroundColor(iconColour) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift index 399ab4cdd..28a828f16 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift @@ -13,7 +13,7 @@ struct MissedReadingEditor: View { var body: some View { Form { - InfoBanner(text: "The app notifies you when no CGM reading has been received for the time you choose below.") + InfoBanner(text: "The app notifies you when no CGM reading has been received for the time you choose below.", alarmType: alarm.type) AlarmGeneralSection(alarm: $alarm) diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index 666b218d1..4d7a178b1 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -22,17 +22,14 @@ struct AddAlarmSheet: View { LazyVGrid(columns: columns, spacing: 16) { ForEach(AlarmType.Group.allCases, id: \.self) { group in if AlarmType.allCases.contains(where: { $0.group == group }) { - Section(header: - Text(group.rawValue) + Section(header: Text(group.rawValue) .font(.headline) .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 4) ) { - ForEach(AlarmType.allCases.filter { $0.group == group }, - id: \.self) { type in + ForEach(AlarmType.allCases.filter { $0.group == group }, id: \.self) { type in AlarmTile(type: type) { onSelect(type) -// dismiss() } } } @@ -104,8 +101,38 @@ struct AlarmListView: View { NavigationStack { List { ForEach(store.value) { alarm in - NavigationLink(alarm.name) { + NavigationLink { AlarmEditor(alarm: binding(for: alarm)) + } label: { + HStack(spacing: 12) { + ZStack { + Image(systemName: alarm.type.icon) + .font(.title3) + .foregroundColor(alarm.isEnabled ? .accentColor : .secondary) + .opacity(iconOpacity(for: alarm)) + .frame(maxWidth: .infinity, maxHeight: .infinity, + alignment: .center) + + ZStack(alignment: .topTrailing) { + if let until = alarm.snoozedUntil, until > Date() { + Image(systemName: "zzz") + .font(.caption2.bold()) + .foregroundColor(.red) + } + + if !alarm.isEnabled { + Image(systemName: "xmark.circle.fill") + .font(.caption2) + .foregroundColor(.red) + } + } + .offset(x: 6, y: -6) + } + .frame(width: 26, height: 26) + + Text(alarm.name) + .frame(maxWidth: .infinity, alignment: .leading) + } } } .onDelete { store.value.remove(atOffsets: $0) } @@ -168,4 +195,10 @@ struct AlarmListView: View { } return $store.value[idx] } + + private func iconOpacity(for alarm: Alarm) -> Double { + if !alarm.isEnabled { return 0.35 } + if let until = alarm.snoozedUntil, until > Date() { return 0.35 } + return 1.0 + } } From cff205084857dab6969cab92d16090acb39ff1b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 13 May 2025 09:37:39 +0200 Subject: [PATCH 039/138] Alarm list --- LoopFollow/Alarm/AlarmListView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index 4d7a178b1..fdc1c8237 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -115,15 +115,15 @@ struct AlarmListView: View { ZStack(alignment: .topTrailing) { if let until = alarm.snoozedUntil, until > Date() { - Image(systemName: "zzz") + Image(systemName: "speaker.zzz.fill") .font(.caption2.bold()) - .foregroundColor(.red) + .foregroundColor(.accentColor) } if !alarm.isEnabled { Image(systemName: "xmark.circle.fill") .font(.caption2) - .foregroundColor(.red) + .foregroundColor(.accentColor) } } .offset(x: 6, y: -6) From 4ae3455bf2958e7c01c065e7a2421dfa73376ad4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 13 May 2025 10:43:38 +0200 Subject: [PATCH 040/138] Show snooze overlay --- LoopFollow/Alarm/AlarmListView.swift | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index fdc1c8237..e8f76fa86 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -108,25 +108,24 @@ struct AlarmListView: View { ZStack { Image(systemName: alarm.type.icon) .font(.title3) - .foregroundColor(alarm.isEnabled ? .accentColor : .secondary) + .foregroundStyle(alarm.isEnabled ? Color.accentColor : Color.secondary) .opacity(iconOpacity(for: alarm)) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) - ZStack(alignment: .topTrailing) { + ZStack(alignment: .bottomTrailing) { if let until = alarm.snoozedUntil, until > Date() { - Image(systemName: "speaker.zzz.fill") - .font(.caption2.bold()) - .foregroundColor(.accentColor) - } - - if !alarm.isEnabled { - Image(systemName: "xmark.circle.fill") - .font(.caption2) - .foregroundColor(.accentColor) + Image(systemName: "zzz") + .font(.system(size: 11, weight: .bold)) + .foregroundStyle(Color(uiColor: .systemBackground)) + .padding(3) + .background( + Circle().fill(Color.accentColor) + ) + .offset(x: 6, y: 6) } } - .offset(x: 6, y: -6) + .frame(width: 26, height: 26) } .frame(width: 26, height: 26) From b36c8e3607b97d85ffced4bb6251ecc809f03a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 13 May 2025 14:19:03 +0200 Subject: [PATCH 041/138] Snooze adjustment --- LoopFollow/Alarm/AlarmListView.swift | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index e8f76fa86..83e5d009b 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -108,24 +108,18 @@ struct AlarmListView: View { ZStack { Image(systemName: alarm.type.icon) .font(.title3) + .symbolRenderingMode(.hierarchical) .foregroundStyle(alarm.isEnabled ? Color.accentColor : Color.secondary) .opacity(iconOpacity(for: alarm)) - .frame(maxWidth: .infinity, maxHeight: .infinity, - alignment: .center) - - ZStack(alignment: .bottomTrailing) { - if let until = alarm.snoozedUntil, until > Date() { - Image(systemName: "zzz") - .font(.system(size: 11, weight: .bold)) - .foregroundStyle(Color(uiColor: .systemBackground)) - .padding(3) - .background( - Circle().fill(Color.accentColor) - ) - .offset(x: 6, y: 6) - } + + if let until = alarm.snoozedUntil, until > Date() { + Image(systemName: "zzz") + .font(.title3) + .foregroundStyle(Color.secondary) + .shadow(color: .black.opacity(1), radius: 2, x: 0, y: 0) + .blendMode(.screen) + .offset(x: 6, y: 6) } - .frame(width: 26, height: 26) } .frame(width: 26, height: 26) @@ -197,7 +191,7 @@ struct AlarmListView: View { private func iconOpacity(for alarm: Alarm) -> Double { if !alarm.isEnabled { return 0.35 } - if let until = alarm.snoozedUntil, until > Date() { return 0.35 } + if let until = alarm.snoozedUntil, until > Date() { return 0.55 } return 1.0 } } From 41cfa368aa7f96a2b2187e05e791650846b4caa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 13 May 2025 20:28:30 +0200 Subject: [PATCH 042/138] Snapped value --- .../AlarmEditing/Components/AlarmBGSection.swift | 15 +++++++++------ .../Editors/FastDropAlarmEditor.swift | 12 ++++-------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift index d86dfb53d..ecefaaca9 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift @@ -32,8 +32,13 @@ struct AlarmBGSection: View { private var unit: HKUnit { UserDefaultsRepository.getPreferredUnit() } - private var displayValue: String { - "\(Localizer.formatQuantity(value)) \(unit.localizedShortUnitString)" + private var snappedValue: Binding { + Binding( + get: { + allValues.min(by: { abs($0 - value) < abs($1 - value) }) ?? value + }, + set: { value = $0 } + ) } private var allValues: [Double] { @@ -54,10 +59,8 @@ struct AlarmBGSection: View { footer: footer.map(Text.init) ) { Picker( - selection: $value, - label: HStack { - Text(title) - } + selection: snappedValue, + label: HStack { Text(title) } ) { ForEach(allValues, id: \.self) { v in Text("\(Localizer.formatQuantity(v)) \(unit.localizedShortUnitString)") diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift index aef02143d..da528025f 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift @@ -26,8 +26,8 @@ struct FastDropAlarmEditor: View { title: "Drop per reading", range: 3...20, value: Binding( - get: { alarm.threshold ?? 4 }, - set: { alarm.threshold = $0 } + get: { alarm.delta ?? 18 }, + set: { alarm.delta = $0 } ) ) @@ -44,10 +44,6 @@ struct FastDropAlarmEditor: View { ) ) - //TODO: Vi måste bestämma var denna bglimit lagras. - //Kanske införa en bgLimit för tydlighets skull -/* - // ────────── BG LIMIT ─────────── Section { Toggle("Only alert when below BG limit", isOn: $useLimit) .onAppear { @@ -69,8 +65,8 @@ struct FastDropAlarmEditor: View { ) .disabled(!useLimit) .opacity(useLimit ? 1 : 0.35) - } // Section -*/ + } + AlarmAudioSection(alarm: $alarm) AlarmActiveSection(alarm: $alarm) From a91cc0e31dd975b4769e6ec1dfdc238fc110cb94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 14 May 2025 08:52:36 +0200 Subject: [PATCH 043/138] FastDropCondition --- LoopFollow.xcodeproj/project.pbxproj | 12 ++++ .../AlarmCondition/FastDropCondition.swift | 44 ++++++++++++ .../Components/AlarmBGLimitSection.swift | 68 +++++++++++++++++++ .../Components/AlarmBGPicker.swift | 49 +++++++++++++ .../Components/AlarmBGSection.swift | 37 ++-------- .../Editors/FastDropAlarmEditor.swift | 30 +++----- LoopFollow/Alarm/AlarmType.swift | 2 +- 7 files changed, 187 insertions(+), 55 deletions(-) create mode 100644 LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Components/AlarmBGPicker.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 1149c9f42..911efa5c5 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -98,6 +98,9 @@ DD7F4B9D2DD1EAE500D449E9 /* AlarmAudioSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4B9C2DD1EAE500D449E9 /* AlarmAudioSection.swift */; }; DD7F4B9F2DD1F92700D449E9 /* AlarmActiveSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4B9E2DD1F92700D449E9 /* AlarmActiveSection.swift */; }; DD7F4BA12DD2193F00D449E9 /* AlarmSnoozeSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4BA02DD2193F00D449E9 /* AlarmSnoozeSection.swift */; }; + DD7F4BA32DD3C8A900D449E9 /* AlarmBGPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4BA22DD3C8A900D449E9 /* AlarmBGPicker.swift */; }; + DD7F4BC52DD3CE0700D449E9 /* AlarmBGLimitSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4BC42DD3CE0700D449E9 /* AlarmBGLimitSection.swift */; }; + DD7F4BC72DD473A600D449E9 /* FastDropCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4BC62DD473A600D449E9 /* FastDropCondition.swift */; }; DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */; }; DD85E9952D739CFE001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */; }; DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */; }; @@ -420,6 +423,9 @@ DD7F4B9C2DD1EAE500D449E9 /* AlarmAudioSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmAudioSection.swift; sourceTree = ""; }; DD7F4B9E2DD1F92700D449E9 /* AlarmActiveSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmActiveSection.swift; sourceTree = ""; }; DD7F4BA02DD2193F00D449E9 /* AlarmSnoozeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSnoozeSection.swift; sourceTree = ""; }; + DD7F4BA22DD3C8A900D449E9 /* AlarmBGPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmBGPicker.swift; sourceTree = ""; }; + DD7F4BC42DD3CE0700D449E9 /* AlarmBGLimitSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmBGLimitSection.swift; sourceTree = ""; }; + DD7F4BC62DD473A600D449E9 /* FastDropCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastDropCondition.swift; sourceTree = ""; }; DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EKEventStore+Extensions.swift"; sourceTree = ""; }; DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnipodDashHeartbeatBluetoothTransmitter.swift; sourceTree = ""; }; DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseConversion.swift; sourceTree = ""; }; @@ -688,6 +694,7 @@ DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = { isa = PBXGroup; children = ( + DD7F4BC62DD473A600D449E9 /* FastDropCondition.swift */, DD0650EA2DCE8385004D3B41 /* LowBGCondition.swift */, DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */, DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */, @@ -921,6 +928,8 @@ DDC7E53B2DBD8A1600EB1127 /* Components */ = { isa = PBXGroup; children = ( + DD7F4BC42DD3CE0700D449E9 /* AlarmBGLimitSection.swift */, + DD7F4BA22DD3C8A900D449E9 /* AlarmBGPicker.swift */, DD7F4BA02DD2193F00D449E9 /* AlarmSnoozeSection.swift */, DD7F4B9E2DD1F92700D449E9 /* AlarmActiveSection.swift */, DD7F4B9C2DD1EAE500D449E9 /* AlarmAudioSection.swift */, @@ -1636,9 +1645,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DD7F4BA32DD3C8A900D449E9 /* AlarmBGPicker.swift in Sources */, DD5334292C6166A500062F9D /* InfoDisplaySettingsView.swift in Sources */, DD48781E2C7DAF2F0048F05C /* PushNotificationManager.swift in Sources */, DD2C2E562D3C3917006413A5 /* DexcomSettingsView.swift in Sources */, + DD7F4BC52DD3CE0700D449E9 /* AlarmBGLimitSection.swift in Sources */, DD7F4B9F2DD1F92700D449E9 /* AlarmActiveSection.swift in Sources */, DD4AFB672DB68C5500BB593F /* UUID+Identifiable.swift in Sources */, DD9ED0CA2D355257000D2A63 /* LogView.swift in Sources */, @@ -1712,6 +1723,7 @@ DDCF979424C0D380002C9752 /* UIViewExtension.swift in Sources */, DD0C0C6D2C48606200DBADDF /* CarbMetric.swift in Sources */, DDC7E5422DBD8A1600EB1127 /* AlarmGeneralSection.swift in Sources */, + DD7F4BC72DD473A600D449E9 /* FastDropCondition.swift in Sources */, DDC7E5432DBD8A1600EB1127 /* SoundFile.swift in Sources */, DDC7E5442DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift in Sources */, DDC7E5462DBD8A1600EB1127 /* LowBgAlarmEditor.swift in Sources */, diff --git a/LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift b/LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift new file mode 100644 index 000000000..e94c76504 --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift @@ -0,0 +1,44 @@ +// +// FastDropCondition.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-14. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import Foundation + +struct FastDropCondition: AlarmCondition { + static let type: AlarmType = .fastDrop + init() {} + + func evaluate(alarm: Alarm, data: AlarmData) -> Bool { + // ──────────────────────────────── + // 0. sanity checks + // ──────────────────────────────── + guard + let dropPerReading = alarm.delta, dropPerReading > 0, + let dropsNeeded = alarm.monitoringWindow, dropsNeeded > 0, + data.bgReadings.count >= dropsNeeded + 1 + else { return false } + + // optional BG-limit guard + if let limit = alarm.threshold { + guard let latest = data.bgReadings.last, latest.sgv > 0 else { return false } + guard Double(latest.sgv) < limit else { return false } + } + + // ──────────────────────────────── + // 1. compute recent deltas + // ──────────────────────────────── + let recent = data.bgReadings.suffix(dropsNeeded + 1) + let readings = Array(recent) + + for i in 1...dropsNeeded { + let delta = Double(readings[i - 1].sgv - readings[i].sgv) + if delta < dropPerReading { return false } + } + + return true + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift new file mode 100644 index 000000000..4c6c84995 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift @@ -0,0 +1,68 @@ +// +// AlarmBGLimitSection.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-13. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import SwiftUI + +struct AlarmBGLimitSection: View { + let header: String? + let footer: String? + let toggleText: String + let pickerTitle: String + let range: ClosedRange + @Binding var value: Double? + + init( + header: String? = nil, + footer: String? = nil, + toggleText: String, + pickerTitle: String, + range: ClosedRange, + value: Binding + ) { + self.header = header + self.footer = footer + self.toggleText = toggleText + self.pickerTitle = pickerTitle + self.range = range + self._value = value + } + + private var isOn: Binding { + Binding( + get: { value != nil }, + set: { on in + if on, value == nil { value = range.lowerBound } + if !on { value = nil } + } + ) + } + + private var pickerValue: Binding { + Binding( + get: { value ?? range.lowerBound }, + set: { newVal in value = newVal } + ) + } + + var body: some View { + Section( + header: header.map(Text.init), + footer: footer.map(Text.init) + ) { + Toggle(toggleText, isOn: isOn) + + if isOn.wrappedValue { + AlarmBGPicker( + title: pickerTitle, + range: range, + value: pickerValue + ) + } + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGPicker.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGPicker.swift new file mode 100644 index 000000000..86a9d21d9 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGPicker.swift @@ -0,0 +1,49 @@ +// +// AlarmBGPicker.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-13. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + + +import SwiftUI +import HealthKit + +struct AlarmBGPicker: View { + let title: String + let range: ClosedRange + @Binding var value: Double + + private var unit: HKUnit { UserDefaultsRepository.getPreferredUnit() } + + private var allValues: [Double] { + if unit == .millimolesPerLiter { + let step = 0.1 + let lower = ceil((range.lowerBound / 18) / step) * step + let upper = floor((range.upperBound / 18) / step) * step + return stride(from: lower, through: upper, by: step).map { $0 * 18 } + } else { + return Array(stride(from: range.lowerBound, through: range.upperBound, by: 1)) + } + } + + private var snappedValue: Binding { + Binding( + get: { + allValues.min(by: { abs($0 - value) < abs($1 - value) }) ?? value + }, + set: { value = $0 } + ) + } + + var body: some View { + Picker(selection: snappedValue, + label: HStack { Text(title) }) { + ForEach(allValues, id: \.self) { v in + Text("\(Localizer.formatQuantity(v)) \(unit.localizedShortUnitString)") + .tag(v) + } + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift index ecefaaca9..5943b28f5 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift @@ -30,43 +30,16 @@ struct AlarmBGSection: View { self._value = value } - private var unit: HKUnit { UserDefaultsRepository.getPreferredUnit() } - - private var snappedValue: Binding { - Binding( - get: { - allValues.min(by: { abs($0 - value) < abs($1 - value) }) ?? value - }, - set: { value = $0 } - ) - } - - private var allValues: [Double] { - if unit == .millimolesPerLiter { - let stepMMOL = 0.1 - let lower = ceil((range.lowerBound / 18) / stepMMOL) * stepMMOL - let upper = floor((range.upperBound / 18) / stepMMOL) * stepMMOL - - return stride(from: lower, through: upper, by: stepMMOL).map { $0 * 18 } - } else { - return Array(stride(from: range.lowerBound, through: range.upperBound, by: 1)) - } - } - var body: some View { Section( header: header.map(Text.init), footer: footer.map(Text.init) ) { - Picker( - selection: snappedValue, - label: HStack { Text(title) } - ) { - ForEach(allValues, id: \.self) { v in - Text("\(Localizer.formatQuantity(v)) \(unit.localizedShortUnitString)") - .tag(v) - } - } + AlarmBGPicker( + title: title, + range: range, + value: $value + ) } } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift index da528025f..f20cd8d06 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift @@ -44,28 +44,14 @@ struct FastDropAlarmEditor: View { ) ) - Section { - Toggle("Only alert when below BG limit", isOn: $useLimit) - .onAppear { - useLimit = (alarm.threshold != nil) - } - .onChange(of: useLimit) { newValue in - if !newValue { alarm.threshold = nil } - } - - AlarmBGSection( - header: nil, - footer: "Ignored unless the toggle above is enabled.", - title: "Dropping below", - range: 40...300, - value: Binding( - get: { alarm.threshold ?? 70 }, - set: { alarm.threshold = $0 } - ) - ) - .disabled(!useLimit) - .opacity(useLimit ? 1 : 0.35) - } + AlarmBGLimitSection( + header: "BG Limit", + footer: "When enabled, this alert only fires if the glucose is below the limit you set.", + toggleText: "Use BG Limit", + pickerTitle: "Dropping below", + range: 40...300, + value: $alarm.threshold + ) AlarmAudioSection(alarm: $alarm) diff --git a/LoopFollow/Alarm/AlarmType.swift b/LoopFollow/Alarm/AlarmType.swift index fcb7f0f84..a20b7f776 100644 --- a/LoopFollow/Alarm/AlarmType.swift +++ b/LoopFollow/Alarm/AlarmType.swift @@ -88,7 +88,7 @@ enum TimeUnit { /// A user-facing label var label: String { switch self { - case .minute: return "minutes" + case .minute: return "min" //Changed from minutes to save ui space case .hour: return "hours" case .day: return "days" } From be84d015f2827f2b4519762809a24a7c8411dc80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 14 May 2025 10:38:52 +0200 Subject: [PATCH 044/138] swiftformat --- LoopFollow.xcodeproj/project.pbxproj | 20 ++++++ Package.swift | 11 +++ Scripts/swiftformat.sh | 102 +++++++++++++++++++++++++++ 3 files changed, 133 insertions(+) create mode 100644 Package.swift create mode 100755 Scripts/swiftformat.sh diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 911efa5c5..344bc5ac6 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -1367,6 +1367,7 @@ isa = PBXNativeTarget; buildConfigurationList = FC97882D2485969C00A7906C /* Build configuration list for PBXNativeTarget "LoopFollow" */; buildPhases = ( + DD7F4BEA2DD48B9600D449E9 /* Swiftformat */, B038D39450A1F9A97D2B8BA4 /* [CP] Check Pods Manifest.lock */, FC9788102485969B00A7906C /* Sources */, FC9788112485969B00A7906C /* Frameworks */, @@ -1611,6 +1612,25 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + DD7F4BEA2DD48B9600D449E9 /* Swiftformat */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = Swiftformat; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "source \"${SRCROOT}\"/Scripts/swiftformat.sh\n"; + }; DDB0AF532BB1AA0900AFA48B /* Capture Build Details */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/Package.swift b/Package.swift new file mode 100644 index 000000000..3055730a4 --- /dev/null +++ b/Package.swift @@ -0,0 +1,11 @@ +// swift-tools-version:5.1 +import PackageDescription + +let package = Package( + name: "BuildTools", + platforms: [.macOS(.v10_11)], + dependencies: [ + .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.41.2") + ], + targets: [.target(name: "BuildTools", path: "")] +) \ No newline at end of file diff --git a/Scripts/swiftformat.sh b/Scripts/swiftformat.sh new file mode 100755 index 000000000..e02aee9a3 --- /dev/null +++ b/Scripts/swiftformat.sh @@ -0,0 +1,102 @@ +#! /bin/sh + +function assertEnvironment { + if [ -z $1 ]; then + echo $2 + exit 127 + fi +} + +assertEnvironment "${SRCROOT}" "Please set SRCROOT to project root folder" + +unset SDKROOT + +swift run -c release --package-path BuildTools swiftformat "${SRCROOT}" \ +--enable fileHeader + +# andOperator,\ +# anyObjectProtocol,\ +# blankLinesAroundMark,\ +# blankLinesAtEndOfScope,\ +# blankLinesAtStartOfScope,\ +# blankLinesBetweenScopes,\ +# consecutiveBlankLines,\ +# consecutiveSpaces,\ +# duplicateImports,\ +# elseOnSameLine,\ +# emptyBraces,\ +# enumNamespaces,\ +# fileHeader,\ +# hoistPatternLet,\ +# indent,\ +# isEmpty,\ +# leadingDelimiters,\ +# linebreakAtEndOfFile,\ +# linebreaks,\ +# modifierOrder,\ +# numberFormatting,\ +# preferKeyPath,\ +# redundantBackticks,\ +# redundantBreak,\ +# redundantExtensionACL,\ +# redundantFileprivate,\ +# redundantGet,\ +# redundantLet,\ +# redundantLetError,\ +# redundantNilInit,\ +# redundantObjc,\ +# redundantParens,\ +# redundantPattern,\ +# redundantRawValues,\ +# redundantReturn,\ +# redundantSelf,\ +# redundantType,\ +# redundantVoidReturnType,\ +# semicolons,\ +# sortedImports,\ +# sortedSwitchCases,\ +# spaceAroundBraces,\ +# spaceAroundBrackets,\ +# spaceAroundComments,\ +# spaceAroundGenerics,\ +# spaceAroundOperators,\ +# spaceAroundParens,\ +# spaceInsideBraces,\ +# spaceInsideBrackets,\ +# spaceInsideComments,\ +# spaceInsideGenerics,\ +# spaceInsideParens,\ +# strongOutlets,\ +# strongifiedSelf,\ +# todos,\ +# trailingCommas,\ +# trailingSpace,\ +# typeSugar,\ +# unusedArguments,\ +# void,\ +# wrap,\ +# wrapArguments,\ +# wrapAttributes,\ +# wrapEnumCases,\ +# wrapMultilineStatementBraces,\ +# wrapSwitchCases \ +# --disable braces,\ +# redundantInit,\ +# trailingClosures \ +# --commas inline \ +# --exponentcase uppercase \ +# --header strip \ +# --hexliteralcase uppercase \ +# --ifdef indent \ +# --indent 4 \ +# --self remove \ +# --semicolons never \ +# --swiftversion 5.2 \ +# --trimwhitespace always \ +# --maxwidth 130 \ +# --wraparguments before-first \ +# --funcattributes same-line \ +# --typeattributes same-line \ +# --varattributes same-line \ +# --wrapcollections before-first \ +--exclude Pods,Generated,R.generated.swift,fastlane/swift,Dependencies, LoopKit, LibreTransmitter,G7SensorKit,OmniKit, dexcom-share-client-swift,CGMBLEKit,RileyLinkKit,OmniBLE,MinimedKit,TidepoolService From 691306095baa773e61e6d2c07783e9642d9d7d10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 14 May 2025 10:41:36 +0200 Subject: [PATCH 045/138] swiftformat --- .gitignore | 1 + Scripts/swiftformat.sh | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 45e9afdf2..6625a9804 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ build/ DerivedData/ R.generated.swift +Package.resolved ## Various settings !default.mode1v3 diff --git a/Scripts/swiftformat.sh b/Scripts/swiftformat.sh index e02aee9a3..1f08a4ce2 100755 --- a/Scripts/swiftformat.sh +++ b/Scripts/swiftformat.sh @@ -12,7 +12,8 @@ assertEnvironment "${SRCROOT}" "Please set SRCROOT to project root folder" unset SDKROOT swift run -c release --package-path BuildTools swiftformat "${SRCROOT}" \ ---enable fileHeader +--enable fileHeader \ +--exclude Pods,Generated,R.generated.swift,fastlane/swift,Dependencies, LoopKit, LibreTransmitter,G7SensorKit,OmniKit, dexcom-share-client-swift,CGMBLEKit,RileyLinkKit,OmniBLE,MinimedKit,TidepoolService # andOperator,\ # anyObjectProtocol,\ From 3fc4d0d1cd172969a7604b29ea1473ba4c398bbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 14 May 2025 10:44:25 +0200 Subject: [PATCH 046/138] swiftformat --- Scripts/swiftformat.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/Scripts/swiftformat.sh b/Scripts/swiftformat.sh index 1f08a4ce2..e5074ca8f 100755 --- a/Scripts/swiftformat.sh +++ b/Scripts/swiftformat.sh @@ -100,4 +100,3 @@ swift run -c release --package-path BuildTools swiftformat "${SRCROOT}" \ # --typeattributes same-line \ # --varattributes same-line \ # --wrapcollections before-first \ ---exclude Pods,Generated,R.generated.swift,fastlane/swift,Dependencies, LoopKit, LibreTransmitter,G7SensorKit,OmniKit, dexcom-share-client-swift,CGMBLEKit,RileyLinkKit,OmniBLE,MinimedKit,TidepoolService From 8de6f5126e1bce087a564c5d637fb90c652f25f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 14 May 2025 10:49:42 +0200 Subject: [PATCH 047/138] swiftformat --- BuildTools/Package.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 BuildTools/Package.swift diff --git a/BuildTools/Package.swift b/BuildTools/Package.swift new file mode 100644 index 000000000..84ad713b3 --- /dev/null +++ b/BuildTools/Package.swift @@ -0,0 +1,11 @@ +// swift-tools-version:5.1 +import PackageDescription + +let package = Package( + name: "BuildTools", + platforms: [.macOS(.v10_11)], + dependencies: [ + .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.41.2") + ], + targets: [.target(name: "BuildTools", path: "")] +) From e7bc8c36eeb02a0d37ef906ae701a0ca866f093c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 14 May 2025 12:47:25 +0200 Subject: [PATCH 048/138] Linting --- BuildTools/Package.swift | 2 +- LoopFollow/Alarm/Alarm.swift | 115 ++-- .../Alarm/AlarmCondition/AlarmCondition.swift | 8 +- .../AlarmCondition/BuildExpireCondition.swift | 2 +- .../AlarmCondition/FastDropCondition.swift | 2 +- .../AlarmCondition/HighBGCondition.swift | 7 +- .../Alarm/AlarmCondition/LowBGCondition.swift | 10 +- .../MissedReadingCondition.swift | 1 - LoopFollow/Alarm/AlarmConfiguration.swift | 18 +- LoopFollow/Alarm/AlarmData.swift | 28 +- .../Alarm/AlarmEditing/AlarmEditor.swift | 9 +- .../Components/AlarmActiveSection.swift | 2 +- .../Components/AlarmAudioSection.swift | 1 - .../Components/AlarmBGLimitSection.swift | 18 +- .../Components/AlarmBGPicker.swift | 6 +- .../Components/AlarmBGSection.swift | 12 +- .../Components/AlarmGeneralSection.swift | 4 +- .../Components/AlarmSnoozeSection.swift | 7 +- .../Components/AlarmStepperSection.swift | 2 +- .../AlarmEditing/Components/InfoBanner.swift | 2 +- .../AlarmEditing/Components/SoundFile.swift | 208 +++---- .../Editors/BuildExpireAlarmEditor.swift | 6 +- .../Editors/FastDropAlarmEditor.swift | 10 +- .../Editors/HighBgAlarmEditor.swift | 10 +- .../Editors/LowBgAlarmEditor.swift | 19 +- .../Editors/MissedReadingEditor.swift | 5 +- LoopFollow/Alarm/AlarmListView.swift | 19 +- LoopFollow/Alarm/AlarmManager.swift | 28 +- LoopFollow/Alarm/AlarmSettingsView.swift | 14 +- LoopFollow/Alarm/AlarmType.swift | 60 +- LoopFollow/Application/AppDelegate.swift | 29 +- LoopFollow/Application/SceneDelegate.swift | 44 +- .../BackgroundRefresh/BT/BLEDevice.swift | 3 +- .../BackgroundRefresh/BT/BLEManager.swift | 18 +- .../BT/BluetoothDevice.swift | 71 ++- .../BT/BluetoothDeviceDelegate.swift | 2 +- .../DexcomHeartbeatBluetoothDevice.swift | 8 +- ...podDashHeartbeatBluetoothTransmitter.swift | 6 +- .../RileyLinkHeartbeatBluetoothDevice.swift | 8 +- .../BT/DexcomG7HeartBeat.swift | 3 +- .../BackgroundRefreshSettingsView.swift | 3 +- .../BackgroundRefreshSettingsViewModel.swift | 4 +- LoopFollow/Contact/ContactColorOption.swift | 12 +- LoopFollow/Contact/ContactImageUpdater.swift | 22 +- LoopFollow/Contact/ContactType.swift | 8 +- .../Settings/ContactSettingsView.swift | 4 +- .../Settings/ContactSettingsViewModel.swift | 16 +- LoopFollow/Controllers/AlarmSound.swift | 243 ++++---- .../Controllers/AppStateController.swift | 55 +- .../Controllers/BackgroundAlertManager.swift | 2 +- LoopFollow/Controllers/Graphs.swift | 522 ++++++++-------- LoopFollow/Controllers/NightScout.swift | 65 +- .../Controllers/Nightscout/BGData.swift | 19 +- LoopFollow/Controllers/Nightscout/CAge.swift | 29 +- .../Controllers/Nightscout/DeviceStatus.swift | 34 +- .../Nightscout/DeviceStatusLoop.swift | 23 +- .../Nightscout/DeviceStatusOpenAPS.swift | 28 +- LoopFollow/Controllers/Nightscout/IAge.swift | 11 +- .../Controllers/Nightscout/NSProfile.swift | 5 +- .../Controllers/Nightscout/Profile.swift | 47 +- .../Nightscout/ProfileManager.swift | 76 +-- LoopFollow/Controllers/Nightscout/SAge.swift | 31 +- .../Controllers/Nightscout/Treatments.swift | 42 +- .../Nightscout/Treatments/BGCheck.swift | 22 +- .../Nightscout/Treatments/Basals.swift | 37 +- .../Nightscout/Treatments/Bolus.swift | 19 +- .../Nightscout/Treatments/Carbs.swift | 45 +- .../Treatments/InsulinCartridgeChange.swift | 1 + .../Nightscout/Treatments/Notes.swift | 24 +- .../Nightscout/Treatments/Overrides.swift | 31 +- .../Nightscout/Treatments/ResumePump.swift | 21 +- .../Nightscout/Treatments/SMB.swift | 19 +- .../Nightscout/Treatments/SensorStart.swift | 7 +- .../Nightscout/Treatments/SiteChange.swift | 1 + .../Nightscout/Treatments/SuspendPump.swift | 21 +- .../Treatments/TemporaryTarget.swift | 18 +- LoopFollow/Controllers/SpeakBG.swift | 45 +- LoopFollow/Controllers/Stats.swift | 45 +- LoopFollow/Controllers/StatsView.swift | 60 +- LoopFollow/Controllers/Timers.swift | 2 +- .../Dexcom/DexcomSettingsViewModel.swift | 7 +- LoopFollow/Extensions/Binding+Optional.swift | 14 +- .../Extensions/EKEventStore+Extensions.swift | 32 +- .../HKQuantity+AnyConvertible.swift | 3 +- LoopFollow/Extensions/HKUnit+Extensions.swift | 8 +- .../Extensions/ShareClientExtension.swift | 51 +- LoopFollow/Extensions/UIViewExtension.swift | 22 +- LoopFollow/Helpers/AnyConvertible.swift | 37 +- LoopFollow/Helpers/AppConstants.swift | 2 +- LoopFollow/Helpers/AppVersionManager.swift | 19 +- LoopFollow/Helpers/BackgroundTaskAudio.swift | 29 +- LoopFollow/Helpers/BuildDetails.swift | 55 +- LoopFollow/Helpers/Chart.swift | 44 +- LoopFollow/Helpers/CycleHelper.swift | 8 +- LoopFollow/Helpers/DataStructs.swift | 13 +- LoopFollow/Helpers/DateTime.swift | 20 +- LoopFollow/Helpers/DictionaryKeyPath.swift | 15 +- LoopFollow/Helpers/GitHubService.swift | 6 +- LoopFollow/Helpers/Globals.swift | 5 +- LoopFollow/Helpers/GlucoseConversion.swift | 2 +- LoopFollow/Helpers/Localizer.swift | 20 +- LoopFollow/Helpers/Mobileprovision.swift | 36 +- LoopFollow/Helpers/NightscoutUtils.swift | 42 +- LoopFollow/Helpers/ObservationToken.swift | 5 +- LoopFollow/Helpers/TextFieldWithToolBar.swift | 6 +- LoopFollow/Helpers/TimeOfDay.swift | 9 +- .../Helpers/Views/HKQuantityInputView.swift | 2 +- LoopFollow/Helpers/carbBolusArrays.swift | 43 +- LoopFollow/Helpers/isOnPhoneCall.swift | 2 +- .../InfoDisplaySettingsViewModel.swift | 4 +- LoopFollow/InfoTable/InfoManager.swift | 6 +- LoopFollow/InfoTable/InfoType.swift | 2 +- LoopFollow/Log/LogManager.swift | 17 +- LoopFollow/Log/LogViewModel.swift | 2 +- LoopFollow/Log/SearchBar.swift | 4 +- .../NightscoutSettingsViewModel.swift | 12 +- .../Loop/LoopNightscoutRemoteView.swift | 27 +- LoopFollow/Remote/Loop/LoopOverrideView.swift | 4 +- .../Remote/Loop/LoopOverrideViewModel.swift | 4 +- .../Nightscout/TrioNightscoutRemoteView.swift | 4 +- LoopFollow/Remote/RemoteViewController.swift | 13 +- .../Remote/Settings/RemoteSettingsView.swift | 10 +- .../Settings/RemoteSettingsViewModel.swift | 28 +- LoopFollow/Remote/TRC/BolusView.swift | 2 +- LoopFollow/Remote/TRC/MealView.swift | 8 +- LoopFollow/Remote/TRC/OverrideView.swift | 2 +- .../Remote/TRC/PushNotificationManager.swift | 29 +- LoopFollow/Remote/TRC/TRCCommandType.swift | 4 +- LoopFollow/Remote/TRC/TempTargetView.swift | 4 +- LoopFollow/Remote/TRC/TreatmentResponse.swift | 24 +- .../TRC/TrioNightscoutRemoteController.swift | 5 +- .../Remote/TRC/TrioRemoteControlView.swift | 2 +- .../TRC/TrioRemoteControlViewModel.swift | 3 +- .../TempTargetPresetManager.swift | 4 +- .../Settings/AdvancedSettingsView.swift | 2 +- .../Settings/AdvancedSettingsViewModel.swift | 24 +- LoopFollow/Snoozer/SnoozerView.swift | 31 +- .../Snoozer/SnoozerViewController.swift | 9 +- LoopFollow/Snoozer/SnoozerViewModel.swift | 2 +- .../ObservableUserDefaultsValue.swift | 14 +- .../Storage/Framework/ObservableValue.swift | 4 +- .../Framework/SecureStorageValue.swift | 11 +- .../Storage/Framework/StorageValue.swift | 11 +- .../Storage/Framework/UserDefaultsValue.swift | 63 +- .../Framework/UserDefaultsValueGroups.swift | 34 +- LoopFollow/Storage/Observable.swift | 14 +- .../Storage/ObservableUserDefaults.swift | 2 +- LoopFollow/Storage/Storage.swift | 4 +- LoopFollow/Storage/UserDefaults.swift | 104 ++-- LoopFollow/Task/BGTask.swift | 8 +- LoopFollow/Task/CalendarTask.swift | 2 +- LoopFollow/Task/MinAgoTask.swift | 38 +- LoopFollow/Task/ProfileTask.swift | 2 +- LoopFollow/Task/Task.swift | 1 - LoopFollow/Task/TaskScheduler.swift | 12 +- .../AppStateViewController.swift | 2 +- .../GeneralSettingsViewController.swift | 562 +++++++++--------- .../GraphSettingsViewController.swift | 355 ++++++----- .../ViewControllers/MainViewController.swift | 323 +++++----- .../NightScoutViewController.swift | 84 ++- .../SettingsViewController.swift | 352 +++++------ .../WatchSettingsViewController.swift | 82 ++- LoopFollowTests/AlwaysTrueCondition.swift | 2 +- .../BuildExpireConditionTests.swift | 2 +- Package.swift | 14 +- 165 files changed, 2699 insertions(+), 2798 deletions(-) diff --git a/BuildTools/Package.swift b/BuildTools/Package.swift index 84ad713b3..2727a28d1 100644 --- a/BuildTools/Package.swift +++ b/BuildTools/Package.swift @@ -5,7 +5,7 @@ let package = Package( name: "BuildTools", platforms: [.macOS(.v10_11)], dependencies: [ - .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.41.2") + .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.41.2"), ], targets: [.target(name: "BuildTools", path: "")] ) diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index fbff4738e..0b4adb97e 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -33,7 +33,7 @@ enum ActiveOption: String, CaseIterable, Codable, DayNightDisplayable { } struct Alarm: Identifiable, Codable, Equatable { - var id: UUID = UUID() + var id: UUID = .init() var type: AlarmType /// Name of the alarm, defaults to alarm type @@ -102,16 +102,12 @@ struct Alarm: Identifiable, Codable, Equatable { /// ...within this many minutes var bolusWindowMinutes: Int? - func checkCondition(data: AlarmData) -> Bool { - return false - } - /// Function for when the alarm is triggered. /// If this alarm, all alarms is disabled or snoozed, then should not be called. This or all alarmd could be muted, then this function will just generate a notification. - func trigger(config : AlarmConfiguration, now: Date) { + func trigger(config: AlarmConfiguration, now: Date) { LogManager.shared.log(category: .alarm, message: "Alarm triggered: \(type.rawValue)") - var playSound: Bool = true + var playSound = true // Global mute if let until = config.muteUntil, until > now { @@ -137,9 +133,9 @@ struct Alarm: Identifiable, Codable, Equatable { let isNight: Bool if nightStart >= dayStart { - isNight = (now >= nightStart) || (now < dayStart) + isNight = (now >= nightStart) || (now < dayStart) } else { - isNight = (now >= nightStart) && (now < dayStart) + isNight = (now >= nightStart) && (now < dayStart) } let isDay = !isNight @@ -159,9 +155,9 @@ struct Alarm: Identifiable, Codable, Equatable { let shouldRepeat: Bool = { switch repeatSoundOption { case .always: return true - case .never: return false - case .day: return isDay - case .night: return isNight + case .never: return false + case .day: return isDay + case .night: return isNight } }() @@ -173,9 +169,9 @@ struct Alarm: Identifiable, Codable, Equatable { content.subtitle += Observable.shared.directionText.value + " " content.subtitle += Observable.shared.deltaText.value content.categoryIdentifier = "category" - // This is needed to trigger vibrate on watch and phone - // See if we can use .Critcal - // See if we should use this method instead of direct sound player + // This is needed to trigger vibrate on watch and phone + // See if we can use .Critcal + // See if we should use this method instead of direct sound player content.sound = .default let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) @@ -186,29 +182,29 @@ struct Alarm: Identifiable, Codable, Equatable { let category = UNNotificationCategory(identifier: "category", actions: [action], intentIdentifiers: [], options: []) UNUserNotificationCenter.current().setNotificationCategories([category]) - /* TODO när vi gör bg alarm sätt timestamp/datum för denna readings tid så vi inte larmar på samma igen, se isBGBased - if snooozedBGReadingTime != nil { - UserDefaultsRepository.snoozedBGReadingTime.value = snooozedBGReadingTime - } - */ + /* TODO: när vi gör bg alarm sätt timestamp/datum för denna readings tid så vi inte larmar på samma igen, se isBGBased + if snooozedBGReadingTime != nil { + UserDefaultsRepository.snoozedBGReadingTime.value = snooozedBGReadingTime + } + */ if playSound { - AlarmSound.setSoundFile(str: self.soundFile.rawValue) + AlarmSound.setSoundFile(str: soundFile.rawValue) AlarmSound.play(repeating: shouldRepeat) } } init(type: AlarmType) { self.type = type - self.name = type.rawValue + name = type.rawValue switch type { case .buildExpire: /// Alert 7 days before the build expires - self.threshold = 7 - self.soundFile = .wrongAnswer - self.snoozeDuration = 1 - self.repeatSoundOption = .always + threshold = 7 + soundFile = .wrongAnswer + snoozeDuration = 1 + repeatSoundOption = .always case .low: soundFile = .indeed case .iob: @@ -257,8 +253,8 @@ extension AlarmType { enum Group: String, CaseIterable { case glucose = "Glucose" case insulin = "Insulin / Food" - case device = "Device / System" - case other = "Override / Target" + case device = "Device / System" + case other = "Override / Target" } var group: Group { @@ -268,7 +264,7 @@ extension AlarmType { case .iob, .bolus, .cob, .missedBolus, .recBolus: return .insulin case .battery, .batteryDrop, .pump, .pumpChange, - .sensorChange, .notLooping, .buildExpire: + .sensorChange, .notLooping, .buildExpire: return .device default: return .other @@ -277,26 +273,22 @@ extension AlarmType { var icon: String { switch self { - case .low : return "arrow.down.to.line" - case .high : return "arrow.up.to.line" - case .fastDrop : return "chevron.down.2" - case .fastRise : return "chevron.up.2" + case .low: return "arrow.down.to.line" + case .high: return "arrow.up.to.line" + case .fastDrop: return "chevron.down.2" + case .fastRise: return "chevron.up.2" case .missedReading: return "wifi.slash" - case .iob, .bolus: return "syringe" - case .cob : return "fork.knife" + case .cob: return "fork.knife" case .missedBolus: return "exclamationmark.arrow.triangle.2.circlepath" - case .recBolus : return "bolt.horizontal" - + case .recBolus: return "bolt.horizontal" case .battery: return "battery.25" case .batteryDrop: return "battery.100.bolt" case .pump: return "drop" case .pumpChange: return "arrow.triangle.2.circlepath" case .sensorChange: return "sensor.tag.radiowaves.forward" - case .notLooping: return "circle.slash" case .buildExpire: return "calendar.badge.exclamationmark" - case .overrideStart: return "play.circle" case .overrideEnd: return "stop.circle" case .tempTargetStart: return "flag" @@ -306,30 +298,27 @@ extension AlarmType { var blurb: String { switch self { - case .low: return "Alerts when BG goes below a limit." - case .high: return "Alerts when BG rises above a limit." - case .fastDrop: return "Rapid downward BG trend." - case .fastRise: return "Rapid upward BG trend." - case .missedReading: return "No CGM data for X minutes." - - case .iob: return "High insulin-on-board." - case .bolus: return "Large individual bolus." - case .cob: return "High carbs-on-board." - case .missedBolus: return "Carbs without bolus." - case .recBolus: return "Recommended bolus issued." - - case .battery: return "Phone battery low." - case .batteryDrop: return "Battery drops quickly." - case .pump: return "Reservoir level low." - case .pumpChange: return "Pump change due." - case .sensorChange: return "Sensor change due." - case .notLooping: return "Loop hasn’t completed." - case .buildExpire: return "Looping-app build expiring." - - case .overrideStart: return "Override just started." - case .overrideEnd: return "Override ended." - case .tempTargetStart:return "Temp target started." - case .tempTargetEnd: return "Temp target ended." + case .low: return "Alerts when BG goes below a limit." + case .high: return "Alerts when BG rises above a limit." + case .fastDrop: return "Rapid downward BG trend." + case .fastRise: return "Rapid upward BG trend." + case .missedReading: return "No CGM data for X minutes." + case .iob: return "High insulin-on-board." + case .bolus: return "Large individual bolus." + case .cob: return "High carbs-on-board." + case .missedBolus: return "Carbs without bolus." + case .recBolus: return "Recommended bolus issued." + case .battery: return "Phone battery low." + case .batteryDrop: return "Battery drops quickly." + case .pump: return "Reservoir level low." + case .pumpChange: return "Pump change due." + case .sensorChange: return "Sensor change due." + case .notLooping: return "Loop hasn’t completed." + case .buildExpire: return "Looping-app build expiring." + case .overrideStart: return "Override just started." + case .overrideEnd: return "Override ended." + case .tempTargetStart: return "Temp target started." + case .tempTargetEnd: return "Temp target ended." } } } diff --git a/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift b/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift index 2a341ae52..09f40fba7 100644 --- a/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift @@ -24,10 +24,10 @@ extension AlarmCondition { if let snooze = alarm.snoozedUntil, snooze > now { return false } // time-of-day guard - let comps = Calendar.current.dateComponents([.hour, .minute], from: now) - let nowMin = (comps.hour! * 60) + comps.minute! - let dStart = config.dayStart.minutesSinceMidnight - let nStart = config.nightStart.minutesSinceMidnight + let comps = Calendar.current.dateComponents([.hour, .minute], from: now) + let nowMin = (comps.hour! * 60) + comps.minute! + let dStart = config.dayStart.minutesSinceMidnight + let nStart = config.nightStart.minutesSinceMidnight let isNight = (nowMin < dStart) || (nowMin >= nStart) switch alarm.activeOption { diff --git a/LoopFollow/Alarm/AlarmCondition/BuildExpireCondition.swift b/LoopFollow/Alarm/AlarmCondition/BuildExpireCondition.swift index 6b28c8e53..2308d03b9 100644 --- a/LoopFollow/Alarm/AlarmCondition/BuildExpireCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/BuildExpireCondition.swift @@ -1,5 +1,5 @@ // -// ExpireCondition.swift +// BuildExpireCondition.swift // LoopFollow // // Created by Jonas Björkert on 2025-04-18. diff --git a/LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift b/LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift index e94c76504..c21d6d182 100644 --- a/LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift @@ -34,7 +34,7 @@ struct FastDropCondition: AlarmCondition { let recent = data.bgReadings.suffix(dropsNeeded + 1) let readings = Array(recent) - for i in 1...dropsNeeded { + for i in 1 ... dropsNeeded { let delta = Double(readings[i - 1].sgv - readings[i].sgv) if delta < dropPerReading { return false } } diff --git a/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift b/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift index cc4a7bf8a..809906fed 100644 --- a/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift @@ -16,12 +16,11 @@ struct HighBGCondition: AlarmCondition { init() {} func evaluate(alarm: Alarm, data: AlarmData) -> Bool { - // ──────────────────────────────── // 0. sanity checks // ──────────────────────────────── guard let threshold = alarm.threshold else { return false } - guard let latest = data.bgReadings.last, latest.sgv > 0 else { return false } + guard let latest = data.bgReadings.last, latest.sgv > 0 else { return false } func isHigh(_ g: GlucoseValue) -> Bool { g.sgv > 0 && Double(g.sgv) >= threshold @@ -32,8 +31,8 @@ struct HighBGCondition: AlarmCondition { // ──────────────────────────────── var persistentOK = true if let persistentMinutes = alarm.persistentMinutes, - persistentMinutes > 0 { - + persistentMinutes > 0 + { let window = Int(ceil(Double(persistentMinutes) / 5.0)) if data.bgReadings.count >= window { diff --git a/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift b/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift index 1d2c54790..c6350d67f 100644 --- a/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift @@ -32,14 +32,14 @@ struct LowBGCondition: AlarmCondition { var predictiveTrigger = false if let predictiveMinutes = alarm.predictiveMinutes, predictiveMinutes > 0, - !data.predictionData.isEmpty { - + !data.predictionData.isEmpty + { let lookAhead = min( data.predictionData.count, Int(ceil(Double(predictiveMinutes) / 5.0)) ) - for i in 0.. 0 { - + persistentMinutes > 0 + { let window = Int(ceil(Double(persistentMinutes) / 5.0)) if data.bgReadings.count >= window { diff --git a/LoopFollow/Alarm/AlarmCondition/MissedReadingCondition.swift b/LoopFollow/Alarm/AlarmCondition/MissedReadingCondition.swift index e415f4a08..c6bdf7ae6 100644 --- a/LoopFollow/Alarm/AlarmCondition/MissedReadingCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/MissedReadingCondition.swift @@ -14,7 +14,6 @@ struct MissedReadingCondition: AlarmCondition { init() {} func evaluate(alarm: Alarm, data: AlarmData) -> Bool { - // ──────────────────────────────── // 0. sanity checks // ──────────────────────────────── diff --git a/LoopFollow/Alarm/AlarmConfiguration.swift b/LoopFollow/Alarm/AlarmConfiguration.swift index 93bda3ecf..8765bb268 100644 --- a/LoopFollow/Alarm/AlarmConfiguration.swift +++ b/LoopFollow/Alarm/AlarmConfiguration.swift @@ -9,26 +9,28 @@ import Foundation struct AlarmConfiguration: Codable, Equatable { // MARK: Core + var snoozeUntil: Date? var muteUntil: Date? var dayStart: TimeOfDay var nightStart: TimeOfDay // MARK: System audio overrides + var overrideSystemOutputVolume: Bool - var forcedOutputVolume: Float // 0 … 1 + var forcedOutputVolume: Float // 0 … 1 var audioDuringCalls: Bool var ignoreZeroBG: Bool var autoSnoozeCGMStart: Bool static let `default` = AlarmConfiguration( - muteUntil: nil, - dayStart: TimeOfDay(hour: 6, minute: 0), - nightStart: TimeOfDay(hour: 22, minute: 0), + muteUntil: nil, + dayStart: TimeOfDay(hour: 6, minute: 0), + nightStart: TimeOfDay(hour: 22, minute: 0), overrideSystemOutputVolume: true, - forcedOutputVolume: 0.5, - audioDuringCalls: true, - ignoreZeroBG: true, - autoSnoozeCGMStart: false, + forcedOutputVolume: 0.5, + audioDuringCalls: true, + ignoreZeroBG: true, + autoSnoozeCGMStart: false, ) } diff --git a/LoopFollow/Alarm/AlarmData.swift b/LoopFollow/Alarm/AlarmData.swift index a1f1ada63..674bf0656 100644 --- a/LoopFollow/Alarm/AlarmData.swift +++ b/LoopFollow/Alarm/AlarmData.swift @@ -15,17 +15,17 @@ struct AlarmData: Codable { } /* -struct AlarmData : Encodable, Decodable{ - let bgReadings: [ShareGlucoseData] -// let iob: Double? -// let cob: Double? - let predictionData: [ShareGlucoseData] -// let latestBoluses: [BolusEntry] -// let batteryLevel: Double? -// let latestCarbs: [CarbEntry] -// let overrideData: [OverrideEntry] -// let tempTargetData: [TempTargetEntry] -// let pumpVolume: Double? - let expireDate: Date? -} -*/ + struct AlarmData : Encodable, Decodable{ + let bgReadings: [ShareGlucoseData] + // let iob: Double? + // let cob: Double? + let predictionData: [ShareGlucoseData] + // let latestBoluses: [BolusEntry] + // let batteryLevel: Double? + // let latestCarbs: [CarbEntry] + // let overrideData: [OverrideEntry] + // let tempTargetData: [TempTargetEntry] + // let pumpVolume: Double? + let expireDate: Date? + } + */ diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index 59ccd05af..c23f35ca5 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -9,9 +9,9 @@ import SwiftUI struct AlarmEditor: View { @Binding var alarm: Alarm - var isNew: Bool = false - var onDone: () -> Void = { } - var onCancel: () -> Void = { } + var isNew: Bool = false + var onDone: () -> Void = {} + var onCancel: () -> Void = {} @Environment(\.dismiss) private var dismiss @@ -51,8 +51,7 @@ struct AlarmEditor: View { MissedReadingEditor(alarm: $alarm) case .fastDrop: FastDropAlarmEditor(alarm: $alarm) - - /* TODO: add other condition types here */ + /* TODO: add other condition types here */ default: Text("No editor for \(alarm.type.rawValue)") .padding() diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmActiveSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmActiveSection.swift index 9ae0f6bc6..7d7c5f639 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmActiveSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmActiveSection.swift @@ -14,7 +14,7 @@ struct AlarmActiveSection: View { var body: some View { Section(header: Text("Active During")) { AlarmEnumMenuPicker(title: "Active", - selection: $alarm.activeOption) + selection: $alarm.activeOption) } } } diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift index 559e91510..6c4b2dad2 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift @@ -6,7 +6,6 @@ // Copyright © 2025 Jon Fawcett. All rights reserved. // - import SwiftUI struct AlarmAudioSection: View { diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift index 4c6c84995..19713e3bb 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift @@ -15,21 +15,21 @@ struct AlarmBGLimitSection: View { let pickerTitle: String let range: ClosedRange @Binding var value: Double? - + init( - header: String? = nil, - footer: String? = nil, - toggleText: String, + header: String? = nil, + footer: String? = nil, + toggleText: String, pickerTitle: String, range: ClosedRange, value: Binding ) { - self.header = header - self.footer = footer - self.toggleText = toggleText + self.header = header + self.footer = footer + self.toggleText = toggleText self.pickerTitle = pickerTitle - self.range = range - self._value = value + self.range = range + _value = value } private var isOn: Binding { diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGPicker.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGPicker.swift index 86a9d21d9..cf532ffef 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGPicker.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGPicker.swift @@ -6,9 +6,8 @@ // Copyright © 2025 Jon Fawcett. All rights reserved. // - -import SwiftUI import HealthKit +import SwiftUI struct AlarmBGPicker: View { let title: String @@ -39,7 +38,8 @@ struct AlarmBGPicker: View { var body: some View { Picker(selection: snappedValue, - label: HStack { Text(title) }) { + label: HStack { Text(title) }) + { ForEach(allValues, id: \.self) { v in Text("\(Localizer.formatQuantity(v)) \(unit.localizedShortUnitString)") .tag(v) diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift index 5943b28f5..dc859074e 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift @@ -6,14 +6,14 @@ // Copyright © 2025 Jon Fawcett. All rights reserved. // -import SwiftUI import HealthKit +import SwiftUI struct AlarmBGSection: View { - let header: String? - let footer: String? - let title: String - let range: ClosedRange + let header: String? + let footer: String? + let title: String + let range: ClosedRange @Binding var value: Double init( @@ -27,7 +27,7 @@ struct AlarmBGSection: View { self.footer = footer self.title = title self.range = range - self._value = value + _value = value } var body: some View { diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmGeneralSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmGeneralSection.swift index f4c69f3c1..8643279fb 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmGeneralSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmGeneralSection.swift @@ -14,8 +14,8 @@ struct AlarmGeneralSection: View { var body: some View { Section( header: Text("General"), - footer: Text("Give each alarm a unique name—especially if you’ve added more than one of the same kind—so you can tell them apart at a glance.")) - { + footer: Text("Give each alarm a unique name—especially if you’ve added more than one of the same kind—so you can tell them apart at a glance.") + ) { HStack { Text("Name") TextField("Alarm Name", text: $alarm.name) diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift index e959ad3e4..83f3b7543 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift @@ -6,13 +6,12 @@ // Copyright © 2025 Jon Fawcett. All rights reserved. // - import SwiftUI struct AlarmSnoozeSection: View { @Binding var alarm: Alarm let range: ClosedRange - let step: Int + let step: Int private var unitLabel: String { alarm.type.timeUnit.label } @@ -48,8 +47,8 @@ struct AlarmSnoozeSection: View { header: Text("SNOOZE"), footer: Text( "“Default Snooze” controls how long the alert stays quiet after " - + "you press Snooze. Toggle “Snoozed” to mute this alarm right now " - + "until the time below." + + "you press Snooze. Toggle “Snoozed” to mute this alarm right now " + + "until the time below." ) ) { Stepper( diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift index 1099ff501..977535e64 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift @@ -32,7 +32,7 @@ struct AlarmStepperSection: View { self.range = range self.step = step self.unitLabel = unitLabel - self._value = value + _value = value } var body: some View { diff --git a/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift b/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift index 4794f25a9..6862b5f20 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift @@ -20,7 +20,7 @@ struct InfoBanner: View { var iconColour: Color = .accentColor /// Background + border tints - var tint : Color = Color.blue.opacity(0.20) + var tint: Color = Color.blue.opacity(0.20) var border: Color = Color.blue.opacity(0.40) // ────────── View ────────── diff --git a/LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift b/LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift index c2020b0d6..145c963e4 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift @@ -1,5 +1,5 @@ // -// File.swift +// SoundFile.swift // SoundFile // // Created by Jonas Björkert on 2025-04-21. @@ -9,113 +9,113 @@ import Foundation enum SoundFile: String, CaseIterable, Identifiable, Codable { - case alarmBuzzer = "Alarm_Buzzer" - case alarmClock = "Alarm_Clock" - case alertToneBusy = "Alert_Tone_Busy" - case alertToneRingtone1 = "Alert_Tone_Ringtone_1" - case alertToneRingtone2 = "Alert_Tone_Ringtone_2" - case alienSiren = "Alien_Siren" - case ambulance = "Ambulance" - case analogWatchAlarm = "Analog_Watch_Alarm" - case bigClockTicking = "Big_Clock_Ticking" - case burglarAlarmSiren1 = "Burglar_Alarm_Siren_1" - case burglarAlarmSiren2 = "Burglar_Alarm_Siren_2" - case cartoonAscendClimbSneaky = "Cartoon_Ascend_Climb_Sneaky" - case cartoonAscendThenDescend = "Cartoon_Ascend_Then_Descend" - case cartoonBounceToCeiling = "Cartoon_Bounce_To_Ceiling" - case cartoonDreamyGlissandoHarp = "Cartoon_Dreamy_Glissando_Harp" - case cartoonFailStringsTrumpet = "Cartoon_Fail_Strings_Trumpet" - case cartoonMachineClumsyLoop = "Cartoon_Machine_Clumsy_Loop" - case cartoonSiren = "Cartoon_Siren" - case cartoonTipToeSneakyWalk = "Cartoon_Tip_Toe_Sneaky_Walk" - case cartoonUhOh = "Cartoon_Uh_Oh" - case cartoonVillainHorns = "Cartoon_Villain_Horns" - case cellPhoneRingTone = "Cell_Phone_Ring_Tone" - case chimesGlassy = "Chimes_Glassy" - case computerMagic = "Computer_Magic" - case csfx2Alarm = "CSFX-2_Alarm" - case cuckooClock = "Cuckoo_Clock" - case dholShuffleloop = "Dhol_Shuffleloop" - case discreet = "Discreet" - case earlySunrise = "Early_Sunrise" + case alarmBuzzer = "Alarm_Buzzer" + case alarmClock = "Alarm_Clock" + case alertToneBusy = "Alert_Tone_Busy" + case alertToneRingtone1 = "Alert_Tone_Ringtone_1" + case alertToneRingtone2 = "Alert_Tone_Ringtone_2" + case alienSiren = "Alien_Siren" + case ambulance = "Ambulance" + case analogWatchAlarm = "Analog_Watch_Alarm" + case bigClockTicking = "Big_Clock_Ticking" + case burglarAlarmSiren1 = "Burglar_Alarm_Siren_1" + case burglarAlarmSiren2 = "Burglar_Alarm_Siren_2" + case cartoonAscendClimbSneaky = "Cartoon_Ascend_Climb_Sneaky" + case cartoonAscendThenDescend = "Cartoon_Ascend_Then_Descend" + case cartoonBounceToCeiling = "Cartoon_Bounce_To_Ceiling" + case cartoonDreamyGlissandoHarp = "Cartoon_Dreamy_Glissando_Harp" + case cartoonFailStringsTrumpet = "Cartoon_Fail_Strings_Trumpet" + case cartoonMachineClumsyLoop = "Cartoon_Machine_Clumsy_Loop" + case cartoonSiren = "Cartoon_Siren" + case cartoonTipToeSneakyWalk = "Cartoon_Tip_Toe_Sneaky_Walk" + case cartoonUhOh = "Cartoon_Uh_Oh" + case cartoonVillainHorns = "Cartoon_Villain_Horns" + case cellPhoneRingTone = "Cell_Phone_Ring_Tone" + case chimesGlassy = "Chimes_Glassy" + case computerMagic = "Computer_Magic" + case csfx2Alarm = "CSFX-2_Alarm" + case cuckooClock = "Cuckoo_Clock" + case dholShuffleloop = "Dhol_Shuffleloop" + case discreet = "Discreet" + case earlySunrise = "Early_Sunrise" case emergencyAlarmCarbonMonoxide = "Emergency_Alarm_Carbon_Monoxide" - case emergencyAlarmSiren = "Emergency_Alarm_Siren" - case emergencyAlarm = "Emergency_Alarm" - case endingReached = "Ending_Reached" - case fly = "Fly" - case ghostHover = "Ghost_Hover" - case goodMorning = "Good_Morning" - case hellYeahSomewhatCalmer = "Hell_Yeah_Somewhat_Calmer" - case inAHurry = "In_A_Hurry" - case indeed = "Indeed" - case insistently = "Insistently" - case jingleAllTheWay = "Jingle_All_The_Way" - case laserShoot = "Laser_Shoot" - case machineCharge = "Machine_Charge" - case magicalTwinkle = "Magical_Twinkle" + case emergencyAlarmSiren = "Emergency_Alarm_Siren" + case emergencyAlarm = "Emergency_Alarm" + case endingReached = "Ending_Reached" + case fly = "Fly" + case ghostHover = "Ghost_Hover" + case goodMorning = "Good_Morning" + case hellYeahSomewhatCalmer = "Hell_Yeah_Somewhat_Calmer" + case inAHurry = "In_A_Hurry" + case indeed = "Indeed" + case insistently = "Insistently" + case jingleAllTheWay = "Jingle_All_The_Way" + case laserShoot = "Laser_Shoot" + case machineCharge = "Machine_Charge" + case magicalTwinkle = "Magical_Twinkle" case marchingHeavyFootedFatElephants = "Marching_Heavy_Footed_Fat_Elephants" - case marimbaDescend = "Marimba_Descend" - case marimbaFlutterOrShake = "Marimba_Flutter_or_Shake" - case martianGun = "Martian_Gun" - case martianScanner = "Martian_Scanner" - case metallic = "Metallic" - case nightguard = "Nightguard" - case notKiddin = "Not_Kiddin" - case openYourEyesAndSee = "Open_Your_Eyes_And_See" - case orchestralHorns = "Orchestral_Horns" - case oringz = "Oringz" - case pagerBeeps = "Pager_Beeps" - case remembersMeOfAsia = "Remembers_Me_Of_Asia" - case riseAndShine = "Rise_And_Shine" - case rush = "Rush" - case sciFiAirRaidAlarm = "Sci-Fi_Air_Raid_Alarm" - case sciFiAlarmLoop1 = "Sci-Fi_Alarm_Loop_1" - case sciFiAlarmLoop2 = "Sci-Fi_Alarm_Loop_2" - case sciFiAlarmLoop3 = "Sci-Fi_Alarm_Loop_3" - case sciFiAlarmLoop4 = "Sci-Fi_Alarm_Loop_4" - case sciFiAlarm = "Sci-Fi_Alarm" - case sciFiComputerConsoleAlarm = "Sci-Fi_Computer_Console_Alarm" - case sciFiConsoleAlarm = "Sci-Fi_Console_Alarm" - case sciFiEerieAlarm = "Sci-Fi_Eerie_Alarm" - case sciFiEngineShutDown = "Sci-Fi_Engine_Shut_Down" - case sciFiIncomingMessageAlert = "Sci-Fi_Incoming_Message_Alert" - case sciFiSpaceshipMessage = "Sci-Fi_Spaceship_Message" - case sciFiSpaceshipWarmUp = "Sci-Fi_Spaceship_Warm_Up" - case sciFiWarning = "Sci-Fi_Warning" - case signatureCorporate = "Signature_Corporate" - case siriAlertCalibrationNeeded = "Siri_Alert_Calibration_Needed" - case siriAlertDeviceMuted = "Siri_Alert_Device_Muted" + case marimbaDescend = "Marimba_Descend" + case marimbaFlutterOrShake = "Marimba_Flutter_or_Shake" + case martianGun = "Martian_Gun" + case martianScanner = "Martian_Scanner" + case metallic = "Metallic" + case nightguard = "Nightguard" + case notKiddin = "Not_Kiddin" + case openYourEyesAndSee = "Open_Your_Eyes_And_See" + case orchestralHorns = "Orchestral_Horns" + case oringz = "Oringz" + case pagerBeeps = "Pager_Beeps" + case remembersMeOfAsia = "Remembers_Me_Of_Asia" + case riseAndShine = "Rise_And_Shine" + case rush = "Rush" + case sciFiAirRaidAlarm = "Sci-Fi_Air_Raid_Alarm" + case sciFiAlarmLoop1 = "Sci-Fi_Alarm_Loop_1" + case sciFiAlarmLoop2 = "Sci-Fi_Alarm_Loop_2" + case sciFiAlarmLoop3 = "Sci-Fi_Alarm_Loop_3" + case sciFiAlarmLoop4 = "Sci-Fi_Alarm_Loop_4" + case sciFiAlarm = "Sci-Fi_Alarm" + case sciFiComputerConsoleAlarm = "Sci-Fi_Computer_Console_Alarm" + case sciFiConsoleAlarm = "Sci-Fi_Console_Alarm" + case sciFiEerieAlarm = "Sci-Fi_Eerie_Alarm" + case sciFiEngineShutDown = "Sci-Fi_Engine_Shut_Down" + case sciFiIncomingMessageAlert = "Sci-Fi_Incoming_Message_Alert" + case sciFiSpaceshipMessage = "Sci-Fi_Spaceship_Message" + case sciFiSpaceshipWarmUp = "Sci-Fi_Spaceship_Warm_Up" + case sciFiWarning = "Sci-Fi_Warning" + case signatureCorporate = "Signature_Corporate" + case siriAlertCalibrationNeeded = "Siri_Alert_Calibration_Needed" + case siriAlertDeviceMuted = "Siri_Alert_Device_Muted" case siriAlertGlucoseDroppingFast = "Siri_Alert_Glucose_Dropping_Fast" - case siriAlertGlucoseRisingFast = "Siri_Alert_Glucose_Rising_Fast" - case siriAlertHighGlucose = "Siri_Alert_High_Glucose" - case siriAlertLowGlucose = "Siri_Alert_Low_Glucose" - case siriAlertMissedReadings = "Siri_Alert_Missed_Readings" + case siriAlertGlucoseRisingFast = "Siri_Alert_Glucose_Rising_Fast" + case siriAlertHighGlucose = "Siri_Alert_High_Glucose" + case siriAlertLowGlucose = "Siri_Alert_Low_Glucose" + case siriAlertMissedReadings = "Siri_Alert_Missed_Readings" case siriAlertTransmitterBatteryLow = "Siri_Alert_Transmitter_Battery_Low" - case siriAlertUrgentHighGlucose = "Siri_Alert_Urgent_High_Glucose" - case siriAlertUrgentLowGlucose = "Siri_Alert_Urgent_Low_Glucose" - case siriCalibrationNeeded = "Siri_Calibration_Needed" - case siriDeviceMuted = "Siri_Device_Muted" - case siriGlucoseDroppingFast = "Siri_Glucose_Dropping_Fast" - case siriGlucoseRisingFast = "Siri_Glucose_Rising_Fast" - case siriHighGlucose = "Siri_High_Glucose" - case siriLowGlucose = "Siri_Low_Glucose" - case siriMissedReadings = "Siri_Missed_Readings" - case siriTransmitterBatteryLow = "Siri_Transmitter_Battery_Low" - case siriUrgentHighGlucose = "Siri_Urgent_High_Glucose" - case siriUrgentLowGlucose = "Siri_Urgent_Low_Glucose" - case softMarimbaPadPositive = "Soft_Marimba_Pad_Positive" - case softWarmAiryOptimistic = "Soft_Warm_Airy_Optimistic" - case softWarmAiryReassuring = "Soft_Warm_Airy_Reassuring" - case storeDoorChime = "Store_Door_Chime" - case sunny = "Sunny" - case thunderSoundFX = "Thunder_Sound_FX" - case timeHasCome = "Time_Has_Come" - case tornadoSiren = "Tornado_Siren" - case twoTurtleDoves = "Two_Turtle_Doves" - case unpaved = "Unpaved" - case wakeUpWillYou = "Wake_Up_Will_You" - case winGain = "Win_Gain" - case wrongAnswer = "Wrong_Answer" + case siriAlertUrgentHighGlucose = "Siri_Alert_Urgent_High_Glucose" + case siriAlertUrgentLowGlucose = "Siri_Alert_Urgent_Low_Glucose" + case siriCalibrationNeeded = "Siri_Calibration_Needed" + case siriDeviceMuted = "Siri_Device_Muted" + case siriGlucoseDroppingFast = "Siri_Glucose_Dropping_Fast" + case siriGlucoseRisingFast = "Siri_Glucose_Rising_Fast" + case siriHighGlucose = "Siri_High_Glucose" + case siriLowGlucose = "Siri_Low_Glucose" + case siriMissedReadings = "Siri_Missed_Readings" + case siriTransmitterBatteryLow = "Siri_Transmitter_Battery_Low" + case siriUrgentHighGlucose = "Siri_Urgent_High_Glucose" + case siriUrgentLowGlucose = "Siri_Urgent_Low_Glucose" + case softMarimbaPadPositive = "Soft_Marimba_Pad_Positive" + case softWarmAiryOptimistic = "Soft_Warm_Airy_Optimistic" + case softWarmAiryReassuring = "Soft_Warm_Airy_Reassuring" + case storeDoorChime = "Store_Door_Chime" + case sunny = "Sunny" + case thunderSoundFX = "Thunder_Sound_FX" + case timeHasCome = "Time_Has_Come" + case tornadoSiren = "Tornado_Siren" + case twoTurtleDoves = "Two_Turtle_Doves" + case unpaved = "Unpaved" + case wakeUpWillYou = "Wake_Up_Will_You" + case winGain = "Win_Gain" + case wrongAnswer = "Wrong_Answer" // Identifiable conformance var id: SoundFile { self } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift index db3d8c3f6..7fca8e317 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift @@ -15,14 +15,14 @@ struct BuildExpireAlarmEditor: View { Form { 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." + + "TestFlight or Xcode expiry date. Currently only works for Trio 0.4 and later." ) AlarmGeneralSection(alarm: $alarm) AlarmStepperSection( footer: "Choose how many days of notice you’d like before the build becomes unusable.", title: "Expires In", - range: 1...14, + range: 1 ... 14, step: 1, unitLabel: alarm.type.timeUnit.label, value: Binding( @@ -35,7 +35,7 @@ struct BuildExpireAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmSnoozeSection( alarm: $alarm, - range: 1...14, + range: 1 ... 14, step: 1 ) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift index f20cd8d06..1dfa73474 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift @@ -24,19 +24,19 @@ struct FastDropAlarmEditor: View { header: "Rate of Fall", footer: "How much the bg must fall to count as a “fast” drop.", title: "Drop per reading", - range: 3...20, + range: 3 ... 20, value: Binding( get: { alarm.delta ?? 18 }, set: { alarm.delta = $0 } ) ) - //TODO: In the migration script, use 1 value less than stored since we are switching from readings to drops + // TODO: In the migration script, use 1 value less than stored since we are switching from readings to drops AlarmStepperSection( header: "Consecutive Drops", footer: "Number of back-to-back drops—each meeting the rate above—required before an alert fires.", title: "Drops in a row", - range: 1...3, + range: 1 ... 3, step: 1, value: Binding( get: { Double(alarm.monitoringWindow ?? 2) }, @@ -49,7 +49,7 @@ struct FastDropAlarmEditor: View { footer: "When enabled, this alert only fires if the glucose is below the limit you set.", toggleText: "Use BG Limit", pickerTitle: "Dropping below", - range: 40...300, + range: 40 ... 300, value: $alarm.threshold ) @@ -59,7 +59,7 @@ struct FastDropAlarmEditor: View { AlarmSnoozeSection( alarm: $alarm, - range: 5...60, + range: 5 ... 60, step: 5 ) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift index c56d82125..84f0fc0a2 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift @@ -15,7 +15,7 @@ struct HighBgAlarmEditor: 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." + + "you set below. Use Persistent if you want to ignore brief spikes." ) AlarmGeneralSection(alarm: $alarm) @@ -24,7 +24,7 @@ struct HighBgAlarmEditor: View { header: "Threshold", footer: "The alarm becomes eligible once any reading is ≥ this value.", title: "BG", - range: 120...350, + range: 120 ... 350, value: Binding( get: { alarm.threshold ?? 180 }, set: { alarm.threshold = $0 } @@ -34,9 +34,9 @@ struct HighBgAlarmEditor: View { AlarmStepperSection( header: "Persistent High", footer: "How long glucose must remain above the threshold before the " - + "alarm actually fires. Set to 0 for an immediate alert.", + + "alarm actually fires. Set to 0 for an immediate alert.", title: "Persistent for", - range: 0...120, + range: 0 ... 120, step: 5, unitLabel: alarm.type.timeUnit.label, value: Binding( @@ -49,7 +49,7 @@ struct HighBgAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmSnoozeSection( alarm: $alarm, - range: 10...120, + range: 10 ... 120, step: 5 ) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift index 1972ad653..7c8a0d371 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift @@ -6,7 +6,6 @@ // Copyright © 2025 Jon Fawcett. All rights reserved. // - import SwiftUI struct LowBgAlarmEditor: View { @@ -15,15 +14,15 @@ 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.") + + "or any predicted value within the look-ahead window — " + + "falls at or below the threshold you set.") AlarmGeneralSection(alarm: $alarm) AlarmBGSection( header: "Threshold", title: "BG", - range: 40...150, + range: 40 ... 150, value: Binding( get: { alarm.threshold ?? 80 }, set: { alarm.threshold = $0 } @@ -33,9 +32,9 @@ struct LowBgAlarmEditor: View { AlarmStepperSection( header: "PERSISTENCE", footer: "Glucose must stay below the threshold for this many minutes " - + "before the alert sounds. Set 0 to alert immediately.", + + "before the alert sounds. Set 0 to alert immediately.", title: "Persistent", - range: 0...120, + range: 0 ... 120, step: 5, unitLabel: alarm.type.timeUnit.label, value: Binding( @@ -47,10 +46,10 @@ struct LowBgAlarmEditor: View { AlarmStepperSection( header: "PREDICTION", 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.", + + "if any future value is at or below the threshold, " + + "you’ll be warned early. Set 0 to disable.", title: "Predictive", - range: 0...60, + range: 0 ... 60, step: 5, unitLabel: alarm.type.timeUnit.label, value: Binding( @@ -65,7 +64,7 @@ struct LowBgAlarmEditor: View { AlarmSnoozeSection( alarm: $alarm, - range: 5...30, + range: 5 ... 30, step: 5 ) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift index 28a828f16..319751ff9 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift @@ -20,7 +20,7 @@ struct MissedReadingEditor: View { AlarmStepperSection( footer: "Choose how long the app should wait before alerting.", title: "No reading for", - range: 11...121, + range: 11 ... 121, step: 5, unitLabel: alarm.type.timeUnit.label, value: Binding( @@ -35,10 +35,9 @@ struct MissedReadingEditor: View { AlarmSnoozeSection( alarm: $alarm, - range: 10...180, + range: 10 ... 180, step: 5 ) - } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index 83e5d009b..3ff1eac58 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -13,7 +13,7 @@ struct AddAlarmSheet: View { @Environment(\.dismiss) private var dismiss private let columns = [ - GridItem(.adaptive(minimum: 110), spacing: 16) + GridItem(.adaptive(minimum: 110), spacing: 16), ] var body: some View { @@ -83,8 +83,8 @@ private enum SheetInfo: Identifiable { var id: UUID { switch self { case .picker: - return UUID(uuidString:"00000000-0000-0000-0000-000000000000")! - case .editor(let id, _): + return UUID(uuidString: "00000000-0000-0000-0000-000000000000")! + case let .editor(id, _): return id } } @@ -136,11 +136,12 @@ struct AlarmListView: View { Button("Done") { dismiss() } } ToolbarItem(placement: .navigationBarTrailing) { - Button { sheetInfo = .picker } label: { Image(systemName:"plus") } + Button { sheetInfo = .picker } label: { Image(systemName: "plus") } } } .sheet(item: $sheetInfo, - onDismiss: handleSheetDismiss) { info in + onDismiss: handleSheetDismiss) + { info in sheetContent(for: info) } } @@ -148,7 +149,8 @@ struct AlarmListView: View { private func handleSheetDismiss() { if let id = deleteAfterDismiss, - let idx = store.value.firstIndex(where: { $0.id == id }) { + let idx = store.value.firstIndex(where: { $0.id == id }) + { store.value.remove(at: idx) } deleteAfterDismiss = nil @@ -157,7 +159,6 @@ struct AlarmListView: View { @ViewBuilder private func sheetContent(for info: SheetInfo) -> some View { switch info { - case .picker: AddAlarmSheet { type in let new = Alarm(type: type) @@ -165,11 +166,11 @@ struct AlarmListView: View { sheetInfo = .editor(id: new.id, isNew: true) } - case .editor(let id, let isNew): + case let .editor(id, isNew): if let idx = store.value.firstIndex(where: { $0.id == id }) { AlarmEditor( alarm: $store.value[idx], - isNew: isNew, + isNew: isNew, onDone: { sheetInfo = nil }, onCancel: { deleteAfterDismiss = id diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index 7a5fa003e..8d1e35907 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -13,7 +13,7 @@ class AlarmManager { static let shared = AlarmManager() private let evaluators: [AlarmType: AlarmCondition] - private var lastBGAlarmTime : Date? + private var lastBGAlarmTime: Date? private init( conditionTypes: [AlarmCondition.Type] = [ @@ -47,12 +47,12 @@ class AlarmManager { switch lhs.type { case .fastDrop, .fastRise: // sort on the per-reading delta - leftVal = lhs.delta ?? (asc ? Double.infinity : -Double.infinity) + leftVal = lhs.delta ?? (asc ? Double.infinity : -Double.infinity) rightVal = rhs.delta ?? (asc ? Double.infinity : -Double.infinity) default: // sort on the BG limit threshold - leftVal = lhs.threshold ?? (asc ? Double.infinity : -Double.infinity) + leftVal = lhs.threshold ?? (asc ? Double.infinity : -Double.infinity) rightVal = rhs.threshold ?? (asc ? Double.infinity : -Double.infinity) } @@ -62,7 +62,7 @@ class AlarmManager { // 3) fallback return false } - var skipType: AlarmType? = nil + var skipType: AlarmType? let isLatestReadingRecent: Bool = { guard let last = data.bgReadings.last else { return false } @@ -76,7 +76,7 @@ class AlarmManager { } // If the alarm is based on bg values, and the value isnt recent, skip to next - if alarm.type.isBGBased && !isLatestReadingRecent { + if alarm.type.isBGBased, !isLatestReadingRecent { continue } @@ -85,7 +85,8 @@ class AlarmManager { if alarm.type.isBGBased, let lastHandled = lastBGAlarmTime, let latestDate = data.bgReadings.last?.date, - !(latestDate > lastHandled) { + !(latestDate > lastHandled) + { continue } @@ -99,12 +100,12 @@ class AlarmManager { // Evaluate the alarm condition. guard let checker = evaluators[alarm.type], checker - .shouldFire( - alarm: alarm, - data: data, - now: now, - config: Storage.shared.alarmConfiguration.value - ) + .shouldFire( + alarm: alarm, + data: data, + now: now, + config: Storage.shared.alarmConfiguration.value + ) else { // If this alarm is active, but no longer fulfill the requirements, stop it. // Continue evaluating other alarams @@ -128,7 +129,8 @@ class AlarmManager { // Store the latest bg time so we don't use it again if alarm.type.isBGBased, - let latestDate = data.bgReadings.last?.date { + let latestDate = data.bgReadings.last?.date + { lastBGAlarmTime = latestDate } break diff --git a/LoopFollow/Alarm/AlarmSettingsView.swift b/LoopFollow/Alarm/AlarmSettingsView.swift index 7b2ef325c..58d87dd7e 100644 --- a/LoopFollow/Alarm/AlarmSettingsView.swift +++ b/LoopFollow/Alarm/AlarmSettingsView.swift @@ -24,7 +24,7 @@ struct AlarmSettingsView: View { Binding( get: { var c = Calendar.current.dateComponents([.year, .month, .day], from: Date()) - c.hour = cfgStore.value.dayStart.hour + c.hour = cfgStore.value.dayStart.hour c.minute = cfgStore.value.dayStart.minute return Calendar.current.date(from: c)! }, @@ -39,7 +39,7 @@ struct AlarmSettingsView: View { Binding( get: { var c = Calendar.current.dateComponents([.year, .month, .day], from: Date()) - c.hour = cfgStore.value.nightStart.hour + c.hour = cfgStore.value.nightStart.hour c.minute = cfgStore.value.nightStart.minute return Calendar.current.date(from: c)! }, @@ -56,10 +56,10 @@ struct AlarmSettingsView: View { Section( header: Text("Snooze & Mute Options"), footer: Text(""" - Snooze All turns everything off, \ - Mute All turns off phone sounds but leaves vibration \ - and iOS notifications on - """) + Snooze All turns everything off, \ + Mute All turns off phone sounds but leaves vibration \ + and iOS notifications on + """) ) { // Snooze All Until DatePicker( @@ -145,7 +145,7 @@ struct AlarmSettingsView: View { get: { Double(cfgStore.value.forcedOutputVolume) }, set: { cfgStore.value.forcedOutputVolume = Float($0) } ), - in: 0...1, + in: 0 ... 1, step: 0.05 ) } diff --git a/LoopFollow/Alarm/AlarmType.swift b/LoopFollow/Alarm/AlarmType.swift index a20b7f776..b7b94933e 100644 --- a/LoopFollow/Alarm/AlarmType.swift +++ b/LoopFollow/Alarm/AlarmType.swift @@ -55,44 +55,44 @@ extension AlarmType { } extension AlarmType { - /// What “unit” we use for snoozeDuration for this alarmType. - var timeUnit: TimeUnit { - switch self { - case .buildExpire: - return .day - case .low, .high, .fastDrop, .fastRise, - .missedReading, .notLooping, .missedBolus, - .iob, .bolus, .cob, .recBolus, - .overrideStart, .overrideEnd, .tempTargetStart, - .tempTargetEnd: - return .minute - case .battery, .batteryDrop, .sensorChange, .pumpChange, - .pump: - return .hour + /// What “unit” we use for snoozeDuration for this alarmType. + var timeUnit: TimeUnit { + switch self { + case .buildExpire: + return .day + case .low, .high, .fastDrop, .fastRise, + .missedReading, .notLooping, .missedBolus, + .iob, .bolus, .cob, .recBolus, + .overrideStart, .overrideEnd, .tempTargetStart, + .tempTargetEnd: + return .minute + case .battery, .batteryDrop, .sensorChange, .pumpChange, + .pump: + return .hour + } } - } } enum TimeUnit { - case minute, hour, day + case minute, hour, day - /// How many seconds in one “unit” - var seconds: TimeInterval { - switch self { - case .minute: return 60 - case .hour: return 60 * 60 - case .day: return 60 * 60 * 24 + /// How many seconds in one “unit” + var seconds: TimeInterval { + switch self { + case .minute: return 60 + case .hour: return 60 * 60 + case .day: return 60 * 60 * 24 + } } - } - /// A user-facing label - var label: String { - switch self { - case .minute: return "min" //Changed from minutes to save ui space - case .hour: return "hours" - case .day: return "days" + /// A user-facing label + var label: String { + switch self { + case .minute: return "min" // Changed from minutes to save ui space + case .hour: return "hours" + case .day: return "days" + } } - } } extension AlarmType { diff --git a/LoopFollow/Application/AppDelegate.swift b/LoopFollow/Application/AppDelegate.swift index e3e825b98..8fc075a40 100644 --- a/LoopFollow/Application/AppDelegate.swift +++ b/LoopFollow/Application/AppDelegate.swift @@ -6,31 +6,30 @@ // Copyright © 2020 Jon Fawcett. All rights reserved. // -import UIKit import CoreData -import UserNotifications import EventKit +import UIKit +import UserNotifications @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? let notificationCenter = UNUserNotificationCenter.current() - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { LogManager.shared.log(category: .general, message: "App started") LogManager.shared.cleanupOldLogs() let options: UNAuthorizationOptions = [.alert, .sound, .badge] notificationCenter.requestAuthorization(options: options) { - (didAllow, error) in + didAllow, _ in if !didAllow { LogManager.shared.log(category: .general, message: "User has declined notifications") } } let store = EKEventStore() - store.requestCalendarAccess { (granted, error) in + store.requestCalendarAccess { granted, error in if !granted { LogManager.shared.log(category: .calendar, message: "Failed to get calendar access: \(String(describing: error))") return @@ -48,7 +47,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } - func applicationWillTerminate(_ application: UIApplication) { + func applicationWillTerminate(_: UIApplication) { if UserDefaultsRepository.alertAppInactive.value { AlarmSound.setSoundFile(str: "Alarm_Buzzer") AlarmSound.playTerminated() @@ -57,7 +56,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // MARK: UISceneSession Lifecycle - func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + func application(_: UIApplication, willFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // set "prevent screen lock" to ON when the app is started for the first time if !UserDefaultsRepository.screenlockSwitchState.exists { UserDefaultsRepository.screenlockSwitchState.value = true @@ -70,13 +69,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } - func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration { // Called when a new scene session is being created. // Use this method to select a configuration to create the new scene with. return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) } - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + func application(_: UIApplication, didDiscardSceneSessions _: Set) { // Called when the user discards a scene session. // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // Use this method to release any resources that were specific to the discarded scenes, as they will not return. @@ -92,7 +91,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { error conditions that could cause the creation of the store to fail. */ let container = NSPersistentCloudKitContainer(name: "LoopFollow") - container.loadPersistentStores(completionHandler: { (storeDescription, error) in + container.loadPersistentStores(completionHandler: { _, error in if let error = error as NSError? { // Replace this implementation with code to handle the error appropriately. // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. @@ -113,7 +112,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // MARK: - Core Data Saving support - func saveContext () { + func saveContext() { let context = persistentContainer.viewContext if context.hasChanges { do { @@ -127,7 +126,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } - func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + func userNotificationCenter(_: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { if response.actionIdentifier == "OPEN_APP_ACTION" { if let window = window { window.rootViewController?.dismiss(animated: true, completion: nil) @@ -144,8 +143,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } extension AppDelegate: UNUserNotificationCenterDelegate { - func userNotificationCenter(_ center: UNUserNotificationCenter, - willPresent notification: UNNotification, + func userNotificationCenter(_: UNUserNotificationCenter, + willPresent _: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler(.alert) diff --git a/LoopFollow/Application/SceneDelegate.swift b/LoopFollow/Application/SceneDelegate.swift index 1d87e90b7..4c15fac80 100644 --- a/LoopFollow/Application/SceneDelegate.swift +++ b/LoopFollow/Application/SceneDelegate.swift @@ -6,49 +6,48 @@ // Copyright © 2020 Jon Fawcett. All rights reserved. // -import UIKit import AVFoundation +import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { - var window: UIWindow? let synthesizer = AVSpeechSynthesizer() let appStateController = AppStateController() - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + + func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). guard let _ = (scene as? UIWindowScene) else { return } - + // get the tabBar guard let tabBarController = window?.rootViewController as? UITabBarController, - let viewControllers = tabBarController.viewControllers + let viewControllers = tabBarController.viewControllers else { - return - } - - // set the main controllers' connection to the app sate - // other controllers that need to know app state are setup programatically - for i in 0.. Void) { + func windowScene(_: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler _: @escaping (Bool) -> Void) { handleShortcutItem(shortcutItem) } diff --git a/LoopFollow/BackgroundRefresh/BT/BLEDevice.swift b/LoopFollow/BackgroundRefresh/BT/BLEDevice.swift index a444390cf..3a1a809d8 100644 --- a/LoopFollow/BackgroundRefresh/BT/BLEDevice.swift +++ b/LoopFollow/BackgroundRefresh/BT/BLEDevice.swift @@ -23,7 +23,8 @@ struct BLEDevice: Identifiable, Codable, Equatable { isConnected: Bool = false, advertisedServices: [String]? = nil, lastSeen: Date = Date(), - lastConnected: Date? = nil) { + lastConnected: Date? = nil) + { self.id = id self.name = name self.rssi = rssi diff --git a/LoopFollow/BackgroundRefresh/BT/BLEManager.swift b/LoopFollow/BackgroundRefresh/BT/BLEManager.swift index 9966ddcb6..6c55a2e09 100644 --- a/LoopFollow/BackgroundRefresh/BT/BLEManager.swift +++ b/LoopFollow/BackgroundRefresh/BT/BLEManager.swift @@ -3,9 +3,9 @@ // LoopFollow // -import Foundation -import CoreBluetooth import Combine +import CoreBluetooth +import Foundation class BLEManager: NSObject, ObservableObject { static let shared = BLEManager() @@ -15,7 +15,7 @@ class BLEManager: NSObject, ObservableObject { private var centralManager: CBCentralManager! private var activeDevice: BluetoothDevice? - private override init() { + override private init() { super.init() centralManager = CBCentralManager( @@ -124,6 +124,7 @@ class BLEManager: NSObject, ObservableObject { } // MARK: - CBCentralManagerDelegate + extension BLEManager: CBCentralManagerDelegate { func centralManagerDidUpdateState(_ central: CBCentralManager) { switch central.state { @@ -134,10 +135,11 @@ extension BLEManager: CBCentralManagerDelegate { } } - func centralManager(_ central: CBCentralManager, + func centralManager(_: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], - rssi RSSI: NSNumber) { + rssi RSSI: NSNumber) + { let uuid = peripheral.identifier let services = (advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID])? .map { $0.uuidString } @@ -198,7 +200,7 @@ extension BLEManager: BluetoothDeviceDelegate { return } - let marginPercentage: Double = 0.15 // 15% margin + let marginPercentage = 0.15 // 15% margin let margin = expectedInterval * marginPercentage let threshold = expectedInterval + margin @@ -253,10 +255,10 @@ extension BLEManager { // If the heartbeat interval isn't a typical 60 or 300 seconds, // we simply return a string indicating that the delay is "up to" the heartbeat interval. - if heartBeatInterval != 60 && heartBeatInterval != 300 { + if heartBeatInterval != 60, heartBeatInterval != 300 { return "up to \(Int(heartBeatInterval)) sec" } - + let effectiveDelay = CycleHelper.computeDelay(sensorOffset: expectedOffset, heartbeatLast: heartbeatLast, heartbeatInterval: heartBeatInterval) return "\(Int(effectiveDelay)) sec" diff --git a/LoopFollow/BackgroundRefresh/BT/BluetoothDevice.swift b/LoopFollow/BackgroundRefresh/BT/BluetoothDevice.swift index b7e3f60df..dbf6e665f 100644 --- a/LoopFollow/BackgroundRefresh/BT/BluetoothDevice.swift +++ b/LoopFollow/BackgroundRefresh/BT/BluetoothDevice.swift @@ -6,30 +6,30 @@ // Copyright © 2025 Jon Fawcett. All rights reserved. // -import Foundation import CoreBluetooth +import Foundation import os import UIKit class BluetoothDevice: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate { public weak var bluetoothDeviceDelegate: BluetoothDeviceDelegate? - private(set) var deviceAddress:String - private(set) var deviceName:String? - private let CBUUID_Advertisement:String? - private let servicesCBUUIDs:[CBUUID]? - private let CBUUID_ReceiveCharacteristic:String + private(set) var deviceAddress: String + private(set) var deviceName: String? + private let CBUUID_Advertisement: String? + private let servicesCBUUIDs: [CBUUID]? + private let CBUUID_ReceiveCharacteristic: String private var centralManager: CBCentralManager? private var peripheral: CBPeripheral? - private var timeStampLastStatusUpdate:Date - private var receiveCharacteristic:CBCharacteristic? + private var timeStampLastStatusUpdate: Date + private var receiveCharacteristic: CBCharacteristic? private let maxTimeToWaitForPeripheralResponse = 5.0 private var connectTimeOutTimer: Timer? var lastHeartbeatTime: Date? - init(address:String, name:String?, CBUUID_Advertisement:String?, servicesCBUUIDs:[CBUUID]?, CBUUID_ReceiveCharacteristic:String, bluetoothDeviceDelegate: BluetoothDeviceDelegate) { - self.lastHeartbeatTime = nil - self.deviceAddress = address - self.deviceName = name + init(address: String, name: String?, CBUUID_Advertisement: String?, servicesCBUUIDs: [CBUUID]?, CBUUID_ReceiveCharacteristic: String, bluetoothDeviceDelegate: BluetoothDeviceDelegate) { + lastHeartbeatTime = nil + deviceAddress = address + deviceName = name self.servicesCBUUIDs = servicesCBUUIDs self.CBUUID_Advertisement = CBUUID_Advertisement @@ -67,11 +67,11 @@ class BluetoothDevice: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate peripheral = nil deviceName = nil - //deviceAddress = nil + // deviceAddress = nil } func stopScanning() { - self.centralManager?.stopScan() + centralManager?.stopScan() } func isScanning() -> Bool { @@ -95,11 +95,11 @@ class BluetoothDevice: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate disconnect() } return .connecting - default:() + default: () } } - var services:[CBUUID]? + var services: [CBUUID]? if let CBUUID_Advertisement = CBUUID_Advertisement { services = [CBUUID(string: CBUUID_Advertisement)] } @@ -122,7 +122,7 @@ class BluetoothDevice: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate return returnValue } } else { - returnValue = .other(reason:"centralManager is nil, can not start scanning") + returnValue = .other(reason: "centralManager is nil, can not start scanning") } return returnValue @@ -141,9 +141,9 @@ class BluetoothDevice: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate fileprivate func stopScanAndconnect(to peripheral: CBPeripheral) { LogManager.shared.log(category: .bluetooth, message: "Stop Scan And Connect", isDebug: true) - self.centralManager?.stopScan() - self.deviceAddress = peripheral.identifier.uuidString - self.deviceName = peripheral.name + centralManager?.stopScan() + deviceAddress = peripheral.identifier.uuidString + deviceName = peripheral.name peripheral.delegate = self self.peripheral = peripheral @@ -167,12 +167,11 @@ class BluetoothDevice: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate connectTimeOutTimer.invalidate() self.connectTimeOutTimer = nil } - } - fileprivate func retrievePeripherals(_ central:CBCentralManager) -> Bool { + fileprivate func retrievePeripherals(_ central: CBCentralManager) -> Bool { if let uuid = UUID(uuidString: deviceAddress) { - //trace(" uuid is not nil", log: log, category: ConstantsLog.categoryBlueToothTransmitter, type: .info) + // trace(" uuid is not nil", log: log, category: ConstantsLog.categoryBlueToothTransmitter, type: .info) let peripheralArr = central.retrievePeripherals(withIdentifiers: [uuid]) if peripheralArr.count > 0 { peripheral = peripheralArr[0] @@ -186,7 +185,7 @@ class BluetoothDevice: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate return false } - func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { + func centralManager(_: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData _: [String: Any], rssi _: NSNumber) { print("[BLE] didDiscover") timeStampLastStatusUpdate = Date() @@ -196,7 +195,7 @@ class BluetoothDevice: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate } } - func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { + func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) { cancelConnectionTimer() timeStampLastStatusUpdate = Date() @@ -206,7 +205,7 @@ class BluetoothDevice: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate peripheral.discoverServices(servicesCBUUIDs) } - func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { + func centralManager(_: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { timeStampLastStatusUpdate = Date() let peripheralName = peripheral.name ?? "Unknown" @@ -227,17 +226,17 @@ class BluetoothDevice: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate } } - func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { + func centralManager(_: CBCentralManager, didDisconnectPeripheral _: CBPeripheral, error _: Error?) { timeStampLastStatusUpdate = Date() bluetoothDeviceDelegate?.didDisconnectFrom(bluetoothDevice: self) - if let ownPeripheral = self.peripheral { + if let ownPeripheral = peripheral { centralManager?.connect(ownPeripheral, options: nil) } } - func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { + func peripheral(_ peripheral: CBPeripheral, didDiscoverServices _: Error?) { timeStampLastStatusUpdate = Date() if let services = peripheral.services { @@ -249,7 +248,7 @@ class BluetoothDevice: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate } } - func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { + func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error _: Error?) { timeStampLastStatusUpdate = Date() if let characteristics = service.characteristics { @@ -262,19 +261,19 @@ class BluetoothDevice: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate } } - func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { + func peripheral(_: CBPeripheral, didWriteValueFor _: CBCharacteristic, error _: Error?) { timeStampLastStatusUpdate = Date() } - func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { + func peripheral(_: CBPeripheral, didUpdateNotificationStateFor _: CBCharacteristic, error _: Error?) { timeStampLastStatusUpdate = Date() } - func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { + func peripheral(_: CBPeripheral, didUpdateValueFor _: CBCharacteristic, error _: Error?) { timeStampLastStatusUpdate = Date() } - func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) { + func centralManager(_: CBCentralManager, willRestoreState _: [String: Any]) { LogManager.shared.log(category: .bluetooth, message: "Restoring BLE after crash/kill") } @@ -295,7 +294,7 @@ class BluetoothDevice: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate case unknown case unauthorized case nfcScanNeeded - case other(reason:String) + case other(reason: String) func description() -> String { switch self { @@ -309,7 +308,7 @@ class BluetoothDevice: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate return "alreadyConnected" case .connecting: return "connecting" - case .other(let reason): + case let .other(reason): return "other reason : " + reason case .unknown: return "unknown" diff --git a/LoopFollow/BackgroundRefresh/BT/BluetoothDeviceDelegate.swift b/LoopFollow/BackgroundRefresh/BT/BluetoothDeviceDelegate.swift index 2050b2806..a1d254bae 100644 --- a/LoopFollow/BackgroundRefresh/BT/BluetoothDeviceDelegate.swift +++ b/LoopFollow/BackgroundRefresh/BT/BluetoothDeviceDelegate.swift @@ -6,8 +6,8 @@ // Copyright © 2025 Jon Fawcett. All rights reserved. // -import Foundation import CoreBluetooth +import Foundation protocol BluetoothDeviceDelegate: AnyObject { func didConnectTo(bluetoothDevice: BluetoothDevice) diff --git a/LoopFollow/BackgroundRefresh/BT/Devices/DexcomHeartbeatBluetoothDevice.swift b/LoopFollow/BackgroundRefresh/BT/Devices/DexcomHeartbeatBluetoothDevice.swift index 325c12658..d16f7b8ad 100644 --- a/LoopFollow/BackgroundRefresh/BT/Devices/DexcomHeartbeatBluetoothDevice.swift +++ b/LoopFollow/BackgroundRefresh/BT/Devices/DexcomHeartbeatBluetoothDevice.swift @@ -6,17 +6,17 @@ // Copyright © 2025 Jon Fawcett. All rights reserved. // +import AVFoundation +import CoreBluetooth import Foundation import os -import CoreBluetooth -import AVFoundation class DexcomHeartbeatBluetoothDevice: BluetoothDevice { private let CBUUID_Service_G7 = "F8083532-849E-531C-C594-30F1F86A4EA5" private let CBUUID_Advertisement_G7 = "FEBC" private let CBUUID_ReceiveCharacteristic_G7 = "F8083535-849E-531C-C594-30F1F86A4EA5" - init(address:String, name:String?, bluetoothDeviceDelegate: BluetoothDeviceDelegate) { + init(address: String, name: String?, bluetoothDeviceDelegate: BluetoothDeviceDelegate) { super.init( address: address, name: name, @@ -29,7 +29,7 @@ class DexcomHeartbeatBluetoothDevice: BluetoothDevice { override func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { super.centralManager(central, didDisconnectPeripheral: peripheral, error: error) - self.bluetoothDeviceDelegate?.heartBeat() + bluetoothDeviceDelegate?.heartBeat() } override func expectedHeartbeatInterval() -> TimeInterval? { diff --git a/LoopFollow/BackgroundRefresh/BT/Devices/OmnipodDashHeartbeatBluetoothTransmitter.swift b/LoopFollow/BackgroundRefresh/BT/Devices/OmnipodDashHeartbeatBluetoothTransmitter.swift index e1a3d5705..e383c4c06 100644 --- a/LoopFollow/BackgroundRefresh/BT/Devices/OmnipodDashHeartbeatBluetoothTransmitter.swift +++ b/LoopFollow/BackgroundRefresh/BT/Devices/OmnipodDashHeartbeatBluetoothTransmitter.swift @@ -6,8 +6,8 @@ // Copyright © 2025 Jon Fawcett. All rights reserved. // -import Foundation import CoreBluetooth +import Foundation class OmnipodDashHeartbeatBluetoothTransmitter: BluetoothDevice { private let CBUUID_Service: String = "1A7E4024-E3ED-4464-8B7E-751E03D0DC5F" @@ -16,7 +16,7 @@ class OmnipodDashHeartbeatBluetoothTransmitter: BluetoothDevice { private let CBUUID_ReceiveCharacteristic_Data: String = "" - init(address:String, name:String?, bluetoothDeviceDelegate: BluetoothDeviceDelegate) { + init(address: String, name: String?, bluetoothDeviceDelegate: BluetoothDeviceDelegate) { super.init( address: address, name: name, @@ -34,7 +34,7 @@ class OmnipodDashHeartbeatBluetoothTransmitter: BluetoothDevice { override func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { super.peripheral(peripheral, didUpdateValueFor: characteristic, error: error) - self.bluetoothDeviceDelegate?.heartBeat() + bluetoothDeviceDelegate?.heartBeat() } override func expectedHeartbeatInterval() -> TimeInterval? { diff --git a/LoopFollow/BackgroundRefresh/BT/Devices/RileyLinkHeartbeatBluetoothDevice.swift b/LoopFollow/BackgroundRefresh/BT/Devices/RileyLinkHeartbeatBluetoothDevice.swift index 19e38a086..a89d2ee50 100644 --- a/LoopFollow/BackgroundRefresh/BT/Devices/RileyLinkHeartbeatBluetoothDevice.swift +++ b/LoopFollow/BackgroundRefresh/BT/Devices/RileyLinkHeartbeatBluetoothDevice.swift @@ -1,20 +1,20 @@ // -// RileyLinkHeartbeatBluetoothTransmitter.swift +// RileyLinkHeartbeatBluetoothDevice.swift // LoopFollow // // Created by Jonas Björkert on 2025-01-08. // Copyright © 2025 Jon Fawcett. All rights reserved. // -import Foundation import CoreBluetooth +import Foundation class RileyLinkHeartbeatBluetoothDevice: BluetoothDevice { private let CBUUID_Service_RileyLink: String = "0235733B-99C5-4197-B856-69219C2A3845" private let CBUUID_ReceiveCharacteristic_TimerTick: String = "6E6C7910-B89E-43A5-78AF-50C5E2B86F7E" private let CBUUID_ReceiveCharacteristic_Data: String = "C842E849-5028-42E2-867C-016ADADA9155" - init(address:String, name:String?, bluetoothDeviceDelegate: BluetoothDeviceDelegate) { + init(address: String, name: String?, bluetoothDeviceDelegate: BluetoothDeviceDelegate) { super.init( address: address, name: name, @@ -36,7 +36,7 @@ class RileyLinkHeartbeatBluetoothDevice: BluetoothDevice { return } - self.bluetoothDeviceDelegate?.heartBeat() + bluetoothDeviceDelegate?.heartBeat() } override func expectedHeartbeatInterval() -> TimeInterval? { diff --git a/LoopFollow/BackgroundRefresh/BT/DexcomG7HeartBeat.swift b/LoopFollow/BackgroundRefresh/BT/DexcomG7HeartBeat.swift index 6dff968c5..861b34739 100644 --- a/LoopFollow/BackgroundRefresh/BT/DexcomG7HeartBeat.swift +++ b/LoopFollow/BackgroundRefresh/BT/DexcomG7HeartBeat.swift @@ -13,7 +13,6 @@ import Foundation /// A simple class to represent the Dexcom G7 Heartbeat. /// It wraps around a `BLEPeripheral` to store relevant information. public class DexcomG7HeartBeat { - // MARK: - Properties /// The BLEPeripheral instance associated with this heartbeat. @@ -27,7 +26,7 @@ public class DexcomG7HeartBeat { /// - name: The name of the BLE device. /// - alias: An optional alias for the device. public init(address: String, name: String, alias: String? = nil) { - self.blePeripheral = BLEPeripheral( + blePeripheral = BLEPeripheral( address: address, name: name, alias: alias, diff --git a/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift b/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift index e9a76dc6e..4afbdfa1c 100644 --- a/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift +++ b/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift @@ -96,8 +96,7 @@ struct BackgroundRefreshSettingsView: View { deviceConnectionStatus(for: storedDevice) - if(storedDevice.rssi != 0) - { + if storedDevice.rssi != 0 { Text("RSSI: \(storedDevice.rssi) dBm") .foregroundColor(.secondary) .font(.footnote) diff --git a/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsViewModel.swift b/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsViewModel.swift index 3cf22a499..a878f11ee 100644 --- a/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsViewModel.swift +++ b/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsViewModel.swift @@ -5,8 +5,8 @@ // Created by Jonas Björkert on 2025-01-02. // -import Foundation import Combine +import Foundation class BackgroundRefreshSettingsViewModel: ObservableObject { @Published var backgroundRefreshType: BackgroundRefreshType @@ -17,7 +17,7 @@ class BackgroundRefreshSettingsViewModel: ObservableObject { private var isInitialSetup = true // Tracks whether the value is being set initially init() { - self.backgroundRefreshType = storage.backgroundRefreshType.value + backgroundRefreshType = storage.backgroundRefreshType.value setupBindings() } diff --git a/LoopFollow/Contact/ContactColorOption.swift b/LoopFollow/Contact/ContactColorOption.swift index 8c4297f82..18e171e9a 100644 --- a/LoopFollow/Contact/ContactColorOption.swift +++ b/LoopFollow/Contact/ContactColorOption.swift @@ -13,15 +13,15 @@ enum ContactColorOption: String, CaseIterable { var uiColor: UIColor { switch self { - case .red: return .red - case .blue: return .blue - case .cyan: return .cyan - case .green: return .green + case .red: return .red + case .blue: return .blue + case .cyan: return .cyan + case .green: return .green case .yellow: return .yellow case .orange: return .orange case .purple: return .purple - case .white: return .white - case .black: return .black + case .white: return .white + case .black: return .black } } } diff --git a/LoopFollow/Contact/ContactImageUpdater.swift b/LoopFollow/Contact/ContactImageUpdater.swift index 87e0f8e10..8898ae410 100644 --- a/LoopFollow/Contact/ContactImageUpdater.swift +++ b/LoopFollow/Contact/ContactImageUpdater.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Jon Fawcett. All rights reserved. // -import Foundation import Contacts +import Foundation import UIKit class ContactImageUpdater { @@ -34,11 +34,11 @@ class ContactImageUpdater { let bundleDisplayName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ?? "LoopFollow" for contactType in ContactType.allCases { - if contactType == .Delta && Storage.shared.contactDelta.value != .separate { + if contactType == .Delta, Storage.shared.contactDelta.value != .separate { continue } - if contactType == .Trend && Storage.shared.contactTrend.value != .separate { + if contactType == .Trend, Storage.shared.contactTrend.value != .separate { continue } @@ -91,25 +91,25 @@ class ContactImageUpdater { // Format extraDelta based on the user's unit preference let unitPreference = UserDefaultsRepository.units.value let yOffset: CGFloat = 48 - if contactType == .Trend && Storage.shared.contactTrend.value == .separate { + if contactType == .Trend, Storage.shared.contactTrend.value == .separate { let trendRect = CGRect(x: 0, y: 46, width: size.width, height: size.height - 80) let trendFontSize = max(40, 200 - CGFloat(trend.count * 15)) let trendAttributes: [NSAttributedString.Key: Any] = [ .font: UIFont.boldSystemFont(ofSize: trendFontSize), .foregroundColor: stale ? UIColor.gray : savedTextUIColor, - .paragraphStyle: paragraphStyle + .paragraphStyle: paragraphStyle, ] trend.draw(in: trendRect, withAttributes: trendAttributes) - } else if contactType == .Delta && Storage.shared.contactDelta.value == .separate { + } else if contactType == .Delta, Storage.shared.contactDelta.value == .separate { let deltaRect = CGRect(x: 0, y: yOffset, width: size.width, height: size.height - 80) let deltaFontSize = max(40, 200 - CGFloat(delta.count * 15)) let deltaAttributes: [NSAttributedString.Key: Any] = [ .font: UIFont.boldSystemFont(ofSize: deltaFontSize), .foregroundColor: stale ? UIColor.gray : savedTextUIColor, - .paragraphStyle: paragraphStyle + .paragraphStyle: paragraphStyle, ] delta.draw(in: deltaRect, withAttributes: deltaAttributes) @@ -121,7 +121,7 @@ class ContactImageUpdater { var bgAttributes: [NSAttributedString.Key: Any] = [ .font: UIFont.boldSystemFont(ofSize: fontSize), .foregroundColor: stale ? UIColor.gray : savedTextUIColor, - .paragraphStyle: paragraphStyle + .paragraphStyle: paragraphStyle, ] if stale { @@ -132,8 +132,8 @@ class ContactImageUpdater { } let bgRect: CGRect = includesExtra - ? CGRect(x: 0, y: yOffset - 20, width: size.width, height: size.height / 2) - : CGRect(x: 0, y: yOffset, width: size.width, height: size.height - 80) + ? CGRect(x: 0, y: yOffset - 20, width: size.width, height: size.height / 2) + : CGRect(x: 0, y: yOffset, width: size.width, height: size.height - 80) bgValue.draw(in: bgRect, withAttributes: bgAttributes) @@ -142,7 +142,7 @@ class ContactImageUpdater { let extraAttributes: [NSAttributedString.Key: Any] = [ .font: UIFont.systemFont(ofSize: 90), .foregroundColor: stale ? UIColor.gray : savedTextUIColor, - .paragraphStyle: paragraphStyle + .paragraphStyle: paragraphStyle, ] let extra = Storage.shared.contactDelta.value == .include ? delta : trend diff --git a/LoopFollow/Contact/ContactType.swift b/LoopFollow/Contact/ContactType.swift index 764e783b5..4eab0fc9a 100644 --- a/LoopFollow/Contact/ContactType.swift +++ b/LoopFollow/Contact/ContactType.swift @@ -1,5 +1,5 @@ // -// ContactSuffix.swift +// ContactType.swift // LoopFollow // // Created by Jonas Björkert on 2025-02-23. @@ -7,7 +7,7 @@ // enum ContactType: String, CaseIterable { - case BG = "BG" - case Trend = "Trend" - case Delta = "Delta" + case BG + case Trend + case Delta } diff --git a/LoopFollow/Contact/Settings/ContactSettingsView.swift b/LoopFollow/Contact/Settings/ContactSettingsView.swift index b49401002..fd94dd267 100644 --- a/LoopFollow/Contact/Settings/ContactSettingsView.swift +++ b/LoopFollow/Contact/Settings/ContactSettingsView.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Jon Fawcett. All rights reserved. // -import SwiftUI import Contacts +import SwiftUI struct ContactSettingsView: View { @ObservedObject var viewModel: ContactSettingsViewModel @@ -102,7 +102,7 @@ struct ContactSettingsView: View { if status == .authorized { // Already authorized, do nothing } else if status == .notDetermined { - contactStore.requestAccess(for: .contacts) { granted, error in + contactStore.requestAccess(for: .contacts) { granted, _ in DispatchQueue.main.async { if !granted { viewModel.contactEnabled = false diff --git a/LoopFollow/Contact/Settings/ContactSettingsViewModel.swift b/LoopFollow/Contact/Settings/ContactSettingsViewModel.swift index 7d058bc6c..374a199bb 100644 --- a/LoopFollow/Contact/Settings/ContactSettingsViewModel.swift +++ b/LoopFollow/Contact/Settings/ContactSettingsViewModel.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Jon Fawcett. All rights reserved. // -import Foundation import Combine +import Foundation extension Bundle { var displayName: String { @@ -53,7 +53,7 @@ class ContactSettingsViewModel: ObservableObject { triggerRefresh() } } - + @Published var contactTextColor: String { didSet { Storage.shared.contactTextColor.value = contactTextColor @@ -65,11 +65,11 @@ class ContactSettingsViewModel: ObservableObject { private var cancellables = Set() init() { - self.contactEnabled = Storage.shared.contactEnabled.value - self.contactTrend = Storage.shared.contactTrend.value - self.contactDelta = Storage.shared.contactDelta.value - self.contactBackgroundColor = Storage.shared.contactBackgroundColor.value - self.contactTextColor = Storage.shared.contactTextColor.value + contactEnabled = Storage.shared.contactEnabled.value + contactTrend = Storage.shared.contactTrend.value + contactDelta = Storage.shared.contactDelta.value + contactBackgroundColor = Storage.shared.contactBackgroundColor.value + contactTextColor = Storage.shared.contactTextColor.value Storage.shared.contactEnabled.$value .assign(to: &$contactEnabled) @@ -82,7 +82,7 @@ class ContactSettingsViewModel: ObservableObject { Storage.shared.contactBackgroundColor.$value .assign(to: &$contactBackgroundColor) - + Storage.shared.contactTextColor.$value .assign(to: &$contactTextColor) } diff --git a/LoopFollow/Controllers/AlarmSound.swift b/LoopFollow/Controllers/AlarmSound.swift index 38ab115be..6f5b3839a 100644 --- a/LoopFollow/Controllers/AlarmSound.swift +++ b/LoopFollow/Controllers/AlarmSound.swift @@ -6,8 +6,8 @@ // Copyright © 2016 private. All rights reserved. // -import Foundation import AVFoundation +import Foundation import MediaPlayer import UIKit @@ -15,233 +15,223 @@ import UIKit * Class that handles the playing and the volume of the alarm sound. */ class AlarmSound { - static var isPlaying: Bool { - return self.audioPlayer?.isPlaying == true + return audioPlayer?.isPlaying == true } - + static var isMuted: Bool { - return self.muted + return muted } + static var whichAlarm: String = "none" static var soundFile = "Indeed" static var isTesting: Bool = false - - //static let volumeChangeDetector = VolumeChangeDetector() - + + // static let volumeChangeDetector = VolumeChangeDetector() + static let vibrate = UserDefaultsRepository.vibrate - + fileprivate static var systemOutputVolumeBeforeOverride: Float? - + fileprivate static var playingTimer: Timer? - + fileprivate static var soundURL = Bundle.main.url(forResource: "Indeed", withExtension: "caf")! fileprivate static var audioPlayer: AVAudioPlayer? fileprivate static let audioPlayerDelegate = AudioPlayerDelegate() - + fileprivate static var muted = false - + fileprivate static var alarmPlayingForTimer = Timer() fileprivate static let alarmPlayingForInterval = 290 - + fileprivate func startAlarmPlayingForTimer(time: TimeInterval) { AlarmSound.alarmPlayingForTimer = Timer.scheduledTimer(timeInterval: time, - target: self, - selector: #selector(AlarmSound.alarmPlayingForTimerDidEnd(_:)), - userInfo: nil, - repeats: true) + target: self, + selector: #selector(AlarmSound.alarmPlayingForTimerDidEnd(_:)), + userInfo: nil, + repeats: true) } - - @objc func alarmPlayingForTimerDidEnd(_ timer:Timer) { + + @objc func alarmPlayingForTimerDidEnd(_: Timer) { if !AlarmSound.isPlaying { return } AlarmSound.stop() } - + /* * Sets the audio volume to 0. */ static func muteVolume() { - self.audioPlayer?.volume = 0 - self.muted = true - self.restoreSystemOutputVolume() + audioPlayer?.volume = 0 + muted = true + restoreSystemOutputVolume() } - + static func setSoundFile(str: String) { - self.soundURL = Bundle.main.url(forResource: str, withExtension: "caf")! + soundURL = Bundle.main.url(forResource: str, withExtension: "caf")! } - + /* * Sets the volume of the alarm back to the volume before it has been muted. */ static func unmuteVolume() { if UserDefaultsRepository.fadeInTimeInterval.value > 0 { - self.audioPlayer?.setVolume(1.0, fadeDuration: UserDefaultsRepository.fadeInTimeInterval.value) + audioPlayer?.setVolume(1.0, fadeDuration: UserDefaultsRepository.fadeInTimeInterval.value) } else { - self.audioPlayer?.volume = 1.0 + audioPlayer?.volume = 1.0 } - self.muted = false + muted = false } - + static func stop() { - self.playingTimer?.invalidate() - self.playingTimer = nil - - self.audioPlayer?.stop() - self.audioPlayer = nil - - self.restoreSystemOutputVolume() + playingTimer?.invalidate() + playingTimer = nil + + audioPlayer?.stop() + audioPlayer = nil + + restoreSystemOutputVolume() } - + static func playTest() { - - guard !self.isPlaying else { + guard !isPlaying else { return } - + do { - self.audioPlayer = try AVAudioPlayer(contentsOf: self.soundURL) - self.audioPlayer!.delegate = self.audioPlayerDelegate - + audioPlayer = try AVAudioPlayer(contentsOf: soundURL) + audioPlayer!.delegate = audioPlayerDelegate + try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category(rawValue: convertFromAVAudioSessionCategory(AVAudioSession.Category.playback))) try AVAudioSession.sharedInstance().setActive(true) - - self.audioPlayer?.numberOfLoops = 0 - + + audioPlayer?.numberOfLoops = 0 + // init volume before start playing (mute if fade-in) - - //self.audioPlayer!.volume = (self.muted || (UserDefaultsRepository.fadeInTimeInterval.value > 0)) ? 0.0 : 1.0 - - if !self.audioPlayer!.prepareToPlay() { + + // self.audioPlayer!.volume = (self.muted || (UserDefaultsRepository.fadeInTimeInterval.value > 0)) ? 0.0 : 1.0 + + if !audioPlayer!.prepareToPlay() { LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed preparing to play") } - - if self.audioPlayer!.play() { - if !self.isPlaying { + + if audioPlayer!.play() { + if !isPlaying { LogManager.shared.log(category: .alarm, message: "AlarmSound - not playing after calling play") - LogManager.shared.log(category: .alarm, message: "AlarmSound - rate value: \(self.audioPlayer!.rate)") + LogManager.shared.log(category: .alarm, message: "AlarmSound - rate value: \(audioPlayer!.rate)") } } else { LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed to play") } - - - } catch let error { + + } catch { LogManager.shared.log(category: .alarm, message: "AlarmSound - unable to play sound; error: \(error)") } } - - + static func play(repeating: Bool) { - guard !self.isPlaying else { + guard !isPlaying else { return } enableAudio() do { - self.audioPlayer = try AVAudioPlayer(contentsOf: self.soundURL) - self.audioPlayer!.delegate = self.audioPlayerDelegate - + audioPlayer = try AVAudioPlayer(contentsOf: soundURL) + audioPlayer!.delegate = audioPlayerDelegate + try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category(rawValue: convertFromAVAudioSessionCategory(AVAudioSession.Category.playback))) try AVAudioSession.sharedInstance().setActive(true) - - self.audioPlayer!.numberOfLoops = repeating ? -1 : 0 + + audioPlayer!.numberOfLoops = repeating ? -1 : 0 // Store existing volume - if self.systemOutputVolumeBeforeOverride == nil { - self.systemOutputVolumeBeforeOverride = AVAudioSession.sharedInstance().outputVolume + if systemOutputVolumeBeforeOverride == nil { + systemOutputVolumeBeforeOverride = AVAudioSession.sharedInstance().outputVolume } - + // init volume before start playing (mute if fade-in) - //self.audioPlayer!.volume = (self.muted || (UserDefaultsRepository.fadeInTimeInterval.value > 0)) ? 0.0 : 1.0 - - if !self.audioPlayer!.prepareToPlay() { + // self.audioPlayer!.volume = (self.muted || (UserDefaultsRepository.fadeInTimeInterval.value > 0)) ? 0.0 : 1.0 + + if !audioPlayer!.prepareToPlay() { LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed preparing to play") } - - if self.audioPlayer!.play() { - if !self.isPlaying { + + if audioPlayer!.play() { + if !isPlaying { LogManager.shared.log(category: .alarm, message: "AlarmSound - not playing after calling play") - LogManager.shared.log(category: .alarm, message: "AlarmSound - rate value: \(self.audioPlayer!.rate)") + LogManager.shared.log(category: .alarm, message: "AlarmSound - rate value: \(audioPlayer!.rate)") } } else { LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed to play") } - - + // do fade-in - //if !self.muted && (UserDefaultsRepository.fadeInTimeInterval.value > 0) { + // if !self.muted && (UserDefaultsRepository.fadeInTimeInterval.value > 0) { // self.audioPlayer!.setVolume(1.0, fadeDuration: UserDefaultsRepository.fadeInTimeInterval.value) - //} - + // } + if Storage.shared.alarmConfiguration.value.overrideSystemOutputVolume { MPVolumeView.setVolume(Storage.shared.alarmConfiguration.value.forcedOutputVolume) } - } catch let error { + } catch { LogManager.shared.log(category: .alarm, message: "AlarmSound - unable to play sound; error: \(error)") } } - + static func playTerminated() { - - guard !self.isPlaying else { + guard !isPlaying else { return } - + do { - self.audioPlayer = try AVAudioPlayer(contentsOf: self.soundURL) - self.audioPlayer!.delegate = self.audioPlayerDelegate - + audioPlayer = try AVAudioPlayer(contentsOf: soundURL) + audioPlayer!.delegate = audioPlayerDelegate + try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category(rawValue: convertFromAVAudioSessionCategory(AVAudioSession.Category.playback))) try AVAudioSession.sharedInstance().setActive(true) - + // Play endless loops - self.audioPlayer!.numberOfLoops = 2 - + audioPlayer!.numberOfLoops = 2 + // Store existing volume - if self.systemOutputVolumeBeforeOverride == nil { - self.systemOutputVolumeBeforeOverride = AVAudioSession.sharedInstance().outputVolume + if systemOutputVolumeBeforeOverride == nil { + systemOutputVolumeBeforeOverride = AVAudioSession.sharedInstance().outputVolume } - - - if !self.audioPlayer!.prepareToPlay() { + + if !audioPlayer!.prepareToPlay() { LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - audio player failed preparing to play") } - - if self.audioPlayer!.play() { - if !self.isPlaying { + + if audioPlayer!.play() { + if !isPlaying { LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - not playing after calling play") - LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - rate value: \(self.audioPlayer!.rate)") + LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - rate value: \(audioPlayer!.rate)") } } else { LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - audio player failed to play") } - - + MPVolumeView.setVolume(1.0) - - - } catch let error { + + } catch { LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - unable to play sound; error: \(error)") } } - fileprivate static func restoreSystemOutputVolume() { - guard UserDefaultsRepository.overrideSystemOutputVolume.value else { return } - + // cancel any volume change observations - // self.volumeChangeDetector.isActive = false - + // self.volumeChangeDetector.isActive = false + // restore system output volume with its value before overriding it - if let volumeBeforeOverride = self.systemOutputVolumeBeforeOverride { + if let volumeBeforeOverride = systemOutputVolumeBeforeOverride { MPVolumeView.setVolume(volumeBeforeOverride) } - - self.systemOutputVolumeBeforeOverride = nil + + systemOutputVolumeBeforeOverride = nil } fileprivate static func enableAudio() { @@ -255,42 +245,39 @@ class AlarmSound { } class AudioPlayerDelegate: NSObject, AVAudioPlayerDelegate { - /* audioPlayerDidFinishPlaying:successfully: is called when a sound has finished playing. This method is NOT called if the player is stopped due to an interruption. */ - func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { + func audioPlayerDidFinishPlaying(_: AVAudioPlayer, successfully flag: Bool) { LogManager.shared.log(category: .alarm, message: "AlarmRule - audioPlayerDidFinishPlaying (\(flag))", isDebug: true) } - + /* if an error occurs while decoding it will be reported to the delegate. */ - func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) { + func audioPlayerDecodeErrorDidOccur(_: AVAudioPlayer, error: Error?) { if let error = error { LogManager.shared.log(category: .alarm, message: "AlarmRule - audioPlayerDecodeErrorDidOccur: \(error)") } else { LogManager.shared.log(category: .alarm, message: "AlarmRule - audioPlayerDecodeErrorDidOccur") } } - + /* AVAudioPlayer INTERRUPTION NOTIFICATIONS ARE DEPRECATED - Use AVAudioSession instead. */ - + /* audioPlayerBeginInterruption: is called when the audio session has been interrupted while the player was playing. The player will have been paused. */ - func audioPlayerBeginInterruption(_ player: AVAudioPlayer) { + func audioPlayerBeginInterruption(_: AVAudioPlayer) { LogManager.shared.log(category: .alarm, message: "AlarmRule - audioPlayerBeginInterruption") } - - + /* audioPlayerEndInterruption:withOptions: is called when the audio session interruption has ended and this player had been interrupted while playing. */ /* Currently the only flag is AVAudioSessionInterruptionFlags_ShouldResume. */ - func audioPlayerEndInterruption(_ player: AVAudioPlayer, withOptions flags: Int) { + func audioPlayerEndInterruption(_: AVAudioPlayer, withOptions flags: Int) { LogManager.shared.log(category: .alarm, message: "AlarmRule - audioPlayerEndInterruption withOptions: \(flags)") } } // Helper function inserted by Swift 4.2 migrator. -fileprivate func convertFromAVAudioSessionCategory(_ input: AVAudioSession.Category) -> String { - return input.rawValue +private func convertFromAVAudioSessionCategory(_ input: AVAudioSession.Category) -> String { + return input.rawValue } - extension MPVolumeView { static func setVolume(_ volume: Float) { // Need to use the MPVolumeView in order to change volume, but don't care about UI set so frame to .zero diff --git a/LoopFollow/Controllers/AppStateController.swift b/LoopFollow/Controllers/AppStateController.swift index 18053ca7c..3e1764b2b 100644 --- a/LoopFollow/Controllers/AppStateController.swift +++ b/LoopFollow/Controllers/AppStateController.swift @@ -16,26 +16,26 @@ import Foundation // Graph Setup Flags enum ChartSettingsChangeEnum: Int { - case chartScaleXChanged = 1 - case showDotsChanged = 2 - case showLinesChanged = 4 - case offsetCarbsBolusChanged = 8 - case hoursToLoadChanged = 16 - case predictionToLoadChanged = 32 - case minBasalScaleChanged = 64 - case minBGScaleChanged = 128 - case overrideDisplayLocationChanged = 256 - case lowLineChanged = 512 - case highLineChanged = 1024 - case smallGraphHeight = 2048 - case showDIALinesChanged = 4096 - case showMidnightLinesChanged = 8192 - case show30MinLineChanged = 16384 - case show90MinLineChanged = 32768 + case chartScaleXChanged = 1 + case showDotsChanged = 2 + case showLinesChanged = 4 + case offsetCarbsBolusChanged = 8 + case hoursToLoadChanged = 16 + case predictionToLoadChanged = 32 + case minBasalScaleChanged = 64 + case minBGScaleChanged = 128 + case overrideDisplayLocationChanged = 256 + case lowLineChanged = 512 + case highLineChanged = 1024 + case smallGraphHeight = 2048 + case showDIALinesChanged = 4096 + case showMidnightLinesChanged = 8192 + case show30MinLineChanged = 16384 + case show90MinLineChanged = 32768 } // General Settings Flags -enum GeneralSettingsChangeEnum: Int { +enum GeneralSettingsChangeEnum: Int { case colorBGTextChange = 1 case speakBGChange = 2 case appBadgeChange = 16 @@ -51,17 +51,16 @@ enum GeneralSettingsChangeEnum: Int { } class AppStateController { - - // add app states & methods here + // add app states & methods here - // General Settings States - var generalSettingsChanged : Bool = false - var generalSettingsChanges : Int = 0 + // General Settings States + var generalSettingsChanged: Bool = false + var generalSettingsChanges: Int = 0 - // Chart Settings State - var chartSettingsChanged : Bool = false // settings change has ocurred - var chartSettingsChanges: Int = 0 // what settings have changed - - // Info Data Settings State; no need for flags - var infoDataSettingsChanged: Bool = false + // Chart Settings State + var chartSettingsChanged: Bool = false // settings change has ocurred + var chartSettingsChanges: Int = 0 // what settings have changed + + // Info Data Settings State; no need for flags + var infoDataSettingsChanged: Bool = false } diff --git a/LoopFollow/Controllers/BackgroundAlertManager.swift b/LoopFollow/Controllers/BackgroundAlertManager.swift index 4b73b2780..614332d20 100644 --- a/LoopFollow/Controllers/BackgroundAlertManager.swift +++ b/LoopFollow/Controllers/BackgroundAlertManager.swift @@ -92,7 +92,7 @@ class BackgroundAlertManager { body: isBluetoothActive ? "App inactive for 18 minutes. Verify Bluetooth connectivity." : "App inactive for 18 minutes. Open to resume." - ) + ), ] for alert in alerts { diff --git a/LoopFollow/Controllers/Graphs.swift b/LoopFollow/Controllers/Graphs.swift index 3f70cda7e..eaf55199e 100644 --- a/LoopFollow/Controllers/Graphs.swift +++ b/LoopFollow/Controllers/Graphs.swift @@ -6,8 +6,8 @@ // Copyright © 2020 Jon Fawcett. All rights reserved. // -import Foundation import Charts +import Foundation import UIKit import Charts @@ -63,13 +63,13 @@ class CompositeRenderer: LineChartRenderer { let triangleRenderer: TriangleRenderer init(dataProvider: LineChartDataProvider?, animator: Animator?, viewPortHandler: ViewPortHandler?, tempTargetDataSetIndex: Int, smbDataSetIndex: Int) { - self.tempTargetRenderer = TempTargetRenderer( + tempTargetRenderer = TempTargetRenderer( dataProvider: dataProvider, animator: animator, viewPortHandler: viewPortHandler, tempTargetDataSetIndex: tempTargetDataSetIndex ) - self.triangleRenderer = TriangleRenderer( + triangleRenderer = TriangleRenderer( dataProvider: dataProvider, animator: animator, viewPortHandler: viewPortHandler, @@ -87,36 +87,36 @@ class CompositeRenderer: LineChartRenderer { class TriangleRenderer: LineChartRenderer { let smbDataSetIndex: Int - + init(dataProvider: LineChartDataProvider?, animator: Animator?, viewPortHandler: ViewPortHandler?, smbDataSetIndex: Int) { self.smbDataSetIndex = smbDataSetIndex super.init(dataProvider: dataProvider!, animator: animator!, viewPortHandler: viewPortHandler!) } - + override func drawExtras(context: CGContext) { super.drawExtras(context: context) - + guard let dataProvider = dataProvider else { return } - + if dataProvider.lineData?.dataSets.count ?? 0 > smbDataSetIndex, let lineDataSet = dataProvider.lineData?.dataSets[smbDataSetIndex] as? LineChartDataSet { let trans = dataProvider.getTransformer(forAxis: lineDataSet.axisDependency) let phaseY = animator.phaseY - + for j in 0 ..< lineDataSet.entryCount { guard let e = lineDataSet.entryForIndex(j) else { continue } - + let pt = trans.pixelForValues(x: e.x, y: e.y * phaseY) - + context.saveGState() context.beginPath() context.move(to: CGPoint(x: pt.x, y: pt.y + 9)) context.addLine(to: CGPoint(x: pt.x - 5, y: pt.y - 1)) context.addLine(to: CGPoint(x: pt.x + 5, y: pt.y - 1)) context.closePath() - + context.setFillColor(lineDataSet.circleColors.first!.cgColor) context.fillPath() - + context.restoreGState() } } @@ -143,7 +143,7 @@ class TempTargetChartDataEntry: ChartDataEntry { self.data = data } - override func copy(with zone: NSZone? = nil) -> Any { + override func copy(with _: NSZone? = nil) -> Any { let copy = TempTargetChartDataEntry( xStart: xStart, xEnd: xEnd, @@ -169,8 +169,8 @@ class TempTargetRenderer: LineChartRenderer { guard let dataProvider = dataProvider else { return } if dataProvider.lineData?.dataSets.count ?? 0 > tempTargetDataSetIndex, - let lineDataSet = dataProvider.lineData?.dataSets[tempTargetDataSetIndex] as? LineChartDataSet { - + let lineDataSet = dataProvider.lineData?.dataSets[tempTargetDataSetIndex] as? LineChartDataSet + { let trans = dataProvider.getTransformer(forAxis: lineDataSet.axisDependency) let phaseY = animator.phaseY @@ -204,7 +204,7 @@ class TempTargetRenderer: LineChartRenderer { } } -let ScaleXMax:Float = 150.0 +let ScaleXMax: Float = 150.0 extension MainViewController { func updateChartRenderers() { let tempTargetDataIndex = GraphDataIndex.tempTarget.rawValue @@ -222,40 +222,39 @@ extension MainViewController { BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() } - - func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight: Highlight) { + + func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight _: Highlight) { if chartView == BGChartFull { BGChart.moveViewToX(entry.x) } - if entry.data as? String == "hide"{ + if entry.data as? String == "hide" { BGChart.highlightValue(nil, callDelegate: false) } - } - - func chartScaled(_ chartView: ChartViewBase, scaleX: CGFloat, scaleY: CGFloat) { + + func chartScaled(_: ChartViewBase, scaleX _: CGFloat, scaleY _: CGFloat) { // dont store huge values - var scale: Float = Float(BGChart.scaleX) - if(scale > ScaleXMax ) { + var scale = Float(BGChart.scaleX) + if scale > ScaleXMax { scale = ScaleXMax } UserDefaultsRepository.chartScaleX.value = Float(scale) } - func createGraph(){ + func createGraph() { // Create the BG Graph Data let bgChartEntry = [ChartDataEntry]() let maxBG: Float = UserDefaultsRepository.minBGScale.value - + // Setup BG line details - let lineBG = LineChartDataSet(entries:bgChartEntry, label: "") + let lineBG = LineChartDataSet(entries: bgChartEntry, label: "") lineBG.circleRadius = CGFloat(globalVariables.dotBG) lineBG.circleColors = [NSUIColor.systemGreen] lineBG.drawCircleHoleEnabled = false lineBG.axisDependency = YAxis.AxisDependency.right lineBG.highlightEnabled = true lineBG.drawValuesEnabled = false - + if UserDefaultsRepository.showLines.value { lineBG.lineWidth = 2 } else { @@ -268,10 +267,10 @@ extension MainViewController { } lineBG.setDrawHighlightIndicators(false) lineBG.valueFont.withSize(50) - + // Setup Prediction line details let predictionChartEntry = [ChartDataEntry]() - let linePrediction = LineChartDataSet(entries:predictionChartEntry, label: "") + let linePrediction = LineChartDataSet(entries: predictionChartEntry, label: "") linePrediction.circleRadius = CGFloat(globalVariables.dotBG) linePrediction.circleColors = [NSUIColor.systemPurple] linePrediction.colors = [NSUIColor.systemPurple] @@ -279,7 +278,7 @@ extension MainViewController { linePrediction.axisDependency = YAxis.AxisDependency.right linePrediction.highlightEnabled = true linePrediction.drawValuesEnabled = false - + if UserDefaultsRepository.showLines.value { linePrediction.lineWidth = 2 } else { @@ -292,11 +291,11 @@ extension MainViewController { } linePrediction.setDrawHighlightIndicators(false) linePrediction.valueFont.withSize(50) - + // create Basal graph data let chartEntry = [ChartDataEntry]() let maxBasal = UserDefaultsRepository.minBasalScale.value - let lineBasal = LineChartDataSet(entries:chartEntry, label: "") + let lineBasal = LineChartDataSet(entries: chartEntry, label: "") lineBasal.setDrawHighlightIndicators(false) lineBasal.setColor(NSUIColor.systemBlue, alpha: 0.5) lineBasal.lineWidth = 0 @@ -308,10 +307,10 @@ extension MainViewController { lineBasal.highlightEnabled = true lineBasal.drawValuesEnabled = false lineBasal.fillFormatter = basalFillFormatter() - + // Boluses let chartEntryBolus = [ChartDataEntry]() - let lineBolus = LineChartDataSet(entries:chartEntryBolus, label: "") + let lineBolus = LineChartDataSet(entries: chartEntryBolus, label: "") lineBolus.circleRadius = CGFloat(globalVariables.dotBolus) lineBolus.circleColors = [NSUIColor.systemBlue.withAlphaComponent(0.75)] lineBolus.drawCircleHoleEnabled = false @@ -323,21 +322,21 @@ extension MainViewController { lineBolus.valueTextColor = NSUIColor.label lineBolus.fillColor = NSUIColor.systemBlue lineBolus.fillAlpha = 0.6 - - lineBolus.drawCirclesEnabled = true - lineBolus.drawFilledEnabled = false - - if UserDefaultsRepository.showValues.value { + + lineBolus.drawCirclesEnabled = true + lineBolus.drawFilledEnabled = false + + if UserDefaultsRepository.showValues.value { lineBolus.drawValuesEnabled = true lineBolus.highlightEnabled = false } else { lineBolus.drawValuesEnabled = false lineBolus.highlightEnabled = true } - + // Carbs let chartEntryCarbs = [ChartDataEntry]() - let lineCarbs = LineChartDataSet(entries:chartEntryCarbs, label: "") + let lineCarbs = LineChartDataSet(entries: chartEntryCarbs, label: "") lineCarbs.circleRadius = CGFloat(globalVariables.dotCarb) lineCarbs.circleColors = [NSUIColor.systemOrange.withAlphaComponent(0.75)] lineCarbs.drawCircleHoleEnabled = false @@ -349,10 +348,10 @@ extension MainViewController { lineCarbs.valueTextColor = NSUIColor.label lineCarbs.fillColor = NSUIColor.systemOrange lineCarbs.fillAlpha = 0.6 - - lineCarbs.drawCirclesEnabled = true - lineCarbs.drawFilledEnabled = false - + + lineCarbs.drawCirclesEnabled = true + lineCarbs.drawFilledEnabled = false + if UserDefaultsRepository.showValues.value { lineCarbs.drawValuesEnabled = true lineCarbs.highlightEnabled = false @@ -360,10 +359,10 @@ extension MainViewController { lineCarbs.drawValuesEnabled = false lineCarbs.highlightEnabled = true } - + // create Scheduled Basal graph data let chartBasalScheduledEntry = [ChartDataEntry]() - let lineBasalScheduled = LineChartDataSet(entries:chartBasalScheduledEntry, label: "") + let lineBasalScheduled = LineChartDataSet(entries: chartBasalScheduledEntry, label: "") lineBasalScheduled.setDrawHighlightIndicators(false) lineBasalScheduled.setColor(NSUIColor.systemBlue, alpha: 0.8) lineBasalScheduled.lineWidth = 2 @@ -373,10 +372,10 @@ extension MainViewController { lineBasalScheduled.highlightEnabled = false lineBasalScheduled.drawValuesEnabled = false lineBasalScheduled.lineDashLengths = [10.0, 5.0] - + // create Override graph data let chartOverrideEntry = [ChartDataEntry]() - let lineOverride = LineChartDataSet(entries:chartOverrideEntry, label: "") + let lineOverride = LineChartDataSet(entries: chartOverrideEntry, label: "") lineOverride.setDrawHighlightIndicators(false) lineOverride.lineWidth = 0 lineOverride.drawFilledEnabled = true @@ -387,10 +386,10 @@ extension MainViewController { lineOverride.axisDependency = YAxis.AxisDependency.right lineOverride.highlightEnabled = true lineOverride.drawValuesEnabled = false - + // BG Check let chartEntryBGCheck = [ChartDataEntry]() - let lineBGCheck = LineChartDataSet(entries:chartEntryBGCheck, label: "") + let lineBGCheck = LineChartDataSet(entries: chartEntryBGCheck, label: "") lineBGCheck.circleRadius = CGFloat(globalVariables.dotOther) lineBGCheck.circleColors = [NSUIColor.systemRed.withAlphaComponent(0.75)] lineBGCheck.drawCircleHoleEnabled = false @@ -402,10 +401,10 @@ extension MainViewController { lineBGCheck.axisDependency = YAxis.AxisDependency.right lineBGCheck.valueFormatter = ChartYDataValueFormatter() lineBGCheck.drawValuesEnabled = false - + // Suspend Pump let chartEntrySuspend = [ChartDataEntry]() - let lineSuspend = LineChartDataSet(entries:chartEntrySuspend, label: "") + let lineSuspend = LineChartDataSet(entries: chartEntrySuspend, label: "") lineSuspend.circleRadius = CGFloat(globalVariables.dotOther) lineSuspend.circleColors = [NSUIColor.systemTeal.withAlphaComponent(0.75)] lineSuspend.drawCircleHoleEnabled = false @@ -417,10 +416,10 @@ extension MainViewController { lineSuspend.axisDependency = YAxis.AxisDependency.right lineSuspend.valueFormatter = ChartYDataValueFormatter() lineSuspend.drawValuesEnabled = false - + // Resume Pump let chartEntryResume = [ChartDataEntry]() - let lineResume = LineChartDataSet(entries:chartEntryResume, label: "") + let lineResume = LineChartDataSet(entries: chartEntryResume, label: "") lineResume.circleRadius = CGFloat(globalVariables.dotOther) lineResume.circleColors = [NSUIColor.systemTeal.withAlphaComponent(0.75)] lineResume.drawCircleHoleEnabled = false @@ -432,10 +431,10 @@ extension MainViewController { lineResume.axisDependency = YAxis.AxisDependency.right lineResume.valueFormatter = ChartYDataValueFormatter() lineResume.drawValuesEnabled = false - + // Sensor Start let chartEntrySensor = [ChartDataEntry]() - let lineSensor = LineChartDataSet(entries:chartEntrySensor, label: "") + let lineSensor = LineChartDataSet(entries: chartEntrySensor, label: "") lineSensor.circleRadius = CGFloat(globalVariables.dotOther) lineSensor.circleColors = [NSUIColor.systemIndigo.withAlphaComponent(0.75)] lineSensor.drawCircleHoleEnabled = false @@ -447,10 +446,10 @@ extension MainViewController { lineSensor.axisDependency = YAxis.AxisDependency.right lineSensor.valueFormatter = ChartYDataValueFormatter() lineSensor.drawValuesEnabled = false - + // Notes let chartEntryNote = [ChartDataEntry]() - let lineNote = LineChartDataSet(entries:chartEntryNote, label: "") + let lineNote = LineChartDataSet(entries: chartEntryNote, label: "") lineNote.circleRadius = CGFloat(globalVariables.dotOther) lineNote.circleColors = [NSUIColor.systemGray.withAlphaComponent(0.75)] lineNote.drawCircleHoleEnabled = false @@ -465,7 +464,7 @@ extension MainViewController { // Setup COB Prediction line details let COBpredictionChartEntry = [ChartDataEntry]() - let COBlinePrediction = LineChartDataSet(entries:COBpredictionChartEntry, label: "") + let COBlinePrediction = LineChartDataSet(entries: COBpredictionChartEntry, label: "") COBlinePrediction.circleRadius = CGFloat(globalVariables.dotBG) COBlinePrediction.circleColors = [NSUIColor.systemPurple] COBlinePrediction.colors = [NSUIColor.systemPurple] @@ -473,7 +472,7 @@ extension MainViewController { COBlinePrediction.axisDependency = YAxis.AxisDependency.right COBlinePrediction.highlightEnabled = true COBlinePrediction.drawValuesEnabled = false - + if UserDefaultsRepository.showLines.value { COBlinePrediction.lineWidth = 2 } else { @@ -486,10 +485,10 @@ extension MainViewController { } COBlinePrediction.setDrawHighlightIndicators(false) COBlinePrediction.valueFont.withSize(50) - + // Setup IOB Prediction line details let IOBpredictionChartEntry = [ChartDataEntry]() - let IOBlinePrediction = LineChartDataSet(entries:IOBpredictionChartEntry, label: "") + let IOBlinePrediction = LineChartDataSet(entries: IOBpredictionChartEntry, label: "") IOBlinePrediction.circleRadius = CGFloat(globalVariables.dotBG) IOBlinePrediction.circleColors = [NSUIColor.systemPurple] IOBlinePrediction.colors = [NSUIColor.systemPurple] @@ -497,7 +496,7 @@ extension MainViewController { IOBlinePrediction.axisDependency = YAxis.AxisDependency.right IOBlinePrediction.highlightEnabled = true IOBlinePrediction.drawValuesEnabled = false - + if UserDefaultsRepository.showLines.value { IOBlinePrediction.lineWidth = 2 } else { @@ -510,10 +509,10 @@ extension MainViewController { } IOBlinePrediction.setDrawHighlightIndicators(false) IOBlinePrediction.valueFont.withSize(50) - + // Setup UAM Prediction line details let UAMpredictionChartEntry = [ChartDataEntry]() - let UAMlinePrediction = LineChartDataSet(entries:UAMpredictionChartEntry, label: "") + let UAMlinePrediction = LineChartDataSet(entries: UAMpredictionChartEntry, label: "") UAMlinePrediction.circleRadius = CGFloat(globalVariables.dotBG) UAMlinePrediction.circleColors = [NSUIColor.systemPurple] UAMlinePrediction.colors = [NSUIColor.systemPurple] @@ -521,7 +520,7 @@ extension MainViewController { UAMlinePrediction.axisDependency = YAxis.AxisDependency.right UAMlinePrediction.highlightEnabled = true UAMlinePrediction.drawValuesEnabled = false - + if UserDefaultsRepository.showLines.value { UAMlinePrediction.lineWidth = 2 } else { @@ -534,10 +533,10 @@ extension MainViewController { } linePrediction.setDrawHighlightIndicators(false) linePrediction.valueFont.withSize(50) - + // Setup ZT Prediction line details let ZTpredictionChartEntry = [ChartDataEntry]() - let ZTlinePrediction = LineChartDataSet(entries:ZTpredictionChartEntry, label: "") + let ZTlinePrediction = LineChartDataSet(entries: ZTpredictionChartEntry, label: "") ZTlinePrediction.circleRadius = CGFloat(globalVariables.dotBG) ZTlinePrediction.circleColors = [NSUIColor.systemPurple] ZTlinePrediction.colors = [NSUIColor.systemPurple] @@ -545,7 +544,7 @@ extension MainViewController { ZTlinePrediction.axisDependency = YAxis.AxisDependency.right ZTlinePrediction.highlightEnabled = true ZTlinePrediction.drawValuesEnabled = false - + if UserDefaultsRepository.showLines.value { ZTlinePrediction.lineWidth = 2 } else { @@ -571,10 +570,10 @@ extension MainViewController { lineSmb.axisDependency = YAxis.AxisDependency.right lineSmb.valueFormatter = ChartYDataValueFormatter() lineSmb.valueTextColor = NSUIColor.label - + lineSmb.drawCirclesEnabled = false lineSmb.drawFilledEnabled = false - + if UserDefaultsRepository.showValues.value { lineSmb.drawValuesEnabled = true lineSmb.highlightEnabled = false @@ -585,7 +584,7 @@ extension MainViewController { // TempTarget graph data let chartTempTargetEntry = [ChartDataEntry]() - let lineTempTarget = LineChartDataSet(entries:chartTempTargetEntry, label: "") + let lineTempTarget = LineChartDataSet(entries: chartTempTargetEntry, label: "") lineTempTarget.setDrawHighlightIndicators(false) lineTempTarget.lineWidth = 0 lineTempTarget.drawFilledEnabled = false @@ -598,7 +597,7 @@ extension MainViewController { // Setup the chart data of all lines let data = LineChartData() - + data.append(lineBG) // Dataset 0 data.append(linePrediction) // Dataset 1 data.append(lineBasal) // Dataset 2 @@ -619,37 +618,37 @@ extension MainViewController { data.append(lineTempTarget) data.setValueFont(UIFont.systemFont(ofSize: 12)) - + // Add marker popups for bolus and carbs let marker = PillMarker(color: .secondarySystemBackground, font: UIFont.boldSystemFont(ofSize: 14), textColor: .label) BGChart.marker = marker - + // Clear limit lines so they don't add multiples when changing the settings BGChart.rightAxis.removeAllLimitLines() - - //Add lower red line based on low alert value + + // Add lower red line based on low alert value let ll = ChartLimitLine() ll.limit = Double(UserDefaultsRepository.lowLine.value) ll.lineColor = NSUIColor.systemRed.withAlphaComponent(0.5) BGChart.rightAxis.addLimitLine(ll) - - //Add upper yellow line based on low alert value + + // Add upper yellow line based on low alert value let ul = ChartLimitLine() ul.limit = Double(UserDefaultsRepository.highLine.value) ul.lineColor = NSUIColor.systemYellow.withAlphaComponent(0.5) BGChart.rightAxis.addLimitLine(ul) - + // Add vertical lines as configured createVerticalLines() startGraphNowTimer() - + // Setup the main graph overall details BGChart.xAxis.valueFormatter = ChartXValueFormatter() BGChart.xAxis.granularity = 1800 BGChart.xAxis.labelTextColor = NSUIColor.label BGChart.xAxis.labelPosition = XAxis.LabelPosition.bottom BGChart.xAxis.drawGridLinesEnabled = false - + BGChart.leftAxis.enabled = true BGChart.leftAxis.labelPosition = YAxis.LabelPosition.insideChart BGChart.leftAxis.axisMaximum = maxBasal @@ -657,7 +656,7 @@ extension MainViewController { BGChart.leftAxis.drawGridLinesEnabled = false BGChart.leftAxis.granularityEnabled = true BGChart.leftAxis.granularity = 0.5 - + BGChart.rightAxis.labelTextColor = NSUIColor.label BGChart.rightAxis.labelPosition = YAxis.LabelPosition.insideChart BGChart.rightAxis.axisMinimum = 0.0 @@ -667,33 +666,33 @@ extension MainViewController { BGChart.rightAxis.valueFormatter = ChartYMMOLValueFormatter() BGChart.rightAxis.granularityEnabled = true BGChart.rightAxis.granularity = 50 - + BGChart.maxHighlightDistance = 15.0 BGChart.legend.enabled = false BGChart.scaleYEnabled = false BGChart.drawGridBackgroundEnabled = true BGChart.gridBackgroundColor = NSUIColor.secondarySystemBackground - + BGChart.highlightValue(nil, callDelegate: false) - + BGChart.data = data BGChart.setExtraOffsets(left: 5, top: 10, right: 5, bottom: 10) } - + func createVerticalLines() { BGChart.xAxis.removeAllLimitLines() BGChartFull.xAxis.removeAllLimitLines() createNowAndDIALines() createMidnightLines() } - + func createNowAndDIALines() { let ul = ChartLimitLine() ul.limit = Double(dateTimeUtils.getNowTimeIntervalUTC()) ul.lineColor = NSUIColor.systemGray.withAlphaComponent(0.5) ul.lineWidth = 1 BGChart.xAxis.addLimitLine(ul) - + if UserDefaultsRepository.show30MinLine.value { let ul2 = ChartLimitLine() ul2.limit = Double(dateTimeUtils.getNowTimeIntervalUTC().advanced(by: -30 * 60)) @@ -701,9 +700,9 @@ extension MainViewController { ul2.lineWidth = 1 BGChart.xAxis.addLimitLine(ul2) } - + if UserDefaultsRepository.showDIALines.value { - for i in 1..<7 { + for i in 1 ..< 7 { let ul = ChartLimitLine() ul.limit = Double(dateTimeUtils.getNowTimeIntervalUTC() - Double(i * 60 * 60)) ul.lineColor = NSUIColor.systemGray.withAlphaComponent(0.3) @@ -723,7 +722,7 @@ extension MainViewController { BGChart.xAxis.addLimitLine(ul3) } } - + func createMidnightLines() { // Draw a line at midnight: useful when showing multiple days of data if UserDefaultsRepository.showMidnightLines.value { @@ -746,12 +745,12 @@ extension MainViewController { sl.lineDashLengths = [CGFloat(2), CGFloat(2)] sl.lineWidth = 1 BGChartFull.xAxis.addLimitLine(sl) - - midnightTimeInterval = midnightTimeInterval.advanced(by: -24*60*60) + + midnightTimeInterval = midnightTimeInterval.advanced(by: -24 * 60 * 60) } } } - + func updateBGGraphSettings() { let dataIndex = 0 let dataIndexPrediction = 1 @@ -771,33 +770,32 @@ extension MainViewController { lineBG.drawCirclesEnabled = false linePrediction.drawCirclesEnabled = false } - + BGChart.rightAxis.axisMinimum = 0 - + // Clear limit lines so they don't add multiples when changing the settings BGChart.rightAxis.removeAllLimitLines() - - //Add lower red line based on low alert value + + // Add lower red line based on low alert value let ll = ChartLimitLine() ll.limit = Double(UserDefaultsRepository.lowLine.value) ll.lineColor = NSUIColor.systemRed.withAlphaComponent(0.5) BGChart.rightAxis.addLimitLine(ll) - - //Add upper yellow line based on low alert value + + // Add upper yellow line based on low alert value let ul = ChartLimitLine() ul.limit = Double(UserDefaultsRepository.highLine.value) ul.lineColor = NSUIColor.systemYellow.withAlphaComponent(0.5) BGChart.rightAxis.addLimitLine(ul) - + // Re-create vertical markers in case their settings changed createVerticalLines() - + BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() - } - + func updateBGGraph() { let dataIndex = 0 let entries = bgData @@ -809,27 +807,26 @@ extension MainViewController { mainChart.removeAll(keepingCapacity: false) smallChart.removeAll(keepingCapacity: false) let maxBGOffset: Float = 50 - + var colors = [NSUIColor]() topBG = UserDefaultsRepository.minBGScale.value - for i in 0.. topBG - maxBGOffset { topBG = Float(entries[i].sgv) + maxBGOffset } let value = ChartDataEntry(x: Double(entries[i].date), y: Double(entries[i].sgv), data: formatPillText(line1: Localizer.toDisplayUnits(String(entries[i].sgv)), time: entries[i].date)) mainChart.append(value) smallChart.append(value) - + if Double(entries[i].sgv) >= Double(UserDefaultsRepository.highLine.value) { colors.append(NSUIColor.systemYellow) } else if Double(entries[i].sgv) <= Double(UserDefaultsRepository.lowLine.value) { - colors.append(NSUIColor.systemRed) + colors.append(NSUIColor.systemRed) } else { colors.append(NSUIColor.systemGreen) } } - // Set Colors let lineBG = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet @@ -841,15 +838,14 @@ extension MainViewController { lineBGSmall.circleColors.removeAll() if colors.count > 0 { - for i in 0.. CGFloat(ScaleXMax) ) { + if scaleX > CGFloat(ScaleXMax) { scaleX = CGFloat(ScaleXMax) UserDefaultsRepository.chartScaleX.value = ScaleXMax } BGChart.zoom(scaleX: scaleX, scaleY: 1, x: 1, y: 1) firstGraphLoad = false } - + // Move to current reading everytime new readings load // Check if auto-scrolling should be performed if autoScrollPauseUntil == nil || Date() > autoScrollPauseUntil! { BGChart.moveViewToAnimated(xValue: dateTimeUtils.getNowTimeIntervalUTC() - (BGChart.visibleXRange * 0.7), yValue: 0.0, axis: .right, duration: 1, easingOption: .easeInBack) } } - + func updatePredictionGraph(color: UIColor? = nil) { let dataIndex = 1 var mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet var smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet mainChart.clear() smallChart.clear() - + var colors = [NSUIColor]() let maxBGOffset: Float = 20 topPredictionBG = UserDefaultsRepository.minBGScale.value - for i in 0.. topPredictionBG - maxBGOffset { topPredictionBG = Float(predictionVal) + maxBGOffset } - + if i == 0 { if UserDefaultsRepository.showDots.value { colors.append((color ?? NSUIColor.systemPurple).withAlphaComponent(0.0)) @@ -907,18 +903,18 @@ extension MainViewController { } else { colors.append(color ?? NSUIColor.systemPurple) } - + let value = ChartDataEntry(x: predictionData[i].date, y: predictionVal, data: formatPillText(line1: Localizer.toDisplayUnits(String(predictionData[i].sgv)), time: predictionData[i].date)) mainChart.addEntry(value) smallChart.addEntry(value) } - + smallChart.circleColors.removeAll() smallChart.colors.removeAll() mainChart.colors.removeAll() mainChart.circleColors.removeAll() if colors.count > 0 { - for i in 0.. maxBasal { + if basalData[i].basalRate > maxBasal { maxBasal = basalData[i].basalRate } if basalData[i].basalRate > maxBasalSmall { maxBasalSmall = basalData[i].basalRate } } - + BGChart.leftAxis.axisMaximum = maxBasal BGChartFull.leftAxis.axisMaximum = maxBasalSmall - + BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() - + if UserDefaultsRepository.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() } } - + func updateBasalScheduledGraph() { var dataIndex = 5 BGChart.lineData?.dataSets[dataIndex].clear() BGChartFull.lineData?.dataSets[dataIndex].clear() - for i in 0.. 0 { - for i in 0.. 0 && UserDefaultsRepository.showAbsorption.value { + if carbData[i].absorptionTime > 0, UserDefaultsRepository.showAbsorption.value { hours = carbData[i].absorptionTime / 60 valueString += " " + String(hours) + "h" } - + // Check overlapping carbs to shift left if needed let carbShift = findNextCarbTime(timeWithin: 250, needle: carbData[i].date, haystack: carbData, startingIndex: i) var dateTimeStamp = carbData[i].date - + colors.append(NSUIColor.systemOrange.withAlphaComponent(1.0)) - + // skip if outside of visible area let graphHours = 24 * UserDefaultsRepository.downloadDays.value if dateTimeStamp < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue } - + if carbShift { dateTimeStamp = dateTimeStamp - 250 } - + let dot = ChartDataEntry(x: Double(dateTimeStamp), y: Double(carbData[i].sgv), data: valueString) BGChart.data?.dataSets[dataIndex].addEntry(dot) if UserDefaultsRepository.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(dot) } - - - } - + // Set Colors let lineCarbs = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet let lineCarbsSmall = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet @@ -1164,16 +1156,16 @@ extension MainViewController { lineCarbs.circleColors.removeAll() lineCarbsSmall.colors.removeAll() lineCarbsSmall.circleColors.removeAll() - + if colors.count > 0 { - for i in 0.. 0 { labelText += "\r\nEntered By: " + thisItem.enteredBy } - - + // Start Dot // Shift dots 30 seconds to create an empty 0 space between consecutive temps let preStartDot = ChartDataEntry(x: Double(thisItem.date), y: yBottom, data: labelText) @@ -1673,7 +1658,7 @@ extension MainViewController { if UserDefaultsRepository.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(preStartDot) } - + let startDot = ChartDataEntry(x: Double(thisItem.date + 1), y: yTop, data: labelText) BGChart.data?.dataSets[dataIndex].addEntry(startDot) if UserDefaultsRepository.smallGraphTreatments.value { @@ -1686,7 +1671,7 @@ extension MainViewController { if UserDefaultsRepository.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(endDot) } - + // Post end dot let postEndDot = ChartDataEntry(x: Double(thisItem.endDate - 1), y: yBottom, data: labelText) BGChart.data?.dataSets[dataIndex].addEntry(postEndDot) @@ -1694,7 +1679,7 @@ extension MainViewController { BGChartFull.data?.dataSets[dataIndex].addEntry(postEndDot) } } - + BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() @@ -1709,8 +1694,9 @@ extension MainViewController { guard let chart = BGChart.lineData, index.rawValue < chart.dataSets.count, let smallChartData = BGChartFull.lineData, - index.rawValue < smallChartData.dataSets.count else { - //print("Warning: Invalid GraphDataIndex \(index.description) or lineData is nil.") + index.rawValue < smallChartData.dataSets.count + else { + // print("Warning: Invalid GraphDataIndex \(index.description) or lineData is nil.") return (nil, nil) } @@ -1731,7 +1717,8 @@ extension MainViewController { let dataIndex = GraphDataIndex.tempTarget.rawValue guard let chartData = BGChart.lineData, chartData.dataSets.count > dataIndex, - let mainChartDataSet = chartData.dataSets[dataIndex] as? LineChartDataSet else { + let mainChartDataSet = chartData.dataSets[dataIndex] as? LineChartDataSet + else { print("Error: Could not retrieve temp target datasets.") return } @@ -1742,7 +1729,8 @@ extension MainViewController { if UserDefaultsRepository.smallGraphTreatments.value, let smallChartData = BGChartFull.lineData, smallChartData.dataSets.count > dataIndex, - let smallDataSet = smallChartData.dataSets[dataIndex] as? LineChartDataSet { + let smallDataSet = smallChartData.dataSets[dataIndex] as? LineChartDataSet + { smallChartDataSet = smallDataSet smallChartDataSet?.clear() } @@ -1867,14 +1855,14 @@ extension MainViewController { let date = Date(timeIntervalSince1970: time) let formattedDate = dateFormatter.string(from: date) - + if let line2 = line2 { return wrappedLine1 + "\r\n" + line2 + "\r\n" + formattedDate } else { return wrappedLine1 + "\r\n" + formattedDate } } - + func updatePredictionGraphGeneric( dataIndex: Int, predictionData: [ShareGlucoseData], @@ -1885,26 +1873,26 @@ extension MainViewController { let smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet mainChart.clear() smallChart.clear() - + var colors = [NSUIColor]() let maxBGOffset: Float = 20 - - for i in 0.. topPredictionBG - maxBGOffset { topPredictionBG = Float(predictionVal) + maxBGOffset } - + if i == 0 { if UserDefaultsRepository.showDots.value { - colors.append((color).withAlphaComponent(0.0)) + colors.append(color.withAlphaComponent(0.0)) } else { - colors.append((color).withAlphaComponent(1.0)) + colors.append(color.withAlphaComponent(1.0)) } } else { colors.append(color) } - + let value = ChartDataEntry( x: predictionData[i].date, y: predictionVal, @@ -1917,7 +1905,7 @@ extension MainViewController { mainChart.addEntry(value) smallChart.addEntry(value) } - + smallChart.circleColors.removeAll() smallChart.colors.removeAll() mainChart.colors.removeAll() @@ -1930,7 +1918,7 @@ extension MainViewController { smallChart.circleColors.append(color) } } - + BGChart.rightAxis.axisMaximum = Double(calculateMaxBgGraphValue()) BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() diff --git a/LoopFollow/Controllers/NightScout.swift b/LoopFollow/Controllers/NightScout.swift index 9e57a384f..728b8e6ac 100644 --- a/LoopFollow/Controllers/NightScout.swift +++ b/LoopFollow/Controllers/NightScout.swift @@ -10,7 +10,7 @@ import Foundation import UIKit extension MainViewController { - //NS Cage Struct + // NS Cage Struct struct cageData: Codable { var created_at: String } @@ -23,98 +23,87 @@ extension MainViewController { var created_at: String } - //NS Basal Profile Struct + // NS Basal Profile Struct struct basalProfileStruct: Codable { var value: Double var time: String var timeAsSeconds: Double } - //NS Basal Data Struct + // NS Basal Data Struct struct basalGraphStruct: Codable { var basalRate: Double var date: TimeInterval } - - //NS Bolus Data Struct + + // NS Bolus Data Struct struct bolusGraphStruct: Codable { var value: Double var date: TimeInterval var sgv: Int } - - //NS Bolus Data Struct + + // NS Bolus Data Struct struct carbGraphStruct: Codable { var value: Double var date: TimeInterval var sgv: Int var absorptionTime: Int } - - func clearOldTempBasal() - { + + func clearOldTempBasal() { basalData.removeAll() updateBasalGraph() } - - func clearOldBolus() - { + + func clearOldBolus() { bolusData.removeAll() updateBolusGraph() } - - func clearOldSmb() - { + + func clearOldSmb() { smbData.removeAll() updateSmbGraph() updateChartRenderers() } - func clearOldCarb() - { + func clearOldCarb() { carbData.removeAll() updateCarbGraph() } - - func clearOldBGCheck() - { + + func clearOldBGCheck() { bgCheckData.removeAll() updateBGCheckGraph() } - - func clearOldOverride() - { + + func clearOldOverride() { overrideGraphData.removeAll() updateOverrideGraph() } - - func clearOldTempTarget() - { + + func clearOldTempTarget() { tempTargetGraphData.removeAll() updateTempTargetGraph() updateChartRenderers() } - func clearOldSuspend() - { + func clearOldSuspend() { suspendGraphData.removeAll() updateSuspendGraph() } - - func clearOldResume() - { + + func clearOldResume() { resumeGraphData.removeAll() updateResumeGraph() } - - func clearOldSensor() - { + + func clearOldSensor() { sensorStartGraphData.removeAll() updateSensorStart() } - - func clearOldNotes() - { + + func clearOldNotes() { noteGraphData.removeAll() updateNotes() } diff --git a/LoopFollow/Controllers/Nightscout/BGData.swift b/LoopFollow/Controllers/Nightscout/BGData.swift index 419654eb7..7755d7929 100644 --- a/LoopFollow/Controllers/Nightscout/BGData.swift +++ b/LoopFollow/Controllers/Nightscout/BGData.swift @@ -16,8 +16,7 @@ extension MainViewController { // Requesting more just for consistency with NS let graphHours = 24 * UserDefaultsRepository.downloadDays.value let count = graphHours * 12 - dexShare?.fetchData(count) { (err, result) -> () in - + dexShare?.fetchData(count) { err, result in if let error = err { LogManager.shared.log(category: .dexcom, message: "Error fetching Dexcom data: \(error.localizedDescription)", limitIdentifier: "Error fetching Dexcom data") self.webLoadNSBGData() @@ -33,14 +32,14 @@ extension MainViewController { // If Dex data is old, load from NS instead let latestDate = data[0].date let now = dateTimeUtils.getNowTimeIntervalUTC() - if (latestDate + 330) < now && IsNightscoutEnabled() { + if (latestDate + 330) < now, IsNightscoutEnabled() { LogManager.shared.log(category: .dexcom, message: "Dexcom data is old, loading from NS instead", limitIdentifier: "Dexcom data is old, loading from NS instead") self.webLoadNSBGData() return } // Dexcom only returns 24 hrs of data. If we need more, call NS. - if graphHours > 24 && IsNightscoutEnabled() { + if graphHours > 24, IsNightscoutEnabled() { self.webLoadNSBGData(dexData: data) } else { self.ProcessDexBGData(data: data, sourceName: "Dexcom") @@ -66,11 +65,11 @@ extension MainViewController { NightscoutUtils.executeRequest(eventType: .sgv, parameters: parameters) { (result: Result<[ShareGlucoseData], Error>) in switch result { - case .success(let entriesResponse): + case let .success(entriesResponse): var nsData = entriesResponse DispatchQueue.main.async { // transform NS data to look like Dex data - for i in 0..= oldestDexDate { + while itemsToRemove < nsData2.count, nsData2[itemsToRemove].date >= oldestDexDate { itemsToRemove += 1 } nsData2.removeFirst(itemsToRemove) @@ -104,7 +103,7 @@ extension MainViewController { // trigger the processor for the data after downloading. self.ProcessDexBGData(data: nsData2, sourceName: sourceName) } - case .failure(let error): + case let .failure(error): LogManager.shared.log(category: .nightscout, message: "Failed to fetch bg data: \(error)", limitIdentifier: "Failed to fetch bg data") DispatchQueue.main.async { TaskScheduler.shared.rescheduleTask( @@ -188,7 +187,7 @@ extension MainViewController { // Process data for graph display. bgData.removeAll() - for i in 0..= dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { let sgvValue = data[data.count - 1 - i].sgv diff --git a/LoopFollow/Controllers/Nightscout/CAge.swift b/LoopFollow/Controllers/Nightscout/CAge.swift index 41306385e..48ce23907 100644 --- a/LoopFollow/Controllers/Nightscout/CAge.swift +++ b/LoopFollow/Controllers/Nightscout/CAge.swift @@ -7,34 +7,35 @@ // import Foundation + extension MainViewController { // NS Cage Web Call func webLoadNSCage() { let currentTimeString = dateTimeUtils.getDateTimeString() - + let parameters: [String: String] = [ "find[eventType]": NightscoutUtils.EventType.cage.rawValue, "find[created_at][$lte]": currentTimeString, - "count": "1" + "count": "1", ] - + NightscoutUtils.executeRequest(eventType: .cage, parameters: parameters) { (result: Result<[cageData], Error>) in switch result { - case .success(let data): + case let .success(data): self.updateCage(data: data) - case .failure(let error): + case let .failure(error): LogManager.shared.log(category: .nightscout, message: "webLoadNSCage, error: \(error.localizedDescription)") } } } - + // NS Cage Response Processor func updateCage(data: [cageData]) { infoManager.clearInfoData(type: .cage) if data.count == 0 { return } - + currentCage = data[0] let lastCageString = data[0].created_at @@ -43,17 +44,17 @@ extension MainViewController { .withTime, .withDashSeparatorInDate, .withColonSeparatorInTime] - UserDefaultsRepository.alertCageInsertTime.value = formatter.date(from: (lastCageString))?.timeIntervalSince1970 as! TimeInterval - if let cageTime = formatter.date(from: (lastCageString))?.timeIntervalSince1970 { + UserDefaultsRepository.alertCageInsertTime.value = formatter.date(from: lastCageString)?.timeIntervalSince1970 as! TimeInterval + if let cageTime = formatter.date(from: lastCageString)?.timeIntervalSince1970 { let now = dateTimeUtils.getNowTimeIntervalUTC() let secondsAgo = now - cageTime - //let days = 24 * 60 * 60 - + // let days = 24 * 60 * 60 + let formatter = DateComponentsFormatter() formatter.unitsStyle = .positional // Use the appropriate positioning for the current locale - formatter.allowedUnits = [ .day, .hour ] // Units to display in the formatted string - formatter.zeroFormattingBehavior = [ .pad ] // Pad with zeroes where appropriate for the locale - + formatter.allowedUnits = [.day, .hour] // Units to display in the formatted string + formatter.zeroFormattingBehavior = [.pad] // Pad with zeroes where appropriate for the locale + if let formattedDuration = formatter.string(from: secondsAgo) { infoManager.updateInfoData(type: .cage, value: formattedDuration) } diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift index d91950e52..8f79a281a 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift @@ -6,16 +6,16 @@ // Copyright © 2023 Jon Fawcett. All rights reserved. // +import Charts import Foundation import UIKit -import Charts extension MainViewController { func webLoadNSDeviceStatus() { - let parameters: [String: String] = ["count": "1"] + let parameters = ["count": "1"] NightscoutUtils.executeDynamicRequest(eventType: .deviceStatus, parameters: parameters) { result in switch result { - case .success(let json): + case let .success(json): if let jsonDeviceStatus = json as? [[String: AnyObject]] { DispatchQueue.main.async { self.updateDeviceStatusDisplay(jsonDeviceStatus: jsonDeviceStatus) @@ -80,7 +80,7 @@ extension MainViewController { } // NS Device Status Response Processor - func updateDeviceStatusDisplay(jsonDeviceStatus: [[String:AnyObject]]) { + func updateDeviceStatusDisplay(jsonDeviceStatus: [[String: AnyObject]]) { infoManager.clearInfoData(types: [.iob, .cob, .override, .battery, .pump, .target, .isf, .carbRatio, .updated, .recBolus, .tdd]) if jsonDeviceStatus.count == 0 { @@ -89,17 +89,17 @@ extension MainViewController { return } - //Process the current data first - let lastDeviceStatus = jsonDeviceStatus[0] as [String : AnyObject]? + // Process the current data first + let lastDeviceStatus = jsonDeviceStatus[0] as [String: AnyObject]? - //pump and uploader + // pump and uploader let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withFullDate, .withTime, .withDashSeparatorInDate, .withColonSeparatorInTime] - if let lastPumpRecord = lastDeviceStatus?["pump"] as! [String : AnyObject]? { - if let lastPumpTime = formatter.date(from: (lastPumpRecord["clock"] as! String))?.timeIntervalSince1970 { + if let lastPumpRecord = lastDeviceStatus?["pump"] as! [String: AnyObject]? { + if let lastPumpTime = formatter.date(from: (lastPumpRecord["clock"] as! String))?.timeIntervalSince1970 { if let reservoirData = lastPumpRecord["reservoir"] as? Double { latestPumpVolume = reservoirData infoManager.updateInfoData(type: .pump, value: String(format: "%.0f", reservoirData) + "U") @@ -109,7 +109,8 @@ extension MainViewController { } if let uploader = lastDeviceStatus?["uploader"] as? [String: AnyObject], - let upbat = uploader["battery"] as? Double { + let upbat = uploader["battery"] as? Double + { let batteryText: String if let isCharging = uploader["isCharging"] as? Bool, isCharging { batteryText = "⚡️ " + String(format: "%.0f", upbat) + "%" @@ -132,20 +133,21 @@ extension MainViewController { } // Loop - handle new data - if let lastLoopRecord = lastDeviceStatus?["loop"] as! [String : AnyObject]? { + if let lastLoopRecord = lastDeviceStatus?["loop"] as! [String: AnyObject]? { DeviceStatusLoop(formatter: formatter, lastLoopRecord: lastLoopRecord) var oText = "" currentOverride = 1.0 if let lastOverride = lastDeviceStatus?["override"] as? [String: AnyObject], - let isActive = lastOverride["active"] as? Bool, isActive { + let isActive = lastOverride["active"] as? Bool, isActive + { if let lastCorrection = lastOverride["currentCorrectionRange"] as? [String: AnyObject], let minValue = lastCorrection["minValue"] as? Double, - let maxValue = lastCorrection["maxValue"] as? Double { - + let maxValue = lastCorrection["maxValue"] as? Double + { if let multiplier = lastOverride["multiplier"] as? Double { currentOverride = multiplier - oText += String(format: "%.0f%%", (multiplier * 100)) + oText += String(format: "%.0f%%", multiplier * 100) } else { oText += "100%" } @@ -161,7 +163,7 @@ extension MainViewController { } // OpenAPS - handle new data - if let lastLoopRecord = lastDeviceStatus?["openaps"] as! [String : AnyObject]? { + if let lastLoopRecord = lastDeviceStatus?["openaps"] as! [String: AnyObject]? { DeviceStatusOpenAPS(formatter: formatter, lastDeviceStatus: lastDeviceStatus, lastLoopRecord: lastLoopRecord) } diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift index bf8177290..c9fc05bf2 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift @@ -6,10 +6,10 @@ // Copyright © 2024 Jon Fawcett. All rights reserved. // -import Foundation -import UIKit import Charts +import Foundation import HealthKit +import UIKit extension MainViewController { func DeviceStatusLoop(formatter: ISO8601DateFormatter, lastLoopRecord: [String: AnyObject]) { @@ -19,7 +19,7 @@ extension MainViewController { Storage.shared.remoteType.value = .none } - if let lastLoopTime = formatter.date(from: (lastLoopRecord["timestamp"] as! String))?.timeIntervalSince1970 { + if let lastLoopTime = formatter.date(from: (lastLoopRecord["timestamp"] as! String))?.timeIntervalSince1970 { let previousLastLoopTime = UserDefaultsRepository.alertLastLoopTime.value UserDefaultsRepository.alertLastLoopTime.value = lastLoopTime if let failure = lastLoopRecord["failureReason"] { @@ -27,11 +27,9 @@ extension MainViewController { latestLoopStatusString = "X" } else { var wasEnacted = false - if let enacted = lastLoopRecord["enacted"] as? [String:AnyObject] { + if let enacted = lastLoopRecord["enacted"] as? [String: AnyObject] { wasEnacted = true - if let lastTempBasal = enacted["rate"] as? Double { - - } + if let lastTempBasal = enacted["rate"] as? Double {} } // ISF @@ -68,11 +66,11 @@ extension MainViewController { latestCOB = cobMetric } - if let predictdata = lastLoopRecord["predicted"] as? [String:AnyObject] { + if let predictdata = lastLoopRecord["predicted"] as? [String: AnyObject] { let prediction = predictdata["values"] as! [Double] PredictionLabel.text = Localizer.toDisplayUnits(String(Int(prediction.last!))) PredictionLabel.textColor = UIColor.systemPurple - if UserDefaultsRepository.downloadPrediction.value && previousLastLoopTime < lastLoopTime { + if UserDefaultsRepository.downloadPrediction.value, previousLastLoopTime < lastLoopTime { predictionData.removeAll() var predictionTime = lastLoopTime let toLoad = Int(UserDefaultsRepository.predictionToLoad.value * 12) @@ -89,7 +87,7 @@ extension MainViewController { } i += 1 } - + if let predMin = prediction.min(), let predMax = prediction.max() { let formattedMin = Localizer.toDisplayUnits(String(predMin)) let formattedMax = Localizer.toDisplayUnits(String(predMax)) @@ -109,13 +107,13 @@ extension MainViewController { infoManager.updateInfoData(type: .recBolus, value: formattedRecBolus) UserDefaultsRepository.deviceRecBolus.value = recBolus } - if let loopStatus = lastLoopRecord["recommendedTempBasal"] as? [String:AnyObject] { + if let loopStatus = lastLoopRecord["recommendedTempBasal"] as? [String: AnyObject] { if let tempBasalTime = formatter.date(from: (loopStatus["timestamp"] as! String))?.timeIntervalSince1970 { var lastBGTime = lastLoopTime if bgData.count > 0 { lastBGTime = bgData[bgData.count - 1].date } - if tempBasalTime > lastBGTime && !wasEnacted { + if tempBasalTime > lastBGTime, !wasEnacted { LoopStatusLabel.text = "⏀" latestLoopStatusString = "⏀" } else { @@ -127,7 +125,6 @@ extension MainViewController { LoopStatusLabel.text = "↻" latestLoopStatusString = "↻" } - } } } diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift index d7aa0d63c..af2c1f0f3 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift @@ -4,8 +4,8 @@ // Copyright © 2024 Jon Fawcett. All rights reserved. import Foundation -import UIKit import HealthKit +import UIKit extension MainViewController { func DeviceStatusOpenAPS(formatter: ISO8601DateFormatter, lastDeviceStatus: [String: AnyObject]?, lastLoopRecord: [String: AnyObject]) { @@ -20,11 +20,12 @@ extension MainViewController { return } - var wasEnacted : Bool + var wasEnacted: Bool if let enacted = lastLoopRecord["enacted"] as? [String: AnyObject] { wasEnacted = true if let timestampString = enacted["timestamp"] as? String, - let lastLoopTime = formatter.date(from: timestampString)?.timeIntervalSince1970 { + let lastLoopTime = formatter.date(from: timestampString)?.timeIntervalSince1970 + { let storedTime = UserDefaultsRepository.alertLastLoopTime.value if lastLoopTime < storedTime { LogManager.shared.log(category: .deviceStatus, message: "Received an old timestamp for enacted: \(lastLoopTime) is older than last stored time \(storedTime), ignoring update.", isDebug: false) @@ -41,7 +42,8 @@ extension MainViewController { var updatedTime: TimeInterval? if let timestamp = enactedOrSuggested["timestamp"] as? String, - let parsedTime = formatter.date(from: timestamp)?.timeIntervalSince1970 { + let parsedTime = formatter.date(from: timestamp)?.timeIntervalSince1970 + { updatedTime = parsedTime let formattedTime = Localizer.formatTimestampToLocalString(parsedTime) infoManager.updateInfoData(type: .updated, value: formattedTime) @@ -97,7 +99,8 @@ extension MainViewController { // Fallback: Extract COB from reason string let cobPattern = "COB: (\\d+(?:\\.\\d+)?)" if let cobRegex = try? NSRegularExpression(pattern: cobPattern), - let cobMatch = cobRegex.firstMatch(in: reasonString, range: NSRange(location: 0, length: reasonString.utf16.count)) { + let cobMatch = cobRegex.firstMatch(in: reasonString, range: NSRange(location: 0, length: reasonString.utf16.count)) + { let cobValueString = (reasonString as NSString).substring(with: cobMatch.range(at: 1)) if let cobValue = Double(cobValueString) { let tempDict: [String: AnyObject] = ["COB": cobValue as AnyObject] @@ -164,13 +167,14 @@ extension MainViewController { infoManager.updateInfoData(type: .tdd, value: tddMetric) } - let predBGsData: [String: AnyObject]? = { if let enacted = lastLoopRecord["suggested"] as? [String: AnyObject], - let predBGs = enacted["predBGs"] as? [String: AnyObject] { + let predBGs = enacted["predBGs"] as? [String: AnyObject] + { return predBGs } else if let suggested = lastLoopRecord["enacted"] as? [String: AnyObject], - let predBGs = suggested["predBGs"] as? [String: AnyObject] { + let predBGs = suggested["predBGs"] as? [String: AnyObject] + { return predBGs } return nil @@ -184,7 +188,7 @@ extension MainViewController { ("ZT", "ZT", 12), ("IOB", "Insulin", 13), ("COB", "LoopYellow", 14), - ("UAM", "UAM", 15) + ("UAM", "UAM", 15), ] var minPredBG = Double.infinity @@ -196,7 +200,7 @@ extension MainViewController { var predictionTime = updatedTime ?? Date().timeIntervalSince1970 let toLoad = Int(UserDefaultsRepository.predictionToLoad.value * 12) - for i in 0...toLoad { + for i in 0 ... toLoad { if i < graphdata.count { let predictionValue = graphdata[i] minPredBG = min(minPredBG, predictionValue) @@ -218,7 +222,7 @@ extension MainViewController { ) } - if minPredBG != Double.infinity && maxPredBG != -Double.infinity { + if minPredBG != Double.infinity, maxPredBG != -Double.infinity { let value = "\(Localizer.toDisplayUnits(String(minPredBG)))/\(Localizer.toDisplayUnits(String(maxPredBG)))" infoManager.updateInfoData(type: .minMax, value: value) } else { @@ -232,7 +236,7 @@ extension MainViewController { if bgData.count > 0 { lastBGTime = bgData[bgData.count - 1].date } - if tempBasalTime > lastBGTime && !wasEnacted { + if tempBasalTime > lastBGTime, !wasEnacted { LoopStatusLabel.text = "⏀" latestLoopStatusString = "⏀" } else { diff --git a/LoopFollow/Controllers/Nightscout/IAge.swift b/LoopFollow/Controllers/Nightscout/IAge.swift index 5406da54c..39efc9561 100644 --- a/LoopFollow/Controllers/Nightscout/IAge.swift +++ b/LoopFollow/Controllers/Nightscout/IAge.swift @@ -7,6 +7,7 @@ // import Foundation + extension MainViewController { // NS Iage Web Call func webLoadNSIage() { @@ -17,16 +18,16 @@ extension MainViewController { "find[eventType]": NightscoutUtils.EventType.iage.rawValue, "find[created_at][$gte]": lastDateString, "find[created_at][$lte]": currentTimeString, - "count": "1" + "count": "1", ] NightscoutUtils.executeRequest(eventType: .iage, parameters: parameters) { (result: Result<[iageData], Error>) in switch result { - case .success(let data): + case let .success(data): DispatchQueue.main.async { self.updateIage(data: data) } - case .failure(let error): + case let .failure(error): LogManager.shared.log(category: .nightscout, message: "webLoadNSIage, failed to fetch data: \(error.localizedDescription)") } } @@ -54,8 +55,8 @@ extension MainViewController { let formatter = DateComponentsFormatter() formatter.unitsStyle = .positional - formatter.allowedUnits = [ .day, .hour] - formatter.zeroFormattingBehavior = [ .pad ] + formatter.allowedUnits = [.day, .hour] + formatter.zeroFormattingBehavior = [.pad] if let formattedDuration = formatter.string(from: secondsAgo) { infoManager.updateInfoData(type: .iage, value: formattedDuration) diff --git a/LoopFollow/Controllers/Nightscout/NSProfile.swift b/LoopFollow/Controllers/Nightscout/NSProfile.swift index 1c318533c..45b540dff 100644 --- a/LoopFollow/Controllers/Nightscout/NSProfile.swift +++ b/LoopFollow/Controllers/Nightscout/NSProfile.swift @@ -12,16 +12,19 @@ struct NSProfile: Decodable { let time: String let timeAsSeconds: Double } + struct SensEntry: Decodable { let value: Double let time: String let timeAsSeconds: Double } + struct CarbRatioEntry: Decodable { let value: Double let time: String let timeAsSeconds: Double } + struct TargetEntry: Decodable { let value: Double let time: String @@ -93,7 +96,7 @@ struct NSProfile: Decodable { case isAPNSProduction case deviceToken case trioOverrides = "overridePresets" - case loopSettings = "loopSettings" + case loopSettings case teamID case expirationDate } diff --git a/LoopFollow/Controllers/Nightscout/Profile.swift b/LoopFollow/Controllers/Nightscout/Profile.swift index 8f444e07e..a298c4b1c 100644 --- a/LoopFollow/Controllers/Nightscout/Profile.swift +++ b/LoopFollow/Controllers/Nightscout/Profile.swift @@ -7,19 +7,20 @@ // import Foundation + extension MainViewController { // NS Profile Web Call func webLoadNSProfile() { NightscoutUtils.executeRequest(eventType: .profile, parameters: [:]) { (result: Result) in switch result { - case .success(let profileData): + case let .success(profileData): self.updateProfile(profileData: profileData) - case .failure(let error): + case let .failure(error): LogManager.shared.log(category: .nightscout, message: "webLoadNSProfile, error fetching profile data: \(error.localizedDescription)") } } } - + // NS Profile Response Processor func updateProfile(profileData: NSProfile) { guard let store = profileData.store["default"] ?? profileData.store["Default"] else { @@ -27,18 +28,18 @@ extension MainViewController { } profileManager.loadProfile(from: profileData) infoManager.updateInfoData(type: .profile, value: profileData.defaultProfile) - + basalProfile.removeAll() for basalEntry in store.basal { let entry = basalProfileStruct(value: basalEntry.value, time: basalEntry.time, timeAsSeconds: basalEntry.timeAsSeconds) basalProfile.append(entry) } - + // Don't process the basal or draw the graph until after the BG has been fully processeed and drawn if firstGraphLoad { return } - + var basalSegments: [DataStructs.basalProfileSegment] = [] - + let graphHours = 24 * UserDefaultsRepository.downloadDays.value // Build scheduled basal segments from right to left by // moving pointers to the current midnight and current basal @@ -55,53 +56,55 @@ extension MainViewController { let graphStart = dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) while end >= graphStart { let entry = DataStructs.basalProfileSegment( - basalRate: basalProfile[basalProfileIndex].value, startDate: start, endDate: end) + basalRate: basalProfile[basalProfileIndex].value, startDate: start, endDate: end + ) basalSegments.append(entry) - + basalProfileIndex -= 1 if basalProfileIndex < 0 { basalProfileIndex = basalProfile.count - 1 - midnight = midnight.advanced(by: -24*60*60) + midnight = midnight.advanced(by: -24 * 60 * 60) } end = start - 1 start = midnight + basalProfile[basalProfileIndex].timeAsSeconds } // reverse the result to get chronological order basalSegments.reverse() - + var firstPass = true // Runs the scheduled basal to the end of the prediction line var predictionEndTime = dateTimeUtils.getNowTimeIntervalUTC() + (3600 * UserDefaultsRepository.predictionToLoad.value) basalScheduleData.removeAll() - - for i in 0.. predictionEndTime || i == basalSegments.count - 1 { endDate = Double(predictionEndTime) } - + let endDot = basalGraphStruct(basalRate: basalSegments[i].basalRate, date: endDate) basalScheduleData.append(endDot) } - + // we need to manually set the first one // Check that this is the first one and there are no existing entries if firstPass == true { // check that the timestamp is > the current entry and < the next entry - if timeStart >= basalSegments[i].startDate && timeStart < basalSegments[i].endDate { + if timeStart >= basalSegments[i].startDate, timeStart < basalSegments[i].endDate { // Set the start time to match the BG start let startDot = basalGraphStruct(basalRate: basalSegments[i].basalRate, date: Double(timeStart + (60 * 5))) basalScheduleData.append(startDot) - + // set the enddot where the next one will start var endDate = basalSegments[i].endDate let endDot = basalGraphStruct(basalRate: basalSegments[i].basalRate, date: endDate) @@ -110,7 +113,7 @@ extension MainViewController { } } } - + if UserDefaultsRepository.graphBasal.value { updateBasalScheduledGraph() } diff --git a/LoopFollow/Controllers/Nightscout/ProfileManager.swift b/LoopFollow/Controllers/Nightscout/ProfileManager.swift index 60b54fa31..adba5251f 100644 --- a/LoopFollow/Controllers/Nightscout/ProfileManager.swift +++ b/LoopFollow/Controllers/Nightscout/ProfileManager.swift @@ -8,9 +8,11 @@ import HealthKit final class ProfileManager { // MARK: - Singleton Instance + static let shared = ProfileManager() // MARK: - Properties + var isfSchedule: [TimeValue] var basalSchedule: [TimeValue] var carbRatioSchedule: [TimeValue] @@ -23,6 +25,7 @@ final class ProfileManager { var defaultProfile: String // MARK: - Nested Structures + struct TimeValue { let timeAsSeconds: Int let value: T @@ -44,39 +47,42 @@ final class ProfileManager { } // MARK: - Initializer + private init() { - self.isfSchedule = [] - self.basalSchedule = [] - self.carbRatioSchedule = [] - self.targetLowSchedule = [] - self.targetHighSchedule = [] - self.loopOverrides = [] - self.trioOverrides = [] - self.units = .millimolesPerLiter - self.timezone = TimeZone.current - self.defaultProfile = "" + isfSchedule = [] + basalSchedule = [] + carbRatioSchedule = [] + targetLowSchedule = [] + targetHighSchedule = [] + loopOverrides = [] + trioOverrides = [] + units = .millimolesPerLiter + timezone = TimeZone.current + defaultProfile = "" } // MARK: - Methods + func loadProfile(from profileData: NSProfile) { guard let store = profileData.store[profileData.defaultProfile] else { return } - self.units = store.units.lowercased() == "mg/dl" ? .milligramsPerDeciliter : .millimolesPerLiter - self.defaultProfile = profileData.defaultProfile + units = store.units.lowercased() == "mg/dl" ? .milligramsPerDeciliter : .millimolesPerLiter + defaultProfile = profileData.defaultProfile - self.timezone = getTimeZone(from: store.timezone) + timezone = getTimeZone(from: store.timezone) - self.isfSchedule = store.sens.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: HKQuantity(unit: self.units, doubleValue: $0.value)) } - self.basalSchedule = store.basal.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: $0.value) } - self.carbRatioSchedule = store.carbratio.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: $0.value) } - self.targetLowSchedule = store.target_low?.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: HKQuantity(unit: self.units, doubleValue: $0.value)) } ?? [] - self.targetHighSchedule = store.target_high?.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: HKQuantity(unit: self.units, doubleValue: $0.value)) } ?? [] + isfSchedule = store.sens.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: HKQuantity(unit: self.units, doubleValue: $0.value)) } + basalSchedule = store.basal.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: $0.value) } + carbRatioSchedule = store.carbratio.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: $0.value) } + targetLowSchedule = store.target_low?.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: HKQuantity(unit: self.units, doubleValue: $0.value)) } ?? [] + targetHighSchedule = store.target_high?.map { TimeValue(timeAsSeconds: Int($0.timeAsSeconds), value: HKQuantity(unit: self.units, doubleValue: $0.value)) } ?? [] if let loopSettings = profileData.loopSettings, - let overridePresets = loopSettings.overridePresets { - self.loopOverrides = overridePresets.map { preset in + let overridePresets = loopSettings.overridePresets + { + loopOverrides = overridePresets.map { preset in let targetRangeQuantities = preset.targetRange?.map { HKQuantity(unit: self.units, doubleValue: $0) } return LoopOverride( name: preset.name, @@ -87,7 +93,7 @@ final class ProfileManager { ) } } else { - self.loopOverrides = [] + loopOverrides = [] } if let trioOverrides = profileData.trioOverrides { @@ -101,7 +107,7 @@ final class ProfileManager { ) } } else { - self.trioOverrides = [] + trioOverrides = [] } Storage.shared.deviceToken.value = profileData.deviceToken ?? "" @@ -143,11 +149,11 @@ final class ProfileManager { let now = Date() var calendar = Calendar.current - calendar.timeZone = self.timezone + calendar.timeZone = timezone let currentTimeInSeconds = calendar.component(.hour, from: now) * 3600 + - calendar.component(.minute, from: now) * 60 + - calendar.component(.second, from: now) + calendar.component(.minute, from: now) * 60 + + calendar.component(.second, from: now) var lastValue: T? for timeValue in schedule { @@ -188,15 +194,15 @@ final class ProfileManager { } func clear() { - self.isfSchedule = [] - self.basalSchedule = [] - self.carbRatioSchedule = [] - self.targetLowSchedule = [] - self.targetHighSchedule = [] - self.loopOverrides = [] - self.trioOverrides = [] - self.units = .millimolesPerLiter - self.timezone = TimeZone.current - self.defaultProfile = "" + isfSchedule = [] + basalSchedule = [] + carbRatioSchedule = [] + targetLowSchedule = [] + targetHighSchedule = [] + loopOverrides = [] + trioOverrides = [] + units = .millimolesPerLiter + timezone = TimeZone.current + defaultProfile = "" } } diff --git a/LoopFollow/Controllers/Nightscout/SAge.swift b/LoopFollow/Controllers/Nightscout/SAge.swift index 3488667ae..1ca3d947b 100644 --- a/LoopFollow/Controllers/Nightscout/SAge.swift +++ b/LoopFollow/Controllers/Nightscout/SAge.swift @@ -1,5 +1,5 @@ // -// SAGE.swift +// SAge.swift // LoopFollow // // Created by Jonas Björkert on 2023-10-05. @@ -7,31 +7,32 @@ // import Foundation + extension MainViewController { // NS Sage Web Call func webLoadNSSage() { let lastDateString = dateTimeUtils.getDateTimeString(addingDays: -60) let currentTimeString = dateTimeUtils.getDateTimeString() - + let parameters: [String: String] = [ "find[eventType]": NightscoutUtils.EventType.sage.rawValue, "find[created_at][$gte]": lastDateString, "find[created_at][$lte]": currentTimeString, - "count": "1" + "count": "1", ] - + NightscoutUtils.executeRequest(eventType: .sage, parameters: parameters) { (result: Result<[sageData], Error>) in switch result { - case .success(let data): + case let .success(data): DispatchQueue.main.async { self.updateSage(data: data) } - case .failure(let error): + case let .failure(error): LogManager.shared.log(category: .nightscout, message: "webLoadNSSage, failed to fetch data: \(error.localizedDescription)") } } } - + // NS Sage Response Processor func updateSage(data: [sageData]) { infoManager.clearInfoData(type: .sage) @@ -41,15 +42,15 @@ extension MainViewController { } currentSage = data[0] var lastSageString = data[0].created_at - + let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withFullDate, .withTime, .withDashSeparatorInDate, .withColonSeparatorInTime] - UserDefaultsRepository.alertSageInsertTime.value = formatter.date(from: (lastSageString))?.timeIntervalSince1970 as! TimeInterval - - if UserDefaultsRepository.alertAutoSnoozeCGMStart.value && (dateTimeUtils.getNowTimeIntervalUTC() - UserDefaultsRepository.alertSageInsertTime.value < 7200) { + UserDefaultsRepository.alertSageInsertTime.value = formatter.date(from: lastSageString)?.timeIntervalSince1970 as! TimeInterval + + if UserDefaultsRepository.alertAutoSnoozeCGMStart.value, dateTimeUtils.getNowTimeIntervalUTC() - UserDefaultsRepository.alertSageInsertTime.value < 7200 { let snoozeTime = Date(timeIntervalSince1970: UserDefaultsRepository.alertSageInsertTime.value + 7200) UserDefaultsRepository.alertSnoozeAllTime.value = snoozeTime UserDefaultsRepository.alertSnoozeAllIsSnoozed.value = true @@ -58,12 +59,12 @@ extension MainViewController { if let sageTime = formatter.date(from: (lastSageString as! String))?.timeIntervalSince1970 { let now = dateTimeUtils.getNowTimeIntervalUTC() let secondsAgo = now - sageTime - + let formatter = DateComponentsFormatter() formatter.unitsStyle = .positional // Use the appropriate positioning for the current locale - formatter.allowedUnits = [ .day, .hour] // Units to display in the formatted string - formatter.zeroFormattingBehavior = [ .pad ] // Pad with zeroes where appropriate for the locale - + formatter.allowedUnits = [.day, .hour] // Units to display in the formatted string + formatter.zeroFormattingBehavior = [.pad] // Pad with zeroes where appropriate for the locale + if let formattedDuration = formatter.string(from: secondsAgo) { infoManager.updateInfoData(type: .sage, value: formattedDuration) } diff --git a/LoopFollow/Controllers/Nightscout/Treatments.swift b/LoopFollow/Controllers/Nightscout/Treatments.swift index 66f8f159e..2785219a5 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments.swift @@ -7,21 +7,22 @@ // import Foundation + extension MainViewController { // NS Treatments Web Call // Downloads Basal, Bolus, Carbs, BG Check, Notes, Overrides func WebLoadNSTreatments() { if !UserDefaultsRepository.downloadTreatments.value { return } - + let startTimeString = dateTimeUtils.getDateTimeString(addingDays: -1 * UserDefaultsRepository.downloadDays.value) let currentTimeString = dateTimeUtils.getDateTimeString(addingHours: 6) let parameters: [String: String] = [ "find[created_at][$gte]": startTimeString, - "find[created_at][$lte]": currentTimeString + "find[created_at][$lte]": currentTimeString, ] NightscoutUtils.executeDynamicRequest(eventType: .treatments, parameters: parameters) { (result: Result) in switch result { - case .success(let data): + case let .success(data): if let entries = data as? [[String: AnyObject]] { DispatchQueue.main.async { self.updateTreatments(entries: entries) @@ -29,25 +30,24 @@ extension MainViewController { } else { LogManager.shared.log(category: .nightscout, message: "WebLoadNSTreatments, Unexpected data structure") } - case .failure(let error): + case let .failure(error): LogManager.shared.log(category: .nightscout, message: "WebLoadNSTreatments, error \(error.localizedDescription)") } } } - + // Process and split out treatments to individual tasks - func updateTreatments(entries: [[String:AnyObject]]) { - - var tempBasal: [[String:AnyObject]] = [] - var bolus: [[String:AnyObject]] = [] - var smb: [[String:AnyObject]] = [] - var carbs: [[String:AnyObject]] = [] - var temporaryOverride: [[String:AnyObject]] = [] - var temporaryTarget: [[String:AnyObject]] = [] - var note: [[String:AnyObject]] = [] - var bgCheck: [[String:AnyObject]] = [] - var suspendPump: [[String:AnyObject]] = [] - var resumePump: [[String:AnyObject]] = [] + func updateTreatments(entries: [[String: AnyObject]]) { + var tempBasal: [[String: AnyObject]] = [] + var bolus: [[String: AnyObject]] = [] + var smb: [[String: AnyObject]] = [] + var carbs: [[String: AnyObject]] = [] + var temporaryOverride: [[String: AnyObject]] = [] + var temporaryTarget: [[String: AnyObject]] = [] + var note: [[String: AnyObject]] = [] + var bgCheck: [[String: AnyObject]] = [] + var suspendPump: [[String: AnyObject]] = [] + var resumePump: [[String: AnyObject]] = [] var pumpSiteChange: [cageData] = [] var cgmSensorStart: [sageData] = [] var insulinCartridge: [iageData] = [] @@ -56,7 +56,7 @@ extension MainViewController { guard let eventType = entry["eventType"] as? String else { continue } - + switch eventType { case "Temp Basal": tempBasal.append(entry) @@ -104,7 +104,7 @@ extension MainViewController { print("No Match: \(String(describing: entry))") } } - + if tempBasal.count > 0 { processNSBasals(entries: tempBasal) } else { @@ -141,14 +141,14 @@ extension MainViewController { clearOldBGCheck() } } - if temporaryOverride.count == 0 && overrideGraphData.count > 0 { + if temporaryOverride.count == 0, overrideGraphData.count > 0 { clearOldOverride() } if temporaryOverride.count > 0 { processNSOverrides(entries: temporaryOverride) } - if temporaryTarget.count == 0 && tempTargetGraphData.count > 0 { + if temporaryTarget.count == 0, tempTargetGraphData.count > 0 { clearOldTempTarget() } if temporaryTarget.count > 0 { diff --git a/LoopFollow/Controllers/Nightscout/Treatments/BGCheck.swift b/LoopFollow/Controllers/Nightscout/Treatments/BGCheck.swift index 90732cad4..d6d0f8a78 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/BGCheck.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/BGCheck.swift @@ -1,5 +1,5 @@ // -// CarbsToday.swift +// BGCheck.swift // LoopFollow // // Created by Jonas Björkert on 2023-10-04. @@ -11,17 +11,18 @@ import UIKit extension MainViewController { // NS BG Check Response Processor - func processNSBGCheck(entries: [[String:AnyObject]]) { + func processNSBGCheck(entries: [[String: AnyObject]]) { bgCheckData.removeAll() - - entries.reversed().forEach { currentEntry in - guard let dateStr = currentEntry["timestamp"] as? String ?? currentEntry["created_at"] as? String else { return } - + + for currentEntry in entries.reversed() { + guard let dateStr = currentEntry["timestamp"] as? String ?? currentEntry["created_at"] as? String else { continue } + guard let parsedDate = NightscoutUtils.parseDate(dateStr), - let glucose = currentEntry["glucose"] as? Double else { - return + let glucose = currentEntry["glucose"] as? Double + else { + continue } - + let units = currentEntry["units"] as? String ?? "mg/dl" let convertedGlucose: Double = units == "mmol" ? glucose * GlucoseConversion.mmolToMgDl : glucose @@ -31,10 +32,9 @@ extension MainViewController { bgCheckData.append(dot) } } - + if UserDefaultsRepository.graphOtherTreatments.value { updateBGCheckGraph() } } } - diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Basals.swift b/LoopFollow/Controllers/Nightscout/Treatments/Basals.swift index 409646211..a611d3a99 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Basals.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Basals.swift @@ -9,9 +9,8 @@ import Foundation extension MainViewController { - // NS Temp Basal Response Processor - func processNSBasals(entries: [[String:AnyObject]]) { + func processNSBasals(entries: [[String: AnyObject]]) { infoManager.clearInfoData(type: .basal) basalData.removeAll() @@ -21,14 +20,15 @@ extension MainViewController { var tempArray = entries tempArray.reverse() - for i in 0.. 0 { - let priorEntry = tempArray[i - 1] as [String : AnyObject]? + let priorEntry = tempArray[i - 1] as [String: AnyObject]? let priorDateStr = priorEntry?["timestamp"] as? String - ?? priorEntry?["created_at"] as? String + ?? priorEntry?["created_at"] as? String if let rawPrior = priorDateStr, - let priorDateParsed = NightscoutUtils.parseDate(rawPrior) { - + let priorDateParsed = NightscoutUtils.parseDate(rawPrior) + { let priorDateTimeStamp = priorDateParsed.timeIntervalSince1970 let priorDuration = priorEntry?["duration"] as? Double ?? 0.0 @@ -55,7 +55,7 @@ extension MainViewController { var midGapTime: TimeInterval = 0 var midGapValue: Double = 0 - for b in 0..= basalScheduleData[b].date { scheduled = basalScheduleData[b].basalRate @@ -101,12 +101,12 @@ extension MainViewController { // Overlap check if i < tempArray.count - 1 { - let nextEntry = tempArray[i + 1] as [String : AnyObject]? + let nextEntry = tempArray[i + 1] as [String: AnyObject]? let nextDateStr = nextEntry?["timestamp"] as? String - ?? nextEntry?["created_at"] as? String + ?? nextEntry?["created_at"] as? String if let rawNext = nextDateStr, - let nextDateParsed = NightscoutUtils.parseDate(rawNext) { - + let nextDateParsed = NightscoutUtils.parseDate(rawNext) + { let nextDateTimeStamp = nextDateParsed.timeIntervalSince1970 if nextDateTimeStamp < (dateTimeStamp + (duration * 60)) { lastDot = nextDateTimeStamp @@ -122,9 +122,9 @@ extension MainViewController { // If last basal was prior to right now, we need to create one last scheduled entry if lastEndDot <= dateTimeUtils.getNowTimeIntervalUTC() { var scheduled = 0.0 - for b in 0..= scheduleTimeToday { scheduled = basalProfile[b].value } @@ -147,7 +147,8 @@ extension MainViewController { } if let profileBasal = profileManager.currentBasal(), - profileBasal != latestBasal { + profileBasal != latestBasal + { latestBasal = "\(profileBasal) → \(latestBasal)" } infoManager.updateInfoData(type: .basal, value: latestBasal) diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Bolus.swift b/LoopFollow/Controllers/Nightscout/Treatments/Bolus.swift index 15916a179..df643539b 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Bolus.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Bolus.swift @@ -7,37 +7,38 @@ // import Foundation + extension MainViewController { // NS Meal Bolus Response Processor - func processNSBolus(entries: [[String:AnyObject]]) { + func processNSBolus(entries: [[String: AnyObject]]) { // because it's a small array, we're going to destroy and reload every time. bolusData.removeAll() var lastFoundIndex = 0 - - entries.reversed().forEach { currentEntry in + + for currentEntry in entries.reversed() { var bolusDate: String if currentEntry["timestamp"] != nil { bolusDate = currentEntry["timestamp"] as! String } else if currentEntry["created_at"] != nil { bolusDate = currentEntry["created_at"] as! String } else { - return + continue } - + guard let parsedDate = NightscoutUtils.parseDate(bolusDate), - let bolus = currentEntry["insulin"] as? Double else { return } - + let bolus = currentEntry["insulin"] as? Double else { continue } + let dateTimeStamp = parsedDate.timeIntervalSince1970 let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex) lastFoundIndex = sgv.foundIndex - + if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (60 * 60)) { // Make the dot let dot = bolusGraphStruct(value: bolus, date: Double(dateTimeStamp), sgv: Int(sgv.sgv + 20)) bolusData.append(dot) } } - + if UserDefaultsRepository.graphBolus.value { updateBolusGraph() } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift b/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift index 285e1f5e4..cb97648ad 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift @@ -7,62 +7,63 @@ // import Foundation + extension MainViewController { // NS Carb Bolus Response Processor - func processNSCarbs(entries: [[String:AnyObject]]) { + func processNSCarbs(entries: [[String: AnyObject]]) { // Because it's a small array, we're going to destroy and reload every time. carbData.removeAll() var lastFoundIndex = 0 var lastFoundBolus = 0 - - entries.reversed().forEach { currentEntry in + + for currentEntry in entries.reversed() { var carbDate: String if currentEntry["timestamp"] != nil { carbDate = currentEntry["timestamp"] as! String } else if currentEntry["created_at"] != nil { carbDate = currentEntry["created_at"] as! String } else { - return + continue } - + let absorptionTime = currentEntry["absorptionTime"] as? Int ?? 0 - + guard let parsedDate = NightscoutUtils.parseDate(carbDate), - let carbs = currentEntry["carbs"] as? Double else { return } - + let carbs = currentEntry["carbs"] as? Double else { continue } + let dateTimeStamp = parsedDate.timeIntervalSince1970 let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex) lastFoundIndex = sgv.foundIndex - + var offset = -50 if sgv.sgv < Double(calculateMaxBgGraphValue() - 100) { let bolusTime = findNearestBolusbyTime(timeWithin: 300, needle: dateTimeStamp, haystack: bolusData, startingIndex: lastFoundBolus) lastFoundBolus = bolusTime.foundIndex - + offset = bolusTime.offset ? 70 : 20 } - + if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (3600 * UserDefaultsRepository.predictionToLoad.value)) { // Make the dot let dot = carbGraphStruct(value: Double(carbs), date: Double(dateTimeStamp), sgv: Int(sgv.sgv + Double(offset)), absorptionTime: absorptionTime) carbData.append(dot) } } - + if UserDefaultsRepository.graphCarbs.value { updateCarbGraph() } } - - func updateTodaysCarbsFromEntries(entries: [[String:AnyObject]]) { - var totalCarbs: Double = 0.0 - + + func updateTodaysCarbsFromEntries(entries: [[String: AnyObject]]) { + var totalCarbs = 0.0 + let calendar = Calendar.current let now = Date() - + for entry in entries { - var carbDate: String = "" - + var carbDate = "" + if let timestamp = entry["timestamp"] as? String { carbDate = timestamp } else if let createdAt = entry["created_at"] as? String { @@ -71,11 +72,11 @@ extension MainViewController { print("Skipping entry with no timestamp or created_at") continue } - + guard let date = NightscoutUtils.parseDate(carbDate) else { continue } - + if calendar.isDateInToday(date) { if let carbs = entry["carbs"] as? Double { totalCarbs += carbs @@ -84,7 +85,7 @@ extension MainViewController { } } } - + let resultString = String(format: "%.0f", totalCarbs) infoManager.updateInfoData(type: .carbsToday, value: resultString) } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/InsulinCartridgeChange.swift b/LoopFollow/Controllers/Nightscout/Treatments/InsulinCartridgeChange.swift index d993e37b2..e583d0904 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/InsulinCartridgeChange.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/InsulinCartridgeChange.swift @@ -7,6 +7,7 @@ // import Foundation + extension MainViewController { func processIage(entries: [iageData]) { if !entries.isEmpty { diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Notes.swift b/LoopFollow/Controllers/Nightscout/Treatments/Notes.swift index 1dea8e0ce..a97ebb6fb 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Notes.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Notes.swift @@ -1,5 +1,5 @@ // -// CarbsToday.swift +// Notes.swift // LoopFollow // // Created by Jonas Björkert on 2023-10-04. @@ -11,30 +11,30 @@ import UIKit extension MainViewController { // NS Note Response Processor - func processNotes(entries: [[String:AnyObject]]) { + func processNotes(entries: [[String: AnyObject]]) { // because it's a small array, we're going to destroy and reload every time. noteGraphData.removeAll() var lastFoundIndex = 0 - - entries.reversed().forEach { currentEntry in - guard let currentEntry = currentEntry as? [String: AnyObject] else { return } - + + for currentEntry in entries.reversed() { + guard let currentEntry = currentEntry as? [String: AnyObject] else { continue } + var date: String if currentEntry["timestamp"] != nil { date = currentEntry["timestamp"] as! String } else if currentEntry["created_at"] != nil { date = currentEntry["created_at"] as! String } else { - return + continue } - + if let parsedDate = NightscoutUtils.parseDate(date) { let dateTimeStamp = parsedDate.timeIntervalSince1970 let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex) lastFoundIndex = sgv.foundIndex - - guard let thisNote = currentEntry["notes"] as? String else { return } - + + guard let thisNote = currentEntry["notes"] as? String else { continue } + if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (60 * 60)) { let dot = DataStructs.noteStruct(date: Double(dateTimeStamp), sgv: Int(sgv.sgv), note: thisNote) noteGraphData.append(dot) @@ -43,7 +43,7 @@ extension MainViewController { print("Failed to parse date") } } - + if UserDefaultsRepository.graphOtherTreatments.value { updateNotes() } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift b/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift index 9fbaa2fa3..a555b9c19 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift @@ -11,40 +11,40 @@ import UIKit extension MainViewController { // NS Override Response Processor - func processNSOverrides(entries: [[String:AnyObject]]) { + func processNSOverrides(entries: [[String: AnyObject]]) { overrideGraphData.removeAll() - var activeOverrideNote: String? = nil + var activeOverrideNote: String? let now = Date().timeIntervalSince1970 let predictionLoadHours = UserDefaultsRepository.predictionToLoad.value let predictionLoadSeconds = predictionLoadHours * 3600 let maxEndDate = now + predictionLoadSeconds - entries.reversed().enumerated().forEach { (index, currentEntry) in - guard let dateStr = currentEntry["timestamp"] as? String ?? currentEntry["created_at"] as? String else { return } - guard let parsedDate = NightscoutUtils.parseDate(dateStr) else { return } + for (index, currentEntry) in entries.reversed().enumerated() { + guard let dateStr = currentEntry["timestamp"] as? String ?? currentEntry["created_at"] as? String else { continue } + guard let parsedDate = NightscoutUtils.parseDate(dateStr) else { continue } var dateTimeStamp = parsedDate.timeIntervalSince1970 let graphHours = 24 * UserDefaultsRepository.downloadDays.value if dateTimeStamp < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { dateTimeStamp = dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) } - + let multiplier = currentEntry["insulinNeedsScaleFactor"] as? Double ?? 1.0 - - var duration: Double = 5.0 + + var duration = 5.0 if let _ = currentEntry["durationType"] as? String, index == entries.count - 1 { duration = dateTimeUtils.getNowTimeIntervalUTC() - dateTimeStamp + (60 * 60) } else { duration = (currentEntry["duration"] as? Double ?? 5.0) * 60 } - - if duration < 300 { return } + + if duration < 300 { continue } let reason = currentEntry["reason"] as? String ?? "" guard let enteredBy = currentEntry["enteredBy"] as? String else { - return + continue } var range: [Int] = [] @@ -53,7 +53,7 @@ extension MainViewController { } else { let low = currentEntry["targetBottom"] as? Int let high = currentEntry["targetTop"] as? Int - if (low == nil && high != nil) || (low != nil && high == nil) { return } + if (low == nil && high != nil) || (low != nil && high == nil) { continue } range = [low ?? 0, high ?? 0] } @@ -64,19 +64,18 @@ extension MainViewController { duration = endDate - dateTimeStamp } - if dateTimeStamp <= now && now < endDate { + if dateTimeStamp <= now, now < endDate { activeOverrideNote = currentEntry["notes"] as? String ?? currentEntry["reason"] as? String } let dot = DataStructs.overrideStruct(insulNeedsScaleFactor: multiplier, date: dateTimeStamp, endDate: endDate, duration: duration, correctionRange: range, enteredBy: enteredBy, reason: reason, sgv: -20) overrideGraphData.append(dot) } - + Observable.shared.override.value = activeOverrideNote if ObservableUserDefaults.shared.device.value == "Trio" { - if let note = activeOverrideNote - { + if let note = activeOverrideNote { infoManager.updateInfoData(type: .override, value: note) } else { infoManager.clearInfoData(type: .override) diff --git a/LoopFollow/Controllers/Nightscout/Treatments/ResumePump.swift b/LoopFollow/Controllers/Nightscout/Treatments/ResumePump.swift index fd803d168..1e2c7a74f 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/ResumePump.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/ResumePump.swift @@ -7,30 +7,31 @@ // import Foundation + extension MainViewController { // NS Resume Pump Response Processor - func processResumePump(entries: [[String:AnyObject]]) { + func processResumePump(entries: [[String: AnyObject]]) { resumeGraphData.removeAll() - + var lastFoundIndex = 0 - - entries.reversed().forEach { currentEntry in - guard let dateStr = currentEntry["timestamp"] as? String ?? currentEntry["created_at"] as? String else { return } - + + for currentEntry in entries.reversed() { + guard let dateStr = currentEntry["timestamp"] as? String ?? currentEntry["created_at"] as? String else { continue } + guard let parsedDate = NightscoutUtils.parseDate(dateStr) else { - return + continue } - + let dateTimeStamp = parsedDate.timeIntervalSince1970 let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex) lastFoundIndex = sgv.foundIndex - + if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (60 * 60)) { let dot = DataStructs.timestampOnlyStruct(date: Double(dateTimeStamp), sgv: Int(sgv.sgv)) resumeGraphData.append(dot) } } - + if UserDefaultsRepository.graphOtherTreatments.value { updateResumeGraph() } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/SMB.swift b/LoopFollow/Controllers/Nightscout/Treatments/SMB.swift index 3d497c111..80108cb04 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/SMB.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/SMB.swift @@ -7,36 +7,37 @@ // import Foundation + extension MainViewController { // NS SMB Processor - func processNSSmb(entries: [[String:AnyObject]]) { + func processNSSmb(entries: [[String: AnyObject]]) { smbData.removeAll() var lastFoundIndex = 0 - - entries.reversed().forEach { currentEntry in + + for currentEntry in entries.reversed() { var bolusDate: String if currentEntry["timestamp"] != nil { bolusDate = currentEntry["timestamp"] as! String } else if currentEntry["created_at"] != nil { bolusDate = currentEntry["created_at"] as! String } else { - return + continue } - + guard let parsedDate = NightscoutUtils.parseDate(bolusDate), - let bolus = currentEntry["insulin"] as? Double else { return } - + let bolus = currentEntry["insulin"] as? Double else { continue } + let dateTimeStamp = parsedDate.timeIntervalSince1970 let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex) lastFoundIndex = sgv.foundIndex - + if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (60 * 60)) { // Make the dot let dot = bolusGraphStruct(value: bolus, date: Double(dateTimeStamp), sgv: Int(sgv.sgv + 20)) smbData.append(dot) } } - + if UserDefaultsRepository.graphBolus.value { updateSmbGraph() updateChartRenderers() diff --git a/LoopFollow/Controllers/Nightscout/Treatments/SensorStart.swift b/LoopFollow/Controllers/Nightscout/Treatments/SensorStart.swift index cd1370b7b..be2d0f174 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/SensorStart.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/SensorStart.swift @@ -7,6 +7,7 @@ // import Foundation + extension MainViewController { func processSage(entries: [sageData]) { if !entries.isEmpty { @@ -17,19 +18,19 @@ extension MainViewController { webLoadNSSage() } } - + // NS Sensor Start Response Processor func processSensorStart(entries: [sageData]) { sensorStartGraphData.removeAll() var lastFoundIndex = 0 for entry in entries { let date = entry.created_at - + if let parsedDate = NightscoutUtils.parseDate(date) { let dateTimeStamp = parsedDate.timeIntervalSince1970 let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex) lastFoundIndex = sgv.foundIndex - + if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (60 * 60)) { let dot = DataStructs.timestampOnlyStruct(date: Double(dateTimeStamp), sgv: Int(sgv.sgv)) sensorStartGraphData.append(dot) diff --git a/LoopFollow/Controllers/Nightscout/Treatments/SiteChange.swift b/LoopFollow/Controllers/Nightscout/Treatments/SiteChange.swift index 9b7a11c65..459c8e801 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/SiteChange.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/SiteChange.swift @@ -7,6 +7,7 @@ // import Foundation + extension MainViewController { func processCage(entries: [cageData]) { if !entries.isEmpty { diff --git a/LoopFollow/Controllers/Nightscout/Treatments/SuspendPump.swift b/LoopFollow/Controllers/Nightscout/Treatments/SuspendPump.swift index 43ba0568b..35cda12d8 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/SuspendPump.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/SuspendPump.swift @@ -7,30 +7,31 @@ // import Foundation + extension MainViewController { // NS Suspend Pump Response Processor - func processSuspendPump(entries: [[String:AnyObject]]) { + func processSuspendPump(entries: [[String: AnyObject]]) { suspendGraphData.removeAll() - + var lastFoundIndex = 0 - - entries.reversed().forEach { currentEntry in - guard let dateStr = currentEntry["timestamp"] as? String ?? currentEntry["created_at"] as? String else { return } - + + for currentEntry in entries.reversed() { + guard let dateStr = currentEntry["timestamp"] as? String ?? currentEntry["created_at"] as? String else { continue } + guard let parsedDate = NightscoutUtils.parseDate(dateStr) else { - return + continue } - + let dateTimeStamp = parsedDate.timeIntervalSince1970 let sgv = findNearestBGbyTime(needle: dateTimeStamp, haystack: bgData, startingIndex: lastFoundIndex) lastFoundIndex = sgv.foundIndex - + if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (60 * 60)) { let dot = DataStructs.timestampOnlyStruct(date: Double(dateTimeStamp), sgv: Int(sgv.sgv)) suspendGraphData.append(dot) } } - + if UserDefaultsRepository.graphOtherTreatments.value { updateSuspendGraph() } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/TemporaryTarget.swift b/LoopFollow/Controllers/Nightscout/Treatments/TemporaryTarget.swift index b34f522a9..38e2e15a8 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/TemporaryTarget.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/TemporaryTarget.swift @@ -7,18 +7,18 @@ // import Foundation -import UIKit import HealthKit +import UIKit extension MainViewController { // NS Temporary Target Response Processor func processNSTemporaryTarget(entries: [[String: AnyObject]]) { tempTargetGraphData.removeAll() - var activeTempTarget: Int? = nil + var activeTempTarget: Int? - entries.reversed().enumerated().forEach { (index, currentEntry) in - guard let dateStr = currentEntry["timestamp"] as? String ?? currentEntry["created_at"] as? String else { return } - guard let parsedDate = NightscoutUtils.parseDate(dateStr) else { return } + for (index, currentEntry) in entries.reversed().enumerated() { + guard let dateStr = currentEntry["timestamp"] as? String ?? currentEntry["created_at"] as? String else { continue } + guard let parsedDate = NightscoutUtils.parseDate(dateStr) else { continue } var dateTimeStamp = parsedDate.timeIntervalSince1970 let graphHours = 24 * UserDefaultsRepository.downloadDays.value @@ -34,17 +34,17 @@ extension MainViewController { tempTargetGraphData[activeIndex].endDate = dateTimeStamp activeTempTarget = nil } - return + continue } if duration < 300 { - return + continue } let reason = currentEntry["reason"] as? String ?? "" guard let enteredBy = currentEntry["enteredBy"] as? String else { - return + continue } let low = currentEntry["targetBottom"] as? Double @@ -52,7 +52,7 @@ extension MainViewController { let targetValue = low ?? high if targetValue == nil { - return + continue } let endDate = dateTimeStamp + duration diff --git a/LoopFollow/Controllers/SpeakBG.swift b/LoopFollow/Controllers/SpeakBG.swift index 293a939b6..6c34ee5d0 100644 --- a/LoopFollow/Controllers/SpeakBG.swift +++ b/LoopFollow/Controllers/SpeakBG.swift @@ -1,13 +1,13 @@ -import Foundation import AVFoundation import CallKit +import Foundation extension MainViewController { func evaluateSpeakConditions(currentValue: Int, previousValue: Int) { if !UserDefaultsRepository.speakBG.value { return } - + let always = UserDefaultsRepository.speakBGAlways.value let lowThreshold = UserDefaultsRepository.speakLowBGLimit.value let fastDropDelta = UserDefaultsRepository.speakFastDropDelta.value @@ -15,7 +15,7 @@ extension MainViewController { let speakLowBG = UserDefaultsRepository.speakLowBG.value let speakProactiveLowBG = UserDefaultsRepository.speakProactiveLowBG.value let speakHighBG = UserDefaultsRepository.speakHighBG.value - + // Speak always if always { speakBG(currentValue: currentValue, previousValue: previousValue) @@ -23,7 +23,7 @@ extension MainViewController { return } - + // Speak if low or last value was low if speakLowBG { if currentValue <= Int(lowThreshold) || previousValue <= Int(lowThreshold) { @@ -32,24 +32,25 @@ extension MainViewController { return } } - + // Speak predictive low if... // * low or last value was low // * next predictive value is low // * fast drop occurs below high if speakProactiveLowBG { let predictiveTrigger = !predictionData.isEmpty && Float(predictionData.first!.sgv) <= lowThreshold - + if predictiveTrigger || currentValue <= Int(lowThreshold) || previousValue <= Int(lowThreshold) || - ((currentValue <= Int(highThreshold) && (previousValue - currentValue) >= Int(fastDropDelta))) { + (currentValue <= Int(highThreshold) && (previousValue - currentValue) >= Int(fastDropDelta)) + { speakBG(currentValue: currentValue, previousValue: previousValue) LogManager.shared.log(category: .general, message: "Speaking because of 'Proactive Low' condition. Predictive trigger: \(predictiveTrigger)", isDebug: true) return } } - - //Speak if high or if last value was high + + // Speak if high or if last value was high if speakHighBG { if currentValue >= Int(highThreshold) || previousValue >= Int(highThreshold) { speakBG(currentValue: currentValue, previousValue: previousValue) @@ -60,13 +61,13 @@ extension MainViewController { LogManager.shared.log(category: .general, message: "No condition met for speaking.", isDebug: true) } - + struct AnnouncementTexts { var stable: String var increase: String var decrease: String var currentBGIs: String - + static func forLanguage(_ language: String) -> AnnouncementTexts { switch language { case "it": @@ -101,21 +102,20 @@ extension MainViewController { } } } - - struct LanguageVoiceMapping { + + enum LanguageVoiceMapping { static let voiceLanguageMap: [String: String] = [ "en": "en-US", "it": "it-IT", "sk": "sk-SK", - "sv": "sv-SE" + "sv": "sv-SE", ] - + static func voiceLanguageCode(forAppLanguage appLanguage: String) -> String { return voiceLanguageMap[appLanguage, default: "en-US"] } } - // Speaks the current blood glucose value and the change from the previous value. // Repeated calls to the function within 30 seconds are prevented. func speakBG(currentValue: Int, previousValue: Int) { @@ -126,7 +126,7 @@ extension MainViewController { } catch { LogManager.shared.log(category: .alarm, message: "speakBG, Failed to set up audio session: \(error)") } - + // Get the current time let currentTime = Date() @@ -139,7 +139,7 @@ extension MainViewController { } // Update the last speech time - self.lastSpeechTime = currentTime + lastSpeechTime = currentTime let bloodGlucoseDifference = currentValue - previousValue @@ -147,11 +147,11 @@ extension MainViewController { let voiceLanguageCode = LanguageVoiceMapping.voiceLanguageCode(forAppLanguage: preferredLanguage) let texts = AnnouncementTexts.forLanguage(preferredLanguage) - + let negligibleThreshold = 3 let localizedCurrentValue = Localizer.toDisplayUnits(String(currentValue)).replacingOccurrences(of: ",", with: ".") let announcementText: String - + if abs(bloodGlucoseDifference) <= negligibleThreshold { announcementText = "\(texts.currentBGIs) \(localizedCurrentValue) \(texts.stable)" } else { @@ -159,11 +159,10 @@ extension MainViewController { let absoluteDifference = Localizer.toDisplayUnits(String(abs(bloodGlucoseDifference))).replacingOccurrences(of: ",", with: ".") announcementText = "\(texts.currentBGIs) \(localizedCurrentValue) \(directionText) \(absoluteDifference)" } - + let speechUtterance = AVSpeechUtterance(string: announcementText) speechUtterance.voice = AVSpeechSynthesisVoice(language: voiceLanguageCode) - + speechSynthesizer.speak(speechUtterance) } - } diff --git a/LoopFollow/Controllers/Stats.swift b/LoopFollow/Controllers/Stats.swift index a359db236..44e0c9621 100644 --- a/LoopFollow/Controllers/Stats.swift +++ b/LoopFollow/Controllers/Stats.swift @@ -8,9 +8,7 @@ import Foundation - class StatsData { - var countLow: Int var percentLow: Float var percentRange: Float @@ -23,38 +21,37 @@ class StatsData { var stdDev: Float var bgDataCount: Int var pie: [DataStructs.pieData] - + init(bgData: [ShareGlucoseData]) { - - self.countLow = 0 - self.countRange = 0 - self.countHigh = 0 - self.totalGlucose = 0 - self.a1C = 0.0 - - for i in 0..= UserDefaultsRepository.highLine.value { - self.countHigh += 1 + countHigh += 1 } else { - self.countRange += 1 + countRange += 1 } - + // set total bg for average totalGlucose += bgData[i].sgv } - + // Set Percents percentLow = Float(countLow) / Float(bgData.count) * 100 percentRange = Float(countRange) / Float(bgData.count) * 100 percentHigh = Float(countHigh) / Float(bgData.count) * 100 - + pie = [ DataStructs.pieData(name: "low", value: Double(percentLow)), DataStructs.pieData(name: "range", value: Double(percentRange)), - DataStructs.pieData(name: "high", value: Double(percentHigh)) + DataStructs.pieData(name: "high", value: Double(percentHigh)), ] // Set Average @@ -63,22 +60,20 @@ class StatsData { avgBG = Float(totalGlucose / bgDataCount) // compute std dev (sigma) - var partialSum: Float = 0; - for i in 0.. 0 { var lastDayOfData = bgData let graphHours = 24 * UserDefaultsRepository.downloadDays.value @@ -22,77 +19,70 @@ extension MainViewController { if graphHours > 24 { let oneDayAgo = dateTimeUtils.getTimeIntervalNHoursAgo(N: 24) var startIndex = 0 - while startIndex < bgData.count && bgData[startIndex].date < oneDayAgo { + while startIndex < bgData.count, bgData[startIndex].date < oneDayAgo { startIndex += 1 } lastDayOfData = Array(bgData.dropFirst(startIndex)) } - + let stats = StatsData(bgData: lastDayOfData) - - statsLowPercent.text = String(format:"%.1f%", stats.percentLow) + "%" - statsInRangePercent.text = String(format:"%.1f%", stats.percentRange) + "%" - statsHighPercent.text = String(format:"%.1f%", stats.percentHigh) + "%" - statsAvgBG.text = Localizer.toDisplayUnits(String(format:"%.0f%", stats.avgBG)) + + statsLowPercent.text = String(format: "%.1f%", stats.percentLow) + "%" + statsInRangePercent.text = String(format: "%.1f%", stats.percentRange) + "%" + statsHighPercent.text = String(format: "%.1f%", stats.percentHigh) + "%" + statsAvgBG.text = Localizer.toDisplayUnits(String(format: "%.0f%", stats.avgBG)) if UserDefaultsRepository.useIFCC.value { - statsEstA1C.text = String(format:"%.0f%", stats.a1C) - } - else - { - statsEstA1C.text = String(format:"%.1f%", stats.a1C) + statsEstA1C.text = String(format: "%.0f%", stats.a1C) + } else { + statsEstA1C.text = String(format: "%.1f%", stats.a1C) } - statsStdDev.text = String(format:"%.2f%", stats.stdDev) - + statsStdDev.text = String(format: "%.2f%", stats.stdDev) + createStatsPie(pieData: stats.pie) } - } - + func createStatsPie(pieData: [DataStructs.pieData]) { statsPieChart.legend.enabled = false statsPieChart.drawEntryLabelsEnabled = false statsPieChart.drawHoleEnabled = false statsPieChart.rotationEnabled = false - + var chartEntry = [PieChartDataEntry]() var colors = [NSUIColor]() - - for i in 0.. 0 { - for i in 0.. out of a Binding by substituting `defaultValue` when nil. - init(_ source: Binding, replacingNilWith defaultValue: Value) { - self.init( - get: { source.wrappedValue ?? defaultValue }, - set: { source.wrappedValue = $0 } - ) - } + /// Create a Binding out of a Binding by substituting `defaultValue` when nil. + init(_ source: Binding, replacingNilWith defaultValue: Value) { + self.init( + get: { source.wrappedValue ?? defaultValue }, + set: { source.wrappedValue = $0 } + ) + } } diff --git a/LoopFollow/Extensions/EKEventStore+Extensions.swift b/LoopFollow/Extensions/EKEventStore+Extensions.swift index 41917baa1..5233d6ba9 100644 --- a/LoopFollow/Extensions/EKEventStore+Extensions.swift +++ b/LoopFollow/Extensions/EKEventStore+Extensions.swift @@ -6,29 +6,29 @@ // Copyright © 2023 Jon Fawcett. All rights reserved. // -import Foundation import EventKit +import Foundation #if swift(>=5.9) -extension EKEventStore { - func requestCalendarAccess(completion: @escaping (Bool, Error?) -> Void) { - if #available(iOS 17, *) { - requestFullAccessToEvents { (granted, error) in - completion(granted, error) - } - } else { - requestAccess(to: .event) { (granted, error) in - completion(granted, error) + extension EKEventStore { + func requestCalendarAccess(completion: @escaping (Bool, Error?) -> Void) { + if #available(iOS 17, *) { + requestFullAccessToEvents { granted, error in + completion(granted, error) + } + } else { + requestAccess(to: .event) { granted, error in + completion(granted, error) + } } } } -} #else -extension EKEventStore { - func requestCalendarAccess(completion: @escaping (Bool, Error?) -> Void) { - requestAccess(to: .event) { (granted, error) in - completion(granted, error) + extension EKEventStore { + func requestCalendarAccess(completion: @escaping (Bool, Error?) -> Void) { + requestAccess(to: .event) { granted, error in + completion(granted, error) + } } } -} #endif diff --git a/LoopFollow/Extensions/HKQuantity+AnyConvertible.swift b/LoopFollow/Extensions/HKQuantity+AnyConvertible.swift index 45d45bae7..a32185235 100644 --- a/LoopFollow/Extensions/HKQuantity+AnyConvertible.swift +++ b/LoopFollow/Extensions/HKQuantity+AnyConvertible.swift @@ -23,7 +23,8 @@ class HKQuantityWrapper: AnyConvertible { // Convert dictionary back to HKQuantity guard let dict = anyValue as? [String: Any], let unitString = dict["unit"] as? String, - let value = dict["value"] as? Double else { + let value = dict["value"] as? Double + else { return nil } diff --git a/LoopFollow/Extensions/HKUnit+Extensions.swift b/LoopFollow/Extensions/HKUnit+Extensions.swift index b506b1c8d..81ce8f903 100644 --- a/LoopFollow/Extensions/HKUnit+Extensions.swift +++ b/LoopFollow/Extensions/HKUnit+Extensions.swift @@ -10,13 +10,9 @@ import Foundation import HealthKit extension HKUnit { - public static let milligramsPerDeciliter: HKUnit = { - return HKUnit.gramUnit(with: .milli).unitDivided(by: .literUnit(with: .deci)) - }() + public static let milligramsPerDeciliter: HKUnit = HKUnit.gramUnit(with: .milli).unitDivided(by: .literUnit(with: .deci)) - public static let millimolesPerLiter: HKUnit = { - return HKUnit.moleUnit(with: .milli, molarMass: HKUnitMolarMassBloodGlucose).unitDivided(by: .liter()) - }() + public static let millimolesPerLiter: HKUnit = HKUnit.moleUnit(with: .milli, molarMass: HKUnitMolarMassBloodGlucose).unitDivided(by: .liter()) var preferredFractionDigits: Int { switch self { diff --git a/LoopFollow/Extensions/ShareClientExtension.swift b/LoopFollow/Extensions/ShareClientExtension.swift index c5ce2925a..825d70d2e 100644 --- a/LoopFollow/Extensions/ShareClientExtension.swift +++ b/LoopFollow/Extensions/ShareClientExtension.swift @@ -15,9 +15,9 @@ public struct ShareGlucoseData: Decodable { var direction: String? enum CodingKeys: String, CodingKey { - case sgv // Sensor Blood Glucose - case mbg // Manual Blood Glucose - case glucose // Other type of entry + case sgv // Sensor Blood Glucose + case mbg // Manual Blood Glucose + case glucose // Other type of entry case date case direction } @@ -25,7 +25,7 @@ public struct ShareGlucoseData: Decodable { // Decoder initializer for handling JSON data public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - + if let glucoseValue = try? container.decode(Double.self, forKey: .sgv) { sgv = Int(glucoseValue.rounded()) } else if let mbgValue = try? container.decode(Double.self, forKey: .mbg) { @@ -35,7 +35,7 @@ public struct ShareGlucoseData: Decodable { } else { throw DecodingError.dataCorruptedError(forKey: .sgv, in: container, debugDescription: "Expected to decode Double for sgv, mbg or glucose.") } - + // Decode the date and optional direction date = try container.decode(TimeInterval.self, forKey: .date) direction = try container.decodeIfPresent(String.self, forKey: .direction) @@ -49,36 +49,33 @@ public struct ShareGlucoseData: Decodable { } private var TrendTable: [String] = [ - "NONE", // 0 - "DoubleUp", // 1 - "SingleUp", // 2 - "FortyFiveUp", // 3 - "Flat", // 4 - "FortyFiveDown", // 5 - "SingleDown", // 6 - "DoubleDown", // 7 - "NOT COMPUTABLE", // 8 - "RATE OUT OF RANGE" // 9 + "NONE", // 0 + "DoubleUp", // 1 + "SingleUp", // 2 + "FortyFiveUp", // 3 + "Flat", // 4 + "FortyFiveDown", // 5 + "SingleDown", // 6 + "DoubleDown", // 7 + "NOT COMPUTABLE", // 8 + "RATE OUT OF RANGE", // 9 ] -extension ShareClient { - - public func fetchData(_ entries: Int, callback: @escaping (ShareError?, [ShareGlucoseData]?) -> Void) { - - self.fetchLast(entries) { (error, result) -> () in +public extension ShareClient { + func fetchData(_ entries: Int, callback: @escaping (ShareError?, [ShareGlucoseData]?) -> Void) { + fetchLast(entries) { error, result in guard error == nil || result != nil else { return callback(error, nil) } - + // parse data to conanical form var shareData = [ShareGlucoseData]() - for i in 0.. TrendTable.count-1) { + if trend < 0 || trend > TrendTable.count - 1 { trend = 0 } - + let newShareData = ShareGlucoseData( sgv: Int(result![i].glucose), date: result![i].timestamp.timeIntervalSince1970, @@ -86,7 +83,7 @@ extension ShareClient { ) shareData.append(newShareData) } - callback(nil,shareData) - } + callback(nil, shareData) + } } } diff --git a/LoopFollow/Extensions/UIViewExtension.swift b/LoopFollow/Extensions/UIViewExtension.swift index dc9472f7f..e93a7b245 100644 --- a/LoopFollow/Extensions/UIViewExtension.swift +++ b/LoopFollow/Extensions/UIViewExtension.swift @@ -13,17 +13,17 @@ extension UIView { enum ViewSide { case Left, Right, Top, Bottom } - + func addBorder(toSide side: ViewSide, withColor color: CGColor, andThickness thickness: CGFloat) { - let border = CALayer() - border.backgroundColor = color - - switch side { - case .Left: border.frame = CGRect(x:0, y: 0, width: thickness, height: frame.height); break - case .Right: border.frame = CGRect(x: frame.width-thickness, y: 0, width: thickness, height: frame.height); break - case .Top: border.frame = CGRect(x: 0, y: 0, width: frame.width, height: thickness); break - case .Bottom: border.frame = CGRect(x: 0, y: frame.height-thickness, width: frame.width, height: thickness); break - } - layer.addSublayer(border) + let border = CALayer() + border.backgroundColor = color + + switch side { + case .Left: border.frame = CGRect(x: 0, y: 0, width: thickness, height: frame.height) + case .Right: border.frame = CGRect(x: frame.width - thickness, y: 0, width: thickness, height: frame.height) + case .Top: border.frame = CGRect(x: 0, y: 0, width: frame.width, height: thickness) + case .Bottom: border.frame = CGRect(x: 0, y: frame.height - thickness, width: frame.width, height: thickness) + } + layer.addSublayer(border) } } diff --git a/LoopFollow/Helpers/AnyConvertible.swift b/LoopFollow/Helpers/AnyConvertible.swift index 90cdf54d3..128a922c0 100644 --- a/LoopFollow/Helpers/AnyConvertible.swift +++ b/LoopFollow/Helpers/AnyConvertible.swift @@ -8,7 +8,7 @@ import Foundation -/// A type that can be converted to/from Any +/// A type that can be converted to/from Any protocol AnyConvertible { func toAny() -> Any static func fromAny(_ anyValue: Any) -> Self? @@ -19,7 +19,7 @@ extension Bool: AnyConvertible { func toAny() -> Any { return self } - + static func fromAny(_ anyValue: Any) -> Bool? { return anyValue as? Bool } @@ -29,7 +29,7 @@ extension String: AnyConvertible { func toAny() -> Any { return self } - + static func fromAny(_ anyValue: Any) -> String? { return anyValue as? String } @@ -39,7 +39,7 @@ extension Int: AnyConvertible { func toAny() -> Any { return self } - + static func fromAny(_ anyValue: Any) -> Int? { return anyValue as? Int } @@ -49,7 +49,7 @@ extension Float: AnyConvertible { func toAny() -> Any { return self } - + static func fromAny(_ anyValue: Any) -> Float? { return anyValue as? Float } @@ -59,7 +59,7 @@ extension Double: AnyConvertible { func toAny() -> Any { return self } - + static func fromAny(_ anyValue: Any) -> Double? { return anyValue as? Double } @@ -69,7 +69,7 @@ extension Date: AnyConvertible { func toAny() -> Any { return self } - + static func fromAny(_ anyValue: Any) -> Date? { return anyValue as? Date } @@ -79,7 +79,7 @@ extension Data: AnyConvertible { func toAny() -> Any { return self } - + static func fromAny(_ anyValue: Any) -> Data? { return anyValue as? Data } @@ -87,19 +87,19 @@ extension Data: AnyConvertible { extension UUID: AnyConvertible { func toAny() -> Any { - return self.uuidString + return uuidString } - + static func fromAny(_ anyValue: Any) -> UUID? { guard let uuidString = anyValue as? String else { return nil } - + return UUID(uuidString: uuidString) } } -//extension Array: AnyConvertible { +// extension Array: AnyConvertible { // func toAny() -> Any { // return self // } @@ -107,13 +107,13 @@ extension UUID: AnyConvertible { // static func fromAny(_ anyValue: Any) -> Array? { // return anyValue as? Array // } -//} +// } extension Array: AnyConvertible where Element: AnyConvertible { func toAny() -> Any { - return self.map { $0.toAny() } + return map { $0.toAny() } } - + static func fromAny(_ anyValue: Any) -> Array? { return (anyValue as? Array)?.compactMap { Element.fromAny($0) } } @@ -122,15 +122,14 @@ extension Array: AnyConvertible where Element: AnyConvertible { extension Optional: AnyConvertible where Wrapped: AnyConvertible { func toAny() -> Any { switch self { - case .some(let value): + case let .some(value): return value.toAny() case .none: return self as Any } } - - static func fromAny(_ anyValue: Any) -> Optional? { + + static func fromAny(_ anyValue: Any) -> Wrapped?? { return Wrapped.fromAny(anyValue) } } - diff --git a/LoopFollow/Helpers/AppConstants.swift b/LoopFollow/Helpers/AppConstants.swift index 26b23303c..faf8fc9bd 100644 --- a/LoopFollow/Helpers/AppConstants.swift +++ b/LoopFollow/Helpers/AppConstants.swift @@ -10,5 +10,5 @@ import Foundation // Class that contains general constants used in different classes class AppConstants { - internal static let APP_GROUP_ID = "group.com.$(unique_id).LoopFollow" + static let APP_GROUP_ID = "group.com.$(unique_id).LoopFollow" } diff --git a/LoopFollow/Helpers/AppVersionManager.swift b/LoopFollow/Helpers/AppVersionManager.swift index 02cdeba6d..603e902bc 100644 --- a/LoopFollow/Helpers/AppVersionManager.swift +++ b/LoopFollow/Helpers/AppVersionManager.swift @@ -10,7 +10,7 @@ import Foundation class AppVersionManager { private let githubService = GitHubService() - + /// Checks for the availability of a new app version and if the current version is blacklisted. /// - Parameter completion: Returns latest version, a boolean for newer version existence, and blacklist status. /// Usage: `versionManager.checkForNewVersion { latestVersion, isNewer, isBlacklisted in ... }` @@ -23,16 +23,17 @@ class AppVersionManager { let latestVersion = UserDefaultsRepository.latestVersion.value let currentVersionBlackListed = UserDefaultsRepository.currentVersionBlackListed.value let cachedForVersion = UserDefaultsRepository.cachedForVersion.value - + // Reset notifications if version has changed if let cachedVersion = cachedForVersion, cachedVersion != currentVersion { UserDefaultsRepository.lastBlacklistNotificationShown.value = Date.distantPast UserDefaultsRepository.lastVersionUpdateNotificationShown.value = Date.distantPast } - + // Check if the cache is still valid if let cachedVersion = cachedForVersion, cachedVersion == currentVersion, - now.timeIntervalSince(latestVersionChecked) < 24 * 3600, let latestVersion = latestVersion { + now.timeIntervalSince(latestVersionChecked) < 24 * 3600, let latestVersion = latestVersion + { let isNewer = isVersion(latestVersion, newerThan: currentVersion) completion(latestVersion, isNewer, currentVersionBlackListed) return @@ -49,16 +50,16 @@ class AppVersionManager { let fetchedVersion = versionData.flatMap { String(data: $0, encoding: .utf8) } .flatMap { self.parseVersionFromConfig(contents: $0) } let isNewer = fetchedVersion.map { self.isVersion($0, newerThan: currentVersion) } ?? false - + let isBlacklisted = (try? blacklistData.flatMap { try JSONDecoder().decode(Blacklist.self, from: $0) }) .map { $0.blacklistedVersions.map { $0.version }.contains(currentVersion) } ?? false - + // Update cache with new data UserDefaultsRepository.latestVersion.value = fetchedVersion UserDefaultsRepository.latestVersionChecked.value = Date() UserDefaultsRepository.currentVersionBlackListed.value = isBlacklisted UserDefaultsRepository.cachedForVersion.value = currentVersion - + // Call completion with new data completion(fetchedVersion, isNewer, isBlacklisted) } @@ -82,9 +83,9 @@ class AppVersionManager { private func isVersion(_ fetchedVersion: String, newerThan currentVersion: String) -> Bool { let fetchedVersionComponents = fetchedVersion.split(separator: ".").map { Int($0) ?? 0 } let currentVersionComponents = currentVersion.split(separator: ".").map { Int($0) ?? 0 } - + let maxCount = max(fetchedVersionComponents.count, currentVersionComponents.count) - for i in 0.. current { diff --git a/LoopFollow/Helpers/BackgroundTaskAudio.swift b/LoopFollow/Helpers/BackgroundTaskAudio.swift index adfe0386c..fbd0889e1 100755 --- a/LoopFollow/Helpers/BackgroundTaskAudio.swift +++ b/LoopFollow/Helpers/BackgroundTaskAudio.swift @@ -1,5 +1,5 @@ // -// BackgroundTask.swift +// BackgroundTaskAudio.swift // // Created by Yaro on 8/27/16. // Copyright © 2016 Yaro. All rights reserved. @@ -8,46 +8,47 @@ import AVFoundation class BackgroundTask { - // MARK: - Vars + var player = AVAudioPlayer() var timer = Timer() - + // MARK: - Methods + func startBackgroundTask() { NotificationCenter.default.addObserver(self, selector: #selector(interruptedAudio), name: AVAudioSession.interruptionNotification, object: AVAudioSession.sharedInstance()) - self.playAudio() + playAudio() } - + func stopBackgroundTask() { NotificationCenter.default.removeObserver(self, name: AVAudioSession.interruptionNotification, object: nil) player.stop() LogManager.shared.log(category: .general, message: "Silent audio stopped", isDebug: true) } - + @objc fileprivate func interruptedAudio(_ notification: Notification) { LogManager.shared.log(category: .general, message: "Silent audio interrupted") - if notification.name == AVAudioSession.interruptionNotification && notification.userInfo != nil { + if notification.name == AVAudioSession.interruptionNotification, notification.userInfo != nil { var info = notification.userInfo! var intValue = 0 (info[AVAudioSessionInterruptionTypeKey]! as AnyObject).getValue(&intValue) if intValue == 1 { playAudio() } } } - + fileprivate func playAudio() { do { let bundle = Bundle.main.path(forResource: "blank", ofType: "wav") let alertSound = URL(fileURLWithPath: bundle!) - // try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback) + // try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback) try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: .mixWithOthers) try AVAudioSession.sharedInstance().setActive(true) - try self.player = AVAudioPlayer(contentsOf: alertSound) + try player = AVAudioPlayer(contentsOf: alertSound) // Play audio forever by setting num of loops to -1 - self.player.numberOfLoops = -1 - self.player.volume = 0.01 - self.player.prepareToPlay() - self.player.play() + player.numberOfLoops = -1 + player.volume = 0.01 + player.prepareToPlay() + player.play() LogManager.shared.log(category: .general, message: "Silent audio playing", isDebug: true) } catch { LogManager.shared.log(category: .general, message: "playAudio, error: \(error)") diff --git a/LoopFollow/Helpers/BuildDetails.swift b/LoopFollow/Helpers/BuildDetails.swift index d70e287b4..b8e462df4 100644 --- a/LoopFollow/Helpers/BuildDetails.swift +++ b/LoopFollow/Helpers/BuildDetails.swift @@ -10,19 +10,20 @@ import Foundation class BuildDetails { static var `default` = BuildDetails() - + let dict: [String: Any] - + init() { guard let url = Bundle.main.url(forResource: "BuildDetails", withExtension: "plist"), let data = try? Data(contentsOf: url), - let parsed = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any] else { + let parsed = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any] + else { dict = [:] return } dict = parsed } - + var buildDateString: String? { return dict["com-LoopFollow-build-date"] as? String } @@ -36,38 +37,38 @@ class BuildDetails { let sha = dict["com-LoopFollow-commit-sha"] as? String ?? "Unknown" return "\(branch) \(sha)" } - + // Determine if the build is from TestFlight func isTestFlightBuild() -> Bool { -#if targetEnvironment(simulator) - return false -#else - if Bundle.main.url(forResource: "embedded", withExtension: "mobileprovision") != nil { + #if targetEnvironment(simulator) return false - } - guard let receiptName = Bundle.main.appStoreReceiptURL?.lastPathComponent else { - return false - } - return "sandboxReceipt".caseInsensitiveCompare(receiptName) == .orderedSame -#endif + #else + if Bundle.main.url(forResource: "embedded", withExtension: "mobileprovision") != nil { + return false + } + guard let receiptName = Bundle.main.appStoreReceiptURL?.lastPathComponent else { + return false + } + return "sandboxReceipt".caseInsensitiveCompare(receiptName) == .orderedSame + #endif } // Determine if the build is for Simulator func isSimulatorBuild() -> Bool { -#if targetEnvironment(simulator) - return true -#else - return false -#endif + #if targetEnvironment(simulator) + return true + #else + return false + #endif } // Determine if the build is for Mac func isMacApp() -> Bool { -#if targetEnvironment(macCatalyst) - return true -#else - return false -#endif + #if targetEnvironment(macCatalyst) + return true + #else + return false + #endif } // Parse the build date string into a Date object @@ -78,7 +79,7 @@ class BuildDetails { let formatter = ISO8601DateFormatter() return formatter.date(from: dateString) } - + // Calculate the expiration date based on the build type func calculateExpirationDate() -> Date { if isTestFlightBuild(), let buildDate = buildDate() { @@ -93,7 +94,7 @@ class BuildDetails { } } } - + // Expiration header based on build type var expirationHeaderString: String { if isTestFlightBuild() { diff --git a/LoopFollow/Helpers/Chart.swift b/LoopFollow/Helpers/Chart.swift index 778732942..3b81019fa 100644 --- a/LoopFollow/Helpers/Chart.swift +++ b/LoopFollow/Helpers/Chart.swift @@ -6,37 +6,34 @@ // Copyright © 2020 Jon Fawcett. All rights reserved. // -import Foundation import Charts +import Foundation final class OverrideFillFormatter: FillFormatter { - func getFillLinePosition(dataSet: Charts.LineChartDataSetProtocol, dataProvider: Charts.LineChartDataProvider) -> CGFloat { + func getFillLinePosition(dataSet: Charts.LineChartDataSetProtocol, dataProvider _: Charts.LineChartDataProvider) -> CGFloat { return CGFloat(dataSet.entryForIndex(0)!.y) - //return 375 + // return 375 } } final class basalFillFormatter: FillFormatter { - func getFillLinePosition(dataSet: Charts.LineChartDataSetProtocol, dataProvider: Charts.LineChartDataProvider) -> CGFloat { + func getFillLinePosition(dataSet _: Charts.LineChartDataSetProtocol, dataProvider _: Charts.LineChartDataProvider) -> CGFloat { return 0 } } final class ChartXValueFormatter: AxisValueFormatter { - - - func stringForValue(_ value: Double, axis: AxisBase?) -> String { - + func stringForValue(_ value: Double, axis _: AxisBase?) -> String { let dateFormatter = DateFormatter() - //let timezoneOffset = TimeZone.current.secondsFromGMT() - //let epochTimezoneOffset = value + Double(timezoneOffset) + // let timezoneOffset = TimeZone.current.secondsFromGMT() + // let epochTimezoneOffset = value + Double(timezoneOffset) if dateTimeUtils.is24Hour() { dateFormatter.setLocalizedDateFormatFromTemplate("HH:mm") } else { dateFormatter.setLocalizedDateFormatFromTemplate("hh:mm") } - - //let date = Date(timeIntervalSince1970: epochTimezoneOffset) + + // let date = Date(timeIntervalSince1970: epochTimezoneOffset) let date = Date(timeIntervalSince1970: value) let formattedDate = dateFormatter.string(from: date) @@ -45,7 +42,7 @@ final class ChartXValueFormatter: AxisValueFormatter { } final class ChartYDataValueFormatter: ValueFormatter { - func stringForValue(_ value: Double, entry: ChartDataEntry, dataSetIndex: Int, viewPortHandler: ViewPortHandler?) -> String { + func stringForValue(_: Double, entry: ChartDataEntry, dataSetIndex _: Int, viewPortHandler _: ViewPortHandler?) -> String { if entry.data != nil { return entry.data as? String ?? "" } else { @@ -55,7 +52,7 @@ final class ChartYDataValueFormatter: ValueFormatter { } final class ChartYOverrideValueFormatter: ValueFormatter { - func stringForValue(_ value: Double, entry: ChartDataEntry, dataSetIndex: Int, viewPortHandler: ViewPortHandler?) -> String { + func stringForValue(_: Double, entry: ChartDataEntry, dataSetIndex _: Int, viewPortHandler _: ViewPortHandler?) -> String { if entry.data != nil { return entry.data as? String ?? "" } else { @@ -65,17 +62,15 @@ final class ChartYOverrideValueFormatter: ValueFormatter { } final class ChartYMMOLValueFormatter: AxisValueFormatter { - func stringForValue(_ value: Double, axis: AxisBase?) -> String { + func stringForValue(_ value: Double, axis _: AxisBase?) -> String { return Localizer.toDisplayUnits(String(value)) } } - class PillMarker: MarkerImage { - - private (set) var color: UIColor - private (set) var font: UIFont - private (set) var textColor: UIColor + private(set) var color: UIColor + private(set) var font: UIFont + private(set) var textColor: UIColor private var labelText: String = "" private var attrs: [NSAttributedString.Key: AnyObject]! @@ -98,7 +93,6 @@ class PillMarker: MarkerImage { } override func draw(context: CGContext, point: CGPoint) { - // custom padding around text let labelWidth = labelText.size(withAttributes: attrs).width + 10 // if you modify labelHeigh you will have to tweak baselineOffset in attrs @@ -109,7 +103,7 @@ class PillMarker: MarkerImage { rectangle.origin.x -= rectangle.width / 2.0 var spacing: CGFloat = 20 if point.y < 300 { spacing = -40 } - + rectangle.origin.y -= rectangle.height + spacing // rounded rect @@ -124,10 +118,10 @@ class PillMarker: MarkerImage { labelText.draw(with: rectangle, options: .usesLineFragmentOrigin, attributes: attrs, context: nil) } - override func refreshContent(entry: ChartDataEntry, highlight: Highlight) { + override func refreshContent(entry: ChartDataEntry, highlight _: Highlight) { if entry.data != nil { - //var multiplier = entry.data as! Double * 100.0 - //labelText = String(format: "%.0f%%", multiplier) + // var multiplier = entry.data as! Double * 100.0 + // labelText = String(format: "%.0f%%", multiplier) labelText = entry.data as? String ?? "" } else { labelText = String(entry.y) diff --git a/LoopFollow/Helpers/CycleHelper.swift b/LoopFollow/Helpers/CycleHelper.swift index 5a0ce4bed..7b7d44097 100644 --- a/LoopFollow/Helpers/CycleHelper.swift +++ b/LoopFollow/Helpers/CycleHelper.swift @@ -8,7 +8,7 @@ import Foundation -struct CycleHelper { +enum CycleHelper { /// Returns a positive modulus value (always between 0 and modulus). static func positiveModulo(_ value: TimeInterval, modulus: TimeInterval) -> TimeInterval { let remainder = value.truncatingRemainder(dividingBy: modulus) @@ -36,7 +36,8 @@ struct CycleHelper { static func computeDelay(sensorReference: Date, sensorInterval: TimeInterval, heartbeatLast: Date, - heartbeatInterval: TimeInterval) -> TimeInterval { + heartbeatInterval: TimeInterval) -> TimeInterval + { let sensorOffset = cycleOffset(for: sensorReference, interval: sensorInterval) let hbOffset = cycleOffset(for: heartbeatLast, interval: heartbeatInterval) return positiveModulo(hbOffset - sensorOffset, modulus: heartbeatInterval) @@ -45,7 +46,8 @@ struct CycleHelper { /// Overloaded version of computeDelay where the sensor cycle offset is already known. static func computeDelay(sensorOffset: TimeInterval, heartbeatLast: Date, - heartbeatInterval: TimeInterval) -> TimeInterval { + heartbeatInterval: TimeInterval) -> TimeInterval + { let hbOffset = cycleOffset(for: heartbeatLast, interval: heartbeatInterval) return positiveModulo(hbOffset - sensorOffset, modulus: heartbeatInterval) } diff --git a/LoopFollow/Helpers/DataStructs.swift b/LoopFollow/Helpers/DataStructs.swift index 2b245280b..5a250f517 100644 --- a/LoopFollow/Helpers/DataStructs.swift +++ b/LoopFollow/Helpers/DataStructs.swift @@ -1,5 +1,5 @@ // -// Enums.swift +// DataStructs.swift // LoopFollow // // Created by Jon Fawcett on 6/23/20. @@ -9,40 +9,39 @@ import Foundation class DataStructs { - // Pie Chart Data struct pieData: Codable { var name: String var value: Double } - //NS Basal Profile Struct + // NS Basal Profile Struct struct basalProfileSegment: Codable { var basalRate: Double var startDate: TimeInterval var endDate: TimeInterval } - //NS Timestamp Only Data Struct + // NS Timestamp Only Data Struct struct timestampOnlyStruct: Codable { var date: TimeInterval var sgv: Int } - //NS Note Data Struct + // NS Note Data Struct struct noteStruct: Codable { var date: TimeInterval var sgv: Int var note: String } - //NS Battery Data Struct + // NS Battery Data Struct struct batteryStruct: Codable { var batteryLevel: Double var timestamp: Date } - //NS Override Data Struct + // NS Override Data Struct struct overrideStruct: Codable { var insulNeedsScaleFactor: Double var date: TimeInterval diff --git a/LoopFollow/Helpers/DateTime.swift b/LoopFollow/Helpers/DateTime.swift index c338a8bf6..8128c457f 100644 --- a/LoopFollow/Helpers/DateTime.swift +++ b/LoopFollow/Helpers/DateTime.swift @@ -8,9 +8,7 @@ import Foundation - class dateTimeUtils { - static func getTimeIntervalMidnightToday() -> TimeInterval { let now = Date() let formatter = DateFormatter() @@ -24,7 +22,7 @@ class dateTimeUtils { guard let midnightTimeInterval = newDate?.timeIntervalSince1970 else { return 0 } return midnightTimeInterval } - + static func getTimeIntervalMidnightYesterday() -> TimeInterval { let now = Date().addingTimeInterval(-86400) let formatter = DateFormatter() @@ -38,13 +36,13 @@ class dateTimeUtils { guard let midnightTimeInterval = newDate?.timeIntervalSince1970 else { return 0 } return midnightTimeInterval } - + static func getTimeIntervalNHoursAgo(N: Int) -> TimeInterval { let today = Date() let nHoursAgo = Calendar.current.date(byAdding: .hour, value: -N, to: today)! return nHoursAgo.timeIntervalSince1970 } - + static func getNowTimeIntervalUTC() -> TimeInterval { let now = Date() let formatter = DateFormatter() @@ -56,24 +54,24 @@ class dateTimeUtils { guard let utcTime = day?.timeIntervalSince1970 else { return 0 } return utcTime } - + static func getDateTimeString(addingHours hours: Int? = nil, addingDays days: Int? = nil) -> String { let currentDate = Date() var date = currentDate - + if let hoursToAdd = hours { date = Calendar.current.date(byAdding: .hour, value: hoursToAdd, to: currentDate)! } - + if let daysToAdd = days { date = Calendar.current.date(byAdding: .day, value: daysToAdd, to: currentDate)! } - + let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" dateFormatter.locale = Locale(identifier: "en_US") dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) - + return dateFormatter.string(from: date) } @@ -84,7 +82,7 @@ class dateTimeUtils { formatter.locale = Locale(identifier: "en_US") return formatter.string(from: date) } - + static func is24Hour() -> Bool { let dateFormat = DateFormatter.dateFormat(fromTemplate: "j", options: 0, locale: Locale.current)! diff --git a/LoopFollow/Helpers/DictionaryKeyPath.swift b/LoopFollow/Helpers/DictionaryKeyPath.swift index f628fab0e..512711e28 100644 --- a/LoopFollow/Helpers/DictionaryKeyPath.swift +++ b/LoopFollow/Helpers/DictionaryKeyPath.swift @@ -6,19 +6,19 @@ extension Dictionary { subscript(keyPath keyPath: String) -> Any? { get { guard let keyPath = Dictionary.keyPathKeys(forKeyPath: keyPath) - else { return nil } + else { return nil } return getValue(forKeyPath: keyPath) } set { guard let keyPath = Dictionary.keyPathKeys(forKeyPath: keyPath), - let newValue = newValue else { return } - self.setValue(newValue, forKeyPath: keyPath) + let newValue = newValue else { return } + setValue(newValue, forKeyPath: keyPath) } } - static private func keyPathKeys(forKeyPath: String) -> [Key]? { + private static func keyPathKeys(forKeyPath: String) -> [Key]? { let keys = forKeyPath.components(separatedBy: ".") - .reversed().flatMap({ $0 as? Key }) + .reversed().flatMap { $0 as? Key } return keys.isEmpty ? nil : keys } @@ -27,7 +27,7 @@ extension Dictionary { private func getValue(forKeyPath keyPath: [Key]) -> Any? { guard let value = self[keyPath.last!] else { return nil } return keyPath.count == 1 ? value : (value as? [Key: Any]) - .flatMap { $0.getValue(forKeyPath: Array(keyPath.dropLast())) } + .flatMap { $0.getValue(forKeyPath: Array(keyPath.dropLast())) } } // recursively (attempt to) access the queried subdictionaries to @@ -36,8 +36,7 @@ extension Dictionary { guard self[keyPath.last!] != nil else { return } if keyPath.count == 1 { (value as? Value).map { self[keyPath.last!] = $0 } - } - else if var subDict = self[keyPath.last!] as? [Key: Value] { + } else if var subDict = self[keyPath.last!] as? [Key: Value] { subDict.setValue(value, forKeyPath: Array(keyPath.dropLast())) (subDict as? Value).map { self[keyPath.last!] = $0 } } diff --git a/LoopFollow/Helpers/GitHubService.swift b/LoopFollow/Helpers/GitHubService.swift index 5f59a8fd6..c0337add8 100644 --- a/LoopFollow/Helpers/GitHubService.swift +++ b/LoopFollow/Helpers/GitHubService.swift @@ -12,7 +12,7 @@ class GitHubService { enum GitHubDataType { case versionConfig case blacklistedVersions - + var url: String { switch self { case .versionConfig: @@ -22,14 +22,14 @@ class GitHubService { } } } - + func fetchData(for dataType: GitHubDataType, completion: @escaping (Data?) -> Void) { let urlString = dataType.url guard let url = URL(string: urlString) else { completion(nil) return } - + URLSession.shared.dataTask(with: url) { data, response, error in guard let data = data, error == nil else { completion(nil) diff --git a/LoopFollow/Helpers/Globals.swift b/LoopFollow/Helpers/Globals.swift index cfa837913..836d61cac 100644 --- a/LoopFollow/Helpers/Globals.swift +++ b/LoopFollow/Helpers/Globals.swift @@ -8,10 +8,9 @@ import Foundation - -struct globalVariables { +enum globalVariables { static var debugLog = "" - + // Graph Settings static let dotBG: Float = 3 static let dotCarb: Float = 5 diff --git a/LoopFollow/Helpers/GlucoseConversion.swift b/LoopFollow/Helpers/GlucoseConversion.swift index 17edd5f7b..ebaee71b2 100644 --- a/LoopFollow/Helpers/GlucoseConversion.swift +++ b/LoopFollow/Helpers/GlucoseConversion.swift @@ -8,7 +8,7 @@ import Foundation -struct GlucoseConversion { +enum GlucoseConversion { static let mgDlToMmolL: Double = 0.0555 static let mmolToMgDl: Double = 18.01559 } diff --git a/LoopFollow/Helpers/Localizer.swift b/LoopFollow/Helpers/Localizer.swift index fbade35dd..edeccb4ee 100644 --- a/LoopFollow/Helpers/Localizer.swift +++ b/LoopFollow/Helpers/Localizer.swift @@ -1,5 +1,5 @@ // -// Units.swift +// Localizer.swift // LoopFollow // // Created by Jon Fawcett on 6/22/20. @@ -63,27 +63,27 @@ class Localizer { static func toDisplayUnits(_ value: String) -> String { let numberFormatter = NumberFormatter() numberFormatter.numberStyle = .decimal - + if UserDefaultsRepository.units.value == "mg/dL" { numberFormatter.maximumFractionDigits = 0 // No decimal places for mg/dL } else { numberFormatter.maximumFractionDigits = 1 // Always one decimal place for mmol/L numberFormatter.minimumFractionDigits = 1 // This ensures even .0 is displayed } - + numberFormatter.locale = Locale.current - + if let number = Float(value) { if UserDefaultsRepository.units.value == "mg/dL" { let numberValue = NSNumber(value: number) return numberFormatter.string(from: numberValue) ?? value } else { - let mmolValue = Double(number) * GlucoseConversion.mgDlToMmolL // Convert number to Double + let mmolValue = Double(number) * GlucoseConversion.mgDlToMmolL // Convert number to Double let numberValue = NSNumber(value: mmolValue) return numberFormatter.string(from: numberValue) ?? value } } - + return value } @@ -95,20 +95,18 @@ class Localizer { } } - extension Float { - // remove the decimal part of the float if it is ".0" and trim whitespaces var cleanValue: String { - return self.truncatingRemainder(dividingBy: 1) == 0 + return truncatingRemainder(dividingBy: 1) == 0 ? String(format: "%5.0f", self).trimmingCharacters(in: CharacterSet.whitespaces) : String(format: "%5.1f", self).trimmingCharacters(in: CharacterSet.whitespaces) } - + var roundTo3f: Float { return round(to: 3) } - + func round(to places: Int) -> Float { let divisor = pow(10.0, Float(places)) return (divisor * self).rounded() / divisor diff --git a/LoopFollow/Helpers/Mobileprovision.swift b/LoopFollow/Helpers/Mobileprovision.swift index c5c64d929..5042d2504 100644 --- a/LoopFollow/Helpers/Mobileprovision.swift +++ b/LoopFollow/Helpers/Mobileprovision.swift @@ -10,14 +10,14 @@ import Foundation /* Decode mobileprovision plist file Usage: - + 1. To get mobileprovision data as embedded in your app: MobileProvision.read() 2. To get mobile provision data from a file on disk: - + MobileProvision.read(from: "my.mobileprovision") - -*/ + + */ struct MobileProvision: Decodable { var name: String @@ -27,8 +27,8 @@ struct MobileProvision: Decodable { var creationDate: Date var expirationDate: Date var entitlements: Entitlements - - private enum CodingKeys : String, CodingKey { + + private enum CodingKeys: String, CodingKey { case name = "Name" case appIDName = "AppIDName" case platform = "Platform" @@ -37,35 +37,35 @@ struct MobileProvision: Decodable { case expirationDate = "ExpirationDate" case entitlements = "Entitlements" } - + // Sublevel: decode entitlements informations struct Entitlements: Decodable { let keychainAccessGroups: [String] let getTaskAllow: Bool let apsEnvironment: Environment - + private enum CodingKeys: String, CodingKey { case keychainAccessGroups = "keychain-access-groups" case getTaskAllow = "get-task-allow" case apsEnvironment = "aps-environment" } - + enum Environment: String, Decodable { case development, production, disabled } - - init(keychainAccessGroups: Array, getTaskAllow: Bool, apsEnvironment: Environment) { + + init(keychainAccessGroups: [String], getTaskAllow: Bool, apsEnvironment: Environment) { self.keychainAccessGroups = keychainAccessGroups self.getTaskAllow = getTaskAllow self.apsEnvironment = apsEnvironment } - + init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let keychainAccessGroups: [String] = (try? container.decode([String].self, forKey: .keychainAccessGroups)) ?? [] let getTaskAllow: Bool = (try? container.decode(Bool.self, forKey: .getTaskAllow)) ?? false let apsEnvironment: Environment = (try? container.decode(Environment.self, forKey: .apsEnvironment)) ?? .disabled - + self.init(keychainAccessGroups: keychainAccessGroups, getTaskAllow: getTaskAllow, apsEnvironment: apsEnvironment) } } @@ -82,17 +82,17 @@ extension MobileProvision { // Read a .mobileprovision file on disk static func read(from profilePath: String) -> MobileProvision? { - guard let plistDataString = try? NSString.init(contentsOfFile: profilePath, - encoding: String.Encoding.isoLatin1.rawValue) else { return nil } - + guard let plistDataString = try? NSString(contentsOfFile: profilePath, + encoding: String.Encoding.isoLatin1.rawValue) else { return nil } + // Skip binary part at the start of the mobile provisionning profile let scanner = Scanner(string: plistDataString as String) guard scanner.scanUpTo("", into: &extractedPlist) != false else { return nil } - + guard let plist = extractedPlist?.appending("").data(using: .isoLatin1) else { return nil } let decoder = PropertyListDecoder() do { diff --git a/LoopFollow/Helpers/NightscoutUtils.swift b/LoopFollow/Helpers/NightscoutUtils.swift index 0a3c900e5..57cc5e3d3 100644 --- a/LoopFollow/Helpers/NightscoutUtils.swift +++ b/LoopFollow/Helpers/NightscoutUtils.swift @@ -78,7 +78,7 @@ class NightscoutUtils { var request = URLRequest(url: url) request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData - let task = URLSession.shared.dataTask(with: request) { data, response, error in + let task = URLSession.shared.dataTask(with: request) { data, _, error in guard let data = data, error == nil else { completion(.failure(error!)) return @@ -90,20 +90,19 @@ class NightscoutUtils { DispatchQueue.main.async { completion(.success(decodedObject)) } - } - catch let decodingError as DecodingError { + } catch let decodingError as DecodingError { print("[ERROR] Failed to decode \(T.self):") switch decodingError { - case .typeMismatch(let type, let context): + case let .typeMismatch(type, context): print("Type mismatch for type \(type), context: \(context.debugDescription)") print("Coding path:", context.codingPath) - case .valueNotFound(let type, let context): + case let .valueNotFound(type, context): print("Value not found for type \(type), context: \(context.debugDescription)") print("Coding path:", context.codingPath) - case .keyNotFound(let key, let context): + case let .keyNotFound(key, context): print("Key '\(key.stringValue)' not found, context: \(context.debugDescription)") print("Coding path:", context.codingPath) - case .dataCorrupted(let context): + case let .dataCorrupted(context): print("Data corrupted, context: \(context.debugDescription)") @unknown default: print("Unknown decoding error") @@ -117,7 +116,6 @@ class NightscoutUtils { task.resume() } - static func executeDynamicRequest(eventType: EventType, parameters: [String: String], completion: @escaping (Result) -> Void) { let baseURL = ObservableUserDefaults.shared.url.value let token = UserDefaultsRepository.token.value @@ -130,7 +128,7 @@ class NightscoutUtils { var request = URLRequest(url: url) request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData - let task = URLSession.shared.dataTask(with: request) { data, response, error in + let task = URLSession.shared.dataTask(with: request) { data, _, error in guard let data = data, error == nil else { completion(.failure(error!)) return @@ -222,8 +220,8 @@ class NightscoutUtils { if let jsonResponse = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], let authorized = jsonResponse["authorized"] as? [String: Any], let token = authorized["token"] as? String, - let permissionGroups = authorized["permissionGroups"] as? [[String]] { - + let permissionGroups = authorized["permissionGroups"] as? [[String]] + { if permissionGroups.contains(where: { $0.contains("*") }) { nsWriteAuth = true nsAdminAuth = true @@ -265,9 +263,9 @@ class NightscoutUtils { if mutableDate.hasSuffix("Z") { mutableDate = String(mutableDate.dropLast()) - } - else if let offsetRange = mutableDate.range(of: "[\\+\\-]\\d{2}:\\d{2}$", - options: .regularExpression) { + } else if let offsetRange = mutableDate.range(of: "[\\+\\-]\\d{2}:\\d{2}$", + options: .regularExpression) + { mutableDate.removeSubrange(offsetRange) } @@ -298,7 +296,8 @@ class NightscoutUtils { } guard let request = createURLRequest(url: urlUser, token: token, path: "/api/v1/status.json"), - urlUser.hasPrefix("http://") || urlUser.hasPrefix("https://") else { + urlUser.hasPrefix("http://") || urlUser.hasPrefix("https://") + else { throw NightscoutError.invalidURL } @@ -317,7 +316,8 @@ class NightscoutUtils { case 200: if let jsonResponse = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], let authorized = jsonResponse["authorized"] as? [String: Any], - let jwtToken = authorized["token"] as? String { + let jwtToken = authorized["token"] as? String + { return jwtToken } else { throw NightscoutError.invalidToken @@ -380,7 +380,7 @@ class NightscoutUtils { let (data, response) = try await session.data(for: request) - var responseString : String + var responseString: String responseString = String(data: data, encoding: .utf8) ?? "" guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { if responseString != "" { @@ -397,15 +397,17 @@ class NightscoutUtils { // 1) Try to parse the entire string as JSON and return the "message" if let data = responseString.data(using: .utf8) { if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], - let message = json["message"] as? String { + let message = json["message"] as? String + { return message } } // 2) If not valid JSON (or no "message"), try to parse it as HTML if let startRange = responseString.range(of: "<title>"), - let endRange = responseString.range(of: "") { - let titleRange = startRange.upperBound..") + { + let titleRange = startRange.upperBound ..< endRange.lowerBound let titleContent = responseString[titleRange].trimmingCharacters(in: .whitespacesAndNewlines) if !titleContent.isEmpty { return titleContent diff --git a/LoopFollow/Helpers/ObservationToken.swift b/LoopFollow/Helpers/ObservationToken.swift index 2f620491d..b09c4237c 100644 --- a/LoopFollow/Helpers/ObservationToken.swift +++ b/LoopFollow/Helpers/ObservationToken.swift @@ -10,13 +10,12 @@ import Foundation /// The token received by an observe when subscribes to its subject. The observer can cancel observation, so the subject will remove it from its observers list. class ObservationToken { - private let cancellationClosure: () -> Void - + init(cancellationClosure: @escaping () -> Void) { self.cancellationClosure = cancellationClosure } - + func cancel() { cancellationClosure() } diff --git a/LoopFollow/Helpers/TextFieldWithToolBar.swift b/LoopFollow/Helpers/TextFieldWithToolBar.swift index 30da312c7..003131004 100644 --- a/LoopFollow/Helpers/TextFieldWithToolBar.swift +++ b/LoopFollow/Helpers/TextFieldWithToolBar.swift @@ -6,9 +6,9 @@ // Copyright © 2024 Jon Fawcett. All rights reserved. // +import HealthKit import SwiftUI import UIKit -import HealthKit public struct TextFieldWithToolBar: UIViewRepresentable { @Binding var quantity: HKQuantity @@ -247,8 +247,8 @@ public struct TextFieldWithToolBar: UIViewRepresentable { } return true } else if let number = Double(sanitizedText) { - let quantity = HKQuantity(unit: self.unit, doubleValue: number) - if self.isWithinLimits(quantity) { + let quantity = HKQuantity(unit: unit, doubleValue: number) + if isWithinLimits(quantity) { DispatchQueue.main.async { self.parent.quantity = quantity } diff --git a/LoopFollow/Helpers/TimeOfDay.swift b/LoopFollow/Helpers/TimeOfDay.swift index 8b5641db1..b83cd88af 100644 --- a/LoopFollow/Helpers/TimeOfDay.swift +++ b/LoopFollow/Helpers/TimeOfDay.swift @@ -6,20 +6,19 @@ // Copyright © 2025 Jon Fawcett. All rights reserved. // - import Foundation /// A time‐of‐day independent of any date struct TimeOfDay: Codable, Equatable { - let hour: Int // 0…23 - let minute: Int // 0…59 + let hour: Int // 0…23 + let minute: Int // 0…59 /// total minutes since midnight var minutesSinceMidnight: Int { hour * 60 + minute } init(hour: Int, minute: Int) { - precondition((0...23).contains(hour)) - precondition((0...59).contains(minute)) + precondition((0 ... 23).contains(hour)) + precondition((0 ... 59).contains(minute)) self.hour = hour self.minute = minute } diff --git a/LoopFollow/Helpers/Views/HKQuantityInputView.swift b/LoopFollow/Helpers/Views/HKQuantityInputView.swift index d1e0a614f..a9a03d953 100644 --- a/LoopFollow/Helpers/Views/HKQuantityInputView.swift +++ b/LoopFollow/Helpers/Views/HKQuantityInputView.swift @@ -7,8 +7,8 @@ // import Foundation -import SwiftUI import HealthKit +import SwiftUI struct HKQuantityInputView: View { var label: String diff --git a/LoopFollow/Helpers/carbBolusArrays.swift b/LoopFollow/Helpers/carbBolusArrays.swift index d8d16c597..0032b3880 100644 --- a/LoopFollow/Helpers/carbBolusArrays.swift +++ b/LoopFollow/Helpers/carbBolusArrays.swift @@ -8,61 +8,53 @@ import Foundation - extension MainViewController { - func findNearestBGbyTime(needle: TimeInterval, haystack: [ShareGlucoseData], startingIndex: Int) -> (sgv: Double, foundIndex: Int) { - // If we can't find a match or things fail, put it at 100 BG if startingIndex > haystack.count { return (100.00, 0) } - for i in startingIndex..= haystack[i].date && needle < haystack[i + 1].date { + + if needle >= haystack[i].date, needle < haystack[i + 1].date { return (Double(haystack[i].sgv), i) } } - + return (100.00, 0) } - - + func findNearestBolusbyTime(timeWithin: Int, needle: TimeInterval, haystack: [bolusGraphStruct], startingIndex: Int) -> (offset: Bool, foundIndex: Int) { - // If we can't find a match or things fail, put it at 100 BG - for i in startingIndex..= Double(-timeWithin) { return (true, i)} - + if timeDiff <= Double(timeWithin), timeDiff >= Double(-timeWithin) { return (true, i) } + if i == haystack.count - 1 { return (false, 0) } - if timeDiff < Double(-timeWithin) { return (false, 0)} - + if timeDiff < Double(-timeWithin) { return (false, 0) } } - - return (false, 0 ) + + return (false, 0) } - + func findNextCarbTime(timeWithin: Int, needle: TimeInterval, haystack: [carbGraphStruct], startingIndex: Int) -> Bool { - if startingIndex > haystack.count - 2 { return false } - if haystack[startingIndex + 1].date - needle < Double(timeWithin) { + if haystack[startingIndex + 1].date - needle < Double(timeWithin) { return true } return false } - + func findNextBolusTime(timeWithin: Int, needle: TimeInterval, haystack: [bolusGraphStruct], startingIndex: Int) -> Bool { - var last = false var next = true if startingIndex > haystack.count - 2 { return false } if startingIndex == 0 { return false } - + // Nothing to right that requires shift - if haystack[startingIndex + 1].date - needle > Double(timeWithin) { + if haystack[startingIndex + 1].date - needle > Double(timeWithin) { return false } else { // Nothing to left preventing shift @@ -70,8 +62,7 @@ extension MainViewController { return true } } - + return false } - } diff --git a/LoopFollow/Helpers/isOnPhoneCall.swift b/LoopFollow/Helpers/isOnPhoneCall.swift index d6ab27414..c159851c9 100644 --- a/LoopFollow/Helpers/isOnPhoneCall.swift +++ b/LoopFollow/Helpers/isOnPhoneCall.swift @@ -6,8 +6,8 @@ // Copyright © 2025 Jon Fawcett. All rights reserved. // -import Foundation import CallKit +import Foundation private let callObserver = CXCallObserver() diff --git a/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsViewModel.swift b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsViewModel.swift index b4f171f94..8748e26e3 100644 --- a/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsViewModel.swift +++ b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsViewModel.swift @@ -14,8 +14,8 @@ class InfoDisplaySettingsViewModel: ObservableObject { @Published var infoVisible: [Bool] init() { - self.infoSort = UserDefaultsRepository.infoSort.value - self.infoVisible = UserDefaultsRepository.infoVisible.value + infoSort = UserDefaultsRepository.infoSort.value + infoVisible = UserDefaultsRepository.infoVisible.value } func toggleVisibility(for sortedIndex: Int) { diff --git a/LoopFollow/InfoTable/InfoManager.swift b/LoopFollow/InfoTable/InfoManager.swift index 22d01ba0e..720965599 100644 --- a/LoopFollow/InfoTable/InfoManager.swift +++ b/LoopFollow/InfoTable/InfoManager.swift @@ -7,15 +7,15 @@ // import Foundation -import UIKit import HealthKit +import UIKit class InfoManager { var tableData: [InfoData] weak var tableView: UITableView? init(tableView: UITableView) { - self.tableData = InfoType.allCases.map { InfoData(name: $0.name) } + tableData = InfoType.allCases.map { InfoData(name: $0.name) } self.tableView = tableView } @@ -57,7 +57,7 @@ class InfoManager { let formattedValue = value.formattedValue() updateInfoData(type: type, value: formattedValue) } - + func clearInfoData(type: InfoType) { tableData[type.rawValue].value = "" tableView?.reloadData() diff --git a/LoopFollow/InfoTable/InfoType.swift b/LoopFollow/InfoTable/InfoType.swift index 4d602896f..17b64c36f 100644 --- a/LoopFollow/InfoTable/InfoType.swift +++ b/LoopFollow/InfoTable/InfoType.swift @@ -45,6 +45,6 @@ enum InfoType: Int, CaseIterable { } var sortOrder: Int { - return self.rawValue + return rawValue } } diff --git a/LoopFollow/Log/LogManager.swift b/LoopFollow/Log/LogManager.swift index bc3a4f19d..1570a4d8d 100644 --- a/LoopFollow/Log/LogManager.swift +++ b/LoopFollow/Log/LogManager.swift @@ -63,7 +63,7 @@ class LogManager { consoleQueue.async { print(logMessage) } - + if category == .taskScheduler && isDebug { return } @@ -85,9 +85,9 @@ class LogManager { } if !isDebug || Storage.shared.debugLogLevel.value { - let logFileURL = self.currentLogFileURL - self.writeVersionHeaderIfNeeded(for: logFileURL) - self.append(logMessage + "\n", to: logFileURL) + let logFileURL = currentLogFileURL + writeVersionHeaderIfNeeded(for: logFileURL) + append(logMessage + "\n", to: logFileURL) } } @@ -95,7 +95,8 @@ class LogManager { private func isLogFileEmpty(at fileURL: URL) -> Bool { if !fileManager.fileExists(atPath: fileURL.path) { return true } if let attributes = try? fileManager.attributesOfItem(atPath: fileURL.path), - let fileSize = attributes[.size] as? UInt64 { + let fileSize = attributes[.size] as? UInt64 + { return fileSize == 0 } return false @@ -119,7 +120,7 @@ class LogManager { // Assemble header information var headerLines = [String]() headerLines.append("LoopFollow Version: \(version)") - if !isMacApp && !isSimulatorBuild { + if !isMacApp, !isSimulatorBuild { headerLines.append("\(expirationHeaderString): \(expiration)") } headerLines.append("Built: \(formattedBuildDate)") @@ -128,7 +129,7 @@ class LogManager { let headerMessage = headerLines.joined(separator: ", ") + "\n" let logMessage = formattedLogMessage(for: .general, message: headerMessage) - self.append(logMessage, to: fileURL) + append(logMessage, to: fileURL) shouldLogVersionHeader = false } } @@ -140,7 +141,7 @@ class LogManager { let logFiles = try fileManager.contentsOfDirectory(at: logDirectory, includingPropertiesForKeys: nil) for logFile in logFiles { let filename = logFile.lastPathComponent - if !filename.contains(today) && !filename.contains(yesterday) { + if !filename.contains(today), !filename.contains(yesterday) { try fileManager.removeItem(at: logFile) } } diff --git a/LoopFollow/Log/LogViewModel.swift b/LoopFollow/Log/LogViewModel.swift index bba616634..f2c54fa5b 100644 --- a/LoopFollow/Log/LogViewModel.swift +++ b/LoopFollow/Log/LogViewModel.swift @@ -6,8 +6,8 @@ // Copyright © 2025 Jon Fawcett. All rights reserved. // -import Foundation import Combine +import Foundation class LogViewModel: ObservableObject { @Published var allLogEntries: [LogEntry] = [] diff --git a/LoopFollow/Log/SearchBar.swift b/LoopFollow/Log/SearchBar.swift index daf4d573a..4efdda0e5 100644 --- a/LoopFollow/Log/SearchBar.swift +++ b/LoopFollow/Log/SearchBar.swift @@ -19,7 +19,7 @@ struct SearchBar: UIViewRepresentable { _text = text } - func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { + func searchBar(_: UISearchBar, textDidChange searchText: String) { text = searchText } @@ -41,7 +41,7 @@ struct SearchBar: UIViewRepresentable { return searchBar } - func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext) { + func updateUIView(_ uiView: UISearchBar, context _: UIViewRepresentableContext) { uiView.text = text } } diff --git a/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift index 28f73ade1..e9eb58923 100644 --- a/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift +++ b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift @@ -6,8 +6,8 @@ // Copyright © 2025 Jon Fawcett. All rights reserved. // -import Foundation import Combine +import Foundation protocol NightscoutSettingsViewModelDelegate: AnyObject { func nightscoutSettingsDidFinish() @@ -27,6 +27,7 @@ class NightscoutSettingsViewModel: ObservableObject { } } } + @Published var nightscoutToken: String = UserDefaultsRepository.token.value { willSet { if newValue != nightscoutToken { @@ -35,6 +36,7 @@ class NightscoutSettingsViewModel: ObservableObject { } } } + @Published var nightscoutStatus: String = "Checking..." private var cancellables = Set() @@ -42,8 +44,8 @@ class NightscoutSettingsViewModel: ObservableObject { private var checkStatusWorkItem: DispatchWorkItem? init() { - self.initialURL = ObservableUserDefaults.shared.url.value - self.initialToken = UserDefaultsRepository.token.value + initialURL = ObservableUserDefaults.shared.url.value + initialToken = UserDefaultsRepository.token.value setupDebounce() checkNightscoutStatus() @@ -98,7 +100,7 @@ class NightscoutSettingsViewModel: ObservableObject { } func checkNightscoutStatus() { - NightscoutUtils.verifyURLAndToken { error, jwtToken, nsWriteAuth, nsAdminAuth in + NightscoutUtils.verifyURLAndToken { error, _, nsWriteAuth, nsAdminAuth in DispatchQueue.main.async { ObservableUserDefaults.shared.nsWriteAuth.value = nsWriteAuth ObservableUserDefaults.shared.nsAdminAuth.value = nsAdminAuth @@ -136,7 +138,7 @@ class NightscoutSettingsViewModel: ObservableObject { nightscoutStatus = "OK (\(authStatus))" - if (nightscoutURL != initialURL || nightscoutToken != initialToken) { + if nightscoutURL != initialURL || nightscoutToken != initialToken { NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil) } } diff --git a/LoopFollow/Remote/Loop/LoopNightscoutRemoteView.swift b/LoopFollow/Remote/Loop/LoopNightscoutRemoteView.swift index c11a98914..a35498d40 100644 --- a/LoopFollow/Remote/Loop/LoopNightscoutRemoteView.swift +++ b/LoopFollow/Remote/Loop/LoopNightscoutRemoteView.swift @@ -17,22 +17,23 @@ struct LoopNightscoutRemoteView: View { if !nsAdmin.value { ErrorMessageView( message: "Please update your token to include the 'admin' role in order to do remote commands with Loop." - )} else { - VStack { - let columns = [ - GridItem(.flexible(), spacing: 16), - GridItem(.flexible(), spacing: 16) - ] + ) + } else { + VStack { + let columns = [ + GridItem(.flexible(), spacing: 16), + GridItem(.flexible(), spacing: 16), + ] - LazyVGrid(columns: columns, spacing: 16) { - CommandButtonView(command: "Overrides", iconName: "slider.horizontal.3", destination: LoopOverrideView()) - } - .padding(.horizontal) - - Spacer() + LazyVGrid(columns: columns, spacing: 16) { + CommandButtonView(command: "Overrides", iconName: "slider.horizontal.3", destination: LoopOverrideView()) } - .navigationBarTitle("Loop Remote Control", displayMode: .inline) + .padding(.horizontal) + + Spacer() } + .navigationBarTitle("Loop Remote Control", displayMode: .inline) + } } } } diff --git a/LoopFollow/Remote/Loop/LoopOverrideView.swift b/LoopFollow/Remote/Loop/LoopOverrideView.swift index 7577bab35..ad1ba94ec 100644 --- a/LoopFollow/Remote/Loop/LoopOverrideView.swift +++ b/LoopFollow/Remote/Loop/LoopOverrideView.swift @@ -6,8 +6,8 @@ // Copyright © 2025 Jon Fawcett. All rights reserved. // -import SwiftUI import HealthKit +import SwiftUI struct LoopOverrideView: View { @Environment(\.presentationMode) private var presentationMode @@ -52,7 +52,6 @@ struct LoopOverrideView: View { message: "Please update your token to include the 'admin' role in order to do remote commands with Loop." ) } else { - Form { if let activeNote = overrideNote.value { Section(header: Text("Active Override")) { @@ -177,6 +176,7 @@ struct LoopOverrideView: View { } // MARK: - Functions + private func formattedDuration(from duration: Int?) -> String { guard let duration = duration, duration != 0 else { return "Indefinitely" diff --git a/LoopFollow/Remote/Loop/LoopOverrideViewModel.swift b/LoopFollow/Remote/Loop/LoopOverrideViewModel.swift index 1350ab7ca..026150b8e 100644 --- a/LoopFollow/Remote/Loop/LoopOverrideViewModel.swift +++ b/LoopFollow/Remote/Loop/LoopOverrideViewModel.swift @@ -18,7 +18,7 @@ final class LoopOverrideViewModel: ObservableObject, Sendable { "eventType": "Temporary Override", "enteredBy": Storage.shared.user.value, "reason": override.name, - "reasonDisplay": "\(override.symbol) \(override.name)" + "reasonDisplay": "\(override.symbol) \(override.name)", ] do { @@ -45,7 +45,7 @@ final class LoopOverrideViewModel: ObservableObject, Sendable { func sendCancelOverrideRequest(completion: @escaping (Bool, String?) -> Void) { Task { let body: [String: Any] = [ - "eventType": "Temporary Override Cancel" + "eventType": "Temporary Override Cancel", ] do { diff --git a/LoopFollow/Remote/Nightscout/TrioNightscoutRemoteView.swift b/LoopFollow/Remote/Nightscout/TrioNightscoutRemoteView.swift index 2e3bd099f..ebe17cecb 100644 --- a/LoopFollow/Remote/Nightscout/TrioNightscoutRemoteView.swift +++ b/LoopFollow/Remote/Nightscout/TrioNightscoutRemoteView.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Jon Fawcett. All rights reserved. // -import SwiftUI import HealthKit +import SwiftUI struct TrioNightscoutRemoteView: View { private let remoteController = TrioNightscoutRemoteController() @@ -249,7 +249,7 @@ struct TrioNightscoutRemoteView: View { private var isButtonDisabled: Bool { return newHKTarget.doubleValue(for: UserDefaultsRepository.getPreferredUnit()) == 0 || - duration.doubleValue(for: HKUnit.minute()) == 0 || isLoading + duration.doubleValue(for: HKUnit.minute()) == 0 || isLoading } private func enactTempTarget() { diff --git a/LoopFollow/Remote/RemoteViewController.swift b/LoopFollow/Remote/RemoteViewController.swift index ec13f04c0..9e0c79f76 100644 --- a/LoopFollow/Remote/RemoteViewController.swift +++ b/LoopFollow/Remote/RemoteViewController.swift @@ -6,14 +6,13 @@ // Copyright © 2024 Jon Fawcett. All rights reserved. // +import Combine import Foundation -import UIKit -import SwiftUI import HealthKit -import Combine +import SwiftUI +import UIKit class RemoteViewController: UIViewController { - private var cancellable: AnyCancellable? private var hostingController: UIHostingController? @@ -24,7 +23,7 @@ class RemoteViewController: UIViewController { Storage.shared.remoteType.$value, ObservableUserDefaults.shared.device.$value ) - .sink { [weak self] newRemoteType, newDevice in + .sink { [weak self] _, _ in DispatchQueue.main.async { self?.updateView() } @@ -80,14 +79,14 @@ class RemoteViewController: UIViewController { hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), hostingController.view.topAnchor.constraint(equalTo: view.topAnchor), - hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor) + hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) hostingController.didMove(toParent: self) } if remoteType == .nightscout, !ObservableUserDefaults.shared.nsWriteAuth.value { - NightscoutUtils.verifyURLAndToken { error, jwtToken, nsWriteAuth, nsAdminAuth in + NightscoutUtils.verifyURLAndToken { _, _, nsWriteAuth, nsAdminAuth in DispatchQueue.main.async { ObservableUserDefaults.shared.nsWriteAuth.value = nsWriteAuth ObservableUserDefaults.shared.nsAdminAuth.value = nsAdminAuth diff --git a/LoopFollow/Remote/Settings/RemoteSettingsView.swift b/LoopFollow/Remote/Settings/RemoteSettingsView.swift index 000537295..167cbe75c 100644 --- a/LoopFollow/Remote/Settings/RemoteSettingsView.swift +++ b/LoopFollow/Remote/Settings/RemoteSettingsView.swift @@ -7,8 +7,8 @@ // Copyright © 2024 Jon Fawcett. All rights reserved. // -import SwiftUI import HealthKit +import SwiftUI struct RemoteSettingsView: View { @ObservedObject var viewModel: RemoteSettingsViewModel @@ -26,6 +26,7 @@ struct RemoteSettingsView: View { NavigationView { Form { // MARK: - Remote Type Section (Custom Rows) + Section(header: Text("Remote Type")) { remoteTypeRow(type: .none, label: "None", isEnabled: true) @@ -51,6 +52,7 @@ struct RemoteSettingsView: View { } // MARK: - User Information Section + if viewModel.remoteType != .none { Section(header: Text("User Information")) { HStack { @@ -64,6 +66,7 @@ struct RemoteSettingsView: View { } // MARK: - Trio Remote Control Settings + if viewModel.remoteType == .trc { Section(header: Text("Trio Remote Control Settings")) { HStack { @@ -96,6 +99,7 @@ struct RemoteSettingsView: View { } // MARK: - Guardrails + Section(header: Text("Guardrails")) { HStack { Text("Max Bolus") @@ -175,6 +179,7 @@ struct RemoteSettingsView: View { } // MARK: - Meal Section + Section(header: Text("Meal Settings")) { Toggle("Meal with Bolus", isOn: $viewModel.mealWithBolus) .toggleStyle(SwitchToggleStyle()) @@ -184,6 +189,7 @@ struct RemoteSettingsView: View { } // MARK: - Debug / Info + Section(header: Text("Debug / Info")) { Text("Device Token: \(Storage.shared.deviceToken.value)") Text("Production Env.: \(Storage.shared.productionEnvironment.value ? "True" : "False")") @@ -216,6 +222,7 @@ struct RemoteSettingsView: View { } // MARK: - Custom Row for Remote Type Selection + private func remoteTypeRow(type: RemoteType, label: String, isEnabled: Bool) -> some View { Button(action: { if isEnabled { @@ -237,6 +244,7 @@ struct RemoteSettingsView: View { } // MARK: - Validation Error Handler + private func handleValidationError(_ message: String) { alertMessage = message alertType = .validation diff --git a/LoopFollow/Remote/Settings/RemoteSettingsViewModel.swift b/LoopFollow/Remote/Settings/RemoteSettingsViewModel.swift index 236fdfe0e..459331cb4 100644 --- a/LoopFollow/Remote/Settings/RemoteSettingsViewModel.swift +++ b/LoopFollow/Remote/Settings/RemoteSettingsViewModel.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Jon Fawcett. All rights reserved. // -import Foundation import Combine +import Foundation import HealthKit class RemoteSettingsViewModel: ObservableObject { @@ -29,17 +29,17 @@ class RemoteSettingsViewModel: ObservableObject { private var cancellables = Set() init() { - self.remoteType = storage.remoteType.value - self.user = storage.user.value - self.sharedSecret = storage.sharedSecret.value - self.apnsKey = storage.apnsKey.value - self.keyId = storage.keyId.value - self.maxBolus = storage.maxBolus.value - self.maxCarbs = storage.maxCarbs.value - self.maxProtein = storage.maxProtein.value - self.maxFat = storage.maxFat.value - self.mealWithBolus = storage.mealWithBolus.value - self.mealWithFatProtein = storage.mealWithFatProtein.value + remoteType = storage.remoteType.value + user = storage.user.value + sharedSecret = storage.sharedSecret.value + apnsKey = storage.apnsKey.value + keyId = storage.keyId.value + maxBolus = storage.maxBolus.value + maxCarbs = storage.maxCarbs.value + maxProtein = storage.maxProtein.value + maxFat = storage.maxFat.value + mealWithBolus = storage.mealWithBolus.value + mealWithFatProtein = storage.mealWithFatProtein.value setupBindings() } @@ -68,7 +68,7 @@ class RemoteSettingsViewModel: ObservableObject { $maxBolus .sink { [weak self] in self?.storage.maxBolus.value = $0 } .store(in: &cancellables) - + $maxCarbs .sink { [weak self] in self?.storage.maxCarbs.value = $0 } .store(in: &cancellables) @@ -90,7 +90,7 @@ class RemoteSettingsViewModel: ObservableObject { .store(in: &cancellables) ObservableUserDefaults.shared.device.$value - .receive(on: DispatchQueue.main ) + .receive(on: DispatchQueue.main) .sink { [weak self] newValue in self?.isTrioDevice = (newValue == "Trio") } diff --git a/LoopFollow/Remote/TRC/BolusView.swift b/LoopFollow/Remote/TRC/BolusView.swift index ddfbbcf62..f8251e8d2 100644 --- a/LoopFollow/Remote/TRC/BolusView.swift +++ b/LoopFollow/Remote/TRC/BolusView.swift @@ -6,9 +6,9 @@ // Copyright © 2024 Jon Fawcett. All rights reserved. // -import SwiftUI import HealthKit import LocalAuthentication +import SwiftUI struct BolusView: View { @Environment(\.presentationMode) private var presentationMode diff --git a/LoopFollow/Remote/TRC/MealView.swift b/LoopFollow/Remote/TRC/MealView.swift index 20c1413a0..a4349b04d 100644 --- a/LoopFollow/Remote/TRC/MealView.swift +++ b/LoopFollow/Remote/TRC/MealView.swift @@ -6,9 +6,9 @@ // Copyright © 2024 Jon Fawcett. All rights reserved. // -import SwiftUI import HealthKit import LocalAuthentication +import SwiftUI struct MealView: View { @Environment(\.presentationMode) private var presentationMode @@ -138,8 +138,9 @@ struct MealView: View { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { guard carbs.doubleValue(for: .gram()) != 0 || - protein.doubleValue(for: .gram()) != 0 || - fat.doubleValue(for: .gram()) != 0 else { + protein.doubleValue(for: .gram()) != 0 || + fat.doubleValue(for: .gram()) != 0 + else { return } if !showAlert { @@ -211,7 +212,6 @@ struct MealView: View { }), secondaryButton: .cancel() ) - case .statusSuccess: return Alert( title: Text("Status"), diff --git a/LoopFollow/Remote/TRC/OverrideView.swift b/LoopFollow/Remote/TRC/OverrideView.swift index 239ada6a7..c2931ff0c 100644 --- a/LoopFollow/Remote/TRC/OverrideView.swift +++ b/LoopFollow/Remote/TRC/OverrideView.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Jon Fawcett. All rights reserved. // -import SwiftUI import HealthKit +import SwiftUI struct OverrideView: View { @Environment(\.presentationMode) private var presentationMode diff --git a/LoopFollow/Remote/TRC/PushNotificationManager.swift b/LoopFollow/Remote/TRC/PushNotificationManager.swift index d07092add..d7e3c6e0d 100644 --- a/LoopFollow/Remote/TRC/PushNotificationManager.swift +++ b/LoopFollow/Remote/TRC/PushNotificationManager.swift @@ -7,8 +7,8 @@ // import Foundation -import SwiftJWT import HealthKit +import SwiftJWT struct APNsJWTClaims: Claims { let iss: String @@ -26,14 +26,14 @@ class PushNotificationManager { private var bundleId: String init() { - self.deviceToken = Storage.shared.deviceToken.value - self.sharedSecret = Storage.shared.sharedSecret.value - self.productionEnvironment = Storage.shared.productionEnvironment.value - self.apnsKey = Storage.shared.apnsKey.value - self.teamId = Storage.shared.teamId.value ?? "" - self.keyId = Storage.shared.keyId.value - self.user = Storage.shared.user.value - self.bundleId = Storage.shared.bundleId.value + deviceToken = Storage.shared.deviceToken.value + sharedSecret = Storage.shared.sharedSecret.value + productionEnvironment = Storage.shared.productionEnvironment.value + apnsKey = Storage.shared.apnsKey.value + teamId = Storage.shared.teamId.value ?? "" + keyId = Storage.shared.keyId.value + user = Storage.shared.user.value + bundleId = Storage.shared.bundleId.value } func sendOverridePushNotification(override: ProfileManager.TrioOverride, completion: @escaping (Bool, String?) -> Void) { @@ -189,10 +189,11 @@ class PushNotificationManager { let lines = pemString.components(separatedBy: "\n") guard let startIndex = lines.firstIndex(of: "-----BEGIN PRIVATE KEY-----"), let endIndex = lines.firstIndex(of: "-----END PRIVATE KEY-----"), - startIndex < endIndex else { + startIndex < endIndex + else { return nil } - let keyLines = lines[(startIndex + 1).. String? { if let cachedJWT = Storage.shared.cachedJWT.value, let expirationDate = Storage.shared.jwtExpirationDate.value { if Date() < expirationDate { diff --git a/LoopFollow/Remote/TRC/TRCCommandType.swift b/LoopFollow/Remote/TRC/TRCCommandType.swift index 5e9f5c59b..b4c892737 100644 --- a/LoopFollow/Remote/TRC/TRCCommandType.swift +++ b/LoopFollow/Remote/TRC/TRCCommandType.swift @@ -9,10 +9,10 @@ import Foundation enum TRCCommandType: String { - case bolus = "bolus" + case bolus case tempTarget = "temp_target" case cancelTempTarget = "cancel_temp_target" - case meal = "meal" + case meal case startOverride = "start_override" case cancelOverride = "cancel_override" } diff --git a/LoopFollow/Remote/TRC/TempTargetView.swift b/LoopFollow/Remote/TRC/TempTargetView.swift index 0559c5ab0..5dc8f2b24 100644 --- a/LoopFollow/Remote/TRC/TempTargetView.swift +++ b/LoopFollow/Remote/TRC/TempTargetView.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Jon Fawcett. All rights reserved. // -import SwiftUI import HealthKit +import SwiftUI struct TempTargetView: View { @Environment(\.presentationMode) private var presentationMode @@ -247,7 +247,7 @@ struct TempTargetView: View { private var isButtonDisabled: Bool { return newHKTarget.doubleValue(for: UserDefaultsRepository.getPreferredUnit()) == 0 || - duration.doubleValue(for: HKUnit.minute()) == 0 || isLoading + duration.doubleValue(for: HKUnit.minute()) == 0 || isLoading } private func enactTempTarget() { diff --git a/LoopFollow/Remote/TRC/TreatmentResponse.swift b/LoopFollow/Remote/TRC/TreatmentResponse.swift index 8e001ded0..fbbc40af3 100644 --- a/LoopFollow/Remote/TRC/TreatmentResponse.swift +++ b/LoopFollow/Remote/TRC/TreatmentResponse.swift @@ -20,14 +20,14 @@ struct TreatmentResponse: Decodable { let id: String enum CodingKeys: String, CodingKey { - case enteredBy = "enteredBy" - case eventType = "eventType" - case reason = "reason" - case targetTop = "targetTop" - case targetBottom = "targetBottom" - case duration = "duration" + case enteredBy + case eventType + case reason + case targetTop + case targetBottom + case duration case createdAt = "created_at" - case utcOffset = "utcOffset" + case utcOffset case id = "_id" } } @@ -42,12 +42,12 @@ struct TreatmentCancelResponse: Decodable { let id: String enum CodingKeys: String, CodingKey { - case enteredBy = "enteredBy" - case eventType = "eventType" - case reason = "reason" - case duration = "duration" + case enteredBy + case eventType + case reason + case duration case createdAt = "created_at" - case utcOffset = "utcOffset" + case utcOffset case id = "_id" } } diff --git a/LoopFollow/Remote/TRC/TrioNightscoutRemoteController.swift b/LoopFollow/Remote/TRC/TrioNightscoutRemoteController.swift index 50de1363a..62e5eeeb5 100644 --- a/LoopFollow/Remote/TRC/TrioNightscoutRemoteController.swift +++ b/LoopFollow/Remote/TRC/TrioNightscoutRemoteController.swift @@ -10,7 +10,6 @@ import Foundation import HealthKit class TrioNightscoutRemoteController { - func cancelExistingTarget(completion: @escaping (Bool) -> Void) { Task { let tempTargetBody: [String: Any] = [ @@ -18,7 +17,7 @@ class TrioNightscoutRemoteController { "eventType": "Temporary Target", "reason": "Manual", "duration": 0, - "created_at": ISO8601DateFormatter().string(from: Date()) + "created_at": ISO8601DateFormatter().string(from: Date()), ] do { @@ -40,7 +39,7 @@ class TrioNightscoutRemoteController { "targetTop": newTarget.doubleValue(for: .milligramsPerDeciliter), "targetBottom": newTarget.doubleValue(for: .milligramsPerDeciliter), "duration": Int(duration.doubleValue(for: .minute())), - "created_at": ISO8601DateFormatter().string(from: Date()) + "created_at": ISO8601DateFormatter().string(from: Date()), ] Task { diff --git a/LoopFollow/Remote/TRC/TrioRemoteControlView.swift b/LoopFollow/Remote/TRC/TrioRemoteControlView.swift index a335edff3..dfc24d2fd 100644 --- a/LoopFollow/Remote/TRC/TrioRemoteControlView.swift +++ b/LoopFollow/Remote/TRC/TrioRemoteControlView.swift @@ -17,7 +17,7 @@ struct TrioRemoteControlView: View { VStack { let columns = [ GridItem(.flexible(), spacing: 16), - GridItem(.flexible(), spacing: 16) + GridItem(.flexible(), spacing: 16), ] LazyVGrid(columns: columns, spacing: 16) { diff --git a/LoopFollow/Remote/TRC/TrioRemoteControlViewModel.swift b/LoopFollow/Remote/TRC/TrioRemoteControlViewModel.swift index 7914ff57e..9ba8d0c71 100644 --- a/LoopFollow/Remote/TRC/TrioRemoteControlViewModel.swift +++ b/LoopFollow/Remote/TRC/TrioRemoteControlViewModel.swift @@ -8,5 +8,4 @@ import Foundation -class TrioRemoteControlViewModel: ObservableObject { -} +class TrioRemoteControlViewModel: ObservableObject {} diff --git a/LoopFollow/Remote/TempTargetPreset/TempTargetPresetManager.swift b/LoopFollow/Remote/TempTargetPreset/TempTargetPresetManager.swift index 89abb7e14..e0c8baab4 100644 --- a/LoopFollow/Remote/TempTargetPreset/TempTargetPresetManager.swift +++ b/LoopFollow/Remote/TempTargetPreset/TempTargetPresetManager.swift @@ -6,9 +6,9 @@ // Copyright © 2024 Jon Fawcett. All rights reserved. // +import Combine import Foundation import HealthKit -import Combine class TempTargetPresetManager: ObservableObject { static let shared = TempTargetPresetManager() @@ -24,7 +24,7 @@ class TempTargetPresetManager: ObservableObject { func loadPresets() { if let data = UserDefaults.standard.data(forKey: presetsKey) { if let decodedPresets = try? JSONDecoder().decode([TempTargetPreset].self, from: data) { - self.presets = decodedPresets + presets = decodedPresets } } } diff --git a/LoopFollow/Settings/AdvancedSettingsView.swift b/LoopFollow/Settings/AdvancedSettingsView.swift index 150abb63a..4cfbb3fda 100644 --- a/LoopFollow/Settings/AdvancedSettingsView.swift +++ b/LoopFollow/Settings/AdvancedSettingsView.swift @@ -23,7 +23,7 @@ struct AdvancedSettingsView: View { Toggle("Graph Carbs", isOn: $viewModel.graphCarbs) Toggle("Graph Other Treatments", isOn: $viewModel.graphOtherTreatments) - Stepper(value: $viewModel.bgUpdateDelay, in: 1...30, step: 1) { + Stepper(value: $viewModel.bgUpdateDelay, in: 1 ... 30, step: 1) { Text("BG Update Delay (Sec): \(viewModel.bgUpdateDelay)") } } diff --git a/LoopFollow/Settings/AdvancedSettingsViewModel.swift b/LoopFollow/Settings/AdvancedSettingsViewModel.swift index 163263e57..76caae214 100644 --- a/LoopFollow/Settings/AdvancedSettingsViewModel.swift +++ b/LoopFollow/Settings/AdvancedSettingsViewModel.swift @@ -14,49 +14,57 @@ class AdvancedSettingsViewModel: ObservableObject { UserDefaultsRepository.downloadTreatments.value = downloadTreatments } } + @Published var downloadPrediction: Bool { didSet { UserDefaultsRepository.downloadPrediction.value = downloadPrediction } } + @Published var graphBasal: Bool { didSet { UserDefaultsRepository.graphBasal.value = graphBasal } } + @Published var graphBolus: Bool { didSet { UserDefaultsRepository.graphBolus.value = graphBolus } } + @Published var graphCarbs: Bool { didSet { UserDefaultsRepository.graphCarbs.value = graphCarbs } } + @Published var graphOtherTreatments: Bool { didSet { UserDefaultsRepository.graphOtherTreatments.value = graphOtherTreatments } } + @Published var bgUpdateDelay: Int { didSet { UserDefaultsRepository.bgUpdateDelay.value = bgUpdateDelay } } + @Published var debugLogLevel: Bool { didSet { Storage.shared.debugLogLevel.value = debugLogLevel } } + init() { - self.downloadTreatments = UserDefaultsRepository.downloadTreatments.value - self.downloadPrediction = UserDefaultsRepository.downloadPrediction.value - self.graphBasal = UserDefaultsRepository.graphBasal.value - self.graphBolus = UserDefaultsRepository.graphBolus.value - self.graphCarbs = UserDefaultsRepository.graphCarbs.value - self.graphOtherTreatments = UserDefaultsRepository.graphOtherTreatments.value - self.bgUpdateDelay = UserDefaultsRepository.bgUpdateDelay.value - self.debugLogLevel = Storage.shared.debugLogLevel.value + downloadTreatments = UserDefaultsRepository.downloadTreatments.value + downloadPrediction = UserDefaultsRepository.downloadPrediction.value + graphBasal = UserDefaultsRepository.graphBasal.value + graphBolus = UserDefaultsRepository.graphBolus.value + graphCarbs = UserDefaultsRepository.graphCarbs.value + graphOtherTreatments = UserDefaultsRepository.graphOtherTreatments.value + bgUpdateDelay = UserDefaultsRepository.bgUpdateDelay.value + debugLogLevel = Storage.shared.debugLogLevel.value } } diff --git a/LoopFollow/Snoozer/SnoozerView.swift b/LoopFollow/Snoozer/SnoozerView.swift index 5451895c4..d6ba43b5b 100644 --- a/LoopFollow/Snoozer/SnoozerView.swift +++ b/LoopFollow/Snoozer/SnoozerView.swift @@ -12,11 +12,11 @@ struct SnoozerView: View { @StateObject private var vm = SnoozerViewModel() @ObservedObject var minAgoText = Observable.shared.minAgoText - @ObservedObject var bgText = Observable.shared.bgText + @ObservedObject var bgText = Observable.shared.bgText @ObservedObject var bgTextColor = Observable.shared.bgTextColor @ObservedObject var directionText = Observable.shared.directionText - @ObservedObject var deltaText = Observable.shared.deltaText - @ObservedObject var bgStale = Observable.shared.bgStale + @ObservedObject var deltaText = Observable.shared.deltaText + @ObservedObject var bgStale = Observable.shared.bgStale var body: some View { GeometryReader { geo in @@ -45,6 +45,7 @@ struct SnoozerView: View { } // MARK: - Left Column (BG / Direction / Delta / Age) + private var leftColumn: some View { VStack(spacing: 0) { Text(bgText.value) @@ -81,6 +82,7 @@ struct SnoozerView: View { } // MARK: - Right Column (Clock/Alert + Snooze Controls) + private var rightColumn: some View { VStack(spacing: 0) { Spacer() @@ -105,10 +107,10 @@ struct SnoozerView: View { } Spacer() Stepper("", value: $vm.snoozeUnits, - in: 1...(alarm.type.timeUnit == .day ? 30 : - alarm.type.timeUnit == .hour ? 24 : 60), + in: 1 ... (alarm.type.timeUnit == .day ? 30 : + alarm.type.timeUnit == .hour ? 24 : 60), step: alarm.type.timeUnit == .minute ? 5 : 1) - .labelsHidden() + .labelsHidden() } .padding(.horizontal, 24) @@ -130,12 +132,11 @@ struct SnoozerView: View { } else { TimelineView(.periodic(from: .now, by: 1)) { context in Text(context.date, format: - Date.FormatStyle(date: .omitted, time: .shortened) - ) - .font(.system(size: 70)) - .minimumScaleFactor(0.5) - .foregroundColor(.white) - .frame(height: 78) + Date.FormatStyle(date: .omitted, time: .shortened)) + .font(.system(size: 70)) + .minimumScaleFactor(0.5) + .foregroundColor(.white) + .frame(height: 78) } Spacer() } @@ -143,13 +144,13 @@ struct SnoozerView: View { } } -fileprivate extension View { +private extension View { func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View { - clipShape( RoundedCorner(radius: radius, corners: corners) ) + clipShape(RoundedCorner(radius: radius, corners: corners)) } } -fileprivate struct RoundedCorner: Shape { +private struct RoundedCorner: Shape { var radius: CGFloat = .infinity var corners: UIRectCorner = .allCorners func path(in rect: CGRect) -> Path { diff --git a/LoopFollow/Snoozer/SnoozerViewController.swift b/LoopFollow/Snoozer/SnoozerViewController.swift index 805f99360..951845284 100644 --- a/LoopFollow/Snoozer/SnoozerViewController.swift +++ b/LoopFollow/Snoozer/SnoozerViewController.swift @@ -6,10 +6,9 @@ // Copyright © 2025 Jon Fawcett. All rights reserved. // - -import UIKit -import SwiftUI import Combine +import SwiftUI +import UIKit class SnoozerViewController: UIViewController { private var hostingController: UIHostingController? @@ -23,7 +22,7 @@ class SnoozerViewController: UIViewController { let snoozerView = SnoozerView() let hosting = UIHostingController(rootView: snoozerView) - self.hostingController = hosting + hostingController = hosting addChild(hosting) view.addSubview(hosting.view) hosting.view.translatesAutoresizingMaskIntoConstraints = false @@ -32,7 +31,7 @@ class SnoozerViewController: UIViewController { hosting.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), hosting.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), hosting.view.topAnchor.constraint(equalTo: view.topAnchor), - hosting.view.bottomAnchor.constraint(equalTo: view.bottomAnchor) + hosting.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) hosting.didMove(toParent: self) diff --git a/LoopFollow/Snoozer/SnoozerViewModel.swift b/LoopFollow/Snoozer/SnoozerViewModel.swift index 51cf6d3f7..3fc1b3052 100644 --- a/LoopFollow/Snoozer/SnoozerViewModel.swift +++ b/LoopFollow/Snoozer/SnoozerViewModel.swift @@ -6,8 +6,8 @@ // Copyright © 2025 Jon Fawcett. All rights reserved. // -import Foundation import Combine +import Foundation final class SnoozerViewModel: ObservableObject { @Published var activeAlarm: Alarm? diff --git a/LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift b/LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift index 2c4807125..1ec11b071 100644 --- a/LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift +++ b/LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Jon Fawcett. All rights reserved. // -import Foundation import Combine +import Foundation class ObservableUserDefaultsValue: ObservableObject, UserDefaultsAnyValue { // user defaults key (UserDefaultsAnyValue protocol implementation) @@ -18,7 +18,7 @@ class ObservableUserDefaultsValue: ObservableObje @Published var value: T { didSet { // Continue only if the new value is different from the old value - guard self.value != oldValue else { return } + guard value != oldValue else { return } if let validation = validation { guard let validatedValue = validation(value) else { @@ -43,7 +43,7 @@ class ObservableUserDefaultsValue: ObservableObje // Notify UserDefaultsValueGroups that value has changed UserDefaultsValueGroups.valueChanged(self) - print("Value for \(self.key) changed to \(self.value)") // Logging + print("Value for \(self.key) changed to \(self.value)") // Logging } } } @@ -51,7 +51,7 @@ class ObservableUserDefaultsValue: ObservableObje /// Get/set the value from Any value (UserDefaultsAnyValue protocol implementation) var anyValue: Any? { get { - return self.value.toAny() + return value.toAny() } set { guard let newValue = T.fromAny(newValue) as T? else { @@ -69,14 +69,14 @@ class ObservableUserDefaultsValue: ObservableObje } // On change closure - private let onChange: ((T) -> ())? + private let onChange: ((T) -> Void)? // Validate & transform closure : given the new value, validate it; if validation passes, return the new value; // if validation fails, transform the value, returning a modified version or return nil and the change will not happen private let validation: ((T) -> T?)? // Value change observers - private var observers: [UUID : (T) -> Void] = [:] + private var observers: [UUID: (T) -> Void] = [:] // User defaults used for persistence private class var defaults: UserDefaults { @@ -91,7 +91,7 @@ class ObservableUserDefaultsValue: ObservableObje if let anyValue = ObservableUserDefaultsValue.defaults.object(forKey: key), let value = T.fromAny(anyValue) as T? { self.value = validation?(value) ?? value } else { - self.value = defaultValue + value = defaultValue } } diff --git a/LoopFollow/Storage/Framework/ObservableValue.swift b/LoopFollow/Storage/Framework/ObservableValue.swift index 2264e477b..e157b9c39 100644 --- a/LoopFollow/Storage/Framework/ObservableValue.swift +++ b/LoopFollow/Storage/Framework/ObservableValue.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Jon Fawcett. All rights reserved. // -import Foundation import Combine +import Foundation import HealthKit import SwiftUI @@ -15,7 +15,7 @@ class ObservableValue: ObservableObject { @Published var value: T init(default: T) { - self.value = `default` + value = `default` } func set(_ newValue: T) { diff --git a/LoopFollow/Storage/Framework/SecureStorageValue.swift b/LoopFollow/Storage/Framework/SecureStorageValue.swift index aa6c07d46..50f3a393a 100644 --- a/LoopFollow/Storage/Framework/SecureStorageValue.swift +++ b/LoopFollow/Storage/Framework/SecureStorageValue.swift @@ -6,15 +6,15 @@ // Copyright © 2024 Jon Fawcett. All rights reserved. // -import Foundation import Combine +import Foundation class SecureStorageValue: ObservableObject { let key: String @Published var value: T { didSet { - guard self.value != oldValue else { return } + guard value != oldValue else { return } if let data = try? NSKeyedArchiver.archivedData(withRootObject: value, requiringSecureCoding: true) { SecureStorageValue.defaults.set(data, forKey: key) } @@ -32,10 +32,11 @@ class SecureStorageValue: ObservableOb init(key: String, defaultValue: T) { self.key = key if let data = SecureStorageValue.defaults.data(forKey: key), - let decodedValue = try? NSKeyedUnarchiver.unarchivedObject(ofClass: T.self, from: data) { - self.value = decodedValue + let decodedValue = try? NSKeyedUnarchiver.unarchivedObject(ofClass: T.self, from: data) + { + value = decodedValue } else { - self.value = defaultValue + value = defaultValue } } diff --git a/LoopFollow/Storage/Framework/StorageValue.swift b/LoopFollow/Storage/Framework/StorageValue.swift index 3677c4cf5..a0f62941e 100644 --- a/LoopFollow/Storage/Framework/StorageValue.swift +++ b/LoopFollow/Storage/Framework/StorageValue.swift @@ -6,15 +6,15 @@ // Copyright © 2024 Jon Fawcett. All rights reserved. // -import Foundation import Combine +import Foundation class StorageValue: ObservableObject { let key: String @Published var value: T { didSet { - guard self.value != oldValue else { return } + guard value != oldValue else { return } if let encodedData = try? JSONEncoder().encode(value) { StorageValue.defaults.set(encodedData, forKey: key) @@ -34,10 +34,11 @@ class StorageValue: ObservableObject { self.key = key if let data = StorageValue.defaults.data(forKey: key), - let decodedValue = try? JSONDecoder().decode(T.self, from: data) { - self.value = decodedValue + let decodedValue = try? JSONDecoder().decode(T.self, from: data) + { + value = decodedValue } else { - self.value = defaultValue + value = defaultValue } } diff --git a/LoopFollow/Storage/Framework/UserDefaultsValue.swift b/LoopFollow/Storage/Framework/UserDefaultsValue.swift index 093abab6f..ff6f851ff 100644 --- a/LoopFollow/Storage/Framework/UserDefaultsValue.swift +++ b/LoopFollow/Storage/Framework/UserDefaultsValue.swift @@ -17,20 +17,17 @@ protocol UserDefaultsAnyValue { /// A value holder that keeps its internal data (the value) synchronized with its UserDefaults value. The value is read from UserDefaults on initialization (if exists, otherwise a default value is used) and written when the value changes. Also a custom validation & onChange closure can be set on initialization. /// /// Another feature of this class is that when the value changes, the change can be observed by multiple observers. There are two observation levels: the instance level, when the observers register directly to the UserDefaultsValue instance, and group level, if the UserDefaultsValue instance is embeded in a group (UserDefaultsValueGroups class manages the groups). It is very convenient to declare related values in the same group, so when one of them changes, the group observers are notified that a change occured in that group (no need to observe each particular UserDefaultsValue instance). -class UserDefaultsValue : UserDefaultsAnyValue { - - +class UserDefaultsValue: UserDefaultsAnyValue { // user defaults key (UserDefaultsAnyValue protocol implementation) let key: String - + typealias ValueType = T - + // the value (strong typed) var value: T { didSet { - // continue only if the new value is different than old value - guard self.value != oldValue else { + guard value != oldValue else { return } @@ -39,58 +36,58 @@ class UserDefaultsValue : UserDefaultsAnyValue { value = oldValue return } - + value = validatedValue } - + // store value to user defaults UserDefaultsValue.defaults.setValue(value.toAny(), forKey: key) - + // execute custom closure onChange?(value) - + // notify observers observers.values.forEach { $0(value) } - + // notify UserDefaultsValueGroups that value has changed UserDefaultsValueGroups.valueChanged(self) } } - + /// get/set the value from Any value (UserDefaultsAnyValue protocol implementation) var anyValue: Any? { get { - return self.value.toAny() + return value.toAny() } - + set { guard let newValue = T.fromAny(newValue) as T? else { return } - - self.value = newValue + + value = newValue } } - + /// is there this key already stored in UserDefaults? var exists: Bool { return UserDefaultsValue.defaults.object(forKey: key) != nil } - + // on change closure - private let onChange: ((T) -> ())? - + private let onChange: ((T) -> Void)? + // validate & transform closure : giving the new value, validate it; if validations passes, return the new value; if fails, transform the value, returning a modified version or ... return nil and the change will not gonna happen private let validation: ((T) -> T?)? - + // value change observers - private var observers: [UUID : (T) -> Void] = [:] - + private var observers: [UUID: (T) -> Void] = [:] + // user defaults used for persistence private class var defaults: UserDefaults { return UserDefaults(suiteName: AppConstants.APP_GROUP_ID)! } - + init(key: String, default defaultValue: T, onChange: ((T) -> Void)? = nil, validation: ((T) -> T?)? = nil) { self.key = key if let anyValue = UserDefaultsValue.defaults.object(forKey: key), let value = T.fromAny(anyValue) as T? { @@ -100,32 +97,30 @@ class UserDefaultsValue : UserDefaultsAnyValue { self.value = value } } else { - self.value = defaultValue + value = defaultValue } self.onChange = onChange self.validation = validation } - + /// Insert this value in a group, useful for observing changes in the whole group, instead of particular values func group(_ groupName: String) -> Self { UserDefaultsValueGroups.add(self, to: groupName) return self } - + /// register observers, will be notified when value changes @discardableResult - func observeChanges(using closure: @escaping(T) -> Void) -> ObservationToken { - + func observeChanges(using closure: @escaping (T) -> Void) -> ObservationToken { let id = UUID() observers[id] = closure - + return ObservationToken { [weak self] in self?.observers.removeValue(forKey: id) } } - - func setNil(key: String){ + + func setNil(key: String) { UserDefaultsValue.defaults.removeObject(forKey: key) } } - diff --git a/LoopFollow/Storage/Framework/UserDefaultsValueGroups.swift b/LoopFollow/Storage/Framework/UserDefaultsValueGroups.swift index 56bfdf9c3..ee92cdf25 100644 --- a/LoopFollow/Storage/Framework/UserDefaultsValueGroups.swift +++ b/LoopFollow/Storage/Framework/UserDefaultsValueGroups.swift @@ -10,65 +10,61 @@ import Foundation /// UserDefaultValue groups manager class, providing change observation capabilities (keep clients informed when a change occured in a given group). class UserDefaultsValueGroups { - class func values(from groupName: String) -> [UserDefaultsAnyValue]? { return groupNameToValues[groupName] } - + class func add(_ value: UserDefaultsAnyValue, to groupName: String) { - // add to "value-key to groupNames" dictionary var groupNames = valueKeyToGroupNames[value.key] ?? [] guard !groupNames.contains(groupName) else { - // already added value to this group! return } - + groupNames.append(groupName) valueKeyToGroupNames[value.key] = groupNames - + // add to "groupName to value" dictionary var values = groupNameToValues[groupName] ?? [] values.append(value) groupNameToValues[groupName] = values } - + @discardableResult - class func observeChanges(in groupName: String, using closure: @escaping(UserDefaultsAnyValue, String) -> Void) -> ObservationToken { - + class func observeChanges(in groupName: String, using closure: @escaping (UserDefaultsAnyValue, String) -> Void) -> ObservationToken { let id = UUID() - + var observers = groupNameToObservers[groupName] ?? [:] observers[id] = closure groupNameToObservers[groupName] = observers - + return ObservationToken { groupNameToObservers[groupName]?.removeValue(forKey: id) } } - + // called by UserDefaultsValue instances when value changes class func valueChanged(_ value: UserDefaultsAnyValue) { - valueKeyToGroupNames[value.key]?.forEach() { groupName in + valueKeyToGroupNames[value.key]?.forEach { groupName in notifyValueChanged(value, in: groupName) } } - + private class func notifyValueChanged(_ value: UserDefaultsAnyValue, in groupName: String) { groupNameToObservers[groupName]?.values.forEach { closure in closure(value, groupName) } } - - static private var groupNameToValues: [String: [UserDefaultsAnyValue]] = [:] - static private var valueKeyToGroupNames: [String: [String]] = [:] - static private var groupNameToObservers: [String: [UUID : (UserDefaultsAnyValue, String) -> Void]] = [:] + + private static var groupNameToValues: [String: [UserDefaultsAnyValue]] = [:] + private static var valueKeyToGroupNames: [String: [String]] = [:] + private static var groupNameToObservers: [String: [UUID: (UserDefaultsAnyValue, String) -> Void]] = [:] } // user default values group definitions extension UserDefaultsValueGroups { - struct GroupNames { + enum GroupNames { static let watchSync = "watchSync" static let alarm = "alarm" } diff --git a/LoopFollow/Storage/Observable.swift b/LoopFollow/Storage/Observable.swift index 9c906108f..abd6c5004 100644 --- a/LoopFollow/Storage/Observable.swift +++ b/LoopFollow/Storage/Observable.swift @@ -1,10 +1,10 @@ - // - // Observable.swift - // LoopFollow - // - // Created by Jonas Björkert on 2024-07-25. - // Copyright © 2024 Jon Fawcett. All rights reserved. - // +// +// Observable.swift +// LoopFollow +// +// Created by Jonas Björkert on 2024-07-25. +// Copyright © 2024 Jon Fawcett. All rights reserved. +// import Foundation import HealthKit diff --git a/LoopFollow/Storage/ObservableUserDefaults.swift b/LoopFollow/Storage/ObservableUserDefaults.swift index a4b999307..669683cd4 100644 --- a/LoopFollow/Storage/ObservableUserDefaults.swift +++ b/LoopFollow/Storage/ObservableUserDefaults.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Jon Fawcett. All rights reserved. // -import Foundation import Combine +import Foundation /* Legacy storage, we are moving away from this diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index dd7230524..562e535be 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -47,7 +47,7 @@ class Storage { var contactEnabled = StorageValue(key: "contactEnabled", defaultValue: false) var contactBackgroundColor = StorageValue(key: "contactBackgroundColor", defaultValue: ContactColorOption.black.rawValue) var contactTextColor = StorageValue(key: "contactTextColor", defaultValue: ContactColorOption.white.rawValue) - + var sensorScheduleOffset = StorageValue(key: "sensorScheduleOffset", defaultValue: nil) var alarms = StorageValue<[Alarm]>(key: "alarms", defaultValue: []) @@ -57,5 +57,5 @@ class Storage { ) static let shared = Storage() - private init() { } + private init() {} } diff --git a/LoopFollow/Storage/UserDefaults.swift b/LoopFollow/Storage/UserDefaults.swift index cf6026c56..9c346315b 100644 --- a/LoopFollow/Storage/UserDefaults.swift +++ b/LoopFollow/Storage/UserDefaults.swift @@ -11,8 +11,8 @@ // import Foundation -import UIKit import HealthKit +import UIKit /* Legacy storage, we are moving away from this @@ -33,7 +33,7 @@ class UserDefaultsRepository { for index in currentValidIndices { if !sortArray.contains(index) { sortArray.append(index) - //print("Added missing index \(index) to sortArray") + // print("Added missing index \(index) to sortArray") } } @@ -42,16 +42,16 @@ class UserDefaultsRepository { // Ensure visibleArray is updated with new entries if visibleArray.count < currentValidIndices.count { - for i in visibleArray.count.. currentValidIndices.count { visibleArray = Array(visibleArray.prefix(currentValidIndices.count)) - //print("Trimmed visibleArray to match current valid indices") + // print("Trimmed visibleArray to match current valid indices") } infoSort.value = sortArray @@ -86,7 +86,7 @@ class UserDefaultsRepository { static let shareUserName = UserDefaultsValue(key: "shareUserName", default: "") static let sharePassword = UserDefaultsValue(key: "sharePassword", default: "") static let shareServer = UserDefaultsValue(key: "shareServer", default: "US") - + // Graph Settings static let chartScaleX = UserDefaultsValue(key: "chartScaleX", default: 18.0) static let showDots = UserDefaultsValue(key: "showDots", default: true) @@ -105,8 +105,7 @@ class UserDefaultsRepository { static let lowLine = UserDefaultsValue(key: "lowLine", default: 70.0) static let highLine = UserDefaultsValue(key: "highLine", default: 180.0) static let smallGraphHeight = UserDefaultsValue(key: "smallGraphHeight", default: 40) - - + // General Settings static let colorBGText = UserDefaultsValue(key: "colorBGText", default: true) static let showStats = UserDefaultsValue(key: "showStats", default: true) @@ -136,10 +135,11 @@ class UserDefaultsRepository { default: UIApplication.shared.isIdleTimerDisabled, onChange: { screenlock in UIApplication.shared.isIdleTimerDisabled = screenlock - }) - + } + ) + // Advanced Settings - //static let onlyDownloadBG = UserDefaultsValue(key: "onlyDownloadBG", default: false) + // static let onlyDownloadBG = UserDefaultsValue(key: "onlyDownloadBG", default: false) static let downloadTreatments = UserDefaultsValue(key: "downloadTreatments", default: true) static let downloadPrediction = UserDefaultsValue(key: "downloadPrediction", default: true) static let graphOtherTreatments = UserDefaultsValue(key: "graphOtherTreatments", default: true) @@ -148,8 +148,7 @@ class UserDefaultsRepository { static let graphCarbs = UserDefaultsValue(key: "graphCarbs", default: true) static let bgUpdateDelay = UserDefaultsValue(key: "bgUpdateDelay", default: 10) static let downloadDays = UserDefaultsValue(key: "downloadDays", default: 1) - - + // Watch Calendar Settings static let calendarIdentifier = UserDefaultsValue(key: "calendarIdentifier", default: "") static let savedEventID = UserDefaultsValue(key: "savedEventID", default: "") @@ -157,36 +156,35 @@ class UserDefaultsRepository { static let writeCalendarEvent = UserDefaultsValue(key: "writeCalendarEvent", default: false) static let watchLine1 = UserDefaultsValue(key: "watchLine1", default: "%BG% %DIRECTION% %DELTA% %MINAGO%") static let watchLine2 = UserDefaultsValue(key: "watchLine2", default: "C:%COB% I:%IOB% B:%BASAL%") - + // Alarm Settings static let systemOutputVolume = UserDefaultsValue(key: "systemOutputVolume", default: 0.5) static let fadeInTimeInterval = UserDefaultsValue(key: "fadeInTimeInterval", default: 0) static let vibrate = UserDefaultsValue(key: "vibrate", default: true) static let overrideSystemOutputVolume = UserDefaultsValue(key: "overrideSystemOutputVolume", default: true) static let forcedOutputVolume = UserDefaultsValue(key: "forcedOutputVolume", default: 0.5) - - + // Alerts - + let components = DateComponents(hour: 20, minute: 0) - static let quietHourStart = UserDefaultsValue(key: "quietHourStart", default: nil) //eventually need to adjust this to night time instead of quiet hour to clean up - static let quietHourEnd = UserDefaultsValue(key: "quietHourEnd", default: nil) //eventually need to adjust this to night time instead of quiet hour to clean up + static let quietHourStart = UserDefaultsValue(key: "quietHourStart", default: nil) // eventually need to adjust this to night time instead of quiet hour to clean up + static let quietHourEnd = UserDefaultsValue(key: "quietHourEnd", default: nil) // eventually need to adjust this to night time instead of quiet hour to clean up static let nightTime = UserDefaultsValue(key: "nightTime", default: false) - + static let snoozedBGReadingTime = UserDefaultsValue(key: "snoozedBGReadingTime", default: 0) - + static let alertIgnoreZero = UserDefaultsValue(key: "alertIgnoreZero", default: true) static let alertAudioDuringPhone = UserDefaultsValue(key: "alertAudioDuringPhone", default: true) static let alertAutoSnoozeCGMStart = UserDefaultsValue(key: "alertAutoSnoozeCGMStart", default: false) - + static let alertCageInsertTime = UserDefaultsValue(key: "alertCageInsertTime", default: 0) static let alertSageInsertTime = UserDefaultsValue(key: "alertSageInsertTime", default: 0) - + static let alertSnoozeAllTime = UserDefaultsValue(key: "alertSnoozeAllTime", default: nil) static let alertSnoozeAllIsSnoozed = UserDefaultsValue(key: "alertSnoozeAllIsSnoozed", default: false) static let alertMuteAllTime = UserDefaultsValue(key: "alertMuteAllTime", default: nil) static let alertMuteAllIsMuted = UserDefaultsValue(key: "alertMuteAllIsMuted", default: false) - + static let alertUrgentLowActive = UserDefaultsValue(key: "alertUrgentLowActive", default: false) static let alertUrgentLowBG = UserDefaultsValue(key: "alertUrgentLowBG", default: 55.0) static let alertUrgentLowPredictiveMinutes = UserDefaultsValue(key: "alertUrgentLowPredictiveMinutes", default: 0) @@ -203,7 +201,7 @@ class UserDefaultsRepository { static let alertUrgentLowAutosnooze = UserDefaultsValue(key: "alertUrgentLowAutosnooze", default: "Never") static let alertUrgentLowAutosnoozeDay = UserDefaultsValue(key: "alertUrgentLowAutosnoozeDay", default: false) static let alertUrgentLowAutosnoozeNight = UserDefaultsValue(key: "alertUrgentLowAutosnoozeNight", default: false) - + static let alertLowActive = UserDefaultsValue(key: "alertLowActive", default: false) static let alertLowBG = UserDefaultsValue(key: "alertLowBG", default: 70.0) static let alertLowPersistent = UserDefaultsValue(key: "alertLowPersistent", default: 0) @@ -221,7 +219,7 @@ class UserDefaultsRepository { static let alertLowAutosnooze = UserDefaultsValue(key: "alertLowAutosnooze", default: "Never") static let alertLowAutosnoozeDay = UserDefaultsValue(key: "alertLowAutosnoozeDay", default: false) static let alertLowAutosnoozeNight = UserDefaultsValue(key: "alertLowAutosnoozeNight", default: false) - + static let alertHighActive = UserDefaultsValue(key: "alertHighActive", default: false) static let alertHighBG = UserDefaultsValue(key: "alertHighBG", default: 180.0) static let alertHighPersistent = UserDefaultsValue(key: "alertHighPersistent", default: 60) @@ -238,7 +236,7 @@ class UserDefaultsRepository { static let alertHighAutosnooze = UserDefaultsValue(key: "alertHighAutosnooze", default: "Never") static let alertHighAutosnoozeDay = UserDefaultsValue(key: "alertHighAutosnoozeDay", default: false) static let alertHighAutosnoozeNight = UserDefaultsValue(key: "alertHighAutosnoozeNight", default: false) - + static let alertUrgentHighActive = UserDefaultsValue(key: "alertUrgentHighActive", default: false) static let alertUrgentHighBG = UserDefaultsValue(key: "alertUrgentHighBG", default: 250.0) static let alertUrgentHighSnooze = UserDefaultsValue(key: "alertUrgentHighSnooze", default: 30) @@ -254,7 +252,7 @@ class UserDefaultsRepository { static let alertUrgentHighAutosnooze = UserDefaultsValue(key: "alertUrgentHighAutosnooze", default: "Never") static let alertUrgentHighAutosnoozeDay = UserDefaultsValue(key: "alertUrgentHighAutosnoozeDay", default: false) static let alertUrgentHighAutosnoozeNight = UserDefaultsValue(key: "alertUrgentHighAutosnoozeNight", default: false) - + static let alertFastDropActive = UserDefaultsValue(key: "alertFastDropDeltaActive", default: false) static let alertFastDropSnooze = UserDefaultsValue(key: "alertFastDropDeltaSnooze", default: 10) static let alertFastDropDelta = UserDefaultsValue(key: "alertFastDropDelta", default: 10.0) @@ -273,7 +271,7 @@ class UserDefaultsRepository { static let alertFastDropAutosnooze = UserDefaultsValue(key: "alertFastDropAutosnooze", default: "Never") static let alertFastDropAutosnoozeDay = UserDefaultsValue(key: "alertFastDropAutosnoozeDay", default: false) static let alertFastDropAutosnoozeNight = UserDefaultsValue(key: "alertFastDropAutosnoozeNight", default: false) - + static let alertFastRiseActive = UserDefaultsValue(key: "alertFastRiseDeltaActive", default: false) static let alertFastRiseSnooze = UserDefaultsValue(key: "alertFastRiseDeltaSnooze", default: 10) static let alertFastRiseDelta = UserDefaultsValue(key: "alertFastRiseDelta", default: 10.0) @@ -292,8 +290,7 @@ class UserDefaultsRepository { static let alertFastRiseAutosnooze = UserDefaultsValue(key: "alertFastRiseAutosnooze", default: "Never") static let alertFastRiseAutosnoozeDay = UserDefaultsValue(key: "alertFastRiseAutosnoozeDay", default: false) static let alertFastRiseAutosnoozeNight = UserDefaultsValue(key: "alertFastRiseAutosnoozeNight", default: false) - - + static let alertMissedReadingActive = UserDefaultsValue(key: "alertMissedReadingActive", default: false) static let alertMissedReading = UserDefaultsValue(key: "alertMissedReading", default: 31) static let alertMissedReadingSnooze = UserDefaultsValue(key: "alertMissedReadingSnooze", default: 30) @@ -309,8 +306,7 @@ class UserDefaultsRepository { static let alertMissedReadingAutosnooze = UserDefaultsValue(key: "alertMissedReadingAutosnooze", default: "Never") static let alertMissedReadingAutosnoozeDay = UserDefaultsValue(key: "alertMissedReadingAutosnoozeDay", default: false) static let alertMissedReadingAutosnoozeNight = UserDefaultsValue(key: "alertMissedReadingAutosnoozeNight", default: false) - - + static let alertNotLoopingActive = UserDefaultsValue(key: "alertNotLoopingActive", default: false) static let alertNotLooping = UserDefaultsValue(key: "alertNotLooping", default: 31) static let alertNotLoopingSnooze = UserDefaultsValue(key: "alertNotLoopingSnooze", default: 30) @@ -330,7 +326,7 @@ class UserDefaultsRepository { static let alertNotLoopingAutosnooze = UserDefaultsValue(key: "alertNotLoopingAutosnooze", default: "Never") static let alertNotLoopingAutosnoozeDay = UserDefaultsValue(key: "alertNotLoopingAutosnoozeDay", default: false) static let alertNotLoopingAutosnoozeNight = UserDefaultsValue(key: "alertNotLoopingAutosnoozeNight", default: false) - + static let alertMissedBolusActive = UserDefaultsValue(key: "alertMissedBolusActive", default: false) static let alertMissedBolus = UserDefaultsValue(key: "alertMissedBolus", default: 10) static let alertMissedBolusSnooze = UserDefaultsValue(key: "alertMissedBolusSnooze", default: 10) @@ -351,9 +347,9 @@ class UserDefaultsRepository { static let alertMissedBolusAutosnooze = UserDefaultsValue(key: "alertMissedBolusAutosnooze", default: "Never") static let alertMissedBolusAutosnoozeDay = UserDefaultsValue(key: "alertMissedBolusAutosnoozeDay", default: false) static let alertMissedBolusAutosnoozeNight = UserDefaultsValue(key: "alertMissedBolusAutosnoozeNight", default: false) - + static let alertSAGEActive = UserDefaultsValue(key: "alertSAGEActive", default: false) - static let alertSAGE = UserDefaultsValue(key: "alertSAGE", default: 8) //Hours + static let alertSAGE = UserDefaultsValue(key: "alertSAGE", default: 8) // Hours static let alertSAGEQuiet = UserDefaultsValue(key: "alertSAGEQuiet", default: false) static let alertSAGERepeat = UserDefaultsValue(key: "alertSAGERepeat", default: "Never") static let alertSAGEDayTime = UserDefaultsValue(key: "alertSAGEDayTime", default: false) @@ -361,7 +357,7 @@ class UserDefaultsRepository { static let alertSAGEAudible = UserDefaultsValue(key: "alertSAGEAudible", default: "Always") static let alertSAGEDayTimeAudible = UserDefaultsValue(key: "alertSAGEDayTimeAudible", default: true) static let alertSAGENightTimeAudible = UserDefaultsValue(key: "alertSAGENightTimeAudible", default: true) - static let alertSAGESnooze = UserDefaultsValue(key: "alertSAGESnooze", default: 2) //Hours + static let alertSAGESnooze = UserDefaultsValue(key: "alertSAGESnooze", default: 2) // Hours static let alertSAGESnoozedTime = UserDefaultsValue(key: "alertSAGESnoozedTime", default: nil) static let alertSAGEIsSnoozed = UserDefaultsValue(key: "alertSAGEIsSnoozed", default: false) static let alertSAGESound = UserDefaultsValue(key: "alertSAGESound", default: "Wake_Up_Will_You") @@ -370,7 +366,7 @@ class UserDefaultsRepository { static let alertSAGEAutosnoozeNight = UserDefaultsValue(key: "alertSAGEAutosnoozeNight", default: true) static let alertCAGEActive = UserDefaultsValue(key: "alertCAGEActive", default: false) - static let alertCAGE = UserDefaultsValue(key: "alertCAGE", default: 4) //Hours + static let alertCAGE = UserDefaultsValue(key: "alertCAGE", default: 4) // Hours static let alertCAGEQuiet = UserDefaultsValue(key: "alertCAGEQuiet", default: false) static let alertCAGERepeat = UserDefaultsValue(key: "alertCAGERepeat", default: "Never") static let alertCAGEDayTime = UserDefaultsValue(key: "alertCAGEDayTime", default: false) @@ -378,7 +374,7 @@ class UserDefaultsRepository { static let alertCAGEAudible = UserDefaultsValue(key: "alertCAGEAudible", default: "Always") static let alertCAGEDayTimeAudible = UserDefaultsValue(key: "alertCAGEDayTimeAudible", default: true) static let alertCAGENightTimeAudible = UserDefaultsValue(key: "alertCAGENightTimeAudible", default: true) - static let alertCAGESnooze = UserDefaultsValue(key: "alertCAGESnooze", default: 2) //Hours + static let alertCAGESnooze = UserDefaultsValue(key: "alertCAGESnooze", default: 2) // Hours static let alertCAGESnoozedTime = UserDefaultsValue(key: "alertCAGESnoozedTime", default: nil) static let alertCAGEIsSnoozed = UserDefaultsValue(key: "alertCAGEIsSnoozed", default: false) static let alertCAGESound = UserDefaultsValue(key: "alertCAGESound", default: "Wake_Up_Will_You") @@ -454,9 +450,9 @@ class UserDefaultsRepository { static let alertTempTargetEndAutosnooze = UserDefaultsValue(key: "alertTempTargetEndAutosnooze", default: "Never") static let alertTempTargetEndAutosnoozeDay = UserDefaultsValue(key: "alertTempTargetEndAutosnoozeDay", default: false) static let alertTempTargetEndAutosnoozeNight = UserDefaultsValue(key: "alertTempTargetEndAutosnoozeNight", default: false) - + static let alertPump = UserDefaultsValue(key: "alertPump", default: false) - static let alertPumpAt = UserDefaultsValue(key: "alertPumpAt", default: 10) //Units + static let alertPumpAt = UserDefaultsValue(key: "alertPumpAt", default: 10) // Units static let alertPumpQuiet = UserDefaultsValue(key: "alertPumpQuiet", default: false) static let alertPumpRepeat = UserDefaultsValue(key: "alertPumpRepeat", default: "Never") static let alertPumpDayTime = UserDefaultsValue(key: "alertPumpDayTime", default: false) @@ -465,7 +461,7 @@ class UserDefaultsRepository { static let alertPumpDayTimeAudible = UserDefaultsValue(key: "alertPumpDayTimeAudible", default: true) static let alertPumpNightTimeAudible = UserDefaultsValue(key: "alertPumpNightTimeAudible", default: true) static let alertPumpSound = UserDefaultsValue(key: "alertPumpSound", default: "Marimba_Descend") - static let alertPumpSnoozeHours = UserDefaultsValue(key: "alertPumpSnoozeHours", default: 5) //Hours + static let alertPumpSnoozeHours = UserDefaultsValue(key: "alertPumpSnoozeHours", default: 5) // Hours static let alertPumpIsSnoozed = UserDefaultsValue(key: "alertPumpIsSnoozed", default: false) static let alertPumpSnoozedTime = UserDefaultsValue(key: "alertPumpSnoozedTime", default: nil) static let alertPumpAutosnooze = UserDefaultsValue(key: "alertPumpAutosnooze", default: "Never") @@ -473,10 +469,10 @@ class UserDefaultsRepository { static let alertPumpAutosnoozeNight = UserDefaultsValue(key: "alertPumpAutosnoozeNight", default: false) static let alertIOB = UserDefaultsValue(key: "alertIOB", default: false) - static let alertIOBAt = UserDefaultsValue(key: "alertIOBAt", default: 1.5) //Units - static let alertIOBNumber = UserDefaultsValue(key: "alertIOBNumber", default: 3) //Number - static let alertIOBBolusesWithin = UserDefaultsValue(key: "alertIOBBolusesWithin", default: 60) //Minutes - static let alertIOBMaxBoluses = UserDefaultsValue(key: "alertIOBMaxBoluses", default: 10) //Units + static let alertIOBAt = UserDefaultsValue(key: "alertIOBAt", default: 1.5) // Units + static let alertIOBNumber = UserDefaultsValue(key: "alertIOBNumber", default: 3) // Number + static let alertIOBBolusesWithin = UserDefaultsValue(key: "alertIOBBolusesWithin", default: 60) // Minutes + static let alertIOBMaxBoluses = UserDefaultsValue(key: "alertIOBMaxBoluses", default: 10) // Units static let alertIOBQuiet = UserDefaultsValue(key: "alertIOBQuiet", default: false) static let alertIOBRepeat = UserDefaultsValue(key: "alertIOBRepeat", default: "Always") static let alertIOBDayTime = UserDefaultsValue(key: "alertIOBDayTime", default: true) @@ -485,7 +481,7 @@ class UserDefaultsRepository { static let alertIOBDayTimeAudible = UserDefaultsValue(key: "alertIOBDayTimeAudible", default: true) static let alertIOBNightTimeAudible = UserDefaultsValue(key: "alertIOBNightTimeAudible", default: true) static let alertIOBSound = UserDefaultsValue(key: "alertIOBSound", default: "Alert_Tone_Ringtone_1") - static let alertIOBSnoozeHours = UserDefaultsValue(key: "alertIOBSnoozeHours", default: 1) //Hours + static let alertIOBSnoozeHours = UserDefaultsValue(key: "alertIOBSnoozeHours", default: 1) // Hours static let alertIOBIsSnoozed = UserDefaultsValue(key: "alertIOBIsSnoozed", default: false) static let alertIOBSnoozedTime = UserDefaultsValue(key: "alertIOBSnoozedTime", default: nil) static let alertIOBAutosnooze = UserDefaultsValue(key: "alertIOBAutosnooze", default: "Never") @@ -493,7 +489,7 @@ class UserDefaultsRepository { static let alertIOBAutosnoozeNight = UserDefaultsValue(key: "alertIOBAutosnoozeNight", default: false) static let alertCOB = UserDefaultsValue(key: "alertCOB", default: false) - static let alertCOBAt = UserDefaultsValue(key: "alertCOBAt", default: 50) //Units + static let alertCOBAt = UserDefaultsValue(key: "alertCOBAt", default: 50) // Units static let alertCOBQuiet = UserDefaultsValue(key: "alertCOBQuiet", default: false) static let alertCOBRepeat = UserDefaultsValue(key: "alertCOBRepeat", default: "Always") static let alertCOBDayTime = UserDefaultsValue(key: "alertCOBDayTime", default: true) @@ -502,7 +498,7 @@ class UserDefaultsRepository { static let alertCOBDayTimeAudible = UserDefaultsValue(key: "alertCOBDayTimeAudible", default: true) static let alertCOBNightTimeAudible = UserDefaultsValue(key: "alertCOBNightTimeAudible", default: true) static let alertCOBSound = UserDefaultsValue(key: "alertCOBSound", default: "Alert_Tone_Ringtone_2") - static let alertCOBSnoozeHours = UserDefaultsValue(key: "alertCOBSnoozeHours", default: 1) //Hours + static let alertCOBSnoozeHours = UserDefaultsValue(key: "alertCOBSnoozeHours", default: 1) // Hours static let alertCOBIsSnoozed = UserDefaultsValue(key: "alertCOBIsSnoozed", default: false) static let alertCOBSnoozedTime = UserDefaultsValue(key: "alertCOBSnoozedTime", default: nil) static let alertCOBAutosnooze = UserDefaultsValue(key: "alertCOBAutosnooze", default: "Never") @@ -528,7 +524,7 @@ class UserDefaultsRepository { static let alertBatteryDropSnoozeHours = UserDefaultsValue(key: "alertBatteryDropSnoozeHours", default: 1) static let alertRecBolusActive = UserDefaultsValue(key: "alertRecBolusActive", default: false) - static let alertRecBolusLevel = UserDefaultsValue(key: "alertRecBolusLevel", default: 1) //Unit[s] + static let alertRecBolusLevel = UserDefaultsValue(key: "alertRecBolusLevel", default: 1) // Unit[s] static let alertRecBolusSound = UserDefaultsValue(key: "alertRecBolusSound", default: "Dhol_Shuffleloop") static let alertRecBolusRepeat = UserDefaultsValue(key: "alertRecBolusRepeat", default: false) static let alertRecBolusIsSnoozed = UserDefaultsValue(key: "alertRecBolusIsSnoozed", default: false) @@ -536,20 +532,20 @@ class UserDefaultsRepository { static let alertRecBolusSnoozedTime = UserDefaultsValue(key: "alertRecBolusSnoozedTime", default: nil) static var deviceRecBolus: UserDefaultsValue = UserDefaultsValue(key: "deviceRecBolus", default: 0.0) - //What version is the cache valid for + // What version is the cache valid for static let cachedForVersion = UserDefaultsValue(key: "cachedForVersion", default: nil) - //Caching of latest version + // Caching of latest version static let latestVersion = UserDefaultsValue(key: "latestVersion", default: nil) static let latestVersionChecked = UserDefaultsValue(key: "latestVersionChecked", default: nil) - //Caching of blacklisted version + // Caching of blacklisted version static let currentVersionBlackListed = UserDefaultsValue(key: "currentVersionBlackListed", default: false) // Tracking notifications to manage frequency static let lastBlacklistNotificationShown = UserDefaultsValue(key: "lastBlacklistNotificationShown", default: nil) static let lastVersionUpdateNotificationShown = UserDefaultsValue(key: "lastVersionUpdateNotificationShown", default: nil) - + // Tracking the last time the expiration notification was shown static let lastExpirationNotificationShown = UserDefaultsValue(key: "lastExpirationNotificationShown", default: nil) } diff --git a/LoopFollow/Task/BGTask.swift b/LoopFollow/Task/BGTask.swift index 526a8c943..66a0939a7 100644 --- a/LoopFollow/Task/BGTask.swift +++ b/LoopFollow/Task/BGTask.swift @@ -33,12 +33,12 @@ extension MainViewController { } // If Dexcom credentials exist, fetch from DexShare - if UserDefaultsRepository.shareUserName.value != "" && - UserDefaultsRepository.sharePassword.value != "" + if UserDefaultsRepository.shareUserName.value != "", + UserDefaultsRepository.sharePassword.value != "" { - self.webLoadDexShare() + webLoadDexShare() } else { - self.webLoadNSBGData() + webLoadNSBGData() } } } diff --git a/LoopFollow/Task/CalendarTask.swift b/LoopFollow/Task/CalendarTask.swift index 9a21ecd8c..67386d337 100644 --- a/LoopFollow/Task/CalendarTask.swift +++ b/LoopFollow/Task/CalendarTask.swift @@ -21,7 +21,7 @@ extension MainViewController { if UserDefaultsRepository.writeCalendarEvent.value, !UserDefaultsRepository.calendarIdentifier.value.isEmpty { - self.writeCalendar() + writeCalendar() } TaskScheduler.shared.rescheduleTask(id: .calendarWrite, to: Date().addingTimeInterval(30)) diff --git a/LoopFollow/Task/MinAgoTask.swift b/LoopFollow/Task/MinAgoTask.swift index b7bb88a5e..3c1ac5a4d 100644 --- a/LoopFollow/Task/MinAgoTask.swift +++ b/LoopFollow/Task/MinAgoTask.swift @@ -25,11 +25,11 @@ extension MainViewController { self.MinAgoText.text = "" Observable.shared.minAgoText.value = "" Observable.shared.bgText.value = "" - /*TODO - if let snoozer = self.tabBarController?.viewControllers?[2] as? SnoozeViewController { - snoozer.BGLabel.attributedText = NSAttributedString(string: "") - } - */ + /* TODO: + if let snoozer = self.tabBarController?.viewControllers?[2] as? SnoozeViewController { + snoozer.BGLabel.attributedText = NSAttributedString(string: "") + } + */ } TaskScheduler.shared.rescheduleTask(id: .minAgoUpdate, to: Date().addingTimeInterval(1)) return @@ -61,19 +61,19 @@ extension MainViewController { self.MinAgoText.text = minAgoDisplayText Observable.shared.minAgoText.value = minAgoDisplayText - /*TODO - if let snoozer = self.tabBarController?.viewControllers?[2] as? SnoozeViewController { - let bgLabelText = snoozer.BGLabel.text ?? "" - let attributeString = NSMutableAttributedString(string: bgLabelText) - attributeString.addAttribute(.strikethroughStyle, - value: NSUnderlineStyle.single.rawValue, - range: NSRange(location: 0, length: attributeString.length)) - attributeString.addAttribute(.strikethroughColor, - value: secondsAgo >= 720 ? UIColor.systemRed : UIColor.clear, - range: NSRange(location: 0, length: attributeString.length)) - snoozer.BGLabel.attributedText = attributeString - } - */ + /* TODO: + if let snoozer = self.tabBarController?.viewControllers?[2] as? SnoozeViewController { + let bgLabelText = snoozer.BGLabel.text ?? "" + let attributeString = NSMutableAttributedString(string: bgLabelText) + attributeString.addAttribute(.strikethroughStyle, + value: NSUnderlineStyle.single.rawValue, + range: NSRange(location: 0, length: attributeString.length)) + attributeString.addAttribute(.strikethroughColor, + value: secondsAgo >= 720 ? UIColor.systemRed : UIColor.clear, + range: NSRange(location: 0, length: attributeString.length)) + snoozer.BGLabel.attributedText = attributeString + } + */ } } @@ -82,7 +82,7 @@ extension MainViewController { if shouldDisplaySeconds { // Update every second when showing seconds nextUpdateInterval = 1.0 - } else if secondsAgo >= 240 && secondsAgo < 720 { + } else if secondsAgo >= 240, secondsAgo < 720 { // Schedule exactly at the transition point to start showing seconds nextUpdateInterval = 270.0 - secondsAgo } else { diff --git a/LoopFollow/Task/ProfileTask.swift b/LoopFollow/Task/ProfileTask.swift index e13287e4e..48b2bad5d 100644 --- a/LoopFollow/Task/ProfileTask.swift +++ b/LoopFollow/Task/ProfileTask.swift @@ -24,7 +24,7 @@ extension MainViewController { return } - self.webLoadNSProfile() + webLoadNSProfile() TaskScheduler.shared.rescheduleTask(id: .profile, to: Date().addingTimeInterval(10 * 60)) } diff --git a/LoopFollow/Task/Task.swift b/LoopFollow/Task/Task.swift index 5c4e51c6d..022b6867c 100644 --- a/LoopFollow/Task/Task.swift +++ b/LoopFollow/Task/Task.swift @@ -9,7 +9,6 @@ import Foundation extension MainViewController { - func scheduleAllTasks() { scheduleBGTask() scheduleProfileTask() diff --git a/LoopFollow/Task/TaskScheduler.swift b/LoopFollow/Task/TaskScheduler.swift index 39bc804c5..7badd6382 100644 --- a/LoopFollow/Task/TaskScheduler.swift +++ b/LoopFollow/Task/TaskScheduler.swift @@ -47,8 +47,8 @@ class TaskScheduler { } func rescheduleTask(id: TaskID, to newRunDate: Date) { - let timeString = self.formatTime(newRunDate) - //LogManager.shared.log(category: .taskScheduler, message: "Reschedule Task \(id): next run = \(timeString)", isDebug: true) + let timeString = formatTime(newRunDate) + // LogManager.shared.log(category: .taskScheduler, message: "Reschedule Task \(id): next run = \(timeString)", isDebug: true) queue.async { guard var existingTask = self.tasks[id] else { return } @@ -79,7 +79,7 @@ class TaskScheduler { let interval = earliestTask.nextRun.timeIntervalSinceNow let safeInterval = max(interval, 0) - let timer = DispatchSource.makeTimerSource(queue: self.queue) + let timer = DispatchSource.makeTimerSource(queue: queue) timer.schedule(deadline: .now() + safeInterval) timer.setEventHandler { [weak self] in guard let self = self else { return } @@ -110,9 +110,9 @@ class TaskScheduler { return checkTask.nextRun <= now || checkTask.nextRun == .distantFuture } if shouldSkip { - guard var existingTask = self.tasks[taskID] else { continue } + guard var existingTask = tasks[taskID] else { continue } existingTask.nextRun = Date().addingTimeInterval(5) - self.tasks[taskID] = existingTask + tasks[taskID] = existingTask continue } } @@ -121,7 +121,7 @@ class TaskScheduler { updatedTask.nextRun = .distantFuture tasks[taskID] = updatedTask - //LogManager.shared.log(category: .taskScheduler, message: "Executing Task \(taskID)", isDebug: true) + // LogManager.shared.log(category: .taskScheduler, message: "Executing Task \(taskID)", isDebug: true) DispatchQueue.main.async { task.action() diff --git a/LoopFollow/ViewControllers/AppStateViewController.swift b/LoopFollow/ViewControllers/AppStateViewController.swift index 199bbdce5..6dca31a2a 100644 --- a/LoopFollow/ViewControllers/AppStateViewController.swift +++ b/LoopFollow/ViewControllers/AppStateViewController.swift @@ -9,5 +9,5 @@ import Foundation class AppStateViewController { - var appStateController: AppStateController? + var appStateController: AppStateController? } diff --git a/LoopFollow/ViewControllers/GeneralSettingsViewController.swift b/LoopFollow/ViewControllers/GeneralSettingsViewController.swift index e6ca65429..3a6e66e4a 100644 --- a/LoopFollow/ViewControllers/GeneralSettingsViewController.swift +++ b/LoopFollow/ViewControllers/GeneralSettingsViewController.swift @@ -1,307 +1,305 @@ // -// GeneralSetingsViewController.swift +// GeneralSettingsViewController.swift // LoopFollow // // Created by Jose Paredes on 7/16/20. // Copyright © 2020 Jon Fawcett. All rights reserved. // -import Foundation import Eureka import EventKit import EventKitUI +import Foundation class GeneralSettingsViewController: FormViewController { - - var appStateController: AppStateController? - - override func viewDidLoad() { - super.viewDidLoad() - - if UserDefaultsRepository.forceDarkMode.value { - overrideUserInterfaceStyle = .dark - } - buildGeneralSettings() - - // Register the GeneralSettingsViewController as an observer for the UIApplication.willEnterForegroundNotification, which will be triggered when the app enters the foreground. This helps ensure that the "Speak BG" switch in the General Settings is updated according to the current setting. - NotificationCenter.default.addObserver(self, selector: #selector(handleAppWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil) - } - - private func buildGeneralSettings() { - form - +++ Section("App Settings") - <<< SwitchRow("appBadge"){ row in - row.title = "Display App Badge" - row.tag = "appBadge" - row.value = UserDefaultsRepository.appBadge.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.appBadge.value = value - // Force main screen update - //guard let mainScreen = self?.tabBarController!.viewControllers?[0] as? MainViewController else { return } - //mainScreen.nightscoutLoader(forceLoad: true) - - // set the appstate to indicate settings change and flags - if let appState = self!.appStateController { - appState.generalSettingsChanged = true - appState.generalSettingsChanges |= GeneralSettingsChangeEnum.appBadgeChange.rawValue - } - + var appStateController: AppStateController? + + override func viewDidLoad() { + super.viewDidLoad() + + if UserDefaultsRepository.forceDarkMode.value { + overrideUserInterfaceStyle = .dark } - <<< SwitchRow("persistentNotification") { row in - row.title = "Persistent Notification" - row.value = UserDefaultsRepository.persistentNotification.value - }.onChange { [weak self] row in - guard let value = row.value else { return } + buildGeneralSettings() + + // Register the GeneralSettingsViewController as an observer for the UIApplication.willEnterForegroundNotification, which will be triggered when the app enters the foreground. This helps ensure that the "Speak BG" switch in the General Settings is updated according to the current setting. + NotificationCenter.default.addObserver(self, selector: #selector(handleAppWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil) + } + + private func buildGeneralSettings() { + form + +++ Section("App Settings") + <<< SwitchRow("appBadge") { row in + row.title = "Display App Badge" + row.tag = "appBadge" + row.value = UserDefaultsRepository.appBadge.value + }.onChange { [weak self] row in + guard let value = row.value else { return } + UserDefaultsRepository.appBadge.value = value + // Force main screen update + // guard let mainScreen = self?.tabBarController!.viewControllers?[0] as? MainViewController else { return } + // mainScreen.nightscoutLoader(forceLoad: true) + + // set the appstate to indicate settings change and flags + if let appState = self!.appStateController { + appState.generalSettingsChanged = true + appState.generalSettingsChanges |= GeneralSettingsChangeEnum.appBadgeChange.rawValue + } + } + <<< SwitchRow("persistentNotification") { row in + row.title = "Persistent Notification" + row.value = UserDefaultsRepository.persistentNotification.value + }.onChange { [weak self] row in + guard let value = row.value else { return } UserDefaultsRepository.persistentNotification.value = value - } - - +++ Section("Display Settings") - <<< SwitchRow("forceDarkMode") { row in - row.title = "Force Dark Mode (Restart App)" - row.value = UserDefaultsRepository.forceDarkMode.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.forceDarkMode.value = value - - } - <<< SwitchRow("showStats") { row in - row.title = "Display Stats" - row.value = UserDefaultsRepository.showStats.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.showStats.value = value - - // set the appstate to indicate settings change and flags - if let appState = self!.appStateController { - appState.generalSettingsChanged = true - appState.generalSettingsChanges |= GeneralSettingsChangeEnum.showStatsChange.rawValue - } - } - <<< SwitchRow("useIFCC") { row in - row.title = "Use IFCC A1C" - row.value = UserDefaultsRepository.useIFCC.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.useIFCC.value = value - - // set the appstate to indicate settings change and flags - if let appState = self!.appStateController { - appState.generalSettingsChanged = true - appState.generalSettingsChanges |= GeneralSettingsChangeEnum.useIFCCChange.rawValue - } - } - <<< SwitchRow("showSmallGraph") { row in - row.title = "Display Small Graph" - row.value = UserDefaultsRepository.showSmallGraph.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.showSmallGraph.value = value - - // set the appstate to indicate settings change and flags - if let appState = self!.appStateController { - appState.generalSettingsChanged = true - appState.generalSettingsChanges |= GeneralSettingsChangeEnum.showSmallGraphChange.rawValue } - } - <<< SwitchRow("colorBGText") { row in - row.title = "Color Main BG Text" - row.value = UserDefaultsRepository.colorBGText.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.colorBGText.value = value - // Force main screen update - //guard let mainScreen = self?.tabBarController!.viewControllers?[0] as? MainViewController else { return } - //mainScreen.setBGTextColor() - - // set the appstate to indicate settings change and flags - if let appState = self!.appStateController { - appState.generalSettingsChanged = true - appState.generalSettingsChanges |= GeneralSettingsChangeEnum.colorBGTextChange.rawValue - } - } - - <<< SwitchRow("screenlockSwitchState") { row in - row.title = "Keep Screen Active" - row.value = UserDefaultsRepository.screenlockSwitchState.value + + +++ Section("Display Settings") + <<< SwitchRow("forceDarkMode") { row in + row.title = "Force Dark Mode (Restart App)" + row.value = UserDefaultsRepository.forceDarkMode.value + }.onChange { [weak self] row in + guard let value = row.value else { return } + UserDefaultsRepository.forceDarkMode.value = value + } + <<< SwitchRow("showStats") { row in + row.title = "Display Stats" + row.value = UserDefaultsRepository.showStats.value + }.onChange { [weak self] row in + guard let value = row.value else { return } + UserDefaultsRepository.showStats.value = value + + // set the appstate to indicate settings change and flags + if let appState = self!.appStateController { + appState.generalSettingsChanged = true + appState.generalSettingsChanges |= GeneralSettingsChangeEnum.showStatsChange.rawValue + } + } + <<< SwitchRow("useIFCC") { row in + row.title = "Use IFCC A1C" + row.value = UserDefaultsRepository.useIFCC.value + }.onChange { [weak self] row in + guard let value = row.value else { return } + UserDefaultsRepository.useIFCC.value = value + + // set the appstate to indicate settings change and flags + if let appState = self!.appStateController { + appState.generalSettingsChanged = true + appState.generalSettingsChanges |= GeneralSettingsChangeEnum.useIFCCChange.rawValue + } + } + <<< SwitchRow("showSmallGraph") { row in + row.title = "Display Small Graph" + row.value = UserDefaultsRepository.showSmallGraph.value + }.onChange { [weak self] row in + guard let value = row.value else { return } + UserDefaultsRepository.showSmallGraph.value = value + + // set the appstate to indicate settings change and flags + if let appState = self!.appStateController { + appState.generalSettingsChanged = true + appState.generalSettingsChanges |= GeneralSettingsChangeEnum.showSmallGraphChange.rawValue + } + } + <<< SwitchRow("colorBGText") { row in + row.title = "Color Main BG Text" + row.value = UserDefaultsRepository.colorBGText.value + }.onChange { [weak self] row in + guard let value = row.value else { return } + UserDefaultsRepository.colorBGText.value = value + // Force main screen update + // guard let mainScreen = self?.tabBarController!.viewControllers?[0] as? MainViewController else { return } + // mainScreen.setBGTextColor() + + // set the appstate to indicate settings change and flags + if let appState = self!.appStateController { + appState.generalSettingsChanged = true + appState.generalSettingsChanges |= GeneralSettingsChangeEnum.colorBGTextChange.rawValue + } + } + + <<< SwitchRow("screenlockSwitchState") { row in + row.title = "Keep Screen Active" + row.value = UserDefaultsRepository.screenlockSwitchState.value }.onChange { [weak self] row in guard let value = row.value else { return } UserDefaultsRepository.screenlockSwitchState.value = value } - - <<< SwitchRow("showDisplayName") { row in - row.title = "Show Display Name" - row.value = UserDefaultsRepository.showDisplayName.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.showDisplayName.value = value - - if let appState = self!.appStateController { - appState.generalSettingsChanged = true - appState.generalSettingsChanges |= GeneralSettingsChangeEnum.showDisplayNameChange.rawValue - } - } - - +++ Section("Speak BG Settings") - <<< SwitchRow("speakBG") { row in - row.title = "Speak BG" - row.value = UserDefaultsRepository.speakBG.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.speakBG.value = value - self?.updateSpeakBGSettingsVisibility() - } - - <<< PushRow("speakLanguage") { row in - row.title = "Speaking Language" - row.options = ["en", "it", "sk", "sv"] - row.value = UserDefaultsRepository.speakLanguage.value - row.displayValueFor = { value in - switch value { - case "en": return "English" - case "it": return "Italian" - case "sk": return "Slovak" - case "sv": return "Swedish" - default: return "Unknown" - } - } - row.hidden = Condition.function(["speakBG"], { form in - let speakBGRow: SwitchRow! = form.rowBy(tag: "speakBG") - return !(speakBGRow.value ?? false) - }) - row.presentationMode = PresentationMode.presentModally( - controllerProvider: ControllerProvider.callback { - return SelectorViewController>> { _ in } - }, - onDismiss: { vc in - vc.dismiss(animated: true) - }) - }.onChange { row in - guard let value = row.value else { return } - UserDefaultsRepository.speakLanguage.value = value - } - - <<< SwitchRow("speakBGAlways") { row in - row.title = "Always" - row.value = UserDefaultsRepository.speakBGAlways.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.speakBGAlways.value = value - self?.updateSpeakBGSettingsVisibility() - } - - <<< SwitchRow("speakLowBG") { row in - row.title = "Low" - row.value = UserDefaultsRepository.speakLowBG.value - }.onChange { [weak self] row in - self?.handleLowProactiveLowToggle(row: row, opposingRowTag: "speakProactiveLowBG") - } - - <<< SwitchRow("speakProactiveLowBG") { row in - row.title = "Proactive Low" - row.value = UserDefaultsRepository.speakProactiveLowBG.value - }.onChange { [weak self] row in - self?.handleLowProactiveLowToggle(row: row, opposingRowTag: "speakLowBG") - } - - <<< StepperRow("speakLowBGLimit") { row in - row.title = "Low BG Limit" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 40 - row.cell.stepper.maximumValue = 108 - row.value = Double(UserDefaultsRepository.speakLowBGLimit.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return Localizer.toDisplayUnits(String(value)) - } - // Visibility depends on either 'speakLowBG' or 'speakProactiveLowBG' being true - row.hidden = Condition.function(["speakLowBG", "speakProactiveLowBG", "speakBG", "speakBGAlways"], { form in - let speakBGRow: SwitchRow! = form.rowBy(tag: "speakBG") - let speakBGAlwaysRow: SwitchRow! = form.rowBy(tag: "speakBGAlways") - let speakLowBGRow: SwitchRow! = form.rowBy(tag: "speakLowBG") - let speakProactiveLowBGRow: SwitchRow! = form.rowBy(tag: "speakProactiveLowBG") - return !(speakLowBGRow.value ?? false) && !(speakProactiveLowBGRow.value ?? false) || !(speakBGRow.value ?? true) || (speakBGAlwaysRow.value ?? false) - }) - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.speakLowBGLimit.value = Float(value) - } - - <<< StepperRow("speakFastDropDelta") { row in - row.title = "Fast Drop Delta" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 3 - row.cell.stepper.maximumValue = 20 - row.value = Double(UserDefaultsRepository.speakFastDropDelta.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return Localizer.toDisplayUnits(String(value)) - } - // Visibility depends on 'speakProactiveLowBG' being true - row.hidden = Condition.function(["speakProactiveLowBG", "speakBG", "speakBGAlways"], { form in - let speakBGRow: SwitchRow! = form.rowBy(tag: "speakBG") - let speakBGAlwaysRow: SwitchRow! = form.rowBy(tag: "speakBGAlways") - let speakProactiveLowBGRow: SwitchRow! = form.rowBy(tag: "speakProactiveLowBG") - return !(speakProactiveLowBGRow.value ?? false) || !(speakBGRow.value ?? true) || (speakBGAlwaysRow.value ?? false) - }) - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.speakFastDropDelta.value = Float(value) - } - - <<< SwitchRow("speakHighBG") { row in - row.title = "High" - row.value = UserDefaultsRepository.speakHighBG.value - }.onChange { row in - guard let value = row.value else { return } - UserDefaultsRepository.speakHighBG.value = value - } - - <<< StepperRow("speakHighBGLimit") { row in - row.title = "High BG Limit" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 140 - row.cell.stepper.maximumValue = 300 - row.value = Double(UserDefaultsRepository.speakHighBGLimit.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return Localizer.toDisplayUnits(String(value)) - } - // Visibility depends on 'speakHighBG' or 'speakProactiveLowBG' being true - row.hidden = Condition.function(["speakHighBG", "speakProactiveLowBG", "speakBG", "speakBGAlways"], { form in - let speakBGRow: SwitchRow! = form.rowBy(tag: "speakBG") - let speakBGAlwaysRow: SwitchRow! = form.rowBy(tag: "speakBGAlways") - let speakHighBGRow: SwitchRow! = form.rowBy(tag: "speakHighBG") - let speakProactiveLowBGRow: SwitchRow! = form.rowBy(tag: "speakProactiveLowBG") - return !(speakHighBGRow.value ?? false) && !(speakProactiveLowBGRow.value ?? false) || !(speakBGRow.value ?? true) || (speakBGAlwaysRow.value ?? false) - }) - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.speakHighBGLimit.value = Float(value) - } - - +++ ButtonRow() { - $0.title = "DONE" - }.onCellSelection { (row, arg) in - self.dismiss(animated:true, completion: nil) - } - - // Call to update initial visibility based on current settings - updateSpeakBGSettingsVisibility() + + <<< SwitchRow("showDisplayName") { row in + row.title = "Show Display Name" + row.value = UserDefaultsRepository.showDisplayName.value + }.onChange { [weak self] row in + guard let value = row.value else { return } + UserDefaultsRepository.showDisplayName.value = value + + if let appState = self!.appStateController { + appState.generalSettingsChanged = true + appState.generalSettingsChanges |= GeneralSettingsChangeEnum.showDisplayNameChange.rawValue + } + } + + +++ Section("Speak BG Settings") + <<< SwitchRow("speakBG") { row in + row.title = "Speak BG" + row.value = UserDefaultsRepository.speakBG.value + }.onChange { [weak self] row in + guard let value = row.value else { return } + UserDefaultsRepository.speakBG.value = value + self?.updateSpeakBGSettingsVisibility() + } + + <<< PushRow("speakLanguage") { row in + row.title = "Speaking Language" + row.options = ["en", "it", "sk", "sv"] + row.value = UserDefaultsRepository.speakLanguage.value + row.displayValueFor = { value in + switch value { + case "en": return "English" + case "it": return "Italian" + case "sk": return "Slovak" + case "sv": return "Swedish" + default: return "Unknown" + } + } + row.hidden = Condition.function(["speakBG"]) { form in + let speakBGRow: SwitchRow! = form.rowBy(tag: "speakBG") + return !(speakBGRow.value ?? false) + } + row.presentationMode = PresentationMode.presentModally( + controllerProvider: ControllerProvider.callback { + SelectorViewController>> { _ in } + }, + onDismiss: { vc in + vc.dismiss(animated: true) + } + ) + }.onChange { row in + guard let value = row.value else { return } + UserDefaultsRepository.speakLanguage.value = value + } + + <<< SwitchRow("speakBGAlways") { row in + row.title = "Always" + row.value = UserDefaultsRepository.speakBGAlways.value + }.onChange { [weak self] row in + guard let value = row.value else { return } + UserDefaultsRepository.speakBGAlways.value = value + self?.updateSpeakBGSettingsVisibility() + } + + <<< SwitchRow("speakLowBG") { row in + row.title = "Low" + row.value = UserDefaultsRepository.speakLowBG.value + }.onChange { [weak self] row in + self?.handleLowProactiveLowToggle(row: row, opposingRowTag: "speakProactiveLowBG") + } + + <<< SwitchRow("speakProactiveLowBG") { row in + row.title = "Proactive Low" + row.value = UserDefaultsRepository.speakProactiveLowBG.value + }.onChange { [weak self] row in + self?.handleLowProactiveLowToggle(row: row, opposingRowTag: "speakLowBG") + } + + <<< StepperRow("speakLowBGLimit") { row in + row.title = "Low BG Limit" + row.cell.stepper.stepValue = 1 + row.cell.stepper.minimumValue = 40 + row.cell.stepper.maximumValue = 108 + row.value = Double(UserDefaultsRepository.speakLowBGLimit.value) + row.displayValueFor = { value in + guard let value = value else { return nil } + return Localizer.toDisplayUnits(String(value)) + } + // Visibility depends on either 'speakLowBG' or 'speakProactiveLowBG' being true + row.hidden = Condition.function(["speakLowBG", "speakProactiveLowBG", "speakBG", "speakBGAlways"]) { form in + let speakBGRow: SwitchRow! = form.rowBy(tag: "speakBG") + let speakBGAlwaysRow: SwitchRow! = form.rowBy(tag: "speakBGAlways") + let speakLowBGRow: SwitchRow! = form.rowBy(tag: "speakLowBG") + let speakProactiveLowBGRow: SwitchRow! = form.rowBy(tag: "speakProactiveLowBG") + return !(speakLowBGRow.value ?? false) && !(speakProactiveLowBGRow.value ?? false) || !(speakBGRow.value ?? true) || (speakBGAlwaysRow.value ?? false) + } + }.onChange { [weak self] row in + guard let value = row.value else { return } + UserDefaultsRepository.speakLowBGLimit.value = Float(value) + } + + <<< StepperRow("speakFastDropDelta") { row in + row.title = "Fast Drop Delta" + row.cell.stepper.stepValue = 1 + row.cell.stepper.minimumValue = 3 + row.cell.stepper.maximumValue = 20 + row.value = Double(UserDefaultsRepository.speakFastDropDelta.value) + row.displayValueFor = { value in + guard let value = value else { return nil } + return Localizer.toDisplayUnits(String(value)) + } + // Visibility depends on 'speakProactiveLowBG' being true + row.hidden = Condition.function(["speakProactiveLowBG", "speakBG", "speakBGAlways"]) { form in + let speakBGRow: SwitchRow! = form.rowBy(tag: "speakBG") + let speakBGAlwaysRow: SwitchRow! = form.rowBy(tag: "speakBGAlways") + let speakProactiveLowBGRow: SwitchRow! = form.rowBy(tag: "speakProactiveLowBG") + return !(speakProactiveLowBGRow.value ?? false) || !(speakBGRow.value ?? true) || (speakBGAlwaysRow.value ?? false) + } + }.onChange { [weak self] row in + guard let value = row.value else { return } + UserDefaultsRepository.speakFastDropDelta.value = Float(value) + } + + <<< SwitchRow("speakHighBG") { row in + row.title = "High" + row.value = UserDefaultsRepository.speakHighBG.value + }.onChange { row in + guard let value = row.value else { return } + UserDefaultsRepository.speakHighBG.value = value + } + + <<< StepperRow("speakHighBGLimit") { row in + row.title = "High BG Limit" + row.cell.stepper.stepValue = 1 + row.cell.stepper.minimumValue = 140 + row.cell.stepper.maximumValue = 300 + row.value = Double(UserDefaultsRepository.speakHighBGLimit.value) + row.displayValueFor = { value in + guard let value = value else { return nil } + return Localizer.toDisplayUnits(String(value)) + } + // Visibility depends on 'speakHighBG' or 'speakProactiveLowBG' being true + row.hidden = Condition.function(["speakHighBG", "speakProactiveLowBG", "speakBG", "speakBGAlways"]) { form in + let speakBGRow: SwitchRow! = form.rowBy(tag: "speakBG") + let speakBGAlwaysRow: SwitchRow! = form.rowBy(tag: "speakBGAlways") + let speakHighBGRow: SwitchRow! = form.rowBy(tag: "speakHighBG") + let speakProactiveLowBGRow: SwitchRow! = form.rowBy(tag: "speakProactiveLowBG") + return !(speakHighBGRow.value ?? false) && !(speakProactiveLowBGRow.value ?? false) || !(speakBGRow.value ?? true) || (speakBGAlwaysRow.value ?? false) + } + }.onChange { [weak self] row in + guard let value = row.value else { return } + UserDefaultsRepository.speakHighBGLimit.value = Float(value) + } + + +++ ButtonRow { + $0.title = "DONE" + }.onCellSelection { _, _ in + self.dismiss(animated: true, completion: nil) + } + + // Call to update initial visibility based on current settings + updateSpeakBGSettingsVisibility() } - + func updateSpeakBGSettingsVisibility() { let alwaysOn = UserDefaultsRepository.speakBGAlways.value let speakBGOn = UserDefaultsRepository.speakBG.value - + // Determine visibility for "Always", "Low", "Proactive Low", and "High" based on "Speak BG" and "Always" let shouldHideAlways = !speakBGOn let shouldHideSettings = alwaysOn || !speakBGOn - + form.rowBy(tag: "speakBGAlways")?.hidden = Condition(booleanLiteral: shouldHideAlways) form.rowBy(tag: "speakBGAlways")?.evaluateHidden() - - ["speakLowBG", "speakProactiveLowBG", "speakHighBG"].forEach { tag in + + for tag in ["speakLowBG", "speakProactiveLowBG", "speakHighBG"] { if let row = form.rowBy(tag: tag) { row.hidden = Condition(booleanLiteral: shouldHideSettings) row.evaluateHidden() @@ -309,23 +307,23 @@ class GeneralSettingsViewController: FormViewController { } } } - + func handleLowProactiveLowToggle(row: BaseRow, opposingRowTag: String) { guard let switchRow = row as? SwitchRow, let value = switchRow.value else { return } - + // Update the UserDefaults value for the current row. if row.tag == "speakLowBG" { UserDefaultsRepository.speakLowBG.value = value } else if row.tag == "speakProactiveLowBG" { UserDefaultsRepository.speakProactiveLowBG.value = value } - + // If the current switch is being turned ON, turn the opposing switch OFF. if value { if let opposingRow = form.rowBy(tag: opposingRowTag) as? SwitchRow { opposingRow.value = false opposingRow.updateCell() - + // Update the UserDefaults value for the opposing row. if opposingRowTag == "speakLowBG" { UserDefaultsRepository.speakLowBG.value = false @@ -335,10 +333,10 @@ class GeneralSettingsViewController: FormViewController { } } } - + // Update the "Speak BG" SwitchRow value in the General Settings when the app enters the foreground. This ensures that the switch reflects the current setting in UserDefaultsRepository, even if it was changed using the Home Screen Quick Action while the app was in the background. @objc func handleAppWillEnterForeground() { - if let row = self.form.rowBy(tag: "speakBG") as? SwitchRow { + if let row = form.rowBy(tag: "speakBG") as? SwitchRow { row.value = UserDefaultsRepository.speakBG.value row.updateCell() } diff --git a/LoopFollow/ViewControllers/GraphSettingsViewController.swift b/LoopFollow/ViewControllers/GraphSettingsViewController.swift index 25942ccc0..086165bf7 100644 --- a/LoopFollow/ViewControllers/GraphSettingsViewController.swift +++ b/LoopFollow/ViewControllers/GraphSettingsViewController.swift @@ -6,26 +6,25 @@ // Copyright © 2020 Jon Fawcett. All rights reserved. // -import Foundation import Eureka import EventKit import EventKitUI +import Foundation class GraphSettingsViewController: FormViewController { + var appStateController: AppStateController? + + override func viewDidLoad() { + super.viewDidLoad() + if UserDefaultsRepository.forceDarkMode.value { + overrideUserInterfaceStyle = .dark + } + + buildGraphSettings() - var appStateController: AppStateController? - - override func viewDidLoad() { - super.viewDidLoad() - if UserDefaultsRepository.forceDarkMode.value { - overrideUserInterfaceStyle = .dark - } - - buildGraphSettings() - showHideNSDetails() - } - + } + func showHideNSDetails() { var isHidden = false var isEnabled = true @@ -33,104 +32,100 @@ class GraphSettingsViewController: FormViewController { isHidden = true isEnabled = false } - + if let row1 = form.rowBy(tag: "predictionToLoad") as? StepperRow { - row1.hidden = .function(["hide"], {form in - return isHidden - }) + row1.hidden = .function(["hide"]) { _ in + isHidden + } row1.evaluateHidden() } if let row2 = form.rowBy(tag: "smallGraphTreatments") as? SwitchRow { - row2.hidden = .function(["hide"], {form in - return isHidden - }) + row2.hidden = .function(["hide"]) { _ in + isHidden + } row2.evaluateHidden() } if let row3 = form.rowBy(tag: "minBasalScale") as? StepperRow { - row3.hidden = .function(["hide"], {form in - return isHidden - }) + row3.hidden = .function(["hide"]) { _ in + isHidden + } row3.evaluateHidden() } - + if let row4 = form.rowBy(tag: "showValues") as? SwitchRow { - row4.hidden = .function(["hide"], {form in - return isHidden - }) + row4.hidden = .function(["hide"]) { _ in + isHidden + } row4.evaluateHidden() } if let row5 = form.rowBy(tag: "showAbsorption") as? SwitchRow { - row5.hidden = .function(["hide"], {form in - return isHidden - }) + row5.hidden = .function(["hide"]) { _ in + isHidden + } row5.evaluateHidden() } - } - + private func buildGraphSettings() { form +++ Section("Graph Settings") - - <<< SwitchRow("switchRowDots"){ row in - row.title = "Display Dots" - row.value = UserDefaultsRepository.showDots.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.showDots.value = value + + <<< SwitchRow("switchRowDots") { row in + row.title = "Display Dots" + row.value = UserDefaultsRepository.showDots.value + }.onChange { [weak self] row in + guard let value = row.value else { return } + UserDefaultsRepository.showDots.value = value // Force main screen update // guard let mainScreen = self?.tabBarController!.viewControllers?[0] as? MainViewController else { return } // mainScreen.updateBGGraphSettings() - + // tell main screen that grap needs updating if let appState = self!.appStateController { - appState.chartSettingsChanged = true - appState.chartSettingsChanges |= ChartSettingsChangeEnum.showDotsChanged.rawValue + appState.chartSettingsChanged = true + appState.chartSettingsChanges |= ChartSettingsChangeEnum.showDotsChanged.rawValue } } - <<< SwitchRow("switchRowLines"){ row in - row.title = "Display Lines" - row.value = UserDefaultsRepository.showLines.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.showLines.value = value - // Force main screen update - //guard let mainScreen = self?.tabBarController!.viewControllers?[0] as? MainViewController else { return } - //mainScreen.updateBGGraphSettings() - - if let appState = self!.appStateController { - appState.chartSettingsChanged = true - appState.chartSettingsChanges |= ChartSettingsChangeEnum.showLinesChanged.rawValue - } - - } - <<< SwitchRow("showValues"){ row in + <<< SwitchRow("switchRowLines") { row in + row.title = "Display Lines" + row.value = UserDefaultsRepository.showLines.value + }.onChange { [weak self] row in + guard let value = row.value else { return } + UserDefaultsRepository.showLines.value = value + // Force main screen update + // guard let mainScreen = self?.tabBarController!.viewControllers?[0] as? MainViewController else { return } + // mainScreen.updateBGGraphSettings() + + if let appState = self!.appStateController { + appState.chartSettingsChanged = true + appState.chartSettingsChanges |= ChartSettingsChangeEnum.showLinesChanged.rawValue + } + } + <<< SwitchRow("showValues") { row in row.title = "Show Carb/Bolus Values" row.value = UserDefaultsRepository.showValues.value }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.showValues.value = value - + guard let value = row.value else { return } + UserDefaultsRepository.showValues.value = value } - <<< SwitchRow("showAbsorption"){ row in - row.title = "Show Carb Absorption" - row.value = UserDefaultsRepository.showAbsorption.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.showAbsorption.value = value - - } - <<< SwitchRow("showDIAMarkers"){ row in + <<< SwitchRow("showAbsorption") { row in + row.title = "Show Carb Absorption" + row.value = UserDefaultsRepository.showAbsorption.value + }.onChange { [weak self] row in + guard let value = row.value else { return } + UserDefaultsRepository.showAbsorption.value = value + } + <<< SwitchRow("showDIAMarkers") { row in row.title = "Show DIA Lines" row.value = UserDefaultsRepository.showDIALines.value }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.showDIALines.value = value - + guard let value = row.value else { return } + UserDefaultsRepository.showDIALines.value = value + // tell main screen that graph needs updating if let appState = self!.appStateController { - appState.chartSettingsChanged = true - appState.chartSettingsChanges |= ChartSettingsChangeEnum.showDIALinesChanged.rawValue + appState.chartSettingsChanged = true + appState.chartSettingsChanges |= ChartSettingsChangeEnum.showDIALinesChanged.rawValue } } <<< SwitchRow("show30MinLine") { row in @@ -139,7 +134,7 @@ class GraphSettingsViewController: FormViewController { }.onChange { [weak self] row in guard let value = row.value else { return } UserDefaultsRepository.show30MinLine.value = value - + // Tell the main screen that graph needs updating if let appState = self!.appStateController { appState.chartSettingsChanged = true @@ -152,20 +147,19 @@ class GraphSettingsViewController: FormViewController { }.onChange { [weak self] row in guard let value = row.value else { return } UserDefaultsRepository.show90MinLine.value = value - + // Tell the main screen that graph needs updating if let appState = self!.appStateController { appState.chartSettingsChanged = true appState.chartSettingsChanges |= ChartSettingsChangeEnum.show90MinLineChanged.rawValue } } - <<< SwitchRow("smallGraphTreatments"){ row in + <<< SwitchRow("smallGraphTreatments") { row in row.title = "Treatments on Small Graph" row.value = UserDefaultsRepository.smallGraphTreatments.value }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.smallGraphTreatments.value = value - + guard let value = row.value else { return } + UserDefaultsRepository.smallGraphTreatments.value = value } <<< StepperRow("smallGraphHeight") { row in row.title = "Small Graph Height" @@ -174,17 +168,17 @@ class GraphSettingsViewController: FormViewController { row.cell.stepper.maximumValue = 80 row.value = Double(UserDefaultsRepository.smallGraphHeight.value) row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } + guard let value = value else { return nil } + return "\(Int(value))" + } }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.smallGraphHeight.value = Int(value) - + guard let value = row.value else { return } + UserDefaultsRepository.smallGraphHeight.value = Int(value) + if let appState = self!.appStateController { - appState.chartSettingsChanged = true + appState.chartSettingsChanged = true appState.chartSettingsChanges |= ChartSettingsChangeEnum.smallGraphHeight.rawValue - } + } } <<< StepperRow("predictionToLoad") { row in row.title = "Hours of Prediction" @@ -193,116 +187,113 @@ class GraphSettingsViewController: FormViewController { row.cell.stepper.maximumValue = 6.0 row.value = Double(UserDefaultsRepository.predictionToLoad.value) }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.predictionToLoad.value = value - } - <<< StepperRow("minBGScale") { row in - row.title = "Min BG Scale" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = Double(UserDefaultsRepository.highLine.value) - row.cell.stepper.maximumValue = 400 - row.value = Double(UserDefaultsRepository.minBGScale.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return Localizer.toDisplayUnits(String(value)) - + guard let value = row.value else { return } + UserDefaultsRepository.predictionToLoad.value = value } - }.onChange { [weak self] row in + <<< StepperRow("minBGScale") { row in + row.title = "Min BG Scale" + row.cell.stepper.stepValue = 1 + row.cell.stepper.minimumValue = Double(UserDefaultsRepository.highLine.value) + row.cell.stepper.maximumValue = 400 + row.value = Double(UserDefaultsRepository.minBGScale.value) + row.displayValueFor = { value in + guard let value = value else { return nil } + return Localizer.toDisplayUnits(String(value)) + } + }.onChange { [weak self] row in guard let value = row.value else { return } UserDefaultsRepository.minBGScale.value = Float(value) - } - - <<< StepperRow("minBasalScale") { row in - row.title = "Min Basal Scale" - row.cell.stepper.stepValue = 0.5 - row.cell.stepper.minimumValue = 0.5 - row.cell.stepper.maximumValue = 20 - row.value = Double(UserDefaultsRepository.minBasalScale.value) - }.onChange { [weak self] row in + + <<< StepperRow("minBasalScale") { row in + row.title = "Min Basal Scale" + row.cell.stepper.stepValue = 0.5 + row.cell.stepper.minimumValue = 0.5 + row.cell.stepper.maximumValue = 20 + row.value = Double(UserDefaultsRepository.minBasalScale.value) + }.onChange { [weak self] row in guard let value = row.value else { return } UserDefaultsRepository.minBasalScale.value = value - } - <<< StepperRow("lowLine") { row in - row.title = "Low BG Display Value" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 40 - row.cell.stepper.maximumValue = 120 - row.value = Double(UserDefaultsRepository.lowLine.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return Localizer.toDisplayUnits(String(value)) } - }.onChange { [weak self] row in + <<< StepperRow("lowLine") { row in + row.title = "Low BG Display Value" + row.cell.stepper.stepValue = 1 + row.cell.stepper.minimumValue = 40 + row.cell.stepper.maximumValue = 120 + row.value = Double(UserDefaultsRepository.lowLine.value) + row.displayValueFor = { value in + guard let value = value else { return nil } + return Localizer.toDisplayUnits(String(value)) + } + }.onChange { [weak self] row in guard let value = row.value else { return } UserDefaultsRepository.lowLine.value = Float(value) - // Force main screen update - //guard let mainScreen = self?.tabBarController!.viewControllers?[0] as? MainViewController else { return } - //mainScreen.updateBGGraphSettings() - - // tell main screen to update - if let appState = self!.appStateController { - appState.chartSettingsChanged = true - appState.chartSettingsChanges |= ChartSettingsChangeEnum.lowLineChanged.rawValue - } - } - <<< StepperRow("highLine") { row in - row.title = "High BG Display Value" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 120 - row.cell.stepper.maximumValue = 400 - row.value = Double(UserDefaultsRepository.highLine.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return Localizer.toDisplayUnits(String(value)) + // Force main screen update + // guard let mainScreen = self?.tabBarController!.viewControllers?[0] as? MainViewController else { return } + // mainScreen.updateBGGraphSettings() + + // tell main screen to update + if let appState = self!.appStateController { + appState.chartSettingsChanged = true + appState.chartSettingsChanges |= ChartSettingsChangeEnum.lowLineChanged.rawValue + } } - }.onChange { [weak self] row in + <<< StepperRow("highLine") { row in + row.title = "High BG Display Value" + row.cell.stepper.stepValue = 1 + row.cell.stepper.minimumValue = 120 + row.cell.stepper.maximumValue = 400 + row.value = Double(UserDefaultsRepository.highLine.value) + row.displayValueFor = { value in + guard let value = value else { return nil } + return Localizer.toDisplayUnits(String(value)) + } + }.onChange { [weak self] row in guard let value = row.value else { return } UserDefaultsRepository.highLine.value = Float(value) - // Force main screen update - //guard let mainScreen = self?.tabBarController!.viewControllers?[0] as? MainViewController else { return } - //mainScreen.updateBGGraphSettings() - - // let app state know of the change - if let appState = self!.appStateController { - appState.chartSettingsChanged = true - appState.chartSettingsChanges |= ChartSettingsChangeEnum.highLineChanged.rawValue - } - } - <<< StepperRow("downloadDays") { row in - // NS supports up to 4 days - row.title = "Show Days Back" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 1 - row.cell.stepper.maximumValue = 4 - row.value = Double(UserDefaultsRepository.downloadDays.value) - row.displayValueFor = { value in + // Force main screen update + // guard let mainScreen = self?.tabBarController!.viewControllers?[0] as? MainViewController else { return } + // mainScreen.updateBGGraphSettings() + + // let app state know of the change + if let appState = self!.appStateController { + appState.chartSettingsChanged = true + appState.chartSettingsChanges |= ChartSettingsChangeEnum.highLineChanged.rawValue + } + } + <<< StepperRow("downloadDays") { row in + // NS supports up to 4 days + row.title = "Show Days Back" + row.cell.stepper.stepValue = 1 + row.cell.stepper.minimumValue = 1 + row.cell.stepper.maximumValue = 4 + row.value = Double(UserDefaultsRepository.downloadDays.value) + row.displayValueFor = { value in guard let value = value else { return nil } return "\(Int(value))" } - }.onChange { [weak self] row in + }.onChange { [weak self] row in guard let value = row.value else { return } UserDefaultsRepository.downloadDays.value = Int(value) - } - <<< SwitchRow("showMidnightMarkers"){ row in - row.title = "Show Midnight Lines" - row.value = UserDefaultsRepository.showMidnightLines.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.showMidnightLines.value = value - - // tell main screen that graph needs updating - if let appState = self!.appStateController { - appState.chartSettingsChanged = true - appState.chartSettingsChanges |= ChartSettingsChangeEnum.showMidnightLinesChanged.rawValue } - } + <<< SwitchRow("showMidnightMarkers") { row in + row.title = "Show Midnight Lines" + row.value = UserDefaultsRepository.showMidnightLines.value + }.onChange { [weak self] row in + guard let value = row.value else { return } + UserDefaultsRepository.showMidnightLines.value = value + + // tell main screen that graph needs updating + if let appState = self!.appStateController { + appState.chartSettingsChanged = true + appState.chartSettingsChanges |= ChartSettingsChangeEnum.showMidnightLinesChanged.rawValue + } + } - - +++ ButtonRow() { - $0.title = "DONE" - }.onCellSelection { (row, arg) in - self.dismiss(animated:true, completion: nil) - } + +++ ButtonRow { + $0.title = "DONE" + }.onCellSelection { _, _ in + self.dismiss(animated: true, completion: nil) + } } } diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 18ad0208c..49a5b90ca 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -1,48 +1,47 @@ // -// FirstViewController.swift +// MainViewController.swift // LoopFollow // // Created by Jon Fawcett on 6/1/20. // Copyright © 2020 Jon Fawcett. All rights reserved. // -import UIKit +import AVFAudio import Charts +import Combine +import CoreBluetooth import EventKit import ShareClient -import UserNotifications -import AVFAudio -import CoreBluetooth -import Combine import SwiftUI +import UIKit +import UserNotifications func IsNightscoutEnabled() -> Bool { return !ObservableUserDefaults.shared.url.value.isEmpty } class MainViewController: UIViewController, UITableViewDataSource, ChartViewDelegate, UNUserNotificationCenterDelegate, UIScrollViewDelegate { - - @IBOutlet weak var BGText: UILabel! - @IBOutlet weak var DeltaText: UILabel! - @IBOutlet weak var DirectionText: UILabel! - @IBOutlet weak var BGChart: LineChartView! - @IBOutlet weak var BGChartFull: LineChartView! - @IBOutlet weak var MinAgoText: UILabel! - @IBOutlet weak var infoTable: UITableView! - @IBOutlet weak var Console: UITableViewCell! - @IBOutlet weak var DragBar: UIImageView! - @IBOutlet weak var PredictionLabel: UILabel! - @IBOutlet weak var LoopStatusLabel: UILabel! - @IBOutlet weak var statsPieChart: PieChartView! - @IBOutlet weak var statsLowPercent: UILabel! - @IBOutlet weak var statsInRangePercent: UILabel! - @IBOutlet weak var statsHighPercent: UILabel! - @IBOutlet weak var statsAvgBG: UILabel! - @IBOutlet weak var statsEstA1C: UILabel! - @IBOutlet weak var statsStdDev: UILabel! - @IBOutlet weak var serverText: UILabel! - @IBOutlet weak var statsView: UIView! - @IBOutlet weak var smallGraphHeightConstraint: NSLayoutConstraint! + @IBOutlet var BGText: UILabel! + @IBOutlet var DeltaText: UILabel! + @IBOutlet var DirectionText: UILabel! + @IBOutlet var BGChart: LineChartView! + @IBOutlet var BGChartFull: LineChartView! + @IBOutlet var MinAgoText: UILabel! + @IBOutlet var infoTable: UITableView! + @IBOutlet var Console: UITableViewCell! + @IBOutlet var DragBar: UIImageView! + @IBOutlet var PredictionLabel: UILabel! + @IBOutlet var LoopStatusLabel: UILabel! + @IBOutlet var statsPieChart: PieChartView! + @IBOutlet var statsLowPercent: UILabel! + @IBOutlet var statsInRangePercent: UILabel! + @IBOutlet var statsHighPercent: UILabel! + @IBOutlet var statsAvgBG: UILabel! + @IBOutlet var statsEstA1C: UILabel! + @IBOutlet var statsStdDev: UILabel! + @IBOutlet var serverText: UILabel! + @IBOutlet var statsView: UIView! + @IBOutlet var smallGraphHeightConstraint: NSLayoutConstraint! var refreshScrollView: UIScrollView! var refreshControl: UIRefreshControl! @@ -53,10 +52,10 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele // Variables for BG Charts var firstGraphLoad: Bool = true var currentOverride = 1.0 - - var currentSage : sageData? - var currentCage : cageData? - var currentIage : iageData? + + var currentSage: sageData? + var currentCage: cageData? + var currentIage: iageData? var backgroundTask = BackgroundTask() @@ -93,7 +92,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele var latestIOB: InsulinMetric? var lastOverrideStartTime: TimeInterval = 0 var lastOverrideEndTime: TimeInterval = 0 - + var topBG: Float = UserDefaultsRepository.minBGScale.value var topPredictionBG: Float = UserDefaultsRepository.minBGScale.value @@ -105,17 +104,17 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele // share var bgDataShare: [ShareGlucoseData] = [] - var dexShare: ShareClient?; - + var dexShare: ShareClient? + // calendar setup let store = EKEventStore() - + // Stores the time of the last speech announcement to prevent repeated announcements. // This is a temporary safeguard until the issue with multiple calls to speakBG is fixed. var lastSpeechTime: Date? var autoScrollPauseUntil: Date? = nil - + var IsNotLooping = false let contactImageUpdater = ContactImageUpdater() @@ -131,7 +130,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele Storage.shared.remoteType.value = .none } - //Migration of UserDefaultsRepository -> Storage handling + // Migration of UserDefaultsRepository -> Storage handling if !UserDefaultsRepository.backgroundRefresh.value { Storage.shared.backgroundRefreshType.value = .none UserDefaultsRepository.backgroundRefresh.value = true @@ -150,27 +149,27 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele infoTable.tableFooterView = UIView(frame: .zero) infoTable.bounces = false infoTable.addBorder(toSide: .Left, withColor: UIColor.darkGray.cgColor, andThickness: 2) - - self.infoManager = InfoManager(tableView: infoTable) + + infoManager = InfoManager(tableView: infoTable) smallGraphHeightConstraint.constant = CGFloat(UserDefaultsRepository.smallGraphHeight.value) - self.view.layoutIfNeeded() + view.layoutIfNeeded() let shareUserName = UserDefaultsRepository.shareUserName.value let sharePassword = UserDefaultsRepository.sharePassword.value let shareServer = UserDefaultsRepository.shareServer.value == "US" ?KnownShareServers.US.rawValue : KnownShareServers.NON_US.rawValue - dexShare = ShareClient(username: shareUserName, password: sharePassword, shareServer: shareServer ) - + dexShare = ShareClient(username: shareUserName, password: sharePassword, shareServer: shareServer) + // setup show/hide small graph and stats BGChartFull.isHidden = !UserDefaultsRepository.showSmallGraph.value statsView.isHidden = !UserDefaultsRepository.showStats.value - + BGChart.delegate = self BGChartFull.delegate = self - + if UserDefaultsRepository.forceDarkMode.value { overrideUserInterfaceStyle = .dark - self.tabBarController?.overrideUserInterfaceStyle = .dark + tabBarController?.overrideUserInterfaceStyle = .dark } // Trigger foreground and background functions @@ -183,10 +182,10 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele createGraph() createSmallBGGraph() } - + // setup display for NS vs Dex showHideNSDetails() - + scheduleAllTasks() // Set up refreshScrollView for BGText @@ -194,21 +193,21 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele refreshScrollView.translatesAutoresizingMaskIntoConstraints = false refreshScrollView.alwaysBounceVertical = true view.addSubview(refreshScrollView) - + NSLayoutConstraint.activate([ refreshScrollView.leadingAnchor.constraint(equalTo: BGText.leadingAnchor), refreshScrollView.trailingAnchor.constraint(equalTo: BGText.trailingAnchor), refreshScrollView.topAnchor.constraint(equalTo: BGText.topAnchor), - refreshScrollView.bottomAnchor.constraint(equalTo: BGText.bottomAnchor) + refreshScrollView.bottomAnchor.constraint(equalTo: BGText.bottomAnchor), ]) - + refreshControl = UIRefreshControl() refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged) refreshScrollView.addSubview(refreshControl) - + // Add this line to prevent scrolling in other directions refreshScrollView.alwaysBounceVertical = true - + refreshScrollView.delegate = self NotificationCenter.default.addObserver(self, selector: #selector(refresh), name: NSNotification.Name("refresh"), object: nil) @@ -246,7 +245,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele deinit { NotificationCenter.default.removeObserver(self, name: NSNotification.Name("refresh"), object: nil) } - + // Clean all timers and start new ones when refreshing @objc func refresh() { LogManager.shared.log(category: .general, message: "Refreshing") @@ -284,7 +283,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele lastSpeechTime = nil refreshControl.endRefreshing() } - + // Scroll down BGText when refreshing func scrollViewDidScroll(_ scrollView: UIScrollView) { if scrollView == refreshScrollView { @@ -296,76 +295,71 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } } } - - override func viewWillAppear(_ animated: Bool) { + + override func viewWillAppear(_: Bool) { // set screen lock - UIApplication.shared.isIdleTimerDisabled = UserDefaultsRepository.screenlockSwitchState.value; - + UIApplication.shared.isIdleTimerDisabled = UserDefaultsRepository.screenlockSwitchState.value + // check the app state - if let appState = self.appStateController { - + if let appState = appStateController { if appState.chartSettingsChanged { - // can look at settings flags to be more fine tuned - self.updateBGGraphSettings() - + updateBGGraphSettings() + if ChartSettingsChangeEnum.smallGraphHeight.rawValue != 0 { smallGraphHeightConstraint.constant = CGFloat(UserDefaultsRepository.smallGraphHeight.value) - self.view.layoutIfNeeded() + view.layoutIfNeeded() } - + // reset the app state appState.chartSettingsChanged = false appState.chartSettingsChanges = 0 } if appState.generalSettingsChanged { - // settings for appBadge changed - if appState.generalSettingsChanges & GeneralSettingsChangeEnum.appBadgeChange.rawValue != 0 { - - } - + if appState.generalSettingsChanges & GeneralSettingsChangeEnum.appBadgeChange.rawValue != 0 {} + // settings for textcolor changed if appState.generalSettingsChanges & GeneralSettingsChangeEnum.colorBGTextChange.rawValue != 0 { - self.setBGTextColor() + setBGTextColor() } - + // settings for showStats changed if appState.generalSettingsChanges & GeneralSettingsChangeEnum.showStatsChange.rawValue != 0 { statsView.isHidden = !UserDefaultsRepository.showStats.value } - + // settings for useIFCC changed if appState.generalSettingsChanges & GeneralSettingsChangeEnum.useIFCCChange.rawValue != 0 { updateStats() } - + // settings for showSmallGraph changed if appState.generalSettingsChanges & GeneralSettingsChangeEnum.showSmallGraphChange.rawValue != 0 { BGChartFull.isHidden = !UserDefaultsRepository.showSmallGraph.value } - + if appState.generalSettingsChanges & GeneralSettingsChangeEnum.showDisplayNameChange.rawValue != 0 { - self.updateServerText() + updateServerText() } - + // reset the app state appState.generalSettingsChanged = false appState.generalSettingsChanges = 0 } if appState.infoDataSettingsChanged { - self.infoTable.reloadData() - + infoTable.reloadData() + // reset appState.infoDataSettingsChanged = false } - + // add more processing of the app state } } // Info Table Functions - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { guard let infoManager = infoManager else { return 0 } @@ -388,11 +382,11 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele @objc func appMovedToBackground() { // Allow screen to turn off - UIApplication.shared.isIdleTimerDisabled = false; - + UIApplication.shared.isIdleTimerDisabled = false + // We want to always come back to the home screen tabBarController?.selectedIndex = 0 - + if Storage.shared.backgroundRefreshType.value == .silentTune { backgroundTask.startBackgroundTask() } @@ -401,30 +395,30 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele BackgroundAlertManager.shared.startBackgroundAlert() } } - + @objc func appCameToForeground() { // reset screenlock state if needed - UIApplication.shared.isIdleTimerDisabled = UserDefaultsRepository.screenlockSwitchState.value; - + UIApplication.shared.isIdleTimerDisabled = UserDefaultsRepository.screenlockSwitchState.value + if Storage.shared.backgroundRefreshType.value == .silentTune { backgroundTask.stopBackgroundTask() } - + if Storage.shared.backgroundRefreshType.value != .none { BackgroundAlertManager.shared.stopBackgroundAlert() } TaskScheduler.shared.checkTasksNow() - + checkAndNotifyVersionStatus() checkAppExpirationStatus() } - + func checkAndNotifyVersionStatus() { let versionManager = AppVersionManager() versionManager.checkForNewVersion { latestVersion, isNewer, isBlacklisted in let now = Date() - + // Check if the current version is blacklisted, or if there is a newer version available if isBlacklisted { let lastBlacklistShown = UserDefaultsRepository.lastBlacklistNotificationShown.value ?? Date.distantPast @@ -435,14 +429,14 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } } else if isNewer { let lastVersionUpdateShown = UserDefaultsRepository.lastVersionUpdateNotificationShown.value ?? Date.distantPast - if now.timeIntervalSince(lastVersionUpdateShown) > 1209600 { // 2 weeks + if now.timeIntervalSince(lastVersionUpdateShown) > 1_209_600 { // 2 weeks self.versionAlert(message: "A new version is available: \(latestVersion ?? "Unknown"). It is recommended to update.") UserDefaultsRepository.lastVersionUpdateNotificationShown.value = now } } } } - + func versionAlert(title: String = "Update Available", message: String) { DispatchQueue.main.async { let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) @@ -450,12 +444,12 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele self.present(alert, animated: true) } } - + func checkAppExpirationStatus() { let now = Date() let expirationDate = BuildDetails.default.calculateExpirationDate() let weekBeforeExpiration = Calendar.current.date(byAdding: .day, value: -7, to: expirationDate)! - + if now >= weekBeforeExpiration { let lastExpirationShown = UserDefaultsRepository.lastExpirationNotificationShown.value ?? Date.distantPast if now.timeIntervalSince(lastExpirationShown) > 86400 { // 24 hours @@ -464,7 +458,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } } } - + func expirationAlert() { DispatchQueue.main.async { let alert = UIAlertController(title: "App Expiration Warning", message: "This app will expire in less than a week. Please rebuild to continue using it.", preferredStyle: .alert) @@ -472,18 +466,18 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele self.present(alert, animated: true) } } - - @objc override func viewDidAppear(_ animated: Bool) { + + @objc override func viewDidAppear(_: Bool) { showHideNSDetails() } - + func stringFromTimeInterval(interval: TimeInterval) -> String { let interval = Int(interval) let minutes = (interval / 60) % 60 let hours = (interval / 3600) return String(format: "%02d:%02d", hours, minutes) } - + func showHideNSDetails() { var isHidden = false var isEnabled = true @@ -491,29 +485,27 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele isHidden = true isEnabled = false } - + LoopStatusLabel.isHidden = isHidden if IsNotLooping { PredictionLabel.isHidden = true - } - else { + } else { PredictionLabel.isHidden = isHidden } infoTable.isHidden = isHidden - + if UserDefaultsRepository.hideInfoTable.value { infoTable.isHidden = true } - + if IsNightscoutEnabled() { isEnabled = true } - - guard let nightscoutTab = self.tabBarController?.tabBar.items![3] else { return } + + guard let nightscoutTab = tabBarController?.tabBar.items![3] else { return } nightscoutTab.isEnabled = isEnabled - } - + func updateBadge(val: Int) { if UserDefaultsRepository.appBadge.value { let latestBG = String(val) @@ -526,7 +518,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele func setBGTextColor() { if bgData.count > 0 { let latestBG = bgData[bgData.count - 1].sgv - var color: NSUIColor = NSUIColor.label + var color = NSUIColor.label if UserDefaultsRepository.colorBGText.value { if Float(latestBG) >= UserDefaultsRepository.highLine.value { color = NSUIColor.systemYellow @@ -539,20 +531,19 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele Observable.shared.bgTextColor.value = .green } } - + BGText.textColor = color } } - - func bgDirectionGraphic(_ value:String)->String - { - let //graphics:[String:String]=["Flat":"\u{2192}","DoubleUp":"\u{21C8}","SingleUp":"\u{2191}","FortyFiveUp":"\u{2197}\u{FE0E}","FortyFiveDown":"\u{2198}\u{FE0E}","SingleDown":"\u{2193}","DoubleDown":"\u{21CA}","None":"-","NOT COMPUTABLE":"-","RATE OUT OF RANGE":"-"] - graphics:[String:String]=["Flat":"→","DoubleUp":"↑↑","SingleUp":"↑","FortyFiveUp":"↗","FortyFiveDown":"↘︎","SingleDown":"↓","DoubleDown":"↓↓","None":"-","NONE":"-","NOT COMPUTABLE":"-","RATE OUT OF RANGE":"-", "": "-"] + + func bgDirectionGraphic(_ value: String) -> String { + let // graphics:[String:String]=["Flat":"\u{2192}","DoubleUp":"\u{21C8}","SingleUp":"\u{2191}","FortyFiveUp":"\u{2197}\u{FE0E}","FortyFiveDown":"\u{2198}\u{FE0E}","SingleDown":"\u{2193}","DoubleDown":"\u{21CA}","None":"-","NOT COMPUTABLE":"-","RATE OUT OF RANGE":"-"] + graphics: [String: String] = ["Flat": "→", "DoubleUp": "↑↑", "SingleUp": "↑", "FortyFiveUp": "↗", "FortyFiveDown": "↘︎", "SingleDown": "↓", "DoubleDown": "↓↓", "None": "-", "NONE": "-", "NOT COMPUTABLE": "-", "RATE OUT OF RANGE": "-", "": "-"] return graphics[value]! } - + func writeCalendar() { - self.store.requestCalendarAccess { (granted, error) in + store.requestCalendarAccess { granted, error in if !granted { LogManager.shared.log(category: .calendar, message: "Failed to get calendar access: \(String(describing: error))") return @@ -564,7 +555,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele func processCalendarUpdates() { if UserDefaultsRepository.calendarIdentifier.value == "" { return } - if self.bgData.count < 1 { return } + if bgData.count < 1 { return } // This lets us fire the method to write Min Ago entries only once a minute starting after 6 minutes but allows new readings through let now = dateTimeUtils.getNowTimeIntervalUTC() @@ -578,38 +569,36 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele // Create Event info var deltaBG = 0 // protect index out of bounds - if self.bgData.count > 1 { - deltaBG = self.bgData[self.bgData.count - 1].sgv - self.bgData[self.bgData.count - 2].sgv as Int + if bgData.count > 1 { + deltaBG = bgData[bgData.count - 1].sgv - bgData[bgData.count - 2].sgv as Int } - let deltaTime = (TimeInterval(Date().timeIntervalSince1970) - self.bgData[self.bgData.count - 1].date) / 60 + let deltaTime = (TimeInterval(Date().timeIntervalSince1970) - bgData[bgData.count - 1].date) / 60 var deltaString = "" if deltaBG < 0 { deltaString = Localizer.toDisplayUnits(String(deltaBG)) - } - else - { + } else { deltaString = "+" + Localizer.toDisplayUnits(String(deltaBG)) } - let direction = self.bgDirectionGraphic(self.bgData[self.bgData.count - 1].direction ?? "") - - var eventStartDate = Date(timeIntervalSince1970: self.bgData[self.bgData.count - 1].date) + let direction = bgDirectionGraphic(bgData[bgData.count - 1].direction ?? "") + + 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) { + var eventTitle = UserDefaultsRepository.watchLine1.value + if UserDefaultsRepository.watchLine2.value.count > 1 { eventTitle += "\n" + UserDefaultsRepository.watchLine2.value } - eventTitle = eventTitle.replacingOccurrences(of: "%BG%", with: Localizer.toDisplayUnits(String(self.bgData[self.bgData.count - 1].sgv))) + eventTitle = eventTitle.replacingOccurrences(of: "%BG%", with: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv))) eventTitle = eventTitle.replacingOccurrences(of: "%DIRECTION%", with: direction) eventTitle = eventTitle.replacingOccurrences(of: "%DELTA%", with: deltaString) - if self.currentOverride != 1.0 { - let val = Int( self.currentOverride*100) + if currentOverride != 1.0 { + let val = Int(currentOverride * 100) // let overrideText = String(format:"%f1", self.currentOverride*100) let text = String(val) + "%" eventTitle = eventTitle.replacingOccurrences(of: "%OVERRIDE%", with: text) } else { eventTitle = eventTitle.replacingOccurrences(of: "%OVERRIDE%", with: "") } - eventTitle = eventTitle.replacingOccurrences(of: "%LOOP%", with: self.latestLoopStatusString) + eventTitle = eventTitle.replacingOccurrences(of: "%LOOP%", with: latestLoopStatusString) var minAgo = "" if deltaTime > 9 { // write old BG reading and continue pushing out end date to show last entry @@ -617,81 +606,75 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele eventEndDate = eventStartDate.addingTimeInterval((60 * 10) + (deltaTime * 60)) } var basal = "~" - if self.latestBasal != "" { - basal = self.latestBasal + if latestBasal != "" { + basal = latestBasal } eventTitle = eventTitle.replacingOccurrences(of: "%MINAGO%", with: minAgo) eventTitle = eventTitle.replacingOccurrences(of: "%IOB%", with: latestIOB?.formattedValue() ?? "0") eventTitle = eventTitle.replacingOccurrences(of: "%COB%", with: latestCOB?.formattedValue() ?? "0") eventTitle = eventTitle.replacingOccurrences(of: "%BASAL%", with: basal) - + // Delete Events from last 2 hours and 2 hours in future - var deleteStartDate = Date().addingTimeInterval(-60*60*2) - var deleteEndDate = Date().addingTimeInterval(60*60*2) + 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 = self.store.calendar(withIdentifier: UserDefaultsRepository.calendarIdentifier.value) as? EKCalendar else { return } - var predicate2 = self.store.predicateForEvents(withStart: deleteStartDate, end: deleteEndDate, calendars: [deleteCalendar]) - var eVDelete = self.store.events(matching: predicate2) as [EKEvent]? + guard let deleteCalendar = store.calendar(withIdentifier: UserDefaultsRepository.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 { for i in eVDelete! { do { - try self.store.remove(i, span: EKSpan.thisEvent, commit: true) - } catch let error { + try store.remove(i, span: EKSpan.thisEvent, commit: true) + } catch { print(error) } } } - + // Write New Event - var event = EKEvent(eventStore: self.store) + var event = EKEvent(eventStore: store) event.title = eventTitle event.startDate = eventStartDate event.endDate = eventEndDate - event.calendar = self.store.calendar(withIdentifier: UserDefaultsRepository.calendarIdentifier.value) + event.calendar = store.calendar(withIdentifier: UserDefaultsRepository.calendarIdentifier.value) do { - try self.store.save(event, span: .thisEvent, commit: true) - self.lastCalendarWriteAttemptTime = now + try store.save(event, span: .thisEvent, commit: true) + lastCalendarWriteAttemptTime = now - self.lastCalDate = self.bgData[self.bgData.count - 1].date - //UserDefaultsRepository.savedEventID.value = event.eventIdentifier //save event id to access this particular event later + 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") } } - - - func persistentNotification(bgTime: TimeInterval) - { + + func persistentNotification(bgTime: TimeInterval) { if UserDefaultsRepository.persistentNotification.value && bgTime > UserDefaultsRepository.persistentNotificationLastBGTime.value && bgData.count > 0 { -/*TODO - guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } - snoozer.sendNotification(self, bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: Observable.shared.directionText.value, deltaVal: latestDeltaString, minAgoVal: Observable.shared.minAgoText.value, alertLabelVal: "Latest BG")*/ + /* TODO: + guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } + snoozer.sendNotification(self, bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: Observable.shared.directionText.value, deltaVal: latestDeltaString, minAgoVal: Observable.shared.minAgoText.value, alertLabelVal: "Latest BG") */ } } // General Notifications - - func sendGeneralNotification(_ sender: Any, title: String, subtitle: String, body: String, timer: TimeInterval) { + + func sendGeneralNotification(_: Any, title: String, subtitle: String, body: String, timer: TimeInterval) { let content = UNMutableNotificationContent() content.title = title content.subtitle = subtitle content.body = body content.categoryIdentifier = "noAction" content.sound = .default - + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timer, repeats: false) let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger) UNUserNotificationCenter.current().add(request, withCompletionHandler: nil) - - - } - - func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - } - + + func userNotificationCenter(_: UNUserNotificationCenter, didReceive _: UNNotificationResponse, withCompletionHandler _: @escaping () -> Void) {} + // User has scrolled the chart - func chartTranslated(_ chartView: ChartViewBase, dX: CGFloat, dY: CGFloat) { + func chartTranslated(_: ChartViewBase, dX _: CGFloat, dY _: CGFloat) { let isViewingLatestData = abs(BGChart.highestVisibleX - BGChart.chartXMax) < 0.001 if isViewingLatestData { autoScrollPauseUntil = nil // User is back at the latest data, allow auto-scrolling diff --git a/LoopFollow/ViewControllers/NightScoutViewController.swift b/LoopFollow/ViewControllers/NightScoutViewController.swift index 5fa6348fc..f4ac7dad9 100644 --- a/LoopFollow/ViewControllers/NightScoutViewController.swift +++ b/LoopFollow/ViewControllers/NightScoutViewController.swift @@ -1,5 +1,5 @@ // -// SecondViewController.swift +// NightScoutViewController.swift // LoopFollow // // Created by Jon Fawcett on 6/1/20. @@ -9,115 +9,107 @@ import UIKit import WebKit - - class NightscoutViewController: UIViewController { + @IBOutlet var webView: WKWebView! - @IBOutlet weak var webView: WKWebView! - var appStateController: AppStateController? - + override func viewDidLoad() { super.viewDidLoad() if UserDefaultsRepository.forceDarkMode.value { overrideUserInterfaceStyle = .dark } - + var url = ObservableUserDefaults.shared.url.value let token = UserDefaultsRepository.token.value - + if token != "" { url = url + "?token=" + token } - - guard let myUrl = URL(string: url) else { return } + + guard let myUrl = URL(string: url) else { return } webView.configuration.preferences.javaScriptEnabled = true webView.navigationDelegate = self webView.uiDelegate = self webView.load(URLRequest(url: myUrl)) - + let refreshControl = UIRefreshControl() refreshControl.addTarget(self, action: #selector(reloadWebView(_:)), for: .valueChanged) webView.scrollView.addSubview(refreshControl) - - self.webView.uiDelegate = self + + webView.uiDelegate = self } - + @objc func reloadWebView(_ sender: UIRefreshControl) { - self.clearWebCache() - self.webView.reload() + clearWebCache() + webView.reload() sender.endRefreshing() } - + // New code to clear web cache func clearWebCache() { let dataStore = WKWebsiteDataStore.default() let cacheTypes = Set([WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache]) let date = Date(timeIntervalSince1970: 0) dataStore.removeData(ofTypes: cacheTypes, modifiedSince: date) { - print("Web cache cleared.") + print("Web cache cleared.") } - } - + } + // this handles target=_blank links by opening them in the same view - func webView(webView: WKWebView!, createWebViewWithConfiguration configuration: WKWebViewConfiguration!, forNavigationAction navigationAction: WKNavigationAction!, windowFeatures: WKWindowFeatures!) -> WKWebView! { + func webView(webView: WKWebView!, createWebViewWithConfiguration _: WKWebViewConfiguration!, forNavigationAction navigationAction: WKNavigationAction!, windowFeatures _: WKWindowFeatures!) -> WKWebView! { if let frame = navigationAction.targetFrame, - frame.isMainFrame { + frame.isMainFrame + { return nil } // for _blank target or non-mainFrame target webView.load(navigationAction.request) - return nil } + return nil + } } -// MARK:- WKUIDelegate implementation +// MARK: - WKUIDelegate implementation + extension NightscoutViewController: WKNavigationDelegate, WKUIDelegate { - - func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) { - + func webView(_: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame _: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) { let alertCtrl = UIAlertController(title: nil, message: message, preferredStyle: .alert) - - alertCtrl.addAction(UIAlertAction(title: "OK", style: .default) { action in + + alertCtrl.addAction(UIAlertAction(title: "OK", style: .default) { _ in completionHandler(true) }) - alertCtrl.addAction(UIAlertAction(title: "Cancel", style: .cancel) { action in + alertCtrl.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in completionHandler(false) }) - + present(alertCtrl, animated: true) } - - func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - + + func webView(_: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { guard let _ = navigationAction.request.url else { decisionHandler(.cancel) return } - + decisionHandler(.allow) } - - func webView(_ webView: WKWebView, shouldStartLoadWith request: URLRequest) -> Bool { - + + func webView(_: WKWebView, shouldStartLoadWith request: URLRequest) -> Bool { guard let url = request.url else { return false } - + NSLog("Should start: \(url.absoluteString)") return true } - - func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { - + + func webView(_ webView: WKWebView, createWebViewWith _: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures _: WKWindowFeatures) -> WKWebView? { if navigationAction.targetFrame == nil { webView.load(navigationAction.request) } - + return nil } - - - } diff --git a/LoopFollow/ViewControllers/SettingsViewController.swift b/LoopFollow/ViewControllers/SettingsViewController.swift index 0ce9ccd45..deae16ae7 100644 --- a/LoopFollow/ViewControllers/SettingsViewController.swift +++ b/LoopFollow/ViewControllers/SettingsViewController.swift @@ -6,11 +6,11 @@ // Copyright © 2020 Jon Fawcett. All rights reserved. // -import UIKit import Eureka import EventKit import EventKitUI import SwiftUI +import UIKit class SettingsViewController: FormViewController, NightscoutSettingsViewModelDelegate { var tokenRow: TextRow? @@ -26,9 +26,9 @@ class SettingsViewController: FormViewController, NightscoutSettingsViewModelDel } if let row1 = form.rowBy(tag: "informationDisplaySettings") as? ButtonRow { - row1.hidden = .function(["hide"], {form in - return isHidden - }) + row1.hidden = .function(["hide"]) { _ in + isHidden + } row1.evaluateHidden() } @@ -36,7 +36,7 @@ class SettingsViewController: FormViewController, NightscoutSettingsViewModelDel isEnabled = true } - guard let nightscoutTab = self.tabBarController?.tabBar.items![3] else { return } + guard let nightscoutTab = tabBarController?.tabBar.items![3] else { return } nightscoutTab.isEnabled = isEnabled } @@ -57,184 +57,190 @@ class SettingsViewController: FormViewController, NightscoutSettingsViewModelDel let isSimulatorBuild = buildDetails.isSimulatorBuild() form - +++ Section(header: "Data Settings", footer: "") - <<< SegmentedRow("units") { row in - row.title = "Units" - row.options = ["mg/dL", "mmol/L"] - row.value = UserDefaultsRepository.units.value - }.onChange { row in - guard let value = row.value else { return } - UserDefaultsRepository.units.value = value - } - <<< ButtonRow("nightscout") { - $0.title = "Nightscout Settings" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - self.presentNightscoutSettingsView() - return UIViewController() - }), onDismiss: nil - ) - } - <<< ButtonRow("dexcom") { - $0.title = "Dexcom Settings" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - self.presentDexcomSettingsView() - return UIViewController() - }), onDismiss: nil - ) - } + +++ Section(header: "Data Settings", footer: "") + <<< SegmentedRow("units") { row in + row.title = "Units" + row.options = ["mg/dL", "mmol/L"] + row.value = UserDefaultsRepository.units.value + }.onChange { row in + guard let value = row.value else { return } + UserDefaultsRepository.units.value = value + } + <<< ButtonRow("nightscout") { + $0.title = "Nightscout Settings" + $0.presentationMode = .show( + controllerProvider: .callback(builder: { + self.presentNightscoutSettingsView() + return UIViewController() + }), onDismiss: nil + ) + } + <<< ButtonRow("dexcom") { + $0.title = "Dexcom Settings" + $0.presentationMode = .show( + controllerProvider: .callback(builder: { + self.presentDexcomSettingsView() + return UIViewController() + }), onDismiss: nil + ) + } - +++ Section("App Settings") - - <<< ButtonRow("backgroundRefreshSettings") { - $0.title = "Background Refresh Settings" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - self.presentBackgroundRefreshSettings() - return UIViewController() - }), - onDismiss: nil - ) - } + +++ Section("App Settings") + + <<< ButtonRow("backgroundRefreshSettings") { + $0.title = "Background Refresh Settings" + $0.presentationMode = .show( + controllerProvider: .callback(builder: { + self.presentBackgroundRefreshSettings() + return UIViewController() + }), + onDismiss: nil + ) + } - <<< ButtonRow() { - $0.title = "General Settings" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - let controller = GeneralSettingsViewController() - controller.appStateController = self.appStateController - return controller - } - ), onDismiss: nil) - } - <<< ButtonRow("graphSettings") { - $0.title = "Graph Settings" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - let controller = GraphSettingsViewController() - controller.appStateController = self.appStateController - return controller - } - ), onDismiss: nil) - } - <<< ButtonRow("informationDisplaySettings") { - $0.title = "Information Display Settings" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - self.presentInfoDisplaySettings() - return UIViewController() - } - ), onDismiss: nil) - } + <<< ButtonRow { + $0.title = "General Settings" + $0.presentationMode = .show( + controllerProvider: .callback(builder: { + let controller = GeneralSettingsViewController() + controller.appStateController = self.appStateController + return controller + } + ), onDismiss: nil + ) + } + <<< ButtonRow("graphSettings") { + $0.title = "Graph Settings" + $0.presentationMode = .show( + controllerProvider: .callback(builder: { + let controller = GraphSettingsViewController() + controller.appStateController = self.appStateController + return controller + } + ), onDismiss: nil + ) + } + <<< ButtonRow("informationDisplaySettings") { + $0.title = "Information Display Settings" + $0.presentationMode = .show( + controllerProvider: .callback(builder: { + self.presentInfoDisplaySettings() + return UIViewController() + } + ), onDismiss: nil + ) + } - <<< ButtonRow("alarmsList") { - $0.title = "Alarms" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - self.presentAlarmList() - return UIViewController() - }), - onDismiss: nil - ) - } + <<< ButtonRow("alarmsList") { + $0.title = "Alarms" + $0.presentationMode = .show( + controllerProvider: .callback(builder: { + self.presentAlarmList() + return UIViewController() + }), + onDismiss: nil + ) + } - <<< ButtonRow("alarmsSettings") { - $0.title = "Alarm Settings" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - self.presentAlarmSettings() - return UIViewController() - }), - onDismiss: nil - ) - } + <<< ButtonRow("alarmsSettings") { + $0.title = "Alarm Settings" + $0.presentationMode = .show( + controllerProvider: .callback(builder: { + self.presentAlarmSettings() + return UIViewController() + }), + onDismiss: nil + ) + } - <<< ButtonRow("remoteSettings") { - $0.title = "Remote Settings" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - self.presentRemoteSettings() - return UIViewController() - }), - onDismiss: nil - ) - } + <<< ButtonRow("remoteSettings") { + $0.title = "Remote Settings" + $0.presentationMode = .show( + controllerProvider: .callback(builder: { + self.presentRemoteSettings() + return UIViewController() + }), + onDismiss: nil + ) + } - +++ Section("Integrations") - <<< ButtonRow() { - $0.title = "Calendar" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - let controller = WatchSettingsViewController() - controller.appStateController = self.appStateController - return controller - } - ), onDismiss: nil) + +++ Section("Integrations") + <<< ButtonRow { + $0.title = "Calendar" + $0.presentationMode = .show( + controllerProvider: .callback(builder: { + let controller = WatchSettingsViewController() + controller.appStateController = self.appStateController + return controller + } + ), onDismiss: nil + ) + } + <<< ButtonRow("contact") { + $0.title = "Contact" + $0.presentationMode = .show( + controllerProvider: .callback(builder: { + self.presentContactSettings() + return UIViewController() + } + ), onDismiss: nil + ) + } + +++ Section("Advanced Settings") + <<< ButtonRow { + $0.title = "Advanced Settings" + $0.presentationMode = .show( + controllerProvider: .callback(builder: { + self.presentAdvancedSettingsView() + return UIViewController() + }), onDismiss: nil + ) + } - } - <<< ButtonRow("contact") { - $0.title = "Contact" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - self.presentContactSettings() - return UIViewController() + +++ Section("Logging") + <<< ButtonRow("viewlog") { + $0.title = "View Log" + $0.presentationMode = .show( + controllerProvider: .callback(builder: { + self.presentLogView() + return UIViewController() + }), onDismiss: nil + ) + } + <<< ButtonRow("shareLogs") { + $0.title = "Share Logs" + $0.cellSetup { cell, _ in + cell.accessibilityIdentifier = "ShareLogsButton" } - ), onDismiss: nil) - } - +++ Section("Advanced Settings") - <<< ButtonRow() { - $0.title = "Advanced Settings" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - self.presentAdvancedSettingsView() - return UIViewController() - }), onDismiss: nil) - } + $0.onCellSelection { [weak self] _, _ in + self?.shareLogs() + } + } - +++ Section("Logging") - <<< ButtonRow("viewlog") { - $0.title = "View Log" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - self.presentLogView() - return UIViewController() - }), onDismiss: nil) - } - <<< ButtonRow("shareLogs") { - $0.title = "Share Logs" - $0.cellSetup { cell, row in - cell.accessibilityIdentifier = "ShareLogsButton" + +++ Section("Build Information") + <<< LabelRow { + $0.title = "Version" + $0.value = version + $0.tag = "currentVersionRow" } - $0.onCellSelection { [weak self] _, _ in - self?.shareLogs() + <<< LabelRow { + $0.title = "Latest version" + $0.value = "Fetching..." + $0.tag = "latestVersionRow" + } + <<< LabelRow { + $0.title = expirationHeaderString + $0.value = expiration + $0.hidden = Condition(booleanLiteral: isMacApp || isSimulatorBuild) + } + <<< LabelRow { + $0.title = "Built" + $0.value = formattedBuildDate + } + <<< LabelRow { + $0.title = "Branch" + $0.value = branchAndSha } - } - - +++ Section("Build Information") - <<< LabelRow() { - $0.title = "Version" - $0.value = version - $0.tag = "currentVersionRow" - } - <<< LabelRow() { - $0.title = "Latest version" - $0.value = "Fetching..." - $0.tag = "latestVersionRow" - } - <<< LabelRow() { - $0.title = expirationHeaderString - $0.value = expiration - $0.hidden = Condition(booleanLiteral: isMacApp || isSimulatorBuild) - } - <<< LabelRow() { - $0.title = "Built" - $0.value = formattedBuildDate - } - <<< LabelRow() { - $0.title = "Branch" - $0.value = branchAndSha - } showHideNSDetails() } @@ -414,7 +420,7 @@ class SettingsViewController: FormViewController, NightscoutSettingsViewModelDel if !logFilesToShare.isEmpty { let activityViewController = UIActivityViewController(activityItems: logFilesToShare, applicationActivities: nil) - activityViewController.popoverPresentationController?.sourceView = self.view + activityViewController.popoverPresentationController?.sourceView = view present(activityViewController, animated: true, completion: nil) } else { let alert = UIAlertController(title: "No Logs Available", message: "There are no logs to share.", preferredStyle: .alert) diff --git a/LoopFollow/ViewControllers/WatchSettingsViewController.swift b/LoopFollow/ViewControllers/WatchSettingsViewController.swift index 68f920116..d6e18ec02 100644 --- a/LoopFollow/ViewControllers/WatchSettingsViewController.swift +++ b/LoopFollow/ViewControllers/WatchSettingsViewController.swift @@ -6,25 +6,24 @@ // Copyright © 2020 Jon Fawcett. All rights reserved. // -import Foundation import Eureka import EventKit import EventKitUI +import Foundation class WatchSettingsViewController: FormViewController { - var appStateController: AppStateController? - - override func viewDidLoad() { + + override func viewDidLoad() { super.viewDidLoad() if UserDefaultsRepository.forceDarkMode.value { overrideUserInterfaceStyle = .dark } - + let eventStore = EKEventStore() - eventStore.requestCalendarAccess { [weak self] (granted, error) in + eventStore.requestCalendarAccess { [weak self] granted, _ in guard let self = self else { return } - + DispatchQueue.main.async { // Update the form based on the calendar access status self.buildWatchSettings(hasCalendarAccess: granted) @@ -32,7 +31,7 @@ class WatchSettingsViewController: FormViewController { } } } - + func showHideNSDetails() { var isHidden = false var isEnabled = true @@ -40,45 +39,42 @@ class WatchSettingsViewController: FormViewController { isHidden = true isEnabled = false } - + let tmpArr = ["IOB", "COB", "BASAL", "LOOP", "OVERRIDE"] - for i in 0.. 0 { - return "\(String(matching[0].title))" } else { return " - " @@ -105,57 +100,54 @@ class WatchSettingsViewController: FormViewController { guard let value = row.value else { return } UserDefaultsRepository.calendarIdentifier.value = value } - <<< TextRow("watchLine1"){ row in + <<< TextRow("watchLine1") { row in row.title = "Line 1" row.value = UserDefaultsRepository.watchLine1.value }.onChange { row in guard let value = row.value else { return } UserDefaultsRepository.watchLine1.value = value } - <<< TextRow("watchLine2"){ row in + <<< TextRow("watchLine2") { row in row.title = "Line 2" row.value = UserDefaultsRepository.watchLine2.value }.onChange { row in guard let value = row.value else { return } UserDefaultsRepository.watchLine2.value = value } - - + +++ Section(header: "Available Variables", footer: "") - <<< LabelRow("BG"){ row in + <<< LabelRow("BG") { row in row.title = "%BG% : Blood Glucose Reading" } - <<< LabelRow("DIRECTION"){ row in + <<< LabelRow("DIRECTION") { row in row.title = "%DIRECTION% : Dexcom Trend Arrow" } - <<< LabelRow("DELTA"){ row in + <<< LabelRow("DELTA") { row in row.title = "%DELTA% : +/- From Last Reading" } - <<< LabelRow("IOB"){ row in + <<< LabelRow("IOB") { row in row.title = "%IOB% : Insulin on Board" } - <<< LabelRow("COB"){ row in + <<< LabelRow("COB") { row in row.title = "%COB% : Carbs on Board" } - <<< LabelRow("BASAL"){ row in + <<< LabelRow("BASAL") { row in row.title = "%BASAL% : Current Basal u/hr" } - <<< LabelRow("LOOP"){ row in + <<< LabelRow("LOOP") { row in row.title = "%LOOP% : Loop Status Symbol" } - <<< LabelRow("OVERRIDE"){ row in + <<< LabelRow("OVERRIDE") { row in row.title = "%OVERRIDE% : Active Override %" } - <<< LabelRow("MINAGO"){ row in + <<< LabelRow("MINAGO") { row in row.title = "%MINAGO% : Only displays for old readings" } - - - +++ ButtonRow() { + + +++ ButtonRow { $0.title = "DONE" - }.onCellSelection { (row, arg) in - self.dismiss(animated:true, completion: nil) - } + }.onCellSelection { _, _ in + self.dismiss(animated: true, completion: nil) + } } - } diff --git a/LoopFollowTests/AlwaysTrueCondition.swift b/LoopFollowTests/AlwaysTrueCondition.swift index 96c231309..9a0ce6d04 100644 --- a/LoopFollowTests/AlwaysTrueCondition.swift +++ b/LoopFollowTests/AlwaysTrueCondition.swift @@ -6,5 +6,5 @@ // Copyright © 2025 Jon Fawcett. All rights reserved. // -import XCTest @testable import LoopFollow +import XCTest diff --git a/LoopFollowTests/BuildExpireConditionTests.swift b/LoopFollowTests/BuildExpireConditionTests.swift index f188b34e0..bfc7052b1 100644 --- a/LoopFollowTests/BuildExpireConditionTests.swift +++ b/LoopFollowTests/BuildExpireConditionTests.swift @@ -6,8 +6,8 @@ // Copyright © 2025 Jon Fawcett. All rights reserved. // -import XCTest @testable import LoopFollow +import XCTest final class BuildExpireConditionTests: XCTestCase { let cond = BuildExpireCondition() diff --git a/Package.swift b/Package.swift index 3055730a4..2727a28d1 100644 --- a/Package.swift +++ b/Package.swift @@ -2,10 +2,10 @@ import PackageDescription let package = Package( - name: "BuildTools", - platforms: [.macOS(.v10_11)], - dependencies: [ - .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.41.2") - ], - targets: [.target(name: "BuildTools", path: "")] -) \ No newline at end of file + name: "BuildTools", + platforms: [.macOS(.v10_11)], + dependencies: [ + .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.41.2"), + ], + targets: [.target(name: "BuildTools", path: "")] +) From 02e7db15e624a42776d4e9ac271969c0a4957d56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 14 May 2025 14:03:33 +0200 Subject: [PATCH 049/138] Not Looping --- LoopFollow.xcodeproj/project.pbxproj | 8 ++++ .../AlarmCondition/NotLoopingCondition.swift | 34 ++++++++++++++ LoopFollow/Alarm/AlarmData.swift | 6 +-- .../Alarm/AlarmEditing/AlarmEditor.swift | 2 + .../Editors/NotLoopingAlarmEditor.swift | 47 +++++++++++++++++++ LoopFollow/Alarm/AlarmManager.swift | 3 ++ LoopFollow/Task/AlarmTask.swift | 3 +- 7 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 LoopFollow/Alarm/AlarmCondition/NotLoopingCondition.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 344bc5ac6..90b6d7db0 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -101,6 +101,8 @@ DD7F4BA32DD3C8A900D449E9 /* AlarmBGPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4BA22DD3C8A900D449E9 /* AlarmBGPicker.swift */; }; DD7F4BC52DD3CE0700D449E9 /* AlarmBGLimitSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4BC42DD3CE0700D449E9 /* AlarmBGLimitSection.swift */; }; DD7F4BC72DD473A600D449E9 /* FastDropCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4BC62DD473A600D449E9 /* FastDropCondition.swift */; }; + DD7F4C032DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C022DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift */; }; + DD7F4C052DD4BBE200D449E9 /* NotLoopingCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C042DD4BBE200D449E9 /* NotLoopingCondition.swift */; }; DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */; }; DD85E9952D739CFE001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */; }; DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */; }; @@ -426,6 +428,8 @@ DD7F4BA22DD3C8A900D449E9 /* AlarmBGPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmBGPicker.swift; sourceTree = ""; }; DD7F4BC42DD3CE0700D449E9 /* AlarmBGLimitSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmBGLimitSection.swift; sourceTree = ""; }; DD7F4BC62DD473A600D449E9 /* FastDropCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastDropCondition.swift; sourceTree = ""; }; + DD7F4C022DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotLoopingAlarmEditor.swift; sourceTree = ""; }; + DD7F4C042DD4BBE200D449E9 /* NotLoopingCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotLoopingCondition.swift; sourceTree = ""; }; DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EKEventStore+Extensions.swift"; sourceTree = ""; }; DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnipodDashHeartbeatBluetoothTransmitter.swift; sourceTree = ""; }; DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseConversion.swift; sourceTree = ""; }; @@ -694,6 +698,7 @@ DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = { isa = PBXGroup; children = ( + DD7F4C042DD4BBE200D449E9 /* NotLoopingCondition.swift */, DD7F4BC62DD473A600D449E9 /* FastDropCondition.swift */, DD0650EA2DCE8385004D3B41 /* LowBGCondition.swift */, DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */, @@ -945,6 +950,7 @@ DDC7E53F2DBD8A1600EB1127 /* Editors */ = { isa = PBXGroup; children = ( + DD7F4C022DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift */, DD0650EC2DCE9371004D3B41 /* HighBgAlarmEditor.swift */, DDC7E53C2DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift */, DDC7E53E2DBD8A1600EB1127 /* LowBgAlarmEditor.swift */, @@ -1676,6 +1682,7 @@ DD9ACA082D32F68B00415D8A /* BGTask.swift in Sources */, DD9ACA102D34129200415D8A /* Task.swift in Sources */, DD4AFB492DB576C200BB593F /* AlarmSettingsView.swift in Sources */, + DD7F4C032DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift in Sources */, DD4878202C7DAF890048F05C /* PushMessage.swift in Sources */, DD0247712DB4337700FCADF6 /* BuildExpireCondition.swift in Sources */, DDF6999B2C5AA32E0058A8D9 /* TempTargetPreset.swift in Sources */, @@ -1685,6 +1692,7 @@ FCC6886724898F8000A0279D /* UserDefaultsValue.swift in Sources */, DDCF979E24C2382A002C9752 /* AppStateController.swift in Sources */, DDEF503F2D32754F00999A5D /* ProfileTask.swift in Sources */, + DD7F4C052DD4BBE200D449E9 /* NotLoopingCondition.swift in Sources */, DD5334212C60EBEE00062F9D /* InsulinCartridgeChange.swift in Sources */, DD7F4B9D2DD1EAE500D449E9 /* AlarmAudioSection.swift in Sources */, FC97881E2485969B00A7906C /* NightScoutViewController.swift in Sources */, diff --git a/LoopFollow/Alarm/AlarmCondition/NotLoopingCondition.swift b/LoopFollow/Alarm/AlarmCondition/NotLoopingCondition.swift new file mode 100644 index 000000000..80f3d958c --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/NotLoopingCondition.swift @@ -0,0 +1,34 @@ +// +// NotLoopingCondition.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-13. +// Copyright © 2025 Jon Fawcett. +// + +import Foundation + +struct NotLoopingCondition: AlarmCondition { + static let type: AlarmType = .notLooping + init() {} + + func evaluate(alarm: Alarm, data: AlarmData) -> Bool { + // ──────────────────────────────── + // 0. sanity checks + // ──────────────────────────────── + guard let thresholdMinutes = alarm.threshold, + thresholdMinutes > 0 else { return false } + + // We need a valid timestamp (seconds-since-1970) of the last Loop run. + guard let lastLoopTime = data.lastLoopTime, + lastLoopTime > 0 else { return false } + + // ──────────────────────────────── + // 1. elapsed-time test + // ──────────────────────────────── + let elapsedSecs = Date().timeIntervalSince1970 - lastLoopTime + let limitSecs = thresholdMinutes * 60 + + return elapsedSecs >= limitSecs + } +} diff --git a/LoopFollow/Alarm/AlarmData.swift b/LoopFollow/Alarm/AlarmData.swift index 674bf0656..1849ea1da 100644 --- a/LoopFollow/Alarm/AlarmData.swift +++ b/LoopFollow/Alarm/AlarmData.swift @@ -12,20 +12,16 @@ struct AlarmData: Codable { let bgReadings: [GlucoseValue] let predictionData: [GlucoseValue] let expireDate: Date? + let lastLoopTime: TimeInterval? } /* - struct AlarmData : Encodable, Decodable{ - let bgReadings: [ShareGlucoseData] // let iob: Double? // let cob: Double? - let predictionData: [ShareGlucoseData] // let latestBoluses: [BolusEntry] // let batteryLevel: Double? // let latestCarbs: [CarbEntry] // let overrideData: [OverrideEntry] // let tempTargetData: [TempTargetEntry] // let pumpVolume: Double? - let expireDate: Date? - } */ diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index c23f35ca5..ff4144c3f 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -51,6 +51,8 @@ struct AlarmEditor: View { MissedReadingEditor(alarm: $alarm) case .fastDrop: FastDropAlarmEditor(alarm: $alarm) + case .notLooping: + NotLoopingAlarmEditor(alarm: $alarm) /* TODO: add other condition types here */ default: Text("No editor for \(alarm.type.rawValue)") diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift new file mode 100644 index 000000000..4bd8d1829 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift @@ -0,0 +1,47 @@ +// +// NotLoopingAlarmEditor.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-14. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import SwiftUI + +struct NotLoopingAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Form { + InfoBanner( + text: "Alerts when no successful loop has occurred for the time " + + "you set below.", alarmType: alarm.type + ) + + AlarmGeneralSection(alarm: $alarm) + + AlarmStepperSection( + header: "No Loop for…", + footer: "Number of minutes since the last successful loop. " + + "When this time has elapsed, the alarm becomes eligible.", + title: "Elapsed time", + range: 16 ... 61, + step: 5, + unitLabel: alarm.type.timeUnit.label, + value: Binding( + get: { alarm.threshold ?? 31 }, + set: { alarm.threshold = $0 } + ) + ) + + AlarmAudioSection(alarm: $alarm) + AlarmActiveSection(alarm: $alarm) + AlarmSnoozeSection( + alarm: $alarm, + range: 10 ... 120, + step: 5 + ) + } + .navigationTitle(alarm.type.rawValue) + } +} diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index 8d1e35907..e51070ef1 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -20,6 +20,8 @@ class AlarmManager { BuildExpireCondition.self, LowBGCondition.self, HighBGCondition.self, + FastDropCondition.self, + NotLoopingCondition.self, // TODO: add other condition types here ] ) { @@ -45,6 +47,7 @@ class AlarmManager { let rightVal: Double switch lhs.type { + // TODO: Make a alarm type setting of this, sortedBy or something like that case .fastDrop, .fastRise: // sort on the per-reading delta leftVal = lhs.delta ?? (asc ? Double.infinity : -Double.infinity) diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index d7f4fc843..858c7f26d 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -26,7 +26,8 @@ extension MainViewController { predictionData: self.predictionData .prefix(12) .map { GlucoseValue(sgv: $0.sgv, date: Date(timeIntervalSince1970: $0.date)) }, /// These are oldest .. newest, Predictions not currently available for Trio - expireDate: Storage.shared.expirationDate.value + expireDate: Storage.shared.expirationDate.value, + lastLoopTime: UserDefaultsRepository.alertLastLoopTime.value ) let finalAlarmData: AlarmData From 878d568b02ec51d81f6ebeafb1325ef61e4c2398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 14 May 2025 14:28:37 +0200 Subject: [PATCH 050/138] Active during fix --- LoopFollow/Alarm/Alarm.swift | 20 +++++++++ .../Components/AlarmAudioSection.swift | 44 +++++++++++++++++-- .../Editors/BuildExpireAlarmEditor.swift | 2 +- .../Editors/FastDropAlarmEditor.swift | 4 +- .../Editors/HighBgAlarmEditor.swift | 2 +- .../Editors/LowBgAlarmEditor.swift | 4 +- .../Editors/MissedReadingEditor.swift | 4 +- .../Editors/NotLoopingAlarmEditor.swift | 2 +- 8 files changed, 66 insertions(+), 16 deletions(-) diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index 0b4adb97e..9b09250ef 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -32,6 +32,26 @@ enum ActiveOption: String, CaseIterable, Codable, DayNightDisplayable { case always, day, night } +extension PlaySoundOption { + static func allowed(for active: ActiveOption) -> [PlaySoundOption] { + switch active { + case .always: return [.always, .day, .night, .never] + case .day: return [.day, .never] + case .night: return [.night, .never] + } + } +} + +extension RepeatSoundOption { + static func allowed(for active: ActiveOption) -> [RepeatSoundOption] { + switch active { + case .always: return [.always, .day, .night, .never] + case .day: return [.day, .never] + case .night: return [.night, .never] + } + } +} + struct Alarm: Identifiable, Codable, Equatable { var id: UUID = .init() var type: AlarmType diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift index 6c4b2dad2..9db4116a2 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift @@ -31,8 +31,26 @@ struct AlarmAudioSection: View { TonePickerSheet(selected: $alarm.soundFile) } - AlarmEnumMenuPicker(title: "Play", selection: $alarm.playSoundOption) - AlarmEnumMenuPicker(title: "Repeat", selection: $alarm.repeatSoundOption) + AlarmEnumMenuPicker( + title: "Play", + selection: $alarm.playSoundOption, + allowed: PlaySoundOption.allowed(for: alarm.activeOption) + ) + AlarmEnumMenuPicker( + title: "Repeat", + selection: $alarm.repeatSoundOption, + allowed: RepeatSoundOption.allowed(for: alarm.activeOption) + ) + }.onChange(of: alarm.activeOption) { newActive in + let playAllowed = PlaySoundOption.allowed(for: newActive) + if !playAllowed.contains(alarm.playSoundOption) { + alarm.playSoundOption = playAllowed.last! + } + + let repeatAllowed = RepeatSoundOption.allowed(for: newActive) + if !repeatAllowed.contains(alarm.repeatSoundOption) { + alarm.repeatSoundOption = repeatAllowed.last! + } } } } @@ -40,18 +58,36 @@ struct AlarmAudioSection: View { struct AlarmEnumMenuPicker: View { let title: String @Binding var selection: E + var allowed: [E] var body: some View { HStack { Text(title) Spacer() Picker("", selection: $selection) { - ForEach(Array(E.allCases), id: \.self) { option in - Text(option.displayName).tag(option) + ForEach(allowed, id: \.self) { opt in + Text(opt.displayName).tag(opt) } } + // if the current selection became invalid, snap to the first allowed + .onAppear { validate() } + .onChange(of: allowed) { _ in validate() } } } + + private func validate() { + if !allowed.contains(selection), let first = allowed.first { + selection = first + } + } +} + +extension AlarmEnumMenuPicker where E: CaseIterable { + init(title: String, selection: Binding) { + self.title = title + _selection = selection + allowed = Array(E.allCases) + } } private struct TonePickerSheet: View { diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift index 7fca8e317..7dd4a2f99 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift @@ -31,8 +31,8 @@ struct BuildExpireAlarmEditor: View { ) ) - AlarmAudioSection(alarm: $alarm) AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection( alarm: $alarm, range: 1 ... 14, diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift index 1dfa73474..9400ee435 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift @@ -53,10 +53,8 @@ struct FastDropAlarmEditor: View { value: $alarm.threshold ) - AlarmAudioSection(alarm: $alarm) - AlarmActiveSection(alarm: $alarm) - + AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection( alarm: $alarm, range: 5 ... 60, diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift index 84f0fc0a2..96133892f 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift @@ -45,8 +45,8 @@ struct HighBgAlarmEditor: View { ) ) - AlarmAudioSection(alarm: $alarm) AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection( alarm: $alarm, range: 10 ... 120, diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift index 7c8a0d371..02e34b80d 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift @@ -58,10 +58,8 @@ struct LowBgAlarmEditor: View { ) ) - AlarmAudioSection(alarm: $alarm) - AlarmActiveSection(alarm: $alarm) - + AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection( alarm: $alarm, range: 5 ... 30, diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift index 319751ff9..765be180b 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift @@ -29,10 +29,8 @@ struct MissedReadingEditor: View { ) ) - AlarmAudioSection(alarm: $alarm) - AlarmActiveSection(alarm: $alarm) - + AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection( alarm: $alarm, range: 10 ... 180, diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift index 4bd8d1829..d8381fbf8 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift @@ -34,8 +34,8 @@ struct NotLoopingAlarmEditor: View { ) ) - AlarmAudioSection(alarm: $alarm) AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection( alarm: $alarm, range: 10 ... 120, From f28c2105eb3268aa470b8bec6a9d3bace8ca4689 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 14 May 2025 14:38:04 +0200 Subject: [PATCH 051/138] Active during fix --- LoopFollow/Alarm/Alarm.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index 9b09250ef..4522f0251 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -35,7 +35,7 @@ enum ActiveOption: String, CaseIterable, Codable, DayNightDisplayable { extension PlaySoundOption { static func allowed(for active: ActiveOption) -> [PlaySoundOption] { switch active { - case .always: return [.always, .day, .night, .never] + case .always: return PlaySoundOption.allCases case .day: return [.day, .never] case .night: return [.night, .never] } @@ -45,7 +45,7 @@ extension PlaySoundOption { extension RepeatSoundOption { static func allowed(for active: ActiveOption) -> [RepeatSoundOption] { switch active { - case .always: return [.always, .day, .night, .never] + case .always: return RepeatSoundOption.allCases case .day: return [.day, .never] case .night: return [.night, .never] } From a2d28cf00eeda3d1ed869b1f1e7e073d18e7425f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 14 May 2025 16:52:40 +0200 Subject: [PATCH 052/138] Day and night --- LoopFollow/Alarm/AlarmSettingsView.swift | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/LoopFollow/Alarm/AlarmSettingsView.swift b/LoopFollow/Alarm/AlarmSettingsView.swift index 58d87dd7e..9d31856d2 100644 --- a/LoopFollow/Alarm/AlarmSettingsView.swift +++ b/LoopFollow/Alarm/AlarmSettingsView.swift @@ -129,6 +129,27 @@ struct AlarmSettingsView: View { ) } + Section( + header: Text("Day / Night Schedule"), + footer: Text("Pick when your day period begins and when your night period begins." + + "Any time from your Day-starts time up until your Night-starts time will count as day; " + + "from Night-starts until the next Day-starts will count as night.") + ) { + DatePicker( + "Day starts", + selection: dayBinding, + displayedComponents: [.hourAndMinute] + ) + .datePickerStyle(.compact) + + DatePicker( + "Night starts", + selection: nightBinding, + displayedComponents: [.hourAndMinute] + ) + .datePickerStyle(.compact) + } + Section(header: Text("Alarm Settings")) { Toggle( "Override System Volume", From 3a240a2c99573ccaaf68077e50282498eb366cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 14 May 2025 18:29:18 +0200 Subject: [PATCH 053/138] Alarm settings --- LoopFollow/Alarm/AlarmSettingsView.swift | 123 +++++++++++------------ 1 file changed, 59 insertions(+), 64 deletions(-) diff --git a/LoopFollow/Alarm/AlarmSettingsView.swift b/LoopFollow/Alarm/AlarmSettingsView.swift index 9d31856d2..e05ebb079 100644 --- a/LoopFollow/Alarm/AlarmSettingsView.swift +++ b/LoopFollow/Alarm/AlarmSettingsView.swift @@ -54,84 +54,79 @@ struct AlarmSettingsView: View { NavigationView { Form { Section( - header: Text("Snooze & Mute Options"), + header: Text("Snooze & Mute Options"), footer: Text(""" - Snooze All turns everything off, \ - Mute All turns off phone sounds but leaves vibration \ - and iOS notifications on + “Snooze All” disables every alarm. \ + “Mute All” silences phone sounds but still vibrates \ + and shows iOS notifications. """) ) { - // Snooze All Until - DatePicker( - "Snooze All Until", - selection: optDateBinding( - Binding( - get: { cfgStore.value.snoozeUntil }, - set: { cfgStore.value.snoozeUntil = $0 } - ) - ), - displayedComponents: [.date, .hourAndMinute] - ) - - Toggle( - "All Alerts Snoozed", - isOn: Binding( - get: { - if let until = cfgStore.value.snoozeUntil { - return until > Date() - } - return false - }, - set: { newOn in - if newOn { - // if turning on, set a default 1h snooze if none or expired - if cfgStore.value.snoozeUntil == nil || cfgStore.value.snoozeUntil! <= Date() { - cfgStore.value.snoozeUntil = Date().addingTimeInterval(3600) - } - } else { - cfgStore.value.snoozeUntil = nil + Toggle("All Alerts Snoozed", isOn: Binding( + get: { + if let until = cfgStore.value.snoozeUntil { return until > Date() } + return false + }, + set: { on in + if on { + let target = cfgStore.value.snoozeUntil ?? Date() + if target <= Date() { + cfgStore.value.snoozeUntil = Date().addingTimeInterval(3600) } + } else { + cfgStore.value.snoozeUntil = nil } + } + )) + + if let until = cfgStore.value.snoozeUntil, until > Date() { + DatePicker( + "Until", + selection: optDateBinding( + Binding( + get: { cfgStore.value.snoozeUntil }, + set: { cfgStore.value.snoozeUntil = $0 } + ) + ), + displayedComponents: [.date, .hourAndMinute] ) - ) - - // Mute All Until - DatePicker( - "Mute All Until", - selection: optDateBinding( - Binding( - get: { cfgStore.value.muteUntil }, - set: { cfgStore.value.muteUntil = $0 } - ) - ), - displayedComponents: [.date, .hourAndMinute] - ) + .datePickerStyle(.compact) + } - Toggle( - "All Sounds Muted", - isOn: Binding( - get: { - if let until = cfgStore.value.muteUntil { - return until > Date() - } - return false - }, - set: { newOn in - if newOn { - if cfgStore.value.muteUntil == nil || cfgStore.value.muteUntil! <= Date() { - cfgStore.value.muteUntil = Date().addingTimeInterval(3600) - } - } else { - cfgStore.value.muteUntil = nil + Toggle("All Sounds Muted", isOn: Binding( + get: { + if let until = cfgStore.value.muteUntil { return until > Date() } + return false + }, + set: { on in + if on { + let target = cfgStore.value.muteUntil ?? Date() + if target <= Date() { + cfgStore.value.muteUntil = Date().addingTimeInterval(3600) } + } else { + cfgStore.value.muteUntil = nil } + } + )) + + if let until = cfgStore.value.muteUntil, until > Date() { + DatePicker( + "Until", + selection: optDateBinding( + Binding( + get: { cfgStore.value.muteUntil }, + set: { cfgStore.value.muteUntil = $0 } + ) + ), + displayedComponents: [.date, .hourAndMinute] ) - ) + .datePickerStyle(.compact) + } } Section( header: Text("Day / Night Schedule"), - footer: Text("Pick when your day period begins and when your night period begins." + + footer: Text("Pick when your day period begins and when your night period begins. " + "Any time from your Day-starts time up until your Night-starts time will count as day; " + "from Night-starts until the next Day-starts will count as night.") ) { From aa849c661739b519960fd76d2b485708fb120eed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 14 May 2025 20:38:53 +0200 Subject: [PATCH 054/138] OverrideStartAlarm --- LoopFollow.xcodeproj/project.pbxproj | 8 +++++ LoopFollow/Alarm/Alarm.swift | 4 +++ .../OverrideStartCondition.swift | 27 +++++++++++++++ LoopFollow/Alarm/AlarmData.swift | 1 + .../Alarm/AlarmEditing/AlarmEditor.swift | 2 ++ .../Components/AlarmAudioSection.swift | 14 +++++--- .../Editors/OverrideStartAlarmEditor.swift | 33 +++++++++++++++++++ LoopFollow/Alarm/AlarmManager.swift | 1 + LoopFollow/Storage/Storage.swift | 7 ++-- LoopFollow/Task/AlarmTask.swift | 5 ++- 10 files changed, 92 insertions(+), 10 deletions(-) create mode 100644 LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 90b6d7db0..c463d6557 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -103,6 +103,8 @@ DD7F4BC72DD473A600D449E9 /* FastDropCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4BC62DD473A600D449E9 /* FastDropCondition.swift */; }; DD7F4C032DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C022DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift */; }; DD7F4C052DD4BBE200D449E9 /* NotLoopingCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C042DD4BBE200D449E9 /* NotLoopingCondition.swift */; }; + DD7F4C072DD5042F00D449E9 /* OverrideStartAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C062DD5042F00D449E9 /* OverrideStartAlarmEditor.swift */; }; + DD7F4C092DD504A700D449E9 /* OverrideStartCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C082DD504A700D449E9 /* OverrideStartCondition.swift */; }; DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */; }; DD85E9952D739CFE001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */; }; DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */; }; @@ -430,6 +432,8 @@ DD7F4BC62DD473A600D449E9 /* FastDropCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastDropCondition.swift; sourceTree = ""; }; DD7F4C022DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotLoopingAlarmEditor.swift; sourceTree = ""; }; DD7F4C042DD4BBE200D449E9 /* NotLoopingCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotLoopingCondition.swift; sourceTree = ""; }; + DD7F4C062DD5042F00D449E9 /* OverrideStartAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStartAlarmEditor.swift; sourceTree = ""; }; + DD7F4C082DD504A700D449E9 /* OverrideStartCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStartCondition.swift; sourceTree = ""; }; DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EKEventStore+Extensions.swift"; sourceTree = ""; }; DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnipodDashHeartbeatBluetoothTransmitter.swift; sourceTree = ""; }; DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseConversion.swift; sourceTree = ""; }; @@ -699,6 +703,7 @@ isa = PBXGroup; children = ( DD7F4C042DD4BBE200D449E9 /* NotLoopingCondition.swift */, + DD7F4C082DD504A700D449E9 /* OverrideStartCondition.swift */, DD7F4BC62DD473A600D449E9 /* FastDropCondition.swift */, DD0650EA2DCE8385004D3B41 /* LowBGCondition.swift */, DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */, @@ -950,6 +955,7 @@ DDC7E53F2DBD8A1600EB1127 /* Editors */ = { isa = PBXGroup; children = ( + DD7F4C062DD5042F00D449E9 /* OverrideStartAlarmEditor.swift */, DD7F4C022DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift */, DD0650EC2DCE9371004D3B41 /* HighBgAlarmEditor.swift */, DDC7E53C2DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift */, @@ -1690,6 +1696,7 @@ DDCF9A822D85FD15004DF4DD /* AlarmType.swift in Sources */, DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */, FCC6886724898F8000A0279D /* UserDefaultsValue.swift in Sources */, + DD7F4C092DD504A700D449E9 /* OverrideStartCondition.swift in Sources */, DDCF979E24C2382A002C9752 /* AppStateController.swift in Sources */, DDEF503F2D32754F00999A5D /* ProfileTask.swift in Sources */, DD7F4C052DD4BBE200D449E9 /* NotLoopingCondition.swift in Sources */, @@ -1700,6 +1707,7 @@ DDCF979824C1489C002C9752 /* GraphSettingsViewController.swift in Sources */, FC3AE7B5249E8E0E00AAE1E0 /* LoopFollow.xcdatamodeld in Sources */, DDC7E5152DBCFA7900EB1127 /* SnoozerViewController.swift in Sources */, + DD7F4C072DD5042F00D449E9 /* OverrideStartAlarmEditor.swift in Sources */, DDDF6F492D479AF000884336 /* NoRemoteView.swift in Sources */, DD0650ED2DCE9371004D3B41 /* HighBgAlarmEditor.swift in Sources */, FCC6886F2489A53800A0279D /* AppConstants.swift in Sources */, diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index 4522f0251..e0edcb599 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -259,12 +259,16 @@ struct Alarm: Identifiable, Codable, Equatable { soundFile = .dholShuffleloop case .overrideStart: soundFile = .endingReached + repeatSoundOption = .never case .overrideEnd: soundFile = .alertToneBusy + repeatSoundOption = .never case .tempTargetStart: soundFile = .endingReached + repeatSoundOption = .never case .tempTargetEnd: soundFile = .alertToneBusy + repeatSoundOption = .never } } } diff --git a/LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift b/LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift new file mode 100644 index 000000000..2bcd9edcc --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift @@ -0,0 +1,27 @@ +// +// OverrideStartCondition.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-14. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import Foundation + +struct OverrideStartCondition: AlarmCondition { + static let type: AlarmType = .overrideStart + init() {} + + func evaluate(alarm _: Alarm, data: AlarmData) -> Bool { + guard let startTS = data.latestOverrideStart, startTS > 0 else { return false } + + let recent = Date().timeIntervalSince1970 - startTS <= 15 * 60 + guard recent else { return false } + + let lastNotified = Storage.shared.lastOverrideStartNotified.value ?? 0 + guard startTS > lastNotified else { return false } + + Storage.shared.lastOverrideStartNotified.value = startTS + return true + } +} diff --git a/LoopFollow/Alarm/AlarmData.swift b/LoopFollow/Alarm/AlarmData.swift index 1849ea1da..89a2452bb 100644 --- a/LoopFollow/Alarm/AlarmData.swift +++ b/LoopFollow/Alarm/AlarmData.swift @@ -13,6 +13,7 @@ struct AlarmData: Codable { let predictionData: [GlucoseValue] let expireDate: Date? let lastLoopTime: TimeInterval? + let latestOverrideStart: TimeInterval? } /* diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index ff4144c3f..4d4e88784 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -53,6 +53,8 @@ struct AlarmEditor: View { FastDropAlarmEditor(alarm: $alarm) case .notLooping: NotLoopingAlarmEditor(alarm: $alarm) + case .overrideStart: + OverrideStartAlarmEditor(alarm: $alarm) /* TODO: add other condition types here */ default: Text("No editor for \(alarm.type.rawValue)") diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift index 9db4116a2..004a7e732 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift @@ -10,6 +10,7 @@ import SwiftUI struct AlarmAudioSection: View { @Binding var alarm: Alarm + var hideRepeat: Bool = false @State private var showingTonePicker = false var body: some View { @@ -36,11 +37,14 @@ struct AlarmAudioSection: View { selection: $alarm.playSoundOption, allowed: PlaySoundOption.allowed(for: alarm.activeOption) ) - AlarmEnumMenuPicker( - title: "Repeat", - selection: $alarm.repeatSoundOption, - allowed: RepeatSoundOption.allowed(for: alarm.activeOption) - ) + + if !hideRepeat { + AlarmEnumMenuPicker( + title: "Repeat", + selection: $alarm.repeatSoundOption, + allowed: RepeatSoundOption.allowed(for: alarm.activeOption) + ) + } }.onChange(of: alarm.activeOption) { newActive in let playAllowed = PlaySoundOption.allowed(for: newActive) if !playAllowed.contains(alarm.playSoundOption) { diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift new file mode 100644 index 000000000..d5b489868 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift @@ -0,0 +1,33 @@ +// +// OverrideStartAlarmEditor.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-14. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import SwiftUI + +struct OverrideStartAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Form { + InfoBanner( + text: "Alerts when an override begins.", + alarmType: alarm.type + ) + + AlarmGeneralSection(alarm: $alarm) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm, hideRepeat: true) + AlarmSnoozeSection( + alarm: $alarm, + range: 10 ... 60, + step: 5 + ) + } + .navigationTitle(alarm.type.rawValue) + } +} diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index e51070ef1..cf7c15ad2 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -22,6 +22,7 @@ class AlarmManager { HighBGCondition.self, FastDropCondition.self, NotLoopingCondition.self, + OverrideStartCondition.self, // TODO: add other condition types here ] ) { diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index 562e535be..57ade4385 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -51,10 +51,9 @@ class Storage { var sensorScheduleOffset = StorageValue(key: "sensorScheduleOffset", defaultValue: nil) var alarms = StorageValue<[Alarm]>(key: "alarms", defaultValue: []) - var alarmConfiguration = StorageValue( - key: "alarmConfiguration", - defaultValue: .default - ) + var alarmConfiguration = StorageValue(key: "alarmConfiguration", defaultValue: .default) + + var lastOverrideStartNotified = StorageValue(key: "lastOverrideStartNotified", defaultValue: nil) static let shared = Storage() private init() {} diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index 858c7f26d..41aaac7ac 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -19,6 +19,8 @@ extension MainViewController { func alarmTaskAction() { DispatchQueue.main.async { + let latestOverrideStart = self.overrideGraphData.last?.date + let alarmData = AlarmData( bgReadings: self.bgData .suffix(24) @@ -27,7 +29,8 @@ extension MainViewController { .prefix(12) .map { GlucoseValue(sgv: $0.sgv, date: Date(timeIntervalSince1970: $0.date)) }, /// These are oldest .. newest, Predictions not currently available for Trio expireDate: Storage.shared.expirationDate.value, - lastLoopTime: UserDefaultsRepository.alertLastLoopTime.value + lastLoopTime: UserDefaultsRepository.alertLastLoopTime.value, + latestOverrideStart: latestOverrideStart ) let finalAlarmData: AlarmData From b74c944625d4e73d351f6202c047c23159160027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 14 May 2025 20:52:33 +0200 Subject: [PATCH 055/138] OverrideEnd --- LoopFollow.xcodeproj/project.pbxproj | 8 +++++ .../AlarmCondition/OverrideEndCondition.swift | 25 ++++++++++++++++ LoopFollow/Alarm/AlarmData.swift | 1 + .../Alarm/AlarmEditing/AlarmEditor.swift | 2 ++ .../Editors/OverrideEndAlarmEditor.swift | 30 +++++++++++++++++++ LoopFollow/Alarm/AlarmManager.swift | 1 + LoopFollow/Storage/Storage.swift | 1 + LoopFollow/Task/AlarmTask.swift | 4 ++- 8 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 LoopFollow/Alarm/AlarmCondition/OverrideEndCondition.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index c463d6557..6c417ad38 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -105,6 +105,8 @@ DD7F4C052DD4BBE200D449E9 /* NotLoopingCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C042DD4BBE200D449E9 /* NotLoopingCondition.swift */; }; DD7F4C072DD5042F00D449E9 /* OverrideStartAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C062DD5042F00D449E9 /* OverrideStartAlarmEditor.swift */; }; DD7F4C092DD504A700D449E9 /* OverrideStartCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C082DD504A700D449E9 /* OverrideStartCondition.swift */; }; + DD7F4C0B2DD51C5500D449E9 /* OverrideEndCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C0A2DD51C5500D449E9 /* OverrideEndCondition.swift */; }; + DD7F4C0D2DD51C8100D449E9 /* OverrideEndAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C0C2DD51C8100D449E9 /* OverrideEndAlarmEditor.swift */; }; DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */; }; DD85E9952D739CFE001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */; }; DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */; }; @@ -434,6 +436,8 @@ DD7F4C042DD4BBE200D449E9 /* NotLoopingCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotLoopingCondition.swift; sourceTree = ""; }; DD7F4C062DD5042F00D449E9 /* OverrideStartAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStartAlarmEditor.swift; sourceTree = ""; }; DD7F4C082DD504A700D449E9 /* OverrideStartCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStartCondition.swift; sourceTree = ""; }; + DD7F4C0A2DD51C5500D449E9 /* OverrideEndCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideEndCondition.swift; sourceTree = ""; }; + DD7F4C0C2DD51C8100D449E9 /* OverrideEndAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideEndAlarmEditor.swift; sourceTree = ""; }; DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EKEventStore+Extensions.swift"; sourceTree = ""; }; DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnipodDashHeartbeatBluetoothTransmitter.swift; sourceTree = ""; }; DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseConversion.swift; sourceTree = ""; }; @@ -702,6 +706,7 @@ DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = { isa = PBXGroup; children = ( + DD7F4C0A2DD51C5500D449E9 /* OverrideEndCondition.swift */, DD7F4C042DD4BBE200D449E9 /* NotLoopingCondition.swift */, DD7F4C082DD504A700D449E9 /* OverrideStartCondition.swift */, DD7F4BC62DD473A600D449E9 /* FastDropCondition.swift */, @@ -955,6 +960,7 @@ DDC7E53F2DBD8A1600EB1127 /* Editors */ = { isa = PBXGroup; children = ( + DD7F4C0C2DD51C8100D449E9 /* OverrideEndAlarmEditor.swift */, DD7F4C062DD5042F00D449E9 /* OverrideStartAlarmEditor.swift */, DD7F4C022DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift */, DD0650EC2DCE9371004D3B41 /* HighBgAlarmEditor.swift */, @@ -1760,6 +1766,7 @@ DD0C0C6D2C48606200DBADDF /* CarbMetric.swift in Sources */, DDC7E5422DBD8A1600EB1127 /* AlarmGeneralSection.swift in Sources */, DD7F4BC72DD473A600D449E9 /* FastDropCondition.swift in Sources */, + DD7F4C0D2DD51C8100D449E9 /* OverrideEndAlarmEditor.swift in Sources */, DDC7E5432DBD8A1600EB1127 /* SoundFile.swift in Sources */, DDC7E5442DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift in Sources */, DDC7E5462DBD8A1600EB1127 /* LowBgAlarmEditor.swift in Sources */, @@ -1788,6 +1795,7 @@ DD9ED0CC2D35526E000D2A63 /* SearchBar.swift in Sources */, DDD10EFF2C510C3C00D76A8E /* ObservableUserDefaultsValue.swift in Sources */, DD58171A2D299EF80041FB98 /* DexcomHeartbeatBluetoothDevice.swift in Sources */, + DD7F4C0B2DD51C5500D449E9 /* OverrideEndCondition.swift in Sources */, DDFF3D872D14280500BF9D9E /* BackgroundRefreshSettingsViewModel.swift in Sources */, DD50C7502D0828800057AE6F /* ContactSettingsViewModel.swift in Sources */, DDAD162F2D2EF9830084BE10 /* RileyLinkHeartbeatBluetoothDevice.swift in Sources */, diff --git a/LoopFollow/Alarm/AlarmCondition/OverrideEndCondition.swift b/LoopFollow/Alarm/AlarmCondition/OverrideEndCondition.swift new file mode 100644 index 000000000..460a9df9e --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/OverrideEndCondition.swift @@ -0,0 +1,25 @@ +// +// OverrideEndCondition.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-14. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import Foundation + +struct OverrideEndCondition: AlarmCondition { + static let type: AlarmType = .overrideEnd + init() {} + + func evaluate(alarm _: Alarm, data: AlarmData) -> Bool { + guard let endTS = data.latestOverrideEnd, endTS > 0 else { return false } + guard Date().timeIntervalSince1970 - endTS <= 15 * 60 else { return false } + + let last = Storage.shared.lastOverrideEndNotified.value ?? 0 + guard endTS > last else { return false } + + Storage.shared.lastOverrideEndNotified.value = endTS + return true + } +} diff --git a/LoopFollow/Alarm/AlarmData.swift b/LoopFollow/Alarm/AlarmData.swift index 89a2452bb..ab1246192 100644 --- a/LoopFollow/Alarm/AlarmData.swift +++ b/LoopFollow/Alarm/AlarmData.swift @@ -14,6 +14,7 @@ struct AlarmData: Codable { let expireDate: Date? let lastLoopTime: TimeInterval? let latestOverrideStart: TimeInterval? + let latestOverrideEnd: TimeInterval? } /* diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index 4d4e88784..e995c013c 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -55,6 +55,8 @@ struct AlarmEditor: View { NotLoopingAlarmEditor(alarm: $alarm) case .overrideStart: OverrideStartAlarmEditor(alarm: $alarm) + case .overrideEnd: + OverrideEndAlarmEditor(alarm: $alarm) /* TODO: add other condition types here */ default: Text("No editor for \(alarm.type.rawValue)") diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift new file mode 100644 index 000000000..1cb0cdb82 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift @@ -0,0 +1,30 @@ +// +// OverrideEndAlarmEditor.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-14. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import SwiftUI + +struct OverrideEndAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Form { + InfoBanner(text: "Alerts when an override ends.", alarmType: alarm.type) + + AlarmGeneralSection(alarm: $alarm) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm, hideRepeat: true) + AlarmSnoozeSection( + alarm: $alarm, + range: 10 ... 60, + step: 5 + ) + } + .navigationTitle(alarm.type.rawValue) + } +} diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index cf7c15ad2..78d934c22 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -23,6 +23,7 @@ class AlarmManager { FastDropCondition.self, NotLoopingCondition.self, OverrideStartCondition.self, + OverrideEndCondition.self, // TODO: add other condition types here ] ) { diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index 57ade4385..e19c12faf 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -54,6 +54,7 @@ class Storage { var alarmConfiguration = StorageValue(key: "alarmConfiguration", defaultValue: .default) var lastOverrideStartNotified = StorageValue(key: "lastOverrideStartNotified", defaultValue: nil) + var lastOverrideEndNotified = StorageValue(key: "lastOverrideEndNotified", defaultValue: nil) static let shared = Storage() private init() {} diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index 41aaac7ac..07d9a57b1 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -20,6 +20,7 @@ extension MainViewController { func alarmTaskAction() { DispatchQueue.main.async { let latestOverrideStart = self.overrideGraphData.last?.date + let latestOverrideEnd = self.overrideGraphData.last?.endDate let alarmData = AlarmData( bgReadings: self.bgData @@ -30,7 +31,8 @@ extension MainViewController { .map { GlucoseValue(sgv: $0.sgv, date: Date(timeIntervalSince1970: $0.date)) }, /// These are oldest .. newest, Predictions not currently available for Trio expireDate: Storage.shared.expirationDate.value, lastLoopTime: UserDefaultsRepository.alertLastLoopTime.value, - latestOverrideStart: latestOverrideStart + latestOverrideStart: latestOverrideStart, + latestOverrideEnd: latestOverrideEnd, ) let finalAlarmData: AlarmData From 70b5800b3facd10cde2d098b035316ec84bbf2b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 14 May 2025 21:00:02 +0200 Subject: [PATCH 056/138] TempTargetStart --- LoopFollow.xcodeproj/project.pbxproj | 8 ++++++ .../TempTargetStartCondition.swift | 26 +++++++++++++++++++ LoopFollow/Alarm/AlarmData.swift | 1 + .../Alarm/AlarmEditing/AlarmEditor.swift | 2 ++ .../Editors/TempTargetStartAlarmEditor.swift | 26 +++++++++++++++++++ LoopFollow/Alarm/AlarmManager.swift | 1 + LoopFollow/Storage/Storage.swift | 1 + LoopFollow/Task/AlarmTask.swift | 2 ++ 8 files changed, 67 insertions(+) create mode 100644 LoopFollow/Alarm/AlarmCondition/TempTargetStartCondition.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 6c417ad38..14761091a 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -107,6 +107,8 @@ DD7F4C092DD504A700D449E9 /* OverrideStartCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C082DD504A700D449E9 /* OverrideStartCondition.swift */; }; DD7F4C0B2DD51C5500D449E9 /* OverrideEndCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C0A2DD51C5500D449E9 /* OverrideEndCondition.swift */; }; DD7F4C0D2DD51C8100D449E9 /* OverrideEndAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C0C2DD51C8100D449E9 /* OverrideEndAlarmEditor.swift */; }; + DD7F4C0F2DD51EC200D449E9 /* TempTargetStartCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C0E2DD51EC200D449E9 /* TempTargetStartCondition.swift */; }; + DD7F4C112DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C102DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift */; }; DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */; }; DD85E9952D739CFE001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */; }; DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */; }; @@ -438,6 +440,8 @@ DD7F4C082DD504A700D449E9 /* OverrideStartCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStartCondition.swift; sourceTree = ""; }; DD7F4C0A2DD51C5500D449E9 /* OverrideEndCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideEndCondition.swift; sourceTree = ""; }; DD7F4C0C2DD51C8100D449E9 /* OverrideEndAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideEndAlarmEditor.swift; sourceTree = ""; }; + DD7F4C0E2DD51EC200D449E9 /* TempTargetStartCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetStartCondition.swift; sourceTree = ""; }; + DD7F4C102DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetStartAlarmEditor.swift; sourceTree = ""; }; DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EKEventStore+Extensions.swift"; sourceTree = ""; }; DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnipodDashHeartbeatBluetoothTransmitter.swift; sourceTree = ""; }; DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseConversion.swift; sourceTree = ""; }; @@ -706,6 +710,7 @@ DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = { isa = PBXGroup; children = ( + DD7F4C0E2DD51EC200D449E9 /* TempTargetStartCondition.swift */, DD7F4C0A2DD51C5500D449E9 /* OverrideEndCondition.swift */, DD7F4C042DD4BBE200D449E9 /* NotLoopingCondition.swift */, DD7F4C082DD504A700D449E9 /* OverrideStartCondition.swift */, @@ -960,6 +965,7 @@ DDC7E53F2DBD8A1600EB1127 /* Editors */ = { isa = PBXGroup; children = ( + DD7F4C102DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift */, DD7F4C0C2DD51C8100D449E9 /* OverrideEndAlarmEditor.swift */, DD7F4C062DD5042F00D449E9 /* OverrideStartAlarmEditor.swift */, DD7F4C022DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift */, @@ -1698,6 +1704,7 @@ DD4878202C7DAF890048F05C /* PushMessage.swift in Sources */, DD0247712DB4337700FCADF6 /* BuildExpireCondition.swift in Sources */, DDF6999B2C5AA32E0058A8D9 /* TempTargetPreset.swift in Sources */, + DD7F4C0F2DD51EC200D449E9 /* TempTargetStartCondition.swift in Sources */, DD9ACA0E2D340BFF00415D8A /* AlarmTask.swift in Sources */, DDCF9A822D85FD15004DF4DD /* AlarmType.swift in Sources */, DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */, @@ -1856,6 +1863,7 @@ DD0C0C642C45A59400DBADDF /* HKUnit+Extensions.swift in Sources */, DDCF979A24C14DB4002C9752 /* WatchSettingsViewController.swift in Sources */, DD493AE52ACF2383009A6922 /* Treatments.swift in Sources */, + DD7F4C112DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift in Sources */, DD50C7552D0862770057AE6F /* ContactImageUpdater.swift in Sources */, DD0650F52DCF303F004D3B41 /* AlarmStepperSection.swift in Sources */, DDCF9A802D85FD0B004DF4DD /* Alarm.swift in Sources */, diff --git a/LoopFollow/Alarm/AlarmCondition/TempTargetStartCondition.swift b/LoopFollow/Alarm/AlarmCondition/TempTargetStartCondition.swift new file mode 100644 index 000000000..982ba89e9 --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/TempTargetStartCondition.swift @@ -0,0 +1,26 @@ +// +// TempTargetStartCondition.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-14. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import Foundation + +/// Fires once when a temp-target is activated. +struct TempTargetStartCondition: AlarmCondition { + static let type: AlarmType = .tempTargetStart + init() {} + + func evaluate(alarm _: Alarm, data: AlarmData) -> Bool { + guard let startTS = data.latestTempTargetStart, startTS > 0 else { return false } + guard Date().timeIntervalSince1970 - startTS <= 15 * 60 else { return false } + + let last = Storage.shared.lastTempTargetStartNotified.value ?? 0 + guard startTS > last else { return false } + + Storage.shared.lastTempTargetStartNotified.value = startTS + return true + } +} diff --git a/LoopFollow/Alarm/AlarmData.swift b/LoopFollow/Alarm/AlarmData.swift index ab1246192..b0e382e60 100644 --- a/LoopFollow/Alarm/AlarmData.swift +++ b/LoopFollow/Alarm/AlarmData.swift @@ -15,6 +15,7 @@ struct AlarmData: Codable { let lastLoopTime: TimeInterval? let latestOverrideStart: TimeInterval? let latestOverrideEnd: TimeInterval? + let latestTempTargetStart: TimeInterval? } /* diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index e995c013c..f3617012e 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -57,6 +57,8 @@ struct AlarmEditor: View { OverrideStartAlarmEditor(alarm: $alarm) case .overrideEnd: OverrideEndAlarmEditor(alarm: $alarm) + case .tempTargetStart: + TempTargetStartAlarmEditor(alarm: $alarm) /* TODO: add other condition types here */ default: Text("No editor for \(alarm.type.rawValue)") diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift new file mode 100644 index 000000000..dd5d8d3e1 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift @@ -0,0 +1,26 @@ +// +// TempTargetStartAlarmEditor.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-14. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import SwiftUI + +struct TempTargetStartAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Form { + InfoBanner(text: "Alerts when a temp target starts.", alarmType: alarm.type) + + AlarmGeneralSection(alarm: $alarm) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm, hideRepeat: true) + AlarmSnoozeSection(alarm: $alarm, range: 10 ... 60, step: 5) + } + .navigationTitle(alarm.type.rawValue) + } +} diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index 78d934c22..0bda22e51 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -24,6 +24,7 @@ class AlarmManager { NotLoopingCondition.self, OverrideStartCondition.self, OverrideEndCondition.self, + TempTargetStartCondition.self, // TODO: add other condition types here ] ) { diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index e19c12faf..10ec2386d 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -55,6 +55,7 @@ class Storage { var lastOverrideStartNotified = StorageValue(key: "lastOverrideStartNotified", defaultValue: nil) var lastOverrideEndNotified = StorageValue(key: "lastOverrideEndNotified", defaultValue: nil) + var lastTempTargetStartNotified = StorageValue(key: "lastTempTargetStartNotified", defaultValue: nil) static let shared = Storage() private init() {} diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index 07d9a57b1..704359c04 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -21,6 +21,7 @@ extension MainViewController { DispatchQueue.main.async { let latestOverrideStart = self.overrideGraphData.last?.date let latestOverrideEnd = self.overrideGraphData.last?.endDate + let latestTempTargetStart = self.tempTargetGraphData.last?.date let alarmData = AlarmData( bgReadings: self.bgData @@ -33,6 +34,7 @@ extension MainViewController { lastLoopTime: UserDefaultsRepository.alertLastLoopTime.value, latestOverrideStart: latestOverrideStart, latestOverrideEnd: latestOverrideEnd, + latestTempTargetStart: latestTempTargetStart ) let finalAlarmData: AlarmData From a36b7acfd2f66ce2d60001297504b3fa8f3c11b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 14 May 2025 21:04:18 +0200 Subject: [PATCH 057/138] TempTargetEnd --- LoopFollow.xcodeproj/project.pbxproj | 8 ++++++ .../TempTargetEndCondition.swift | 26 +++++++++++++++++++ LoopFollow/Alarm/AlarmData.swift | 1 + .../Alarm/AlarmEditing/AlarmEditor.swift | 2 ++ .../Editors/TempTargetEndAlarmEditor.swift | 26 +++++++++++++++++++ LoopFollow/Alarm/AlarmManager.swift | 1 + LoopFollow/Storage/Storage.swift | 1 + LoopFollow/Task/AlarmTask.swift | 4 ++- 8 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 LoopFollow/Alarm/AlarmCondition/TempTargetEndCondition.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 14761091a..fadc8fee8 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -109,6 +109,8 @@ DD7F4C0D2DD51C8100D449E9 /* OverrideEndAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C0C2DD51C8100D449E9 /* OverrideEndAlarmEditor.swift */; }; DD7F4C0F2DD51EC200D449E9 /* TempTargetStartCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C0E2DD51EC200D449E9 /* TempTargetStartCondition.swift */; }; DD7F4C112DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C102DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift */; }; + DD7F4C132DD51FD500D449E9 /* TempTargetEndCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C122DD51FD500D449E9 /* TempTargetEndCondition.swift */; }; + DD7F4C152DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C142DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift */; }; DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */; }; DD85E9952D739CFE001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */; }; DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */; }; @@ -442,6 +444,8 @@ DD7F4C0C2DD51C8100D449E9 /* OverrideEndAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideEndAlarmEditor.swift; sourceTree = ""; }; DD7F4C0E2DD51EC200D449E9 /* TempTargetStartCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetStartCondition.swift; sourceTree = ""; }; DD7F4C102DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetStartAlarmEditor.swift; sourceTree = ""; }; + DD7F4C122DD51FD500D449E9 /* TempTargetEndCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetEndCondition.swift; sourceTree = ""; }; + DD7F4C142DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetEndAlarmEditor.swift; sourceTree = ""; }; DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EKEventStore+Extensions.swift"; sourceTree = ""; }; DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnipodDashHeartbeatBluetoothTransmitter.swift; sourceTree = ""; }; DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseConversion.swift; sourceTree = ""; }; @@ -710,6 +714,7 @@ DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = { isa = PBXGroup; children = ( + DD7F4C122DD51FD500D449E9 /* TempTargetEndCondition.swift */, DD7F4C0E2DD51EC200D449E9 /* TempTargetStartCondition.swift */, DD7F4C0A2DD51C5500D449E9 /* OverrideEndCondition.swift */, DD7F4C042DD4BBE200D449E9 /* NotLoopingCondition.swift */, @@ -965,6 +970,7 @@ DDC7E53F2DBD8A1600EB1127 /* Editors */ = { isa = PBXGroup; children = ( + DD7F4C142DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift */, DD7F4C102DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift */, DD7F4C0C2DD51C8100D449E9 /* OverrideEndAlarmEditor.swift */, DD7F4C062DD5042F00D449E9 /* OverrideStartAlarmEditor.swift */, @@ -1856,6 +1862,7 @@ DDD10F052C529DA200D76A8E /* ObservableValue.swift in Sources */, FC1BDD2D24A23204001B652C /* StatsView.swift in Sources */, DD4878102C7B74BF0048F05C /* TrioRemoteControlView.swift in Sources */, + DD7F4C152DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift in Sources */, DD2C2E542D3C37DC006413A5 /* DexcomSettingsViewModel.swift in Sources */, FCE537BC249A4D7D00F80BF8 /* carbBolusArrays.swift in Sources */, DD5334232C60ED3600062F9D /* IAge.swift in Sources */, @@ -1867,6 +1874,7 @@ DD50C7552D0862770057AE6F /* ContactImageUpdater.swift in Sources */, DD0650F52DCF303F004D3B41 /* AlarmStepperSection.swift in Sources */, DDCF9A802D85FD0B004DF4DD /* Alarm.swift in Sources */, + DD7F4C132DD51FD500D449E9 /* TempTargetEndCondition.swift in Sources */, DDD10F012C510C6B00D76A8E /* ObservableUserDefaults.swift in Sources */, DD16AF0D2C98485400FB655A /* SecureStorageValue.swift in Sources */, DDC7E5CF2DC77C2000EB1127 /* SnoozerViewModel.swift in Sources */, diff --git a/LoopFollow/Alarm/AlarmCondition/TempTargetEndCondition.swift b/LoopFollow/Alarm/AlarmCondition/TempTargetEndCondition.swift new file mode 100644 index 000000000..902ab1409 --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/TempTargetEndCondition.swift @@ -0,0 +1,26 @@ +// +// TempTargetEndCondition.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-14. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import Foundation + +/// Fires once when the active temp target ends. +struct TempTargetEndCondition: AlarmCondition { + static let type: AlarmType = .tempTargetEnd + init() {} + + func evaluate(alarm _: Alarm, data: AlarmData) -> Bool { + guard let endTS = data.latestTempTargetEnd, endTS > 0 else { return false } + guard Date().timeIntervalSince1970 - endTS <= 15 * 60 else { return false } + + let last = Storage.shared.lastTempTargetEndNotified.value ?? 0 + guard endTS > last else { return false } + + Storage.shared.lastTempTargetEndNotified.value = endTS + return true + } +} diff --git a/LoopFollow/Alarm/AlarmData.swift b/LoopFollow/Alarm/AlarmData.swift index b0e382e60..4a6a527a4 100644 --- a/LoopFollow/Alarm/AlarmData.swift +++ b/LoopFollow/Alarm/AlarmData.swift @@ -16,6 +16,7 @@ struct AlarmData: Codable { let latestOverrideStart: TimeInterval? let latestOverrideEnd: TimeInterval? let latestTempTargetStart: TimeInterval? + let latestTempTargetEnd: TimeInterval? } /* diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index f3617012e..551b6c6d7 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -59,6 +59,8 @@ struct AlarmEditor: View { OverrideEndAlarmEditor(alarm: $alarm) case .tempTargetStart: TempTargetStartAlarmEditor(alarm: $alarm) + case .tempTargetEnd: + TempTargetEndAlarmEditor(alarm: $alarm) /* TODO: add other condition types here */ default: Text("No editor for \(alarm.type.rawValue)") diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift new file mode 100644 index 000000000..7fd315e05 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift @@ -0,0 +1,26 @@ +// +// TempTargetEndAlarmEditor.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-14. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import SwiftUI + +struct TempTargetEndAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Form { + InfoBanner(text: "Alerts when a temp target ends.", alarmType: alarm.type) + + AlarmGeneralSection(alarm: $alarm) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm, hideRepeat: true) + AlarmSnoozeSection(alarm: $alarm, range: 10 ... 60, step: 5) + } + .navigationTitle(alarm.type.rawValue) + } +} diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index 0bda22e51..f4483bb49 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -25,6 +25,7 @@ class AlarmManager { OverrideStartCondition.self, OverrideEndCondition.self, TempTargetStartCondition.self, + TempTargetEndCondition.self, // TODO: add other condition types here ] ) { diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index 10ec2386d..00fe27dcd 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -56,6 +56,7 @@ class Storage { var lastOverrideStartNotified = StorageValue(key: "lastOverrideStartNotified", defaultValue: nil) var lastOverrideEndNotified = StorageValue(key: "lastOverrideEndNotified", defaultValue: nil) var lastTempTargetStartNotified = StorageValue(key: "lastTempTargetStartNotified", defaultValue: nil) + var lastTempTargetEndNotified = StorageValue(key: "lastTempTargetEndNotified", defaultValue: nil) static let shared = Storage() private init() {} diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index 704359c04..1fb636eeb 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -22,6 +22,7 @@ extension MainViewController { let latestOverrideStart = self.overrideGraphData.last?.date let latestOverrideEnd = self.overrideGraphData.last?.endDate let latestTempTargetStart = self.tempTargetGraphData.last?.date + let latestTempTargetEnd = self.tempTargetGraphData.last?.endDate let alarmData = AlarmData( bgReadings: self.bgData @@ -34,7 +35,8 @@ extension MainViewController { lastLoopTime: UserDefaultsRepository.alertLastLoopTime.value, latestOverrideStart: latestOverrideStart, latestOverrideEnd: latestOverrideEnd, - latestTempTargetStart: latestTempTargetStart + latestTempTargetStart: latestTempTargetStart, + latestTempTargetEnd: latestTempTargetEnd, ) let finalAlarmData: AlarmData From e94054684d7a6a519d27e08759faf164438c7408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Thu, 15 May 2025 18:17:36 +0200 Subject: [PATCH 058/138] RecBolus --- LoopFollow.xcodeproj/project.pbxproj | 8 ++++ .../AlarmCondition/RecBolusCondition.swift | 36 ++++++++++++++++ LoopFollow/Alarm/AlarmData.swift | 3 +- .../Alarm/AlarmEditing/AlarmEditor.swift | 2 + .../Editors/RecBolusAlarmEditor.swift | 42 +++++++++++++++++++ LoopFollow/Alarm/AlarmManager.swift | 1 + LoopFollow/Storage/Storage.swift | 1 + LoopFollow/Task/AlarmTask.swift | 2 + 8 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 LoopFollow/Alarm/AlarmCondition/RecBolusCondition.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index fadc8fee8..b8b200cb1 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -111,6 +111,8 @@ DD7F4C112DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C102DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift */; }; DD7F4C132DD51FD500D449E9 /* TempTargetEndCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C122DD51FD500D449E9 /* TempTargetEndCondition.swift */; }; DD7F4C152DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C142DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift */; }; + DD7F4C172DD63FA700D449E9 /* RecBolusCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C162DD63FA700D449E9 /* RecBolusCondition.swift */; }; + DD7F4C192DD63FD500D449E9 /* RecBolusAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C182DD63FD500D449E9 /* RecBolusAlarmEditor.swift */; }; DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */; }; DD85E9952D739CFE001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */; }; DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */; }; @@ -446,6 +448,8 @@ DD7F4C102DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetStartAlarmEditor.swift; sourceTree = ""; }; DD7F4C122DD51FD500D449E9 /* TempTargetEndCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetEndCondition.swift; sourceTree = ""; }; DD7F4C142DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetEndAlarmEditor.swift; sourceTree = ""; }; + DD7F4C162DD63FA700D449E9 /* RecBolusCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecBolusCondition.swift; sourceTree = ""; }; + DD7F4C182DD63FD500D449E9 /* RecBolusAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecBolusAlarmEditor.swift; sourceTree = ""; }; DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EKEventStore+Extensions.swift"; sourceTree = ""; }; DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnipodDashHeartbeatBluetoothTransmitter.swift; sourceTree = ""; }; DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseConversion.swift; sourceTree = ""; }; @@ -714,6 +718,7 @@ DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = { isa = PBXGroup; children = ( + DD7F4C162DD63FA700D449E9 /* RecBolusCondition.swift */, DD7F4C122DD51FD500D449E9 /* TempTargetEndCondition.swift */, DD7F4C0E2DD51EC200D449E9 /* TempTargetStartCondition.swift */, DD7F4C0A2DD51C5500D449E9 /* OverrideEndCondition.swift */, @@ -970,6 +975,7 @@ DDC7E53F2DBD8A1600EB1127 /* Editors */ = { isa = PBXGroup; children = ( + DD7F4C182DD63FD500D449E9 /* RecBolusAlarmEditor.swift */, DD7F4C142DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift */, DD7F4C102DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift */, DD7F4C0C2DD51C8100D449E9 /* OverrideEndAlarmEditor.swift */, @@ -1729,6 +1735,7 @@ DD7F4C072DD5042F00D449E9 /* OverrideStartAlarmEditor.swift in Sources */, DDDF6F492D479AF000884336 /* NoRemoteView.swift in Sources */, DD0650ED2DCE9371004D3B41 /* HighBgAlarmEditor.swift in Sources */, + DD7F4C172DD63FA700D449E9 /* RecBolusCondition.swift in Sources */, FCC6886F2489A53800A0279D /* AppConstants.swift in Sources */, DD7E19842ACDA50C00DBD158 /* Overrides.swift in Sources */, DDF2C0102BEFA991007A20E6 /* GitHubService.swift in Sources */, @@ -1810,6 +1817,7 @@ DD58171A2D299EF80041FB98 /* DexcomHeartbeatBluetoothDevice.swift in Sources */, DD7F4C0B2DD51C5500D449E9 /* OverrideEndCondition.swift in Sources */, DDFF3D872D14280500BF9D9E /* BackgroundRefreshSettingsViewModel.swift in Sources */, + DD7F4C192DD63FD500D449E9 /* RecBolusAlarmEditor.swift in Sources */, DD50C7502D0828800057AE6F /* ContactSettingsViewModel.swift in Sources */, DDAD162F2D2EF9830084BE10 /* RileyLinkHeartbeatBluetoothDevice.swift in Sources */, FC97881C2485969B00A7906C /* MainViewController.swift in Sources */, diff --git a/LoopFollow/Alarm/AlarmCondition/RecBolusCondition.swift b/LoopFollow/Alarm/AlarmCondition/RecBolusCondition.swift new file mode 100644 index 000000000..54400d47b --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/RecBolusCondition.swift @@ -0,0 +1,36 @@ +// +// RecBolusCondition.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-15. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import Foundation + +/// Fires once when the recommended bolus (units) is ≥ the user-set threshold. +struct RecBolusCondition: AlarmCondition { + static let type: AlarmType = .recBolus + init() {} + + func evaluate(alarm: Alarm, data: AlarmData) -> Bool { + // ──────────────────────────────── + // 0. sanity checks + // ──────────────────────────────── + guard let threshold = alarm.threshold, threshold > 0 else { return false } + guard let rec = data.latestRecBolus, rec >= threshold else { + Storage.shared.lastRecBolusNotified.value = nil + return false + } + + // ──────────────────────────────── + // 1. has it INCREASED past the last-notified value? + // ──────────────────────────────── + if let last = Storage.shared.lastRecBolusNotified.value { + if rec <= last + 1e-4 { return false } + } + + Storage.shared.lastRecBolusNotified.value = rec + return true + } +} diff --git a/LoopFollow/Alarm/AlarmData.swift b/LoopFollow/Alarm/AlarmData.swift index 4a6a527a4..024e00d30 100644 --- a/LoopFollow/Alarm/AlarmData.swift +++ b/LoopFollow/Alarm/AlarmData.swift @@ -17,6 +17,7 @@ struct AlarmData: Codable { let latestOverrideEnd: TimeInterval? let latestTempTargetStart: TimeInterval? let latestTempTargetEnd: TimeInterval? + let latestRecBolus: Double? } /* @@ -25,7 +26,5 @@ struct AlarmData: Codable { // let latestBoluses: [BolusEntry] // let batteryLevel: Double? // let latestCarbs: [CarbEntry] - // let overrideData: [OverrideEntry] - // let tempTargetData: [TempTargetEntry] // let pumpVolume: Double? */ diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index 551b6c6d7..b1e08a708 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -61,6 +61,8 @@ struct AlarmEditor: View { TempTargetStartAlarmEditor(alarm: $alarm) case .tempTargetEnd: TempTargetEndAlarmEditor(alarm: $alarm) + case .recBolus: + RecBolusAlarmEditor(alarm: $alarm) /* TODO: add other condition types here */ default: Text("No editor for \(alarm.type.rawValue)") diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift new file mode 100644 index 000000000..2812ad658 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift @@ -0,0 +1,42 @@ +// +// RecBolusAlarmEditor.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-15. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import SwiftUI + +struct RecBolusAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Form { + InfoBanner( + text: "Alerts when the recommended bolus equals or exceeds the " + + "threshold you set below.", + alarmType: alarm.type + ) + + AlarmGeneralSection(alarm: $alarm) + + AlarmStepperSection( + header: "Threshold", + footer: "Alert when recommended bolus ≥ this value.", + title: "Units", + range: 0.1 ... 50, + step: 0.1, + value: Binding( + get: { alarm.threshold ?? 1.0 }, + set: { alarm.threshold = $0 } + ) + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm, range: 5 ... 60, step: 5) + } + .navigationTitle(alarm.type.rawValue) + } +} diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index f4483bb49..0290d5f9d 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -26,6 +26,7 @@ class AlarmManager { OverrideEndCondition.self, TempTargetStartCondition.self, TempTargetEndCondition.self, + RecBolusCondition.self, // TODO: add other condition types here ] ) { diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index 00fe27dcd..b54a29d7e 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -57,6 +57,7 @@ class Storage { var lastOverrideEndNotified = StorageValue(key: "lastOverrideEndNotified", defaultValue: nil) var lastTempTargetStartNotified = StorageValue(key: "lastTempTargetStartNotified", defaultValue: nil) var lastTempTargetEndNotified = StorageValue(key: "lastTempTargetEndNotified", defaultValue: nil) + var lastRecBolusNotified = StorageValue(key: "lastRecBolusNotified", defaultValue: nil) static let shared = Storage() private init() {} diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index 1fb636eeb..b8fe1a819 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -23,6 +23,7 @@ extension MainViewController { let latestOverrideEnd = self.overrideGraphData.last?.endDate let latestTempTargetStart = self.tempTargetGraphData.last?.date let latestTempTargetEnd = self.tempTargetGraphData.last?.endDate + let latestRecBolus = UserDefaultsRepository.deviceRecBolus.value let alarmData = AlarmData( bgReadings: self.bgData @@ -37,6 +38,7 @@ extension MainViewController { latestOverrideEnd: latestOverrideEnd, latestTempTargetStart: latestTempTargetStart, latestTempTargetEnd: latestTempTargetEnd, + latestRecBolus: latestRecBolus ) let finalAlarmData: AlarmData From 4c0ef75ec02dd8af342ef6dabfcdf7dbe2999425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Thu, 15 May 2025 18:22:57 +0200 Subject: [PATCH 059/138] RecBolus --- LoopFollow/Storage/Observable.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/LoopFollow/Storage/Observable.swift b/LoopFollow/Storage/Observable.swift index abd6c5004..426344bf5 100644 --- a/LoopFollow/Storage/Observable.swift +++ b/LoopFollow/Storage/Observable.swift @@ -19,7 +19,6 @@ class Observable { var tempTarget = ObservableValue(default: nil) var override = ObservableValue(default: nil) - var lastRecBolusTriggered = ObservableValue(default: nil) var minAgoText = ObservableValue(default: "?? min ago") var bgText = ObservableValue(default: "BG") From b47134cc6b5b22b8f9768e1789d4c2b7187ea80d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Thu, 15 May 2025 19:11:32 +0200 Subject: [PATCH 060/138] COBAlarm --- LoopFollow.xcodeproj/project.pbxproj | 8 ++++ .../Alarm/AlarmCondition/COBCondition.swift | 31 ++++++++++++ .../AlarmCondition/RecBolusCondition.swift | 2 +- LoopFollow/Alarm/AlarmData.swift | 4 +- .../Alarm/AlarmEditing/AlarmEditor.swift | 2 + .../AlarmEditing/Editors/COBAlarmEditor.swift | 47 +++++++++++++++++++ LoopFollow/Alarm/AlarmManager.swift | 2 + LoopFollow/Alarm/AlarmType.swift | 4 +- LoopFollow/Storage/Storage.swift | 1 + LoopFollow/Task/AlarmTask.swift | 6 ++- 10 files changed, 100 insertions(+), 7 deletions(-) create mode 100644 LoopFollow/Alarm/AlarmCondition/COBCondition.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index b8b200cb1..3ef0b1541 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -113,6 +113,8 @@ DD7F4C152DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C142DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift */; }; DD7F4C172DD63FA700D449E9 /* RecBolusCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C162DD63FA700D449E9 /* RecBolusCondition.swift */; }; DD7F4C192DD63FD500D449E9 /* RecBolusAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C182DD63FD500D449E9 /* RecBolusAlarmEditor.swift */; }; + DD7F4C1B2DD6501D00D449E9 /* COBCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C1A2DD6501D00D449E9 /* COBCondition.swift */; }; + DD7F4C1D2DD650D500D449E9 /* COBAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C1C2DD650D500D449E9 /* COBAlarmEditor.swift */; }; DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */; }; DD85E9952D739CFE001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */; }; DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */; }; @@ -450,6 +452,8 @@ DD7F4C142DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetEndAlarmEditor.swift; sourceTree = ""; }; DD7F4C162DD63FA700D449E9 /* RecBolusCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecBolusCondition.swift; sourceTree = ""; }; DD7F4C182DD63FD500D449E9 /* RecBolusAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecBolusAlarmEditor.swift; sourceTree = ""; }; + DD7F4C1A2DD6501D00D449E9 /* COBCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = COBCondition.swift; sourceTree = ""; }; + DD7F4C1C2DD650D500D449E9 /* COBAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = COBAlarmEditor.swift; sourceTree = ""; }; DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EKEventStore+Extensions.swift"; sourceTree = ""; }; DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnipodDashHeartbeatBluetoothTransmitter.swift; sourceTree = ""; }; DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseConversion.swift; sourceTree = ""; }; @@ -718,6 +722,7 @@ DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = { isa = PBXGroup; children = ( + DD7F4C1A2DD6501D00D449E9 /* COBCondition.swift */, DD7F4C162DD63FA700D449E9 /* RecBolusCondition.swift */, DD7F4C122DD51FD500D449E9 /* TempTargetEndCondition.swift */, DD7F4C0E2DD51EC200D449E9 /* TempTargetStartCondition.swift */, @@ -975,6 +980,7 @@ DDC7E53F2DBD8A1600EB1127 /* Editors */ = { isa = PBXGroup; children = ( + DD7F4C1C2DD650D500D449E9 /* COBAlarmEditor.swift */, DD7F4C182DD63FD500D449E9 /* RecBolusAlarmEditor.swift */, DD7F4C142DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift */, DD7F4C102DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift */, @@ -1713,6 +1719,7 @@ DD9ACA102D34129200415D8A /* Task.swift in Sources */, DD4AFB492DB576C200BB593F /* AlarmSettingsView.swift in Sources */, DD7F4C032DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift in Sources */, + DD7F4C1B2DD6501D00D449E9 /* COBCondition.swift in Sources */, DD4878202C7DAF890048F05C /* PushMessage.swift in Sources */, DD0247712DB4337700FCADF6 /* BuildExpireCondition.swift in Sources */, DDF6999B2C5AA32E0058A8D9 /* TempTargetPreset.swift in Sources */, @@ -1758,6 +1765,7 @@ DDBE3ABD2CB5A961006B37DC /* OverrideView.swift in Sources */, DDB0AF522BB1A8BE00AFA48B /* BuildDetails.swift in Sources */, DDA9ACAA2D6A6B8300E6F1A9 /* ContactIncludeOption.swift in Sources */, + DD7F4C1D2DD650D500D449E9 /* COBAlarmEditor.swift in Sources */, DD0C0C622C4175FD00DBADDF /* NSProfile.swift in Sources */, DD58171E2D299FCA0041FB98 /* BluetoothDeviceDelegate.swift in Sources */, DDE69ED22C7256260013EAEC /* RemoteType.swift in Sources */, diff --git a/LoopFollow/Alarm/AlarmCondition/COBCondition.swift b/LoopFollow/Alarm/AlarmCondition/COBCondition.swift new file mode 100644 index 000000000..34a6a66b0 --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/COBCondition.swift @@ -0,0 +1,31 @@ +// +// COBCondition.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-15. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import Foundation + +struct COBCondition: AlarmCondition { + static let type: AlarmType = .cob + init() {} + + func evaluate(alarm: Alarm, data: AlarmData) -> Bool { + guard let threshold = alarm.threshold, threshold > 0 else { return false } + guard let cob = data.COB, cob >= threshold else { + Storage.shared.lastCOBNotified.value = nil + return false + } + + if let last = Storage.shared.lastCOBNotified.value, + !(cob > last) + { + return false + } + + Storage.shared.lastCOBNotified.value = cob + return true + } +} diff --git a/LoopFollow/Alarm/AlarmCondition/RecBolusCondition.swift b/LoopFollow/Alarm/AlarmCondition/RecBolusCondition.swift index 54400d47b..8f9a8a650 100644 --- a/LoopFollow/Alarm/AlarmCondition/RecBolusCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/RecBolusCondition.swift @@ -18,7 +18,7 @@ struct RecBolusCondition: AlarmCondition { // 0. sanity checks // ──────────────────────────────── guard let threshold = alarm.threshold, threshold > 0 else { return false } - guard let rec = data.latestRecBolus, rec >= threshold else { + guard let rec = data.recBolus, rec >= threshold else { Storage.shared.lastRecBolusNotified.value = nil return false } diff --git a/LoopFollow/Alarm/AlarmData.swift b/LoopFollow/Alarm/AlarmData.swift index 024e00d30..82380c2f9 100644 --- a/LoopFollow/Alarm/AlarmData.swift +++ b/LoopFollow/Alarm/AlarmData.swift @@ -17,12 +17,12 @@ struct AlarmData: Codable { let latestOverrideEnd: TimeInterval? let latestTempTargetStart: TimeInterval? let latestTempTargetEnd: TimeInterval? - let latestRecBolus: Double? + let recBolus: Double? + let COB: Double? } /* // let iob: Double? - // let cob: Double? // let latestBoluses: [BolusEntry] // let batteryLevel: Double? // let latestCarbs: [CarbEntry] diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index b1e08a708..b0abb508c 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -63,6 +63,8 @@ struct AlarmEditor: View { TempTargetEndAlarmEditor(alarm: $alarm) case .recBolus: RecBolusAlarmEditor(alarm: $alarm) + case .cob: + COBAlarmEditor(alarm: $alarm) /* TODO: add other condition types here */ default: Text("No editor for \(alarm.type.rawValue)") diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift new file mode 100644 index 000000000..36443c190 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift @@ -0,0 +1,47 @@ +// +// COBAlarmEditor.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-15. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import SwiftUI + +struct COBAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Form { + InfoBanner( + text: "Alerts when Carbs-on-Board exceeds the amount you set below.", + alarmType: alarm.type + ) + + AlarmGeneralSection(alarm: $alarm) + + AlarmStepperSection( + header: "Threshold", + footer: "Alert when COB ≥ this many grams.", + title: "COB", + range: 1 ... 200, + step: 1, + unitLabel: "g", + value: Binding( + get: { alarm.threshold ?? 20 }, + set: { alarm.threshold = $0 } + ) + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + + AlarmSnoozeSection( + alarm: $alarm, + range: 1 ... 6, + step: 1 + ) + } + .navigationTitle(alarm.type.rawValue) + } +} diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index 0290d5f9d..671101a5f 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -27,6 +27,8 @@ class AlarmManager { TempTargetStartCondition.self, TempTargetEndCondition.self, RecBolusCondition.self, + COBCondition.self, + MissedReadingCondition.self, // TODO: add other condition types here ] ) { diff --git a/LoopFollow/Alarm/AlarmType.swift b/LoopFollow/Alarm/AlarmType.swift index b7b94933e..b946058d9 100644 --- a/LoopFollow/Alarm/AlarmType.swift +++ b/LoopFollow/Alarm/AlarmType.swift @@ -62,11 +62,11 @@ extension AlarmType { return .day case .low, .high, .fastDrop, .fastRise, .missedReading, .notLooping, .missedBolus, - .iob, .bolus, .cob, .recBolus, + .bolus, .recBolus, .overrideStart, .overrideEnd, .tempTargetStart, .tempTargetEnd: return .minute - case .battery, .batteryDrop, .sensorChange, .pumpChange, + case .battery, .batteryDrop, .sensorChange, .pumpChange, .cob, .iob, .pump: return .hour } diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index b54a29d7e..5102ba1da 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -58,6 +58,7 @@ class Storage { var lastTempTargetStartNotified = StorageValue(key: "lastTempTargetStartNotified", defaultValue: nil) var lastTempTargetEndNotified = StorageValue(key: "lastTempTargetEndNotified", defaultValue: nil) var lastRecBolusNotified = StorageValue(key: "lastRecBolusNotified", defaultValue: nil) + var lastCOBNotified = StorageValue(key: "lastCOBNotified", defaultValue: nil) static let shared = Storage() private init() {} diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index b8fe1a819..b7a5e3272 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -23,7 +23,8 @@ extension MainViewController { let latestOverrideEnd = self.overrideGraphData.last?.endDate let latestTempTargetStart = self.tempTargetGraphData.last?.date let latestTempTargetEnd = self.tempTargetGraphData.last?.endDate - let latestRecBolus = UserDefaultsRepository.deviceRecBolus.value + let recBolus = UserDefaultsRepository.deviceRecBolus.value + let COB = self.latestCOB?.value let alarmData = AlarmData( bgReadings: self.bgData @@ -38,7 +39,8 @@ extension MainViewController { latestOverrideEnd: latestOverrideEnd, latestTempTargetStart: latestTempTargetStart, latestTempTargetEnd: latestTempTargetEnd, - latestRecBolus: latestRecBolus + recBolus: recBolus, + COB: COB ) let finalAlarmData: AlarmData From 1b28e0067dd555fc11686f9ba4f0b9cdbf6aa90d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Thu, 15 May 2025 20:58:46 +0200 Subject: [PATCH 061/138] FastRise --- LoopFollow.xcodeproj/project.pbxproj | 8 +++ .../AlarmCondition/FastRiseCondition.swift | 35 ++++++++++ .../Alarm/AlarmEditing/AlarmEditor.swift | 2 + .../Editors/FastDropAlarmEditor.swift | 2 +- .../Editors/FastRiseAlarmEditor.swift | 65 +++++++++++++++++++ LoopFollow/Alarm/AlarmManager.swift | 1 + 6 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 LoopFollow/Alarm/AlarmCondition/FastRiseCondition.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 3ef0b1541..2e0619564 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -115,6 +115,8 @@ DD7F4C192DD63FD500D449E9 /* RecBolusAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C182DD63FD500D449E9 /* RecBolusAlarmEditor.swift */; }; DD7F4C1B2DD6501D00D449E9 /* COBCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C1A2DD6501D00D449E9 /* COBCondition.swift */; }; DD7F4C1D2DD650D500D449E9 /* COBAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C1C2DD650D500D449E9 /* COBAlarmEditor.swift */; }; + DD7F4C1F2DD6648B00D449E9 /* FastRiseCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C1E2DD6648B00D449E9 /* FastRiseCondition.swift */; }; + DD7F4C212DD66BB200D449E9 /* FastRiseAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C202DD66BB100D449E9 /* FastRiseAlarmEditor.swift */; }; DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */; }; DD85E9952D739CFE001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */; }; DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */; }; @@ -454,6 +456,8 @@ DD7F4C182DD63FD500D449E9 /* RecBolusAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecBolusAlarmEditor.swift; sourceTree = ""; }; DD7F4C1A2DD6501D00D449E9 /* COBCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = COBCondition.swift; sourceTree = ""; }; DD7F4C1C2DD650D500D449E9 /* COBAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = COBAlarmEditor.swift; sourceTree = ""; }; + DD7F4C1E2DD6648B00D449E9 /* FastRiseCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastRiseCondition.swift; sourceTree = ""; }; + DD7F4C202DD66BB100D449E9 /* FastRiseAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastRiseAlarmEditor.swift; sourceTree = ""; }; DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EKEventStore+Extensions.swift"; sourceTree = ""; }; DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnipodDashHeartbeatBluetoothTransmitter.swift; sourceTree = ""; }; DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseConversion.swift; sourceTree = ""; }; @@ -722,6 +726,7 @@ DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = { isa = PBXGroup; children = ( + DD7F4C1E2DD6648B00D449E9 /* FastRiseCondition.swift */, DD7F4C1A2DD6501D00D449E9 /* COBCondition.swift */, DD7F4C162DD63FA700D449E9 /* RecBolusCondition.swift */, DD7F4C122DD51FD500D449E9 /* TempTargetEndCondition.swift */, @@ -980,6 +985,7 @@ DDC7E53F2DBD8A1600EB1127 /* Editors */ = { isa = PBXGroup; children = ( + DD7F4C202DD66BB100D449E9 /* FastRiseAlarmEditor.swift */, DD7F4C1C2DD650D500D449E9 /* COBAlarmEditor.swift */, DD7F4C182DD63FD500D449E9 /* RecBolusAlarmEditor.swift */, DD7F4C142DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift */, @@ -1771,6 +1777,7 @@ DDE69ED22C7256260013EAEC /* RemoteType.swift in Sources */, DDD10F032C518A6500D76A8E /* TreatmentResponse.swift in Sources */, DDD10F0B2C54192A00D76A8E /* TemporaryTarget.swift in Sources */, + DD7F4C1F2DD6648B00D449E9 /* FastRiseCondition.swift in Sources */, DDDF6F472D479AD200884336 /* LoopOverrideViewModel.swift in Sources */, DD5334272C61668800062F9D /* InfoDisplaySettingsViewModel.swift in Sources */, DD0247592DB2E89600FCADF6 /* AlarmCondition.swift in Sources */, @@ -1865,6 +1872,7 @@ DD4878132C7B750D0048F05C /* TempTargetView.swift in Sources */, DD0C0C682C48529400DBADDF /* Metric.swift in Sources */, FCC6886D2489909D00A0279D /* AnyConvertible.swift in Sources */, + DD7F4C212DD66BB200D449E9 /* FastRiseAlarmEditor.swift in Sources */, FC97881A2485969B00A7906C /* SceneDelegate.swift in Sources */, DD0C0C662C46E54C00DBADDF /* InfoDataSeparator.swift in Sources */, DD58171C2D299F940041FB98 /* BluetoothDevice.swift in Sources */, diff --git a/LoopFollow/Alarm/AlarmCondition/FastRiseCondition.swift b/LoopFollow/Alarm/AlarmCondition/FastRiseCondition.swift new file mode 100644 index 000000000..7086c0c2f --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/FastRiseCondition.swift @@ -0,0 +1,35 @@ +// +// FastRiseCondition.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-15. +// + +import Foundation + +struct FastRiseCondition: AlarmCondition { + static let type: AlarmType = .fastRise + init() {} + + func evaluate(alarm: Alarm, data: AlarmData) -> Bool { + guard + let risePerReading = alarm.delta, risePerReading > 0, + let risesNeeded = alarm.monitoringWindow, risesNeeded > 0, + data.bgReadings.count >= risesNeeded + 1 + else { return false } + + if let limit = alarm.threshold { + guard let latest = data.bgReadings.last, latest.sgv > 0 else { return false } + guard Double(latest.sgv) > limit else { return false } + } + + let recent = data.bgReadings.suffix(risesNeeded + 1) + let readings = Array(recent) + + for i in 1 ... risesNeeded { + let delta = Double(readings[i].sgv - readings[i - 1].sgv) + if delta < risePerReading { return false } + } + return true + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index b0abb508c..1dcc755f0 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -65,6 +65,8 @@ struct AlarmEditor: View { RecBolusAlarmEditor(alarm: $alarm) case .cob: COBAlarmEditor(alarm: $alarm) + case .fastRise: + FastRiseAlarmEditor(alarm: $alarm) /* TODO: add other condition types here */ default: Text("No editor for \(alarm.type.rawValue)") diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift index 9400ee435..036aadec3 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift @@ -34,7 +34,7 @@ struct FastDropAlarmEditor: View { // TODO: In the migration script, use 1 value less than stored since we are switching from readings to drops AlarmStepperSection( header: "Consecutive Drops", - footer: "Number of back-to-back drops—each meeting the rate above—required before an alert fires.", + footer: "Number of drops—each meeting the rate above—required before an alert fires.", title: "Drops in a row", range: 1 ... 3, step: 1, diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift new file mode 100644 index 000000000..8fce2ae89 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift @@ -0,0 +1,65 @@ +// +// FastRiseAlarmEditor.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-15. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import SwiftUI + +struct FastRiseAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Form { + InfoBanner( + text: "Alerts when glucose readings rise rapidly. For example, " + + "three straight readings each climbing by at least the amount " + + "you set. Optionally limit alerts to only fire above a certain BG.", + alarmType: alarm.type + ) + + AlarmGeneralSection(alarm: $alarm) + + AlarmBGSection( + header: "Rate of Rise", + footer: "How much the BG must rise to count as a “fast” rise.", + title: "Rise per reading", + range: 3 ... 20, + value: Binding( + get: { alarm.delta ?? 3 }, + set: { alarm.delta = $0 } + ) + ) + + AlarmStepperSection( + header: "Consecutive Rises", + footer: "Number of rises—each meeting the rate above—" + + "required before an alert fires.", + title: "Rises in a row", + range: 1 ... 3, + step: 1, + value: Binding( + get: { Double(alarm.monitoringWindow ?? 2) }, + set: { alarm.monitoringWindow = Int($0) } + ) + ) + + AlarmBGLimitSection( + header: "BG Limit", + footer: "When enabled, this alert only fires if the glucose is " + + "above the limit you set.", + toggleText: "Use BG Limit", + pickerTitle: "Rising above", + range: 40 ... 300, + value: $alarm.threshold + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm, range: 5 ... 60, step: 5) + } + .navigationTitle(alarm.type.rawValue) + } +} diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index 671101a5f..95ff41054 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -29,6 +29,7 @@ class AlarmManager { RecBolusCondition.self, COBCondition.self, MissedReadingCondition.self, + FastRiseCondition.self, // TODO: add other condition types here ] ) { From 667217c449f79eb2a227d2180bf0399fa7735a6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 16 May 2025 19:17:37 +0200 Subject: [PATCH 062/138] Refactoring --- LoopFollow.xcodeproj/project.pbxproj | 4 ++ LoopFollow/Alarm/Alarm.swift | 7 ++- .../Alarm/AlarmCondition/AlarmCondition.swift | 40 +++++++++++++++ .../AlarmCondition/FastDropCondition.swift | 8 +-- .../AlarmCondition/FastRiseCondition.swift | 23 ++++----- .../AlarmCondition/HighBGCondition.swift | 22 +++------ .../Alarm/AlarmCondition/LowBGCondition.swift | 8 ++- .../Editors/FastDropAlarmEditor.swift | 2 +- .../Editors/FastRiseAlarmEditor.swift | 2 +- .../Editors/HighBgAlarmEditor.swift | 4 +- .../Editors/LowBgAlarmEditor.swift | 4 +- LoopFollow/Alarm/AlarmManager.swift | 35 ++++++------- .../Alarm/AlarmType+SortDirection.swift | 49 +++++++++++++++++++ LoopFollow/Alarm/AlarmType.swift | 14 ------ 14 files changed, 138 insertions(+), 84 deletions(-) create mode 100644 LoopFollow/Alarm/AlarmType+SortDirection.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 2e0619564..f6c35cd66 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -117,6 +117,7 @@ DD7F4C1D2DD650D500D449E9 /* COBAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C1C2DD650D500D449E9 /* COBAlarmEditor.swift */; }; DD7F4C1F2DD6648B00D449E9 /* FastRiseCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C1E2DD6648B00D449E9 /* FastRiseCondition.swift */; }; DD7F4C212DD66BB200D449E9 /* FastRiseAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C202DD66BB100D449E9 /* FastRiseAlarmEditor.swift */; }; + DD7F4C232DD7A62200D449E9 /* AlarmType+SortDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C222DD7A62200D449E9 /* AlarmType+SortDirection.swift */; }; DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */; }; DD85E9952D739CFE001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */; }; DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */; }; @@ -458,6 +459,7 @@ DD7F4C1C2DD650D500D449E9 /* COBAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = COBAlarmEditor.swift; sourceTree = ""; }; DD7F4C1E2DD6648B00D449E9 /* FastRiseCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastRiseCondition.swift; sourceTree = ""; }; DD7F4C202DD66BB100D449E9 /* FastRiseAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastRiseAlarmEditor.swift; sourceTree = ""; }; + DD7F4C222DD7A62200D449E9 /* AlarmType+SortDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlarmType+SortDirection.swift"; sourceTree = ""; }; DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EKEventStore+Extensions.swift"; sourceTree = ""; }; DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnipodDashHeartbeatBluetoothTransmitter.swift; sourceTree = ""; }; DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseConversion.swift; sourceTree = ""; }; @@ -1036,6 +1038,7 @@ DDCF9A8B2D86005E004DF4DD /* AlarmManager.swift */, DDCF9A872D85FD33004DF4DD /* AlarmData.swift */, DDCF9A812D85FD14004DF4DD /* AlarmType.swift */, + DD7F4C222DD7A62200D449E9 /* AlarmType+SortDirection.swift */, DDCF9A7F2D85FD09004DF4DD /* Alarm.swift */, ); path = Alarm; @@ -1788,6 +1791,7 @@ DD493AE72ACF23CF009A6922 /* DeviceStatus.swift in Sources */, DDC7E5162DBCFA7F00EB1127 /* SnoozerView.swift in Sources */, FCFEECA2248857A600402A7F /* SettingsViewController.swift in Sources */, + DD7F4C232DD7A62200D449E9 /* AlarmType+SortDirection.swift in Sources */, DD0650F72DCFDA26004D3B41 /* InfoBanner.swift in Sources */, DD4AFB612DB68BBC00BB593F /* AlarmListView.swift in Sources */, DD7B0D442D730A3B0063DCB6 /* CycleHelper.swift in Sources */, diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index e0edcb599..06b5e679d 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -64,8 +64,11 @@ struct Alarm: Identifiable, Codable, Equatable { /// If the alarm is manually snoozed, we store the end time for the snooze here var snoozedUntil: Date? - /// Alarm threashold, it can be a bgvalue (in mg/Dl), or day for example - /// Also used as bg limit for drop alarms for example + /// BG alarm threasholds + var aboveBG: Double? + var belowBG: Double? + + /// Alarm threashold, it can be a day for example var threshold: Double? /// If the alarm looks at predictions, this is how long into the future to look diff --git a/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift b/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift index 09f40fba7..04e77b052 100644 --- a/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift @@ -16,6 +16,44 @@ protocol AlarmCondition { } extension AlarmCondition { + /// Returns `true` when the alarm is allowed to continue evaluating + /// after BG-limit checks; `false` blocks it immediately. + func passesBGLimits(alarm: Alarm, data: AlarmData) -> Bool { + let bgReading = data.bgReadings.last?.sgv + let haveBG = (bgReading ?? 0) > 0 + let bgValue = Double(bgReading ?? 0) + + // ──────────────────────────────────── + // 1. BG-based alarms always need data + // ──────────────────────────────────── + if alarm.type.isBGBased && !haveBG { return false } + + // ──────────────────────────────────── + // 2. No limits? we’re done. + // ──────────────────────────────────── + if alarm.belowBG == nil && alarm.aboveBG == nil { return true } + + // If we reach here, there *are* limits. + // Non-BG alarms without a reading must fail; + // BG-based alarms already bailed out above. + guard haveBG else { return false } + + switch (alarm.belowBG, alarm.aboveBG) { + case let (lo?, hi?): + return lo < hi ? (bgValue <= lo || bgValue >= hi) // fire outside band + : (hi <= bgValue && bgValue <= lo) // fire inside band + + case let (lo?, nil): + return bgValue <= lo + + case let (nil, hi?): + return bgValue >= hi + + default: + return true + } + } + /// applies every global & per-alarm guard exactly once func shouldFire(alarm: Alarm, data: AlarmData, now: Date, config: AlarmConfiguration) -> Bool { // master on/off @@ -23,6 +61,8 @@ extension AlarmCondition { // per-alarm snooze if let snooze = alarm.snoozedUntil, snooze > now { return false } + if !passesBGLimits(alarm: alarm, data: data) { return false } + // time-of-day guard let comps = Calendar.current.dateComponents([.hour, .minute], from: now) let nowMin = (comps.hour! * 60) + comps.minute! diff --git a/LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift b/LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift index c21d6d182..f075568d3 100644 --- a/LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift @@ -22,14 +22,9 @@ struct FastDropCondition: AlarmCondition { data.bgReadings.count >= dropsNeeded + 1 else { return false } - // optional BG-limit guard - if let limit = alarm.threshold { - guard let latest = data.bgReadings.last, latest.sgv > 0 else { return false } - guard Double(latest.sgv) < limit else { return false } - } - // ──────────────────────────────── // 1. compute recent deltas + // (BG-limit check is now handled by passesBGLimits) // ──────────────────────────────── let recent = data.bgReadings.suffix(dropsNeeded + 1) let readings = Array(recent) @@ -38,7 +33,6 @@ struct FastDropCondition: AlarmCondition { let delta = Double(readings[i - 1].sgv - readings[i].sgv) if delta < dropPerReading { return false } } - return true } } diff --git a/LoopFollow/Alarm/AlarmCondition/FastRiseCondition.swift b/LoopFollow/Alarm/AlarmCondition/FastRiseCondition.swift index 7086c0c2f..6201424f6 100644 --- a/LoopFollow/Alarm/AlarmCondition/FastRiseCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/FastRiseCondition.swift @@ -7,29 +7,24 @@ import Foundation +/// Fires when N consecutive BG deltas are ≥ `delta` mg/dL. struct FastRiseCondition: AlarmCondition { static let type: AlarmType = .fastRise init() {} func evaluate(alarm: Alarm, data: AlarmData) -> Bool { guard - let risePerReading = alarm.delta, risePerReading > 0, - let risesNeeded = alarm.monitoringWindow, risesNeeded > 0, - data.bgReadings.count >= risesNeeded + 1 + let rise = alarm.delta, rise > 0, + let streak = alarm.monitoringWindow, streak > 0, + data.bgReadings.count >= streak + 1 else { return false } - if let limit = alarm.threshold { - guard let latest = data.bgReadings.last, latest.sgv > 0 else { return false } - guard Double(latest.sgv) > limit else { return false } - } - - let recent = data.bgReadings.suffix(risesNeeded + 1) - let readings = Array(recent) + // grab the last (streak + 1) readings, newest last + let recent = data.bgReadings.suffix(streak + 1).map(\.sgv) - for i in 1 ... risesNeeded { - let delta = Double(readings[i].sgv - readings[i - 1].sgv) - if delta < risePerReading { return false } + // every forward delta must hit the threshold + return zip(recent.dropFirst(), recent).allSatisfy { + Double($0 - $1) >= rise } - return true } } diff --git a/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift b/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift index 809906fed..bc136a2dd 100644 --- a/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift @@ -8,27 +8,23 @@ import Foundation -/// Fires when **every** BG in `persistentMinutes` (if set) **and** the latest BG -/// are ≥ `threshold`. -/// — No predictive branch for highs. +/// Fires when the latest BG – and, if requested, every BG in a persistent-window – is **≥ aboveBG**. struct HighBGCondition: AlarmCondition { static let type: AlarmType = .high init() {} func evaluate(alarm: Alarm, data: AlarmData) -> Bool { // ──────────────────────────────── - // 0. sanity checks + // 0. get the limit // ──────────────────────────────── - guard let threshold = alarm.threshold else { return false } - guard let latest = data.bgReadings.last, latest.sgv > 0 else { return false } + guard let high = alarm.aboveBG else { return false } func isHigh(_ g: GlucoseValue) -> Bool { - g.sgv > 0 && Double(g.sgv) >= threshold + g.sgv > 0 && Double(g.sgv) >= high } - // ──────────────────────────────── - // 1. persistent-window guard - // ──────────────────────────────── + // we already know from `passesBGLimits` that the **latest** reading is ≥ high, + // but we still need to honour the “persistent for N minutes” option. var persistentOK = true if let persistentMinutes = alarm.persistentMinutes, persistentMinutes > 0 @@ -44,10 +40,6 @@ struct HighBGCondition: AlarmCondition { } } - // ──────────────────────────────── - // 2. final decision - // ──────────────────────────────── - let currentHigh = isHigh(latest) - return currentHigh && persistentOK + return persistentOK } } diff --git a/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift b/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift index c6350d67f..8f8596bb7 100644 --- a/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift @@ -19,11 +19,10 @@ struct LowBGCondition: AlarmCondition { // ──────────────────────────────── // 0. sanity checks // ──────────────────────────────── - guard let threshold = alarm.threshold else { return false } - guard let latest = data.bgReadings.last, latest.sgv > 0 else { return false } + guard let belowBG = alarm.belowBG else { return false } func isLow(_ g: GlucoseValue) -> Bool { - g.sgv > 0 && Double(g.sgv) <= threshold + g.sgv > 0 && Double(g.sgv) <= belowBG } // ──────────────────────────────── @@ -66,7 +65,6 @@ struct LowBGCondition: AlarmCondition { // ──────────────────────────────── // 3. final decision // ──────────────────────────────── - let currentLow = isLow(latest) - return (currentLow && persistentOK) || predictiveTrigger + return persistentOK || predictiveTrigger } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift index 036aadec3..33155525b 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift @@ -50,7 +50,7 @@ struct FastDropAlarmEditor: View { toggleText: "Use BG Limit", pickerTitle: "Dropping below", range: 40 ... 300, - value: $alarm.threshold + value: $alarm.belowBG ) AlarmActiveSection(alarm: $alarm) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift index 8fce2ae89..002f33bb7 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift @@ -53,7 +53,7 @@ struct FastRiseAlarmEditor: View { toggleText: "Use BG Limit", pickerTitle: "Rising above", range: 40 ... 300, - value: $alarm.threshold + value: $alarm.aboveBG ) AlarmActiveSection(alarm: $alarm) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift index 96133892f..da662054a 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift @@ -26,8 +26,8 @@ struct HighBgAlarmEditor: View { title: "BG", range: 120 ... 350, value: Binding( - get: { alarm.threshold ?? 180 }, - set: { alarm.threshold = $0 } + get: { alarm.aboveBG ?? 180 }, + set: { alarm.aboveBG = $0 } ) ) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift index 02e34b80d..fec435a46 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift @@ -24,8 +24,8 @@ struct LowBgAlarmEditor: View { title: "BG", range: 40 ... 150, value: Binding( - get: { alarm.threshold ?? 80 }, - set: { alarm.threshold = $0 } + get: { alarm.belowBG ?? 80 }, + set: { alarm.belowBG = $0 } ) ) diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index 95ff41054..a85f976d9 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -43,34 +43,27 @@ class AlarmManager { let alarms = Storage.shared.alarms.value let sorted = alarms.sorted { lhs, rhs in - // 1) by type priority + // 1) type-level priority (hard-coded table in AlarmType) if lhs.type.priority != rhs.type.priority { return lhs.type.priority < rhs.type.priority } - // 2) by “main” value for that type - if let asc = lhs.type.thresholdSortAscending { - // pick the right field: - let leftVal: Double - let rightVal: Double - - switch lhs.type { - // TODO: Make a alarm type setting of this, sortedBy or something like that - case .fastDrop, .fastRise: - // sort on the per-reading delta - leftVal = lhs.delta ?? (asc ? Double.infinity : -Double.infinity) - rightVal = rhs.delta ?? (asc ? Double.infinity : -Double.infinity) - - default: - // sort on the BG limit threshold - leftVal = lhs.threshold ?? (asc ? Double.infinity : -Double.infinity) - rightVal = rhs.threshold ?? (asc ? Double.infinity : -Double.infinity) + // 2) per-type “main value” ordering + if lhs.type == rhs.type, // only makes sense within the same type + let spec = lhs.type.sortSpec + { // (direction, key extractor) + let lv = spec.key(lhs) + let rv = spec.key(rhs) + + switch spec.direction { + case .ascending: // smaller ⇒ more urgent + return (lv ?? Double.infinity) < (rv ?? Double.infinity) + case .descending: // bigger ⇒ more urgent + return (lv ?? -Double.infinity) > (rv ?? -Double.infinity) } - - return asc ? (leftVal < rightVal) : (leftVal > rightVal) } - // 3) fallback + // 3) fallback – keep original insertion order return false } var skipType: AlarmType? diff --git a/LoopFollow/Alarm/AlarmType+SortDirection.swift b/LoopFollow/Alarm/AlarmType+SortDirection.swift new file mode 100644 index 000000000..838f5bbda --- /dev/null +++ b/LoopFollow/Alarm/AlarmType+SortDirection.swift @@ -0,0 +1,49 @@ +// +// AlarmType+SortDirection.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-16. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import Foundation + +// MARK: – Sorting helpers + +extension AlarmType { + /// Asc ⇢ “smaller number = more urgent”, Desc ⇢ “bigger number = more urgent” + enum SortDirection { case ascending, descending } + + /// Convenience tuple type + typealias SortSpec = (direction: SortDirection, key: (Alarm) -> Double?) + + /// The single place that says “sort _this_ type on _that_ field, in _this_ direction”. + var sortSpec: SortSpec? { + switch self { + case .low: + return (direction: .ascending, + key: { $0.belowBG ?? Double.nan }) + + case .high: + return (direction: .descending, + key: { $0.aboveBG ?? -Double.infinity }) + + case .fastDrop, .fastRise: + return (direction: .descending, + key: { guard let d = $0.delta else { return nil } + return abs(d) + }) + + case .missedReading, .notLooping, .missedBolus, .buildExpire: + return (direction: .ascending, + key: { $0.threshold }) + + case .iob, .cob: + return (direction: .descending, + key: { $0.threshold }) + + default: + return nil + } + } +} diff --git a/LoopFollow/Alarm/AlarmType.swift b/LoopFollow/Alarm/AlarmType.swift index b946058d9..d444c3cac 100644 --- a/LoopFollow/Alarm/AlarmType.swift +++ b/LoopFollow/Alarm/AlarmType.swift @@ -40,20 +40,6 @@ extension AlarmType { } } -extension AlarmType { - /// Should alarms of this type sort their thresholds ascending (true) or descending (false) - var thresholdSortAscending: Bool? { - switch self { - case .low, .fastDrop, .fastRise, .missedReading, .notLooping, .missedBolus, .buildExpire: - return true - case .high, .iob, .cob: - return false - default: - return nil - } - } -} - extension AlarmType { /// What “unit” we use for snoozeDuration for this alarmType. var timeUnit: TimeUnit { From 54d8de5456cc64769b783547f6e631d0a92a1550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 16 May 2025 20:17:44 +0200 Subject: [PATCH 063/138] AlarmType --- BuildTools/Package.swift | 18 ++++++-- LoopFollow.xcodeproj/project.pbxproj | 4 ++ LoopFollow/Alarm/AlarmType+timeUnit.swift | 50 +++++++++++++++++++++++ LoopFollow/Alarm/AlarmType.swift | 41 ------------------- Package.swift | 11 ----- 5 files changed, 68 insertions(+), 56 deletions(-) create mode 100644 LoopFollow/Alarm/AlarmType+timeUnit.swift delete mode 100644 Package.swift diff --git a/BuildTools/Package.swift b/BuildTools/Package.swift index 2727a28d1..f65ebe50c 100644 --- a/BuildTools/Package.swift +++ b/BuildTools/Package.swift @@ -1,11 +1,21 @@ -// swift-tools-version:5.1 +// swift-tools-version:5.3 import PackageDescription let package = Package( name: "BuildTools", - platforms: [.macOS(.v10_11)], + platforms: [ + .macOS(.v10_11), + ], dependencies: [ - .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.41.2"), + .package( + url: "https://github.com/nicklockwood/SwiftFormat.git", + from: "0.41.2" + ), ], - targets: [.target(name: "BuildTools", path: "")] + targets: [ + .target( + name: "BuildTools", + path: "" + ), + ] ) diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index f6c35cd66..71cb64097 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -118,6 +118,7 @@ DD7F4C1F2DD6648B00D449E9 /* FastRiseCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C1E2DD6648B00D449E9 /* FastRiseCondition.swift */; }; DD7F4C212DD66BB200D449E9 /* FastRiseAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C202DD66BB100D449E9 /* FastRiseAlarmEditor.swift */; }; DD7F4C232DD7A62200D449E9 /* AlarmType+SortDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C222DD7A62200D449E9 /* AlarmType+SortDirection.swift */; }; + DD7F4C252DD7B20700D449E9 /* AlarmType+timeUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C242DD7B20700D449E9 /* AlarmType+timeUnit.swift */; }; DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */; }; DD85E9952D739CFE001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */; }; DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */; }; @@ -460,6 +461,7 @@ DD7F4C1E2DD6648B00D449E9 /* FastRiseCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastRiseCondition.swift; sourceTree = ""; }; DD7F4C202DD66BB100D449E9 /* FastRiseAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastRiseAlarmEditor.swift; sourceTree = ""; }; DD7F4C222DD7A62200D449E9 /* AlarmType+SortDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlarmType+SortDirection.swift"; sourceTree = ""; }; + DD7F4C242DD7B20700D449E9 /* AlarmType+timeUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlarmType+timeUnit.swift"; sourceTree = ""; }; DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EKEventStore+Extensions.swift"; sourceTree = ""; }; DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnipodDashHeartbeatBluetoothTransmitter.swift; sourceTree = ""; }; DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseConversion.swift; sourceTree = ""; }; @@ -1039,6 +1041,7 @@ DDCF9A872D85FD33004DF4DD /* AlarmData.swift */, DDCF9A812D85FD14004DF4DD /* AlarmType.swift */, DD7F4C222DD7A62200D449E9 /* AlarmType+SortDirection.swift */, + DD7F4C242DD7B20700D449E9 /* AlarmType+timeUnit.swift */, DDCF9A7F2D85FD09004DF4DD /* Alarm.swift */, ); path = Alarm; @@ -1747,6 +1750,7 @@ DD608A0A2C23593900F91132 /* SMB.swift in Sources */, DDCF979824C1489C002C9752 /* GraphSettingsViewController.swift in Sources */, FC3AE7B5249E8E0E00AAE1E0 /* LoopFollow.xcdatamodeld in Sources */, + DD7F4C252DD7B20700D449E9 /* AlarmType+timeUnit.swift in Sources */, DDC7E5152DBCFA7900EB1127 /* SnoozerViewController.swift in Sources */, DD7F4C072DD5042F00D449E9 /* OverrideStartAlarmEditor.swift in Sources */, DDDF6F492D479AF000884336 /* NoRemoteView.swift in Sources */, diff --git a/LoopFollow/Alarm/AlarmType+timeUnit.swift b/LoopFollow/Alarm/AlarmType+timeUnit.swift new file mode 100644 index 000000000..5d7d67d2d --- /dev/null +++ b/LoopFollow/Alarm/AlarmType+timeUnit.swift @@ -0,0 +1,50 @@ +// +// AlarmType+timeUnit.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-16. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import Foundation + +extension AlarmType { + /// What “unit” we use for snoozeDuration for this alarmType. + var timeUnit: TimeUnit { + switch self { + case .buildExpire: + return .day + case .low, .high, .fastDrop, .fastRise, + .missedReading, .notLooping, .missedBolus, + .bolus, .recBolus, + .overrideStart, .overrideEnd, .tempTargetStart, + .tempTargetEnd: + return .minute + case .battery, .batteryDrop, .sensorChange, .pumpChange, .cob, .iob, + .pump: + return .hour + } + } +} + +enum TimeUnit { + case minute, hour, day + + /// How many seconds in one “unit” + var seconds: TimeInterval { + switch self { + case .minute: return 60 + case .hour: return 60 * 60 + case .day: return 60 * 60 * 24 + } + } + + /// A user-facing label + var label: String { + switch self { + case .minute: return "min" // Changed from minutes to save ui space + case .hour: return "hours" + case .day: return "days" + } + } +} diff --git a/LoopFollow/Alarm/AlarmType.swift b/LoopFollow/Alarm/AlarmType.swift index d444c3cac..7cb3c3c6c 100644 --- a/LoopFollow/Alarm/AlarmType.swift +++ b/LoopFollow/Alarm/AlarmType.swift @@ -40,47 +40,6 @@ extension AlarmType { } } -extension AlarmType { - /// What “unit” we use for snoozeDuration for this alarmType. - var timeUnit: TimeUnit { - switch self { - case .buildExpire: - return .day - case .low, .high, .fastDrop, .fastRise, - .missedReading, .notLooping, .missedBolus, - .bolus, .recBolus, - .overrideStart, .overrideEnd, .tempTargetStart, - .tempTargetEnd: - return .minute - case .battery, .batteryDrop, .sensorChange, .pumpChange, .cob, .iob, - .pump: - return .hour - } - } -} - -enum TimeUnit { - case minute, hour, day - - /// How many seconds in one “unit” - var seconds: TimeInterval { - switch self { - case .minute: return 60 - case .hour: return 60 * 60 - case .day: return 60 * 60 * 24 - } - } - - /// A user-facing label - var label: String { - switch self { - case .minute: return "min" // Changed from minutes to save ui space - case .hour: return "hours" - case .day: return "days" - } - } -} - extension AlarmType { /// `true` for alarms whose primary trigger is a blood-glucose value /// or its rate of change. diff --git a/Package.swift b/Package.swift deleted file mode 100644 index 2727a28d1..000000000 --- a/Package.swift +++ /dev/null @@ -1,11 +0,0 @@ -// swift-tools-version:5.1 -import PackageDescription - -let package = Package( - name: "BuildTools", - platforms: [.macOS(.v10_11)], - dependencies: [ - .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.41.2"), - ], - targets: [.target(name: "BuildTools", path: "")] -) From 8d284be0ac660979dd7d032287c50d43f2823877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 16 May 2025 20:20:24 +0200 Subject: [PATCH 064/138] snoozeTimeUnit --- LoopFollow.xcodeproj/project.pbxproj | 14 +++++++++++--- .../Components/AlarmSnoozeSection.swift | 4 ++-- .../Editors/BuildExpireAlarmEditor.swift | 2 +- .../AlarmEditing/Editors/HighBgAlarmEditor.swift | 2 +- .../AlarmEditing/Editors/LowBgAlarmEditor.swift | 4 ++-- .../AlarmEditing/Editors/MissedReadingEditor.swift | 2 +- .../Editors/NotLoopingAlarmEditor.swift | 2 +- LoopFollow/Alarm/AlarmManager.swift | 2 +- .../{ => AlarmType}/AlarmType+SortDirection.swift | 0 .../Alarm/{ => AlarmType}/AlarmType+timeUnit.swift | 2 +- LoopFollow/Alarm/{ => AlarmType}/AlarmType.swift | 0 LoopFollow/Snoozer/SnoozerView.swift | 6 +++--- LoopFollow/Snoozer/SnoozerViewModel.swift | 2 +- 13 files changed, 25 insertions(+), 17 deletions(-) rename LoopFollow/Alarm/{ => AlarmType}/AlarmType+SortDirection.swift (100%) rename LoopFollow/Alarm/{ => AlarmType}/AlarmType+timeUnit.swift (97%) rename LoopFollow/Alarm/{ => AlarmType}/AlarmType.swift (100%) diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 71cb64097..f84fb6fbb 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -959,6 +959,16 @@ path = Scripts; sourceTree = ""; }; + DDC6CA3B2DD7B9050060EE25 /* AlarmType */ = { + isa = PBXGroup; + children = ( + DDCF9A812D85FD14004DF4DD /* AlarmType.swift */, + DD7F4C222DD7A62200D449E9 /* AlarmType+SortDirection.swift */, + DD7F4C242DD7B20700D449E9 /* AlarmType+timeUnit.swift */, + ); + path = AlarmType; + sourceTree = ""; + }; DDC7E5142DBCE1B900EB1127 /* Snoozer */ = { isa = PBXGroup; children = ( @@ -1031,6 +1041,7 @@ DDCF9A7E2D85FCE6004DF4DD /* Alarm */ = { isa = PBXGroup; children = ( + DDC6CA3B2DD7B9050060EE25 /* AlarmType */, DD5DA27B2DC930D6003D44FC /* GlucoseValue.swift */, DD4AFB602DB68BBC00BB593F /* AlarmListView.swift */, DDC7E5412DBD8A1600EB1127 /* AlarmEditing */, @@ -1039,9 +1050,6 @@ DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */, DDCF9A8B2D86005E004DF4DD /* AlarmManager.swift */, DDCF9A872D85FD33004DF4DD /* AlarmData.swift */, - DDCF9A812D85FD14004DF4DD /* AlarmType.swift */, - DD7F4C222DD7A62200D449E9 /* AlarmType+SortDirection.swift */, - DD7F4C242DD7B20700D449E9 /* AlarmType+timeUnit.swift */, DDCF9A7F2D85FD09004DF4DD /* Alarm.swift */, ); path = Alarm; diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift index 83f3b7543..51ce7f1ab 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift @@ -13,7 +13,7 @@ struct AlarmSnoozeSection: View { let range: ClosedRange let step: Int - private var unitLabel: String { alarm.type.timeUnit.label } + private var unitLabel: String { alarm.type.snoozeTimeUnit.label } private var defaultSnoozeBinding: Binding { Binding( @@ -31,7 +31,7 @@ struct AlarmSnoozeSection: View { set: { on in if on { if alarm.snoozedUntil == nil || alarm.snoozedUntil! < Date() { - let secs = alarm.type.timeUnit.seconds + let secs = alarm.type.snoozeTimeUnit.seconds alarm.snoozedUntil = Date() .addingTimeInterval(Double(alarm.snoozeDuration) * secs) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift index 7dd4a2f99..6d96e7705 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift @@ -24,7 +24,7 @@ struct BuildExpireAlarmEditor: View { title: "Expires In", range: 1 ... 14, step: 1, - unitLabel: alarm.type.timeUnit.label, + unitLabel: alarm.type.snoozeTimeUnit.label, value: Binding( get: { alarm.threshold ?? 1 }, set: { alarm.threshold = $0 } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift index da662054a..e4f3ab611 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift @@ -38,7 +38,7 @@ struct HighBgAlarmEditor: View { title: "Persistent for", range: 0 ... 120, step: 5, - unitLabel: alarm.type.timeUnit.label, + unitLabel: alarm.type.snoozeTimeUnit.label, value: Binding( get: { Double(alarm.persistentMinutes ?? 0) }, set: { alarm.persistentMinutes = Int($0) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift index fec435a46..e3c352148 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift @@ -36,7 +36,7 @@ struct LowBgAlarmEditor: View { title: "Persistent", range: 0 ... 120, step: 5, - unitLabel: alarm.type.timeUnit.label, + unitLabel: alarm.type.snoozeTimeUnit.label, value: Binding( get: { Double(alarm.persistentMinutes ?? 0) }, set: { alarm.persistentMinutes = Int($0) } @@ -51,7 +51,7 @@ struct LowBgAlarmEditor: View { title: "Predictive", range: 0 ... 60, step: 5, - unitLabel: alarm.type.timeUnit.label, + unitLabel: alarm.type.snoozeTimeUnit.label, value: Binding( get: { Double(alarm.predictiveMinutes ?? 0) }, set: { alarm.predictiveMinutes = Int($0) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift index 765be180b..7db03d291 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift @@ -22,7 +22,7 @@ struct MissedReadingEditor: View { title: "No reading for", range: 11 ... 121, step: 5, - unitLabel: alarm.type.timeUnit.label, + unitLabel: alarm.type.snoozeTimeUnit.label, value: Binding( get: { alarm.threshold ?? 16 }, set: { alarm.threshold = $0 } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift index d8381fbf8..616afdb45 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift @@ -27,7 +27,7 @@ struct NotLoopingAlarmEditor: View { title: "Elapsed time", range: 16 ... 61, step: 5, - unitLabel: alarm.type.timeUnit.label, + unitLabel: alarm.type.snoozeTimeUnit.label, value: Binding( get: { alarm.threshold ?? 31 }, set: { alarm.threshold = $0 } diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index a85f976d9..1893d8c2b 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -147,7 +147,7 @@ class AlarmManager { if let idx = alarms.firstIndex(where: { $0.id == alarmID }) { let alarm = alarms[idx] let units = snoozeUnits ?? alarm.snoozeDuration - let snoozeSeconds = Double(units) * alarm.type.timeUnit.seconds + let snoozeSeconds = Double(units) * alarm.type.snoozeTimeUnit.seconds alarms[idx].snoozedUntil = Date().addingTimeInterval(snoozeSeconds) Storage.shared.alarms.value = alarms stopAlarm() diff --git a/LoopFollow/Alarm/AlarmType+SortDirection.swift b/LoopFollow/Alarm/AlarmType/AlarmType+SortDirection.swift similarity index 100% rename from LoopFollow/Alarm/AlarmType+SortDirection.swift rename to LoopFollow/Alarm/AlarmType/AlarmType+SortDirection.swift diff --git a/LoopFollow/Alarm/AlarmType+timeUnit.swift b/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift similarity index 97% rename from LoopFollow/Alarm/AlarmType+timeUnit.swift rename to LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift index 5d7d67d2d..62f1b5e40 100644 --- a/LoopFollow/Alarm/AlarmType+timeUnit.swift +++ b/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift @@ -10,7 +10,7 @@ import Foundation extension AlarmType { /// What “unit” we use for snoozeDuration for this alarmType. - var timeUnit: TimeUnit { + var snoozeTimeUnit: TimeUnit { switch self { case .buildExpire: return .day diff --git a/LoopFollow/Alarm/AlarmType.swift b/LoopFollow/Alarm/AlarmType/AlarmType.swift similarity index 100% rename from LoopFollow/Alarm/AlarmType.swift rename to LoopFollow/Alarm/AlarmType/AlarmType.swift diff --git a/LoopFollow/Snoozer/SnoozerView.swift b/LoopFollow/Snoozer/SnoozerView.swift index d6ba43b5b..740b04eb4 100644 --- a/LoopFollow/Snoozer/SnoozerView.swift +++ b/LoopFollow/Snoozer/SnoozerView.swift @@ -107,9 +107,9 @@ struct SnoozerView: View { } Spacer() Stepper("", value: $vm.snoozeUnits, - in: 1 ... (alarm.type.timeUnit == .day ? 30 : - alarm.type.timeUnit == .hour ? 24 : 60), - step: alarm.type.timeUnit == .minute ? 5 : 1) + in: 1 ... (alarm.type.snoozeTimeUnit == .day ? 30 : + alarm.type.snoozeTimeUnit == .hour ? 24 : 60), + step: alarm.type.snoozeTimeUnit == .minute ? 5 : 1) .labelsHidden() } .padding(.horizontal, 24) diff --git a/LoopFollow/Snoozer/SnoozerViewModel.swift b/LoopFollow/Snoozer/SnoozerViewModel.swift index 3fc1b3052..cb67a3d3e 100644 --- a/LoopFollow/Snoozer/SnoozerViewModel.swift +++ b/LoopFollow/Snoozer/SnoozerViewModel.swift @@ -27,7 +27,7 @@ final class SnoozerViewModel: ObservableObject { self?.activeAlarm = alarm if let a = alarm { self?.snoozeUnits = a.snoozeDuration - self?.timeUnitLabel = a.type.timeUnit.label + self?.timeUnitLabel = a.type.snoozeTimeUnit.label } } .store(in: &cancellables) From e1d5e64386514af5dc05b3676eaacfef98712d90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 16 May 2025 21:57:40 +0200 Subject: [PATCH 065/138] Temporary alert --- LoopFollow.xcodeproj/project.pbxproj | 8 +++ LoopFollow/Alarm/Alarm.swift | 8 ++- .../AlarmCondition/TemporaryCondition.swift | 24 ++++++++ .../Alarm/AlarmEditing/AlarmEditor.swift | 2 + .../Editors/TemporaryAlarmEditor.swift | 56 +++++++++++++++++++ LoopFollow/Alarm/AlarmManager.swift | 10 ++++ .../Alarm/AlarmType/AlarmType+timeUnit.swift | 4 +- LoopFollow/Alarm/AlarmType/AlarmType.swift | 3 +- LoopFollow/Alarm/GlucoseValue.swift | 1 + 9 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 LoopFollow/Alarm/AlarmCondition/TemporaryCondition.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index f84fb6fbb..d051c48a0 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -141,6 +141,8 @@ DDB0AF522BB1A8BE00AFA48B /* BuildDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB0AF512BB1A8BE00AFA48B /* BuildDetails.swift */; }; DDB0AF552BB1B24A00AFA48B /* BuildDetails.plist in Resources */ = {isa = PBXBuildFile; fileRef = DDB0AF542BB1B24A00AFA48B /* BuildDetails.plist */; }; DDBE3ABD2CB5A961006B37DC /* OverrideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDBE3ABC2CB5A961006B37DC /* OverrideView.swift */; }; + DDC6CA3D2DD7C6090060EE25 /* TemporaryCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA3C2DD7C6090060EE25 /* TemporaryCondition.swift */; }; + DDC6CA3F2DD7C6340060EE25 /* TemporaryAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA3E2DD7C6340060EE25 /* TemporaryAlarmEditor.swift */; }; DDC7E5152DBCFA7900EB1127 /* SnoozerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5132DBCE1B900EB1127 /* SnoozerViewController.swift */; }; DDC7E5162DBCFA7F00EB1127 /* SnoozerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5122DBCE1B900EB1127 /* SnoozerView.swift */; }; DDC7E5382DBD887400EB1127 /* isOnPhoneCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5372DBD887400EB1127 /* isOnPhoneCall.swift */; }; @@ -485,6 +487,8 @@ DDB0AF512BB1A8BE00AFA48B /* BuildDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildDetails.swift; sourceTree = ""; }; DDB0AF542BB1B24A00AFA48B /* BuildDetails.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = BuildDetails.plist; sourceTree = ""; }; DDBE3ABC2CB5A961006B37DC /* OverrideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideView.swift; sourceTree = ""; }; + DDC6CA3C2DD7C6090060EE25 /* TemporaryCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryCondition.swift; sourceTree = ""; }; + DDC6CA3E2DD7C6340060EE25 /* TemporaryAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryAlarmEditor.swift; sourceTree = ""; }; DDC7E5122DBCE1B900EB1127 /* SnoozerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozerView.swift; sourceTree = ""; }; DDC7E5132DBCE1B900EB1127 /* SnoozerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozerViewController.swift; sourceTree = ""; }; DDC7E5372DBD887400EB1127 /* isOnPhoneCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = isOnPhoneCall.swift; sourceTree = ""; }; @@ -730,6 +734,7 @@ DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = { isa = PBXGroup; children = ( + DDC6CA3C2DD7C6090060EE25 /* TemporaryCondition.swift */, DD7F4C1E2DD6648B00D449E9 /* FastRiseCondition.swift */, DD7F4C1A2DD6501D00D449E9 /* COBCondition.swift */, DD7F4C162DD63FA700D449E9 /* RecBolusCondition.swift */, @@ -999,6 +1004,7 @@ DDC7E53F2DBD8A1600EB1127 /* Editors */ = { isa = PBXGroup; children = ( + DDC6CA3E2DD7C6340060EE25 /* TemporaryAlarmEditor.swift */, DD7F4C202DD66BB100D449E9 /* FastRiseAlarmEditor.swift */, DD7F4C1C2DD650D500D449E9 /* COBAlarmEditor.swift */, DD7F4C182DD63FD500D449E9 /* RecBolusAlarmEditor.swift */, @@ -1744,6 +1750,7 @@ DD0247712DB4337700FCADF6 /* BuildExpireCondition.swift in Sources */, DDF6999B2C5AA32E0058A8D9 /* TempTargetPreset.swift in Sources */, DD7F4C0F2DD51EC200D449E9 /* TempTargetStartCondition.swift in Sources */, + DDC6CA3D2DD7C6090060EE25 /* TemporaryCondition.swift in Sources */, DD9ACA0E2D340BFF00415D8A /* AlarmTask.swift in Sources */, DDCF9A822D85FD15004DF4DD /* AlarmType.swift in Sources */, DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */, @@ -1835,6 +1842,7 @@ DD4878052C7B2C970048F05C /* Storage.swift in Sources */, DD493AE12ACF22FE009A6922 /* Profile.swift in Sources */, DD493ADF2ACF22BB009A6922 /* SAge.swift in Sources */, + DDC6CA3F2DD7C6340060EE25 /* TemporaryAlarmEditor.swift in Sources */, DDF699992C5AA3060058A8D9 /* TempTargetPresetManager.swift in Sources */, DD0C0C722C4B000800DBADDF /* TrioNightscoutRemoteView.swift in Sources */, DD493ADB2ACF21A3009A6922 /* Bolus.swift in Sources */, diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index 06b5e679d..085148375 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -272,6 +272,8 @@ struct Alarm: Identifiable, Codable, Equatable { case .tempTargetEnd: soundFile = .alertToneBusy repeatSoundOption = .never + case .temporary: + soundFile = .indeed } } } @@ -286,14 +288,14 @@ extension AlarmType { var group: Group { switch self { - case .low, .high, .fastDrop, .fastRise, .missedReading: + case .low, .high, .fastDrop, .fastRise, .missedReading, .temporary: return .glucose case .iob, .bolus, .cob, .missedBolus, .recBolus: return .insulin case .battery, .batteryDrop, .pump, .pumpChange, .sensorChange, .notLooping, .buildExpire: return .device - default: + case .overrideStart, .overrideEnd, .tempTargetStart, .tempTargetEnd: return .other } } @@ -320,6 +322,7 @@ extension AlarmType { case .overrideEnd: return "stop.circle" case .tempTargetStart: return "flag" case .tempTargetEnd: return "flag.slash" + case .temporary: return "bell" } } @@ -346,6 +349,7 @@ extension AlarmType { case .overrideEnd: return "Override ended." case .tempTargetStart: return "Temp target started." case .tempTargetEnd: return "Temp target ended." + case .temporary: return "One-time BG limit alert." } } } diff --git a/LoopFollow/Alarm/AlarmCondition/TemporaryCondition.swift b/LoopFollow/Alarm/AlarmCondition/TemporaryCondition.swift new file mode 100644 index 000000000..54bd67a0f --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/TemporaryCondition.swift @@ -0,0 +1,24 @@ +// +// TemporaryCondition.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-16. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import Foundation + +/// A throw-away, single-fire BG-limit alarm. +struct TemporaryCondition: AlarmCondition { + static let type: AlarmType = .temporary + init() {} + + func evaluate(alarm: Alarm, data _: AlarmData) -> Bool { + // Needs at least ONE limit + guard alarm.belowBG != nil || alarm.aboveBG != nil else { return false } + + // BG-limit checks are handled in shouldFire → passesBGLimits. + // If we get here, the limits are satisfied ⇒ fire. + return true + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index 1dcc755f0..b3009da3a 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -67,6 +67,8 @@ struct AlarmEditor: View { COBAlarmEditor(alarm: $alarm) case .fastRise: FastRiseAlarmEditor(alarm: $alarm) + case .temporary: + TemporaryAlarmEditor(alarm: $alarm) /* TODO: add other condition types here */ default: Text("No editor for \(alarm.type.rawValue)") diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift new file mode 100644 index 000000000..7e804cf2b --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift @@ -0,0 +1,56 @@ +// +// TemporaryAlarmEditor.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-16. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import SwiftUI + +struct TemporaryAlarmEditor: View { + @Binding var alarm: Alarm + + // Shared BG range + private let bgRange: ClosedRange = 40 ... 300 + + var body: some View { + Form { + InfoBanner( + text: "This alert fires once when glucose crosses either of the limits you set below, and then disables itself.", + alarmType: alarm.type + ) + + AlarmGeneralSection(alarm: $alarm) + + AlarmBGLimitSection( + header: "Low Limit", + footer: "Alert if BG is equal to or below this value.", + toggleText: "Enable low limit", + pickerTitle: "≤ BG", + range: bgRange, + value: $alarm.belowBG + ) + + AlarmBGLimitSection( + header: "High Limit", + footer: "Alert if BG is equal to or above this value.", + toggleText: "Enable high limit", + pickerTitle: "≥ BG", + range: bgRange, + value: $alarm.aboveBG + ) + + // Validation: ensure at least one limit is on + if alarm.belowBG == nil && alarm.aboveBG == nil { + Text("⚠️ Please enable at least one limit.") + .foregroundColor(.red) + } + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm, range: 5 ... 60, step: 5) + } + .navigationTitle(alarm.type.rawValue) + } +} diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index 1893d8c2b..ac9f827b6 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -30,6 +30,7 @@ class AlarmManager { COBCondition.self, MissedReadingCondition.self, FastRiseCondition.self, + TemporaryCondition.self, // TODO: add other condition types here ] ) { @@ -137,6 +138,15 @@ class AlarmManager { { lastBGAlarmTime = latestDate } + + if alarm.type == .temporary { + // turn it off and persist + var list = Storage.shared.alarms.value + if let idx = list.firstIndex(where: { $0.id == alarm.id }) { + list[idx].isEnabled = false + Storage.shared.alarms.value = list + } + } break } } diff --git a/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift b/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift index 62f1b5e40..487eb6458 100644 --- a/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift +++ b/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift @@ -18,7 +18,7 @@ extension AlarmType { .missedReading, .notLooping, .missedBolus, .bolus, .recBolus, .overrideStart, .overrideEnd, .tempTargetStart, - .tempTargetEnd: + .tempTargetEnd, .temporary: return .minute case .battery, .batteryDrop, .sensorChange, .pumpChange, .cob, .iob, .pump: @@ -42,7 +42,7 @@ enum TimeUnit { /// A user-facing label var label: String { switch self { - case .minute: return "min" // Changed from minutes to save ui space + case .minute: return "min" case .hour: return "hours" case .day: return "days" } diff --git a/LoopFollow/Alarm/AlarmType/AlarmType.swift b/LoopFollow/Alarm/AlarmType/AlarmType.swift index 7cb3c3c6c..de006bb72 100644 --- a/LoopFollow/Alarm/AlarmType/AlarmType.swift +++ b/LoopFollow/Alarm/AlarmType/AlarmType.swift @@ -11,6 +11,7 @@ import Foundation /// Categorizes alarms into distinct types, prioritized in the order they appear here. /// Multiple user-defined alarms may share the same type but differ in configuration. enum AlarmType: String, CaseIterable, Codable { + case temporary = "Temporary Alert" case iob = "IOB Alert" case bolus = "Bolus Alert" case cob = "COB Alert" @@ -45,7 +46,7 @@ extension AlarmType { /// or its rate of change. var isBGBased: Bool { switch self { - case .low, .high, .fastDrop, .fastRise, .missedReading: + case .low, .high, .fastDrop, .fastRise, .missedReading, .temporary: return true default: return false diff --git a/LoopFollow/Alarm/GlucoseValue.swift b/LoopFollow/Alarm/GlucoseValue.swift index a25e7a2bc..e2e1696cf 100644 --- a/LoopFollow/Alarm/GlucoseValue.swift +++ b/LoopFollow/Alarm/GlucoseValue.swift @@ -8,6 +8,7 @@ import Foundation +// Make use of this more clean glucose struct in more places struct GlucoseValue: Codable { let sgv: Int let date: Date From ced56729bbf6fcd1728d16808fd041b46778f0bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 16 May 2025 21:58:20 +0200 Subject: [PATCH 066/138] Temporary alert --- LoopFollow/Alarm/AlarmManager.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index ac9f827b6..30f932121 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -144,6 +144,7 @@ class AlarmManager { var list = Storage.shared.alarms.value if let idx = list.firstIndex(where: { $0.id == alarm.id }) { list[idx].isEnabled = false + list[idx].snoozedUntil = nil Storage.shared.alarms.value = list } } From 7c11619edb838cb78a5f778b9a99eb278d68730f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 17 May 2025 16:12:05 +0200 Subject: [PATCH 067/138] SensorAge --- LoopFollow.xcodeproj/project.pbxproj | 8 ++++ .../AlarmCondition/SensorAgeCondition.swift | 34 ++++++++++++++ LoopFollow/Alarm/AlarmData.swift | 1 + .../Alarm/AlarmEditing/AlarmEditor.swift | 2 + .../Editors/SensorAgeAlarmEditor.swift | 45 +++++++++++++++++++ LoopFollow/Alarm/AlarmManager.swift | 1 + .../AlarmType/AlarmType+SortDirection.swift | 4 ++ LoopFollow/Task/AlarmTask.swift | 4 +- 8 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 LoopFollow/Alarm/AlarmCondition/SensorAgeCondition.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index d051c48a0..59b803085 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -143,6 +143,8 @@ DDBE3ABD2CB5A961006B37DC /* OverrideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDBE3ABC2CB5A961006B37DC /* OverrideView.swift */; }; DDC6CA3D2DD7C6090060EE25 /* TemporaryCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA3C2DD7C6090060EE25 /* TemporaryCondition.swift */; }; DDC6CA3F2DD7C6340060EE25 /* TemporaryAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA3E2DD7C6340060EE25 /* TemporaryAlarmEditor.swift */; }; + DDC6CA412DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA402DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift */; }; + DDC6CA432DD8CED20060EE25 /* SensorAgeCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA422DD8CED20060EE25 /* SensorAgeCondition.swift */; }; DDC7E5152DBCFA7900EB1127 /* SnoozerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5132DBCE1B900EB1127 /* SnoozerViewController.swift */; }; DDC7E5162DBCFA7F00EB1127 /* SnoozerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5122DBCE1B900EB1127 /* SnoozerView.swift */; }; DDC7E5382DBD887400EB1127 /* isOnPhoneCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5372DBD887400EB1127 /* isOnPhoneCall.swift */; }; @@ -489,6 +491,8 @@ DDBE3ABC2CB5A961006B37DC /* OverrideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideView.swift; sourceTree = ""; }; DDC6CA3C2DD7C6090060EE25 /* TemporaryCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryCondition.swift; sourceTree = ""; }; DDC6CA3E2DD7C6340060EE25 /* TemporaryAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryAlarmEditor.swift; sourceTree = ""; }; + DDC6CA402DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorAgeAlarmEditor.swift; sourceTree = ""; }; + DDC6CA422DD8CED20060EE25 /* SensorAgeCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorAgeCondition.swift; sourceTree = ""; }; DDC7E5122DBCE1B900EB1127 /* SnoozerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozerView.swift; sourceTree = ""; }; DDC7E5132DBCE1B900EB1127 /* SnoozerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozerViewController.swift; sourceTree = ""; }; DDC7E5372DBD887400EB1127 /* isOnPhoneCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = isOnPhoneCall.swift; sourceTree = ""; }; @@ -734,6 +738,7 @@ DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = { isa = PBXGroup; children = ( + DDC6CA422DD8CED20060EE25 /* SensorAgeCondition.swift */, DDC6CA3C2DD7C6090060EE25 /* TemporaryCondition.swift */, DD7F4C1E2DD6648B00D449E9 /* FastRiseCondition.swift */, DD7F4C1A2DD6501D00D449E9 /* COBCondition.swift */, @@ -1004,6 +1009,7 @@ DDC7E53F2DBD8A1600EB1127 /* Editors */ = { isa = PBXGroup; children = ( + DDC6CA402DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift */, DDC6CA3E2DD7C6340060EE25 /* TemporaryAlarmEditor.swift */, DD7F4C202DD66BB100D449E9 /* FastRiseAlarmEditor.swift */, DD7F4C1C2DD650D500D449E9 /* COBAlarmEditor.swift */, @@ -1896,6 +1902,7 @@ DD4878132C7B750D0048F05C /* TempTargetView.swift in Sources */, DD0C0C682C48529400DBADDF /* Metric.swift in Sources */, FCC6886D2489909D00A0279D /* AnyConvertible.swift in Sources */, + DDC6CA432DD8CED20060EE25 /* SensorAgeCondition.swift in Sources */, DD7F4C212DD66BB200D449E9 /* FastRiseAlarmEditor.swift in Sources */, FC97881A2485969B00A7906C /* SceneDelegate.swift in Sources */, DD0C0C662C46E54C00DBADDF /* InfoDataSeparator.swift in Sources */, @@ -1917,6 +1924,7 @@ 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 */, DD50C7552D0862770057AE6F /* ContactImageUpdater.swift in Sources */, diff --git a/LoopFollow/Alarm/AlarmCondition/SensorAgeCondition.swift b/LoopFollow/Alarm/AlarmCondition/SensorAgeCondition.swift new file mode 100644 index 000000000..8b8bfd518 --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/SensorAgeCondition.swift @@ -0,0 +1,34 @@ +// +// SensorAgeCondition.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-17. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import Foundation + +/// Fires once when we are **≤ threshold hours** away from the +/// Dexcom 10-day hard-stop. No repeats once triggered. +struct SensorAgeCondition: AlarmCondition { + static let type: AlarmType = .sensorChange + init() {} + + /// Dexcom hard-stop = 10 days = 240 h + private let lifetime: TimeInterval = 10 * 24 * 60 * 60 + + func evaluate(alarm: Alarm, data: AlarmData) -> Bool { + // 0. basic guards + guard let warnAheadHrs = alarm.threshold, warnAheadHrs > 0 else { return false } + guard let insertTS = data.sageInsertTime else { return false } + + // convert UNIX timestamp to Date + let insertedAt = Date(timeIntervalSince1970: insertTS) + + // 1. compute trigger moment + let expiry = insertedAt.addingTimeInterval(lifetime) + let trigger = expiry.addingTimeInterval(-warnAheadHrs * 3600) + + return Date() >= trigger + } +} diff --git a/LoopFollow/Alarm/AlarmData.swift b/LoopFollow/Alarm/AlarmData.swift index 82380c2f9..14be132b6 100644 --- a/LoopFollow/Alarm/AlarmData.swift +++ b/LoopFollow/Alarm/AlarmData.swift @@ -19,6 +19,7 @@ struct AlarmData: Codable { let latestTempTargetEnd: TimeInterval? let recBolus: Double? let COB: Double? + let sageInsertTime: TimeInterval? } /* diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index b3009da3a..b367c82b6 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -69,6 +69,8 @@ struct AlarmEditor: View { FastRiseAlarmEditor(alarm: $alarm) case .temporary: TemporaryAlarmEditor(alarm: $alarm) + case .sensorChange: + SensorAgeAlarmEditor(alarm: $alarm) /* TODO: add other condition types here */ default: Text("No editor for \(alarm.type.rawValue)") diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift new file mode 100644 index 000000000..c0e72df8d --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift @@ -0,0 +1,45 @@ +// +// SensorAgeAlarmEditor.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-17. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// + +import SwiftUI + +struct SensorAgeAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Form { + InfoBanner( + text: "Warn me this many hours before the sensor’s 10-day change-over.", + alarmType: alarm.type + ) + + AlarmGeneralSection(alarm: $alarm) + + AlarmStepperSection( + header: "Advance warning", + footer: "Number of hours before the 10-day mark that the alert " + + "will fire.", + title: "Hours", + range: 1 ... 24, + step: 1, + unitLabel: "hours", + value: Binding( + get: { alarm.threshold ?? 12 }, + set: { alarm.threshold = $0 } + ) + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm, + range: 1 ... 24, + step: 1) + } + .navigationTitle(alarm.type.rawValue) + } +} diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index 30f932121..4f67e1df2 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -31,6 +31,7 @@ class AlarmManager { MissedReadingCondition.self, FastRiseCondition.self, TemporaryCondition.self, + SensorAgeCondition.self, // TODO: add other condition types here ] ) { diff --git a/LoopFollow/Alarm/AlarmType/AlarmType+SortDirection.swift b/LoopFollow/Alarm/AlarmType/AlarmType+SortDirection.swift index 838f5bbda..6d9350c1d 100644 --- a/LoopFollow/Alarm/AlarmType/AlarmType+SortDirection.swift +++ b/LoopFollow/Alarm/AlarmType/AlarmType+SortDirection.swift @@ -42,6 +42,10 @@ extension AlarmType { return (direction: .descending, key: { $0.threshold }) + case .sensorChange: + return (direction: .ascending, + key: { $0.threshold }) + default: return nil } diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index b7a5e3272..886136adb 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -25,6 +25,7 @@ extension MainViewController { let latestTempTargetEnd = self.tempTargetGraphData.last?.endDate let recBolus = UserDefaultsRepository.deviceRecBolus.value let COB = self.latestCOB?.value + let sensorInsertedAt = UserDefaultsRepository.alertSageInsertTime.value let alarmData = AlarmData( bgReadings: self.bgData @@ -40,7 +41,8 @@ extension MainViewController { latestTempTargetStart: latestTempTargetStart, latestTempTargetEnd: latestTempTargetEnd, recBolus: recBolus, - COB: COB + COB: COB, + sageInsertTime: sensorInsertedAt ) let finalAlarmData: AlarmData From 6d518b815e2593fcd1bf6f0123ddb7d22bc598ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 17 May 2025 16:28:41 +0200 Subject: [PATCH 068/138] Header change --- LoopFollow/Alarm/Alarm.swift | 10 +++------- .../Alarm/AlarmCondition/AlarmCondition.swift | 10 +++------- .../AlarmCondition/BuildExpireCondition.swift | 10 +++------- LoopFollow/Alarm/AlarmCondition/COBCondition.swift | 10 +++------- .../Alarm/AlarmCondition/FastDropCondition.swift | 10 +++------- .../Alarm/AlarmCondition/FastRiseCondition.swift | 9 +++------ .../Alarm/AlarmCondition/HighBGCondition.swift | 10 +++------- .../Alarm/AlarmCondition/LowBGCondition.swift | 10 +++------- .../AlarmCondition/MissedReadingCondition.swift | 10 +++------- .../Alarm/AlarmCondition/NotLoopingCondition.swift | 10 +++------- .../AlarmCondition/OverrideEndCondition.swift | 10 +++------- .../AlarmCondition/OverrideStartCondition.swift | 10 +++------- .../Alarm/AlarmCondition/RecBolusCondition.swift | 10 +++------- .../Alarm/AlarmCondition/SensorAgeCondition.swift | 10 +++------- .../AlarmCondition/TempTargetEndCondition.swift | 10 +++------- .../AlarmCondition/TempTargetStartCondition.swift | 10 +++------- .../Alarm/AlarmCondition/TemporaryCondition.swift | 10 +++------- LoopFollow/Alarm/AlarmConfiguration.swift | 7 ++----- LoopFollow/Alarm/AlarmData.swift | 10 +++------- LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift | 4 ++++ .../Components/AlarmActiveSection.swift | 10 +++------- .../Components/AlarmAudioSection.swift | 10 +++------- .../Components/AlarmBGLimitSection.swift | 10 +++------- .../AlarmEditing/Components/AlarmBGPicker.swift | 10 +++------- .../AlarmEditing/Components/AlarmBGSection.swift | 10 +++------- .../Components/AlarmGeneralSection.swift | 10 +++------- .../Components/AlarmSnoozeSection.swift | 10 +++------- .../Components/AlarmStepperSection.swift | 10 +++------- .../Alarm/AlarmEditing/Components/InfoBanner.swift | 10 +++------- .../Alarm/AlarmEditing/Components/SoundFile.swift | 10 +++------- .../Editors/BuildExpireAlarmEditor.swift | 10 +++------- .../AlarmEditing/Editors/COBAlarmEditor.swift | 10 +++------- .../AlarmEditing/Editors/FastDropAlarmEditor.swift | 10 +++------- .../AlarmEditing/Editors/FastRiseAlarmEditor.swift | 10 +++------- .../AlarmEditing/Editors/HighBgAlarmEditor.swift | 10 +++------- .../AlarmEditing/Editors/LowBgAlarmEditor.swift | 10 +++------- .../AlarmEditing/Editors/MissedReadingEditor.swift | 10 +++------- .../Editors/NotLoopingAlarmEditor.swift | 10 +++------- .../Editors/OverrideEndAlarmEditor.swift | 10 +++------- .../Editors/OverrideStartAlarmEditor.swift | 10 +++------- .../AlarmEditing/Editors/RecBolusAlarmEditor.swift | 10 +++------- .../Editors/SensorAgeAlarmEditor.swift | 10 +++------- .../Editors/TempTargetEndAlarmEditor.swift | 10 +++------- .../Editors/TempTargetStartAlarmEditor.swift | 10 +++------- .../Editors/TemporaryAlarmEditor.swift | 10 +++------- LoopFollow/Alarm/AlarmListView.swift | 10 +++------- LoopFollow/Alarm/AlarmManager.swift | 10 +++------- LoopFollow/Alarm/AlarmSettingsView.swift | 10 +++------- .../Alarm/AlarmType/AlarmType+SortDirection.swift | 10 +++------- .../Alarm/AlarmType/AlarmType+timeUnit.swift | 10 +++------- LoopFollow/Alarm/AlarmType/AlarmType.swift | 10 +++------- LoopFollow/Alarm/GlucoseValue.swift | 10 +++------- LoopFollow/Alarm/SnoozeState.swift | 10 +++------- LoopFollow/Application/AppDelegate.swift | 10 +++------- LoopFollow/Application/SceneDelegate.swift | 10 +++------- LoopFollow/BackgroundRefresh/BT/BLEDevice.swift | 9 +++------ .../BT/BLEDeviceSelectionView.swift | 7 +++---- LoopFollow/BackgroundRefresh/BT/BLEManager.swift | 7 +++---- .../BackgroundRefresh/BT/BluetoothDevice.swift | 10 +++------- .../BT/BluetoothDeviceDelegate.swift | 10 +++------- .../Devices/DexcomHeartbeatBluetoothDevice.swift | 10 +++------- .../OmnipodDashHeartbeatBluetoothTransmitter.swift | 10 +++------- .../RileyLinkHeartbeatBluetoothDevice.swift | 10 +++------- .../BackgroundRefresh/BT/DexcomG7HeartBeat.swift | 10 +++------- .../BackgroundRefreshSettingsView.swift | 7 +++---- .../BackgroundRefreshSettingsViewModel.swift | 9 +++------ .../BackgroundRefresh/BackgroundRefreshType.swift | 9 +++------ LoopFollow/Contact/ContactColorOption.swift | 10 +++------- LoopFollow/Contact/ContactImageUpdater.swift | 10 +++------- LoopFollow/Contact/ContactIncludeOption.swift | 10 +++------- LoopFollow/Contact/ContactType.swift | 10 +++------- .../Contact/Settings/ContactSettingsView.swift | 10 +++------- .../Settings/ContactSettingsViewModel.swift | 10 +++------- LoopFollow/Controllers/AlarmSound.swift | 10 +++------- LoopFollow/Controllers/AppStateController.swift | 10 +++------- .../Controllers/BackgroundAlertManager.swift | 10 +++------- LoopFollow/Controllers/Graphs.swift | 10 +++------- LoopFollow/Controllers/NightScout.swift | 10 +++------- LoopFollow/Controllers/Nightscout/BGData.swift | 10 +++------- LoopFollow/Controllers/Nightscout/CAge.swift | 10 +++------- .../Controllers/Nightscout/DeviceStatus.swift | 10 +++------- .../Controllers/Nightscout/DeviceStatusLoop.swift | 10 +++------- .../Nightscout/DeviceStatusOpenAPS.swift | 5 ++--- LoopFollow/Controllers/Nightscout/IAge.swift | 10 +++------- LoopFollow/Controllers/Nightscout/NSProfile.swift | 5 ++--- LoopFollow/Controllers/Nightscout/Profile.swift | 10 +++------- .../Controllers/Nightscout/ProfileManager.swift | 5 ++--- LoopFollow/Controllers/Nightscout/SAge.swift | 10 +++------- LoopFollow/Controllers/Nightscout/Treatments.swift | 10 +++------- .../Nightscout/Treatments/BGCheck.swift | 10 +++------- .../Controllers/Nightscout/Treatments/Basals.swift | 10 +++------- .../Controllers/Nightscout/Treatments/Bolus.swift | 10 +++------- .../Controllers/Nightscout/Treatments/Carbs.swift | 10 +++------- .../Treatments/InsulinCartridgeChange.swift | 10 +++------- .../Controllers/Nightscout/Treatments/Notes.swift | 10 +++------- .../Nightscout/Treatments/Overrides.swift | 10 +++------- .../Nightscout/Treatments/ResumePump.swift | 10 +++------- .../Controllers/Nightscout/Treatments/SMB.swift | 10 +++------- .../Nightscout/Treatments/SensorStart.swift | 10 +++------- .../Nightscout/Treatments/SiteChange.swift | 10 +++------- .../Nightscout/Treatments/SuspendPump.swift | 10 +++------- .../Nightscout/Treatments/TemporaryTarget.swift | 10 +++------- LoopFollow/Controllers/SpeakBG.swift | 4 ++++ LoopFollow/Controllers/Stats.swift | 10 +++------- LoopFollow/Controllers/StatsView.swift | 10 +++------- LoopFollow/Controllers/Timers.swift | 10 +++------- LoopFollow/Dexcom/DexcomSettingsView.swift | 10 +++------- LoopFollow/Dexcom/DexcomSettingsViewModel.swift | 10 +++------- LoopFollow/Extensions/Binding+Optional.swift | 10 +++------- .../Extensions/EKEventStore+Extensions.swift | 10 +++------- .../Extensions/HKQuantity+AnyConvertible.swift | 9 +++------ LoopFollow/Extensions/HKUnit+Extensions.swift | 10 +++------- LoopFollow/Extensions/ShareClientExtension.swift | 10 +++------- LoopFollow/Extensions/UIViewExtension.swift | 10 +++------- LoopFollow/Extensions/UUID+Identifiable.swift | 10 +++------- LoopFollow/Helpers/AnyConvertible.swift | 10 +++------- LoopFollow/Helpers/AppConstants.swift | 10 +++------- LoopFollow/Helpers/AppVersionManager.swift | 10 +++------- LoopFollow/Helpers/BackgroundTaskAudio.swift | 9 +++------ LoopFollow/Helpers/BuildDetails.swift | 10 +++------- LoopFollow/Helpers/Chart.swift | 10 +++------- LoopFollow/Helpers/CycleHelper.swift | 10 +++------- LoopFollow/Helpers/DataStructs.swift | 10 +++------- LoopFollow/Helpers/DateTime.swift | 10 +++------- LoopFollow/Helpers/DictionaryKeyPath.swift | 4 ++++ LoopFollow/Helpers/GitHubService.swift | 10 +++------- LoopFollow/Helpers/Globals.swift | 10 +++------- LoopFollow/Helpers/GlucoseConversion.swift | 10 +++------- LoopFollow/Helpers/Localizer.swift | 10 +++------- LoopFollow/Helpers/Mobileprovision.swift | 4 ++++ LoopFollow/Helpers/NightscoutUtils.swift | 10 +++------- LoopFollow/Helpers/ObservationToken.swift | 10 +++------- LoopFollow/Helpers/TextFieldWithToolBar.swift | 10 +++------- LoopFollow/Helpers/TimeOfDay.swift | 10 +++------- LoopFollow/Helpers/Views/ErrorMessageView.swift | 10 +++------- LoopFollow/Helpers/Views/HKQuantityInputView.swift | 10 +++------- LoopFollow/Helpers/Views/LoadingButtonView.swift | 10 +++------- LoopFollow/Helpers/carbBolusArrays.swift | 10 +++------- LoopFollow/Helpers/isOnPhoneCall.swift | 10 +++------- .../InfoDisplaySettingsView.swift | 10 +++------- .../InfoDisplaySettingsViewModel.swift | 10 +++------- LoopFollow/InfoTable/InfoData.swift | 10 +++------- LoopFollow/InfoTable/InfoDataSeparator.swift | 10 +++------- LoopFollow/InfoTable/InfoManager.swift | 10 +++------- LoopFollow/InfoTable/InfoType.swift | 10 +++------- LoopFollow/Log/LogEntry.swift | 10 +++------- LoopFollow/Log/LogManager.swift | 10 +++------- LoopFollow/Log/LogView.swift | 10 +++------- LoopFollow/Log/LogViewModel.swift | 10 +++------- LoopFollow/Log/SearchBar.swift | 10 +++------- LoopFollow/Metric/CarbMetric.swift | 10 +++------- LoopFollow/Metric/InsulinMetric.swift | 10 +++------- LoopFollow/Metric/Metric.swift | 10 +++------- LoopFollow/Nightscout/NightscoutSettingsView.swift | 10 +++------- .../Nightscout/NightscoutSettingsViewModel.swift | 10 +++------- .../Remote/Loop/LoopNightscoutRemoteView.swift | 10 +++------- LoopFollow/Remote/Loop/LoopOverrideView.swift | 10 +++------- LoopFollow/Remote/Loop/LoopOverrideViewModel.swift | 10 +++------- .../Nightscout/TrioNightscoutRemoteView.swift | 10 +++------- LoopFollow/Remote/NoRemoteView.swift | 10 +++------- LoopFollow/Remote/RemoteType.swift | 10 +++------- LoopFollow/Remote/RemoteViewController.swift | 10 +++------- .../Remote/Settings/RemoteSettingsView.swift | 11 +++-------- .../Remote/Settings/RemoteSettingsViewModel.swift | 10 +++------- LoopFollow/Remote/TRC/BolusView.swift | 10 +++------- LoopFollow/Remote/TRC/MealView.swift | 10 +++------- LoopFollow/Remote/TRC/OverrideView.swift | 10 +++------- LoopFollow/Remote/TRC/PushMessage.swift | 10 +++------- .../Remote/TRC/PushNotificationManager.swift | 10 +++------- LoopFollow/Remote/TRC/TRCCommandType.swift | 10 +++------- LoopFollow/Remote/TRC/TempTargetView.swift | 10 +++------- LoopFollow/Remote/TRC/TreatmentResponse.swift | 10 +++------- .../TRC/TrioNightscoutRemoteController.swift | 10 +++------- LoopFollow/Remote/TRC/TrioRemoteControlView.swift | 10 +++------- .../Remote/TRC/TrioRemoteControlViewModel.swift | 10 +++------- .../Remote/TempTargetPreset/TempTargetPreset.swift | 10 +++------- .../TempTargetPreset/TempTargetPresetManager.swift | 10 +++------- LoopFollow/Settings/AdvancedSettingsView.swift | 10 +++------- .../Settings/AdvancedSettingsViewModel.swift | 10 +++------- LoopFollow/Snoozer/SnoozerView.swift | 10 +++------- LoopFollow/Snoozer/SnoozerViewController.swift | 10 +++------- LoopFollow/Snoozer/SnoozerViewModel.swift | 10 +++------- .../Framework/ObservableUserDefaultsValue.swift | 10 +++------- LoopFollow/Storage/Framework/ObservableValue.swift | 10 +++------- .../Storage/Framework/SecureStorageValue.swift | 10 +++------- LoopFollow/Storage/Framework/StorageValue.swift | 10 +++------- .../Storage/Framework/UserDefaultsValue.swift | 10 +++------- .../Framework/UserDefaultsValueGroups.swift | 10 +++------- LoopFollow/Storage/Observable.swift | 10 +++------- LoopFollow/Storage/ObservableUserDefaults.swift | 10 +++------- LoopFollow/Storage/Storage.swift | 10 +++------- LoopFollow/Storage/UserDefaults.swift | 14 +++----------- LoopFollow/Task/AlarmTask.swift | 10 +++------- LoopFollow/Task/BGTask.swift | 10 +++------- LoopFollow/Task/CalendarTask.swift | 10 +++------- LoopFollow/Task/DeviceStatusTask.swift | 10 +++------- LoopFollow/Task/MinAgoTask.swift | 10 +++------- LoopFollow/Task/ProfileTask.swift | 10 +++------- LoopFollow/Task/Task.swift | 10 +++------- LoopFollow/Task/TaskScheduler.swift | 10 +++------- LoopFollow/Task/TreatmentsTask.swift | 10 +++------- .../ViewControllers/AppStateViewController.swift | 10 +++------- .../GeneralSettingsViewController.swift | 10 +++------- .../GraphSettingsViewController.swift | 10 +++------- .../ViewControllers/MainViewController.swift | 10 +++------- .../ViewControllers/NightScoutViewController.swift | 10 +++------- .../ViewControllers/SettingsViewController.swift | 10 +++------- .../WatchSettingsViewController.swift | 10 +++------- LoopFollowTests/AlwaysTrueCondition.swift | 10 +++------- LoopFollowTests/BuildExpireConditionTests.swift | 10 +++------- Scripts/swiftformat.sh | 4 ++-- 211 files changed, 632 insertions(+), 1420 deletions(-) diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index 085148375..002642b90 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -1,10 +1,6 @@ -// -// Alarm.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-03-15. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// Alarm.swift +// Created by Jonas Björkert on 2025-04-26. import Foundation import HealthKit diff --git a/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift b/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift index 04e77b052..d7c2a59b6 100644 --- a/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift @@ -1,10 +1,6 @@ -// -// AlarmCondition.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-04-18. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// AlarmCondition.swift +// Created by Jonas Björkert on 2025-04-26. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/BuildExpireCondition.swift b/LoopFollow/Alarm/AlarmCondition/BuildExpireCondition.swift index 2308d03b9..192d98107 100644 --- a/LoopFollow/Alarm/AlarmCondition/BuildExpireCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/BuildExpireCondition.swift @@ -1,10 +1,6 @@ -// -// BuildExpireCondition.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-04-18. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// BuildExpireCondition.swift +// Created by Jonas Björkert on 2025-04-26. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/COBCondition.swift b/LoopFollow/Alarm/AlarmCondition/COBCondition.swift index 34a6a66b0..1812ca66d 100644 --- a/LoopFollow/Alarm/AlarmCondition/COBCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/COBCondition.swift @@ -1,10 +1,6 @@ -// -// COBCondition.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-15. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// COBCondition.swift +// Created by Jonas Björkert on 2025-05-15. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift b/LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift index f075568d3..cd528be30 100644 --- a/LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift @@ -1,10 +1,6 @@ -// -// FastDropCondition.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-14. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// FastDropCondition.swift +// Created by Jonas Björkert on 2025-05-14. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/FastRiseCondition.swift b/LoopFollow/Alarm/AlarmCondition/FastRiseCondition.swift index 6201424f6..a192a2caf 100644 --- a/LoopFollow/Alarm/AlarmCondition/FastRiseCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/FastRiseCondition.swift @@ -1,9 +1,6 @@ -// -// FastRiseCondition.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-15. -// +// LoopFollow +// FastRiseCondition.swift +// Created by Jonas Björkert on 2025-05-15. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift b/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift index bc136a2dd..c77def6e3 100644 --- a/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift @@ -1,10 +1,6 @@ -// -// HighBGCondition.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-10. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// HighBGCondition.swift +// Created by Jonas Björkert on 2025-05-09. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift b/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift index 8f8596bb7..3b74e1adf 100644 --- a/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift @@ -1,10 +1,6 @@ -// -// LowBGCondition.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-09. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// LowBGCondition.swift +// Created by Jonas Björkert on 2025-05-09. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/MissedReadingCondition.swift b/LoopFollow/Alarm/AlarmCondition/MissedReadingCondition.swift index c6bdf7ae6..7a612aae0 100644 --- a/LoopFollow/Alarm/AlarmCondition/MissedReadingCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/MissedReadingCondition.swift @@ -1,10 +1,6 @@ -// -// MissedReadingCondition.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-09. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// MissedReadingCondition.swift +// Created by Jonas Björkert on 2025-05-10. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/NotLoopingCondition.swift b/LoopFollow/Alarm/AlarmCondition/NotLoopingCondition.swift index 80f3d958c..dbadd1b06 100644 --- a/LoopFollow/Alarm/AlarmCondition/NotLoopingCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/NotLoopingCondition.swift @@ -1,10 +1,6 @@ -// -// NotLoopingCondition.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-13. -// Copyright © 2025 Jon Fawcett. -// +// LoopFollow +// NotLoopingCondition.swift +// Created by Jonas Björkert on 2025-05-14. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/OverrideEndCondition.swift b/LoopFollow/Alarm/AlarmCondition/OverrideEndCondition.swift index 460a9df9e..2960777f8 100644 --- a/LoopFollow/Alarm/AlarmCondition/OverrideEndCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/OverrideEndCondition.swift @@ -1,10 +1,6 @@ -// -// OverrideEndCondition.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-14. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// OverrideEndCondition.swift +// Created by Jonas Björkert on 2025-05-14. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift b/LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift index 2bcd9edcc..c89b98f01 100644 --- a/LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift @@ -1,10 +1,6 @@ -// -// OverrideStartCondition.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-14. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// OverrideStartCondition.swift +// Created by Jonas Björkert on 2025-05-14. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/RecBolusCondition.swift b/LoopFollow/Alarm/AlarmCondition/RecBolusCondition.swift index 8f9a8a650..1210389bc 100644 --- a/LoopFollow/Alarm/AlarmCondition/RecBolusCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/RecBolusCondition.swift @@ -1,10 +1,6 @@ -// -// RecBolusCondition.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-15. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// RecBolusCondition.swift +// Created by Jonas Björkert on 2025-05-15. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/SensorAgeCondition.swift b/LoopFollow/Alarm/AlarmCondition/SensorAgeCondition.swift index 8b8bfd518..9a8d9b88a 100644 --- a/LoopFollow/Alarm/AlarmCondition/SensorAgeCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/SensorAgeCondition.swift @@ -1,10 +1,6 @@ -// -// SensorAgeCondition.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-17. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// SensorAgeCondition.swift +// Created by Jonas Björkert on 2025-05-17. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/TempTargetEndCondition.swift b/LoopFollow/Alarm/AlarmCondition/TempTargetEndCondition.swift index 902ab1409..b16b03890 100644 --- a/LoopFollow/Alarm/AlarmCondition/TempTargetEndCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/TempTargetEndCondition.swift @@ -1,10 +1,6 @@ -// -// TempTargetEndCondition.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-14. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// TempTargetEndCondition.swift +// Created by Jonas Björkert on 2025-05-14. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/TempTargetStartCondition.swift b/LoopFollow/Alarm/AlarmCondition/TempTargetStartCondition.swift index 982ba89e9..754541df7 100644 --- a/LoopFollow/Alarm/AlarmCondition/TempTargetStartCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/TempTargetStartCondition.swift @@ -1,10 +1,6 @@ -// -// TempTargetStartCondition.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-14. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// TempTargetStartCondition.swift +// Created by Jonas Björkert on 2025-05-14. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/TemporaryCondition.swift b/LoopFollow/Alarm/AlarmCondition/TemporaryCondition.swift index 54bd67a0f..489a13807 100644 --- a/LoopFollow/Alarm/AlarmCondition/TemporaryCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/TemporaryCondition.swift @@ -1,10 +1,6 @@ -// -// TemporaryCondition.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-16. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// TemporaryCondition.swift +// Created by Jonas Björkert on 2025-05-16. import Foundation diff --git a/LoopFollow/Alarm/AlarmConfiguration.swift b/LoopFollow/Alarm/AlarmConfiguration.swift index 8765bb268..993539ff3 100644 --- a/LoopFollow/Alarm/AlarmConfiguration.swift +++ b/LoopFollow/Alarm/AlarmConfiguration.swift @@ -1,9 +1,6 @@ -// AlarmConfiguration.swift // LoopFollow -// -// Created by Jonas Björkert on 2025‑04‑20. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// AlarmConfiguration.swift +// Created by Jonas Björkert on 2025-04-26. import Foundation diff --git a/LoopFollow/Alarm/AlarmData.swift b/LoopFollow/Alarm/AlarmData.swift index 14be132b6..010a170df 100644 --- a/LoopFollow/Alarm/AlarmData.swift +++ b/LoopFollow/Alarm/AlarmData.swift @@ -1,10 +1,6 @@ -// -// AlarmData.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-03-15. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// AlarmData.swift +// Created by Jonas Björkert on 2025-04-26. import Foundation diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index b367c82b6..cabe3ce3d 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -1,3 +1,7 @@ +// LoopFollow +// AlarmEditor.swift +// Created by Jonas Björkert on 2025-04-26. + // // AlarmEditor.swift // LoopFollow diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmActiveSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmActiveSection.swift index 7d7c5f639..66b594083 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmActiveSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmActiveSection.swift @@ -1,10 +1,6 @@ -// -// AlarmActiveSection.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-12. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// AlarmActiveSection.swift +// Created by Jonas Björkert on 2025-05-12. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift index 004a7e732..ce34800be 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift @@ -1,10 +1,6 @@ -// -// AlarmAudioSection.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-12. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// AlarmAudioSection.swift +// Created by Jonas Björkert on 2025-05-12. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift index 19713e3bb..174230bf4 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift @@ -1,10 +1,6 @@ -// -// AlarmBGLimitSection.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-13. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// AlarmBGLimitSection.swift +// Created by Jonas Björkert on 2025-05-14. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGPicker.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGPicker.swift index cf532ffef..5b51f0a02 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGPicker.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGPicker.swift @@ -1,10 +1,6 @@ -// -// AlarmBGPicker.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-13. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// AlarmBGPicker.swift +// Created by Jonas Björkert on 2025-05-14. import HealthKit import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift index dc859074e..444694447 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift @@ -1,10 +1,6 @@ -// -// AlarmBGSection.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-06. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// AlarmBGSection.swift +// Created by Jonas Björkert on 2025-05-06. import HealthKit import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmGeneralSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmGeneralSection.swift index 8643279fb..95e19fc05 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmGeneralSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmGeneralSection.swift @@ -1,10 +1,6 @@ -// -// AlarmGeneralSection.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-04-21. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// AlarmGeneralSection.swift +// Created by Jonas Björkert on 2025-05-12. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift index 51ce7f1ab..2167838bd 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift @@ -1,10 +1,6 @@ -// -// AlarmSnoozeSection.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-12. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// AlarmSnoozeSection.swift +// Created by Jonas Björkert on 2025-05-12. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift index 977535e64..b5f7829ce 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift @@ -1,10 +1,6 @@ -// -// AlarmStepperSection.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-10. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// AlarmStepperSection.swift +// Created by Jonas Björkert on 2025-05-10. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift b/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift index 6862b5f20..00fffcd7e 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift @@ -1,10 +1,6 @@ -// -// InfoBanner.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-10. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// InfoBanner.swift +// Created by Jonas Björkert on 2025-05-10. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift b/LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift index 145c963e4..62c8e96d0 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift @@ -1,10 +1,6 @@ -// -// SoundFile.swift -// SoundFile -// -// Created by Jonas Björkert on 2025-04-21. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// SoundFile.swift +// Created by Jonas Björkert on 2025-04-26. import Foundation diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift index 6d96e7705..9ad71c1a9 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift @@ -1,10 +1,6 @@ -// -// BuildExpireAlarmEditor.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-04-21. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// BuildExpireAlarmEditor.swift +// Created by Jonas Björkert on 2025-04-26. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift index 36443c190..87f06cc8c 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift @@ -1,10 +1,6 @@ -// -// COBAlarmEditor.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-15. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// COBAlarmEditor.swift +// Created by Jonas Björkert on 2025-05-15. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift index 33155525b..d4bc03e0e 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift @@ -1,10 +1,6 @@ -// -// FastDropAlarmEditor.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-10. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// FastDropAlarmEditor.swift +// Created by Jonas Björkert on 2025-05-11. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift index 002f33bb7..04833262e 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift @@ -1,10 +1,6 @@ -// -// FastRiseAlarmEditor.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-15. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// FastRiseAlarmEditor.swift +// Created by Jonas Björkert on 2025-05-15. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift index e4f3ab611..0fbcfb47c 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift @@ -1,10 +1,6 @@ -// -// HighBgAlarmEditor.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-09. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// HighBgAlarmEditor.swift +// Created by Jonas Björkert on 2025-04-26. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift index e3c352148..dec295a94 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift @@ -1,10 +1,6 @@ -// -// LowBgAlarmEditor.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-04-21. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// LowBgAlarmEditor.swift +// Created by Jonas Björkert on 2025-04-26. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift index 7db03d291..7052355cb 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift @@ -1,10 +1,6 @@ -// -// MissedReadingEditor.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-09. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// MissedReadingEditor.swift +// Created by Jonas Björkert on 2025-04-26. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift index 616afdb45..6150958ec 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift @@ -1,10 +1,6 @@ -// -// NotLoopingAlarmEditor.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-14. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// NotLoopingAlarmEditor.swift +// Created by Jonas Björkert on 2025-04-26. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift index 1cb0cdb82..1dc6a0dd0 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift @@ -1,10 +1,6 @@ -// -// OverrideEndAlarmEditor.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-14. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// OverrideEndAlarmEditor.swift +// Created by Jonas Björkert on 2025-05-14. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift index d5b489868..6915810bb 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift @@ -1,10 +1,6 @@ -// -// OverrideStartAlarmEditor.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-14. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// OverrideStartAlarmEditor.swift +// Created by Jonas Björkert on 2025-05-14. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift index 2812ad658..dc4366360 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift @@ -1,10 +1,6 @@ -// -// RecBolusAlarmEditor.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-15. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// RecBolusAlarmEditor.swift +// Created by Jonas Björkert on 2025-05-15. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift index c0e72df8d..a55514085 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift @@ -1,10 +1,6 @@ -// -// SensorAgeAlarmEditor.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-17. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// SensorAgeAlarmEditor.swift +// Created by Jonas Björkert on 2025-05-17. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift index 7fd315e05..28ed1282c 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift @@ -1,10 +1,6 @@ -// -// TempTargetEndAlarmEditor.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-14. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// TempTargetEndAlarmEditor.swift +// Created by Jonas Björkert on 2025-05-14. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift index dd5d8d3e1..d710c1367 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift @@ -1,10 +1,6 @@ -// -// TempTargetStartAlarmEditor.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-14. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// TempTargetStartAlarmEditor.swift +// Created by Jonas Björkert on 2025-05-14. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift index 7e804cf2b..a54721edf 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift @@ -1,10 +1,6 @@ -// -// TemporaryAlarmEditor.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-16. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// TemporaryAlarmEditor.swift +// Created by Jonas Björkert on 2025-05-16. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index 3ff1eac58..d8cab080d 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -1,10 +1,6 @@ -// -// AlarmListView.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-04-21. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// AlarmListView.swift +// Created by Jonas Björkert on 2025-04-26. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index 4f67e1df2..c1cfe2727 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -1,10 +1,6 @@ -// -// AlarmManager.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-03-15. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// AlarmManager.swift +// Created by Jonas Björkert on 2025-04-26. import Foundation import UserNotifications diff --git a/LoopFollow/Alarm/AlarmSettingsView.swift b/LoopFollow/Alarm/AlarmSettingsView.swift index e05ebb079..19d222fff 100644 --- a/LoopFollow/Alarm/AlarmSettingsView.swift +++ b/LoopFollow/Alarm/AlarmSettingsView.swift @@ -1,10 +1,6 @@ -// -// AlarmSettingsView.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025‑04‑20. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// AlarmSettingsView.swift +// Created by Jonas Björkert on 2025-04-26. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmType/AlarmType+SortDirection.swift b/LoopFollow/Alarm/AlarmType/AlarmType+SortDirection.swift index 6d9350c1d..288bf1fa2 100644 --- a/LoopFollow/Alarm/AlarmType/AlarmType+SortDirection.swift +++ b/LoopFollow/Alarm/AlarmType/AlarmType+SortDirection.swift @@ -1,10 +1,6 @@ -// -// AlarmType+SortDirection.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-16. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// AlarmType+SortDirection.swift +// Created by Jonas Björkert on 2025-05-16. import Foundation diff --git a/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift b/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift index 487eb6458..06b75be13 100644 --- a/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift +++ b/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift @@ -1,10 +1,6 @@ -// -// AlarmType+timeUnit.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-16. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// AlarmType+timeUnit.swift +// Created by Jonas Björkert on 2025-05-16. import Foundation diff --git a/LoopFollow/Alarm/AlarmType/AlarmType.swift b/LoopFollow/Alarm/AlarmType/AlarmType.swift index de006bb72..6b0b58c1d 100644 --- a/LoopFollow/Alarm/AlarmType/AlarmType.swift +++ b/LoopFollow/Alarm/AlarmType/AlarmType.swift @@ -1,10 +1,6 @@ -// -// AlarmType.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-03-15. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// AlarmType.swift +// Created by Jonas Björkert on 2025-04-26. import Foundation diff --git a/LoopFollow/Alarm/GlucoseValue.swift b/LoopFollow/Alarm/GlucoseValue.swift index e2e1696cf..bd80a0ad6 100644 --- a/LoopFollow/Alarm/GlucoseValue.swift +++ b/LoopFollow/Alarm/GlucoseValue.swift @@ -1,10 +1,6 @@ -// -// GlucoseValue.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-05. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// GlucoseValue.swift +// Created by Jonas Björkert on 2025-05-06. import Foundation diff --git a/LoopFollow/Alarm/SnoozeState.swift b/LoopFollow/Alarm/SnoozeState.swift index 42d3e14eb..8a3a10bc5 100644 --- a/LoopFollow/Alarm/SnoozeState.swift +++ b/LoopFollow/Alarm/SnoozeState.swift @@ -1,10 +1,6 @@ -// -// SnoozeState.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-03-15. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// SnoozeState.swift +// Created by Jonas Björkert on 2025-04-26. import Foundation diff --git a/LoopFollow/Application/AppDelegate.swift b/LoopFollow/Application/AppDelegate.swift index 8fc075a40..d3f213c50 100644 --- a/LoopFollow/Application/AppDelegate.swift +++ b/LoopFollow/Application/AppDelegate.swift @@ -1,10 +1,6 @@ -// -// AppDelegate.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/1/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// AppDelegate.swift +// Created by Jon Fawcett on 2020-06-01. import CoreData import EventKit diff --git a/LoopFollow/Application/SceneDelegate.swift b/LoopFollow/Application/SceneDelegate.swift index 4c15fac80..d45572b4d 100644 --- a/LoopFollow/Application/SceneDelegate.swift +++ b/LoopFollow/Application/SceneDelegate.swift @@ -1,10 +1,6 @@ -// -// SceneDelegate.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/1/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// SceneDelegate.swift +// Created by Jon Fawcett on 2020-06-01. import AVFoundation import UIKit diff --git a/LoopFollow/BackgroundRefresh/BT/BLEDevice.swift b/LoopFollow/BackgroundRefresh/BT/BLEDevice.swift index 3a1a809d8..bac066e72 100644 --- a/LoopFollow/BackgroundRefresh/BT/BLEDevice.swift +++ b/LoopFollow/BackgroundRefresh/BT/BLEDevice.swift @@ -1,9 +1,6 @@ -// -// BLEDevice.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-02. -// +// LoopFollow +// BLEDevice.swift +// Created by Jonas Björkert on 2025-01-13. import Foundation diff --git a/LoopFollow/BackgroundRefresh/BT/BLEDeviceSelectionView.swift b/LoopFollow/BackgroundRefresh/BT/BLEDeviceSelectionView.swift index 77002f366..ada5167c8 100644 --- a/LoopFollow/BackgroundRefresh/BT/BLEDeviceSelectionView.swift +++ b/LoopFollow/BackgroundRefresh/BT/BLEDeviceSelectionView.swift @@ -1,7 +1,6 @@ -// -// BLEDeviceSelectionView.swift -// LoopFollow -// +// LoopFollow +// BLEDeviceSelectionView.swift +// Created by Jonas Björkert on 2025-01-13. import SwiftUI diff --git a/LoopFollow/BackgroundRefresh/BT/BLEManager.swift b/LoopFollow/BackgroundRefresh/BT/BLEManager.swift index 6c55a2e09..11f83522a 100644 --- a/LoopFollow/BackgroundRefresh/BT/BLEManager.swift +++ b/LoopFollow/BackgroundRefresh/BT/BLEManager.swift @@ -1,7 +1,6 @@ -// -// BLEManager.swift -// LoopFollow -// +// LoopFollow +// BLEManager.swift +// Created by Jonas Björkert on 2025-01-13. import Combine import CoreBluetooth diff --git a/LoopFollow/BackgroundRefresh/BT/BluetoothDevice.swift b/LoopFollow/BackgroundRefresh/BT/BluetoothDevice.swift index dbf6e665f..aed1f0845 100644 --- a/LoopFollow/BackgroundRefresh/BT/BluetoothDevice.swift +++ b/LoopFollow/BackgroundRefresh/BT/BluetoothDevice.swift @@ -1,10 +1,6 @@ -// -// BluetoothDevice.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-04. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// BluetoothDevice.swift +// Created by Jonas Björkert on 2025-01-13. import CoreBluetooth import Foundation diff --git a/LoopFollow/BackgroundRefresh/BT/BluetoothDeviceDelegate.swift b/LoopFollow/BackgroundRefresh/BT/BluetoothDeviceDelegate.swift index a1d254bae..b9c946adc 100644 --- a/LoopFollow/BackgroundRefresh/BT/BluetoothDeviceDelegate.swift +++ b/LoopFollow/BackgroundRefresh/BT/BluetoothDeviceDelegate.swift @@ -1,10 +1,6 @@ -// -// BluetoothDeviceDelegate.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-04. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// BluetoothDeviceDelegate.swift +// Created by Jonas Björkert on 2025-01-13. import CoreBluetooth import Foundation diff --git a/LoopFollow/BackgroundRefresh/BT/Devices/DexcomHeartbeatBluetoothDevice.swift b/LoopFollow/BackgroundRefresh/BT/Devices/DexcomHeartbeatBluetoothDevice.swift index d16f7b8ad..fa2347779 100644 --- a/LoopFollow/BackgroundRefresh/BT/Devices/DexcomHeartbeatBluetoothDevice.swift +++ b/LoopFollow/BackgroundRefresh/BT/Devices/DexcomHeartbeatBluetoothDevice.swift @@ -1,10 +1,6 @@ -// -// DexcomHeartbeatBluetoothDevice.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-04. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// DexcomHeartbeatBluetoothDevice.swift +// Created by Jonas Björkert on 2025-01-13. import AVFoundation import CoreBluetooth diff --git a/LoopFollow/BackgroundRefresh/BT/Devices/OmnipodDashHeartbeatBluetoothTransmitter.swift b/LoopFollow/BackgroundRefresh/BT/Devices/OmnipodDashHeartbeatBluetoothTransmitter.swift index e383c4c06..2ae9a07af 100644 --- a/LoopFollow/BackgroundRefresh/BT/Devices/OmnipodDashHeartbeatBluetoothTransmitter.swift +++ b/LoopFollow/BackgroundRefresh/BT/Devices/OmnipodDashHeartbeatBluetoothTransmitter.swift @@ -1,10 +1,6 @@ -// -// OmnipodDashHeartbeatBluetoothTransmitter.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-03-01. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// OmnipodDashHeartbeatBluetoothTransmitter.swift +// Created by Jonas Björkert on 2025-01-13. import CoreBluetooth import Foundation diff --git a/LoopFollow/BackgroundRefresh/BT/Devices/RileyLinkHeartbeatBluetoothDevice.swift b/LoopFollow/BackgroundRefresh/BT/Devices/RileyLinkHeartbeatBluetoothDevice.swift index a89d2ee50..c00f42625 100644 --- a/LoopFollow/BackgroundRefresh/BT/Devices/RileyLinkHeartbeatBluetoothDevice.swift +++ b/LoopFollow/BackgroundRefresh/BT/Devices/RileyLinkHeartbeatBluetoothDevice.swift @@ -1,10 +1,6 @@ -// -// RileyLinkHeartbeatBluetoothDevice.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-08. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// RileyLinkHeartbeatBluetoothDevice.swift +// Created by Jonas Björkert on 2025-01-13. import CoreBluetooth import Foundation diff --git a/LoopFollow/BackgroundRefresh/BT/DexcomG7HeartBeat.swift b/LoopFollow/BackgroundRefresh/BT/DexcomG7HeartBeat.swift index 861b34739..5a9639c1a 100644 --- a/LoopFollow/BackgroundRefresh/BT/DexcomG7HeartBeat.swift +++ b/LoopFollow/BackgroundRefresh/BT/DexcomG7HeartBeat.swift @@ -1,10 +1,6 @@ -// -// DexcomG7HeartBeat.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-04. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// DexcomG7HeartBeat.swift +// Created by Jonas Björkert on 2025-01-13. // Denna behövs diff --git a/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift b/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift index 4afbdfa1c..e26e26452 100644 --- a/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift +++ b/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift @@ -1,7 +1,6 @@ -// -// BackgroundRefreshSettingsView.swift -// LoopFollow -// +// LoopFollow +// BackgroundRefreshSettingsView.swift +// Created by Jonas Björkert on 2025-01-13. import SwiftUI diff --git a/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsViewModel.swift b/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsViewModel.swift index a878f11ee..746721640 100644 --- a/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsViewModel.swift +++ b/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsViewModel.swift @@ -1,9 +1,6 @@ -// -// BackgroundRefreshSettingsViewModel.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-02. -// +// LoopFollow +// BackgroundRefreshSettingsViewModel.swift +// Created by Jonas Björkert on 2025-01-13. import Combine import Foundation diff --git a/LoopFollow/BackgroundRefresh/BackgroundRefreshType.swift b/LoopFollow/BackgroundRefresh/BackgroundRefreshType.swift index 7e2da5177..ae38ccf17 100644 --- a/LoopFollow/BackgroundRefresh/BackgroundRefreshType.swift +++ b/LoopFollow/BackgroundRefresh/BackgroundRefreshType.swift @@ -1,9 +1,6 @@ -// -// BackgroundRefreshType.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-02. -// +// LoopFollow +// BackgroundRefreshType.swift +// Created by Jonas Björkert on 2025-01-13. import Foundation diff --git a/LoopFollow/Contact/ContactColorOption.swift b/LoopFollow/Contact/ContactColorOption.swift index 18e171e9a..0641fb524 100644 --- a/LoopFollow/Contact/ContactColorOption.swift +++ b/LoopFollow/Contact/ContactColorOption.swift @@ -1,10 +1,6 @@ -// -// ContactColorOption.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-02-22. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// ContactColorOption.swift +// Created by Jonas Björkert on 2025-02-23. import UIKit diff --git a/LoopFollow/Contact/ContactImageUpdater.swift b/LoopFollow/Contact/ContactImageUpdater.swift index 8898ae410..96477ab83 100644 --- a/LoopFollow/Contact/ContactImageUpdater.swift +++ b/LoopFollow/Contact/ContactImageUpdater.swift @@ -1,10 +1,6 @@ -// -// ContactImageUpdater.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-12-10. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// ContactImageUpdater.swift +// Created by Jonas Björkert on 2024-12-10. import Contacts import Foundation diff --git a/LoopFollow/Contact/ContactIncludeOption.swift b/LoopFollow/Contact/ContactIncludeOption.swift index cf2bbb1ab..b41d5bc95 100644 --- a/LoopFollow/Contact/ContactIncludeOption.swift +++ b/LoopFollow/Contact/ContactIncludeOption.swift @@ -1,10 +1,6 @@ -// -// ContactIncludeOption.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-02-22. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// ContactIncludeOption.swift +// Created by Jonas Björkert on 2025-02-23. enum ContactIncludeOption: String, Codable, Equatable, CaseIterable { case off = "Off" diff --git a/LoopFollow/Contact/ContactType.swift b/LoopFollow/Contact/ContactType.swift index 4eab0fc9a..6181dd360 100644 --- a/LoopFollow/Contact/ContactType.swift +++ b/LoopFollow/Contact/ContactType.swift @@ -1,10 +1,6 @@ -// -// ContactType.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-02-23. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// ContactType.swift +// Created by Jonas Björkert on 2025-02-23. enum ContactType: String, CaseIterable { case BG diff --git a/LoopFollow/Contact/Settings/ContactSettingsView.swift b/LoopFollow/Contact/Settings/ContactSettingsView.swift index fd94dd267..0d041aa4a 100644 --- a/LoopFollow/Contact/Settings/ContactSettingsView.swift +++ b/LoopFollow/Contact/Settings/ContactSettingsView.swift @@ -1,10 +1,6 @@ -// -// ContactSettingsView.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-12-10. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// ContactSettingsView.swift +// Created by Jonas Björkert on 2024-12-10. import Contacts import SwiftUI diff --git a/LoopFollow/Contact/Settings/ContactSettingsViewModel.swift b/LoopFollow/Contact/Settings/ContactSettingsViewModel.swift index 374a199bb..a9ee26a52 100644 --- a/LoopFollow/Contact/Settings/ContactSettingsViewModel.swift +++ b/LoopFollow/Contact/Settings/ContactSettingsViewModel.swift @@ -1,10 +1,6 @@ -// -// ContactSettingsViewModel.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-12-10. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// ContactSettingsViewModel.swift +// Created by Jonas Björkert on 2024-12-10. import Combine import Foundation diff --git a/LoopFollow/Controllers/AlarmSound.swift b/LoopFollow/Controllers/AlarmSound.swift index 6f5b3839a..3755d15e2 100644 --- a/LoopFollow/Controllers/AlarmSound.swift +++ b/LoopFollow/Controllers/AlarmSound.swift @@ -1,10 +1,6 @@ -// -// AlarmSound.swift -// scoutwatch -// -// Created by Dirk Hermanns on 03.01.16. -// Copyright © 2016 private. All rights reserved. -// +// LoopFollow +// AlarmSound.swift +// Created by Jon Fawcett on 2020-06-07. import AVFoundation import Foundation diff --git a/LoopFollow/Controllers/AppStateController.swift b/LoopFollow/Controllers/AppStateController.swift index 3e1764b2b..e412ec9d3 100644 --- a/LoopFollow/Controllers/AppStateController.swift +++ b/LoopFollow/Controllers/AppStateController.swift @@ -1,10 +1,6 @@ -// -// AppStateController.swift -// LoopFollow -// -// Created by Jose Paredes on 7/17/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// AppStateController.swift +// Created by Jose Paredes on 2020-07-18. import Foundation diff --git a/LoopFollow/Controllers/BackgroundAlertManager.swift b/LoopFollow/Controllers/BackgroundAlertManager.swift index 614332d20..1aba069d6 100644 --- a/LoopFollow/Controllers/BackgroundAlertManager.swift +++ b/LoopFollow/Controllers/BackgroundAlertManager.swift @@ -1,10 +1,6 @@ -// -// BackgroundAlertManager.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-06-22. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// BackgroundAlertManager.swift +// Created by Jonas Björkert on 2024-06-22. import Foundation import UserNotifications diff --git a/LoopFollow/Controllers/Graphs.swift b/LoopFollow/Controllers/Graphs.swift index eaf55199e..b511e11ce 100644 --- a/LoopFollow/Controllers/Graphs.swift +++ b/LoopFollow/Controllers/Graphs.swift @@ -1,10 +1,6 @@ -// -// Graphs.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/16/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// Graphs.swift +// Created by Jon Fawcett on 2020-06-17. import Charts import Foundation diff --git a/LoopFollow/Controllers/NightScout.swift b/LoopFollow/Controllers/NightScout.swift index 728b8e6ac..dfb8b14bd 100644 --- a/LoopFollow/Controllers/NightScout.swift +++ b/LoopFollow/Controllers/NightScout.swift @@ -1,10 +1,6 @@ -// -// NightScout.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/16/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// NightScout.swift +// Created by Jon Fawcett on 2020-06-17. import Foundation import UIKit diff --git a/LoopFollow/Controllers/Nightscout/BGData.swift b/LoopFollow/Controllers/Nightscout/BGData.swift index 7755d7929..e98ec84b9 100644 --- a/LoopFollow/Controllers/Nightscout/BGData.swift +++ b/LoopFollow/Controllers/Nightscout/BGData.swift @@ -1,10 +1,6 @@ -// -// BGData.swift -// LoopFollow -// -// Created by Jonas Björkert on 2023-10-05. -// Copyright © 2023 Jon Fawcett. All rights reserved. -// +// LoopFollow +// BGData.swift +// Created by Jonas Björkert on 2023-10-05. import Foundation import UIKit diff --git a/LoopFollow/Controllers/Nightscout/CAge.swift b/LoopFollow/Controllers/Nightscout/CAge.swift index 48ce23907..224040894 100644 --- a/LoopFollow/Controllers/Nightscout/CAge.swift +++ b/LoopFollow/Controllers/Nightscout/CAge.swift @@ -1,10 +1,6 @@ -// -// CAge.swift -// LoopFollow -// -// Created by Jonas Björkert on 2023-10-05. -// Copyright © 2023 Jon Fawcett. All rights reserved. -// +// LoopFollow +// CAge.swift +// Created by Jonas Björkert on 2023-10-05. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift index 8f79a281a..7aac5c966 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift @@ -1,10 +1,6 @@ -// -// DeviceStatus.swift -// LoopFollow -// -// Created by Jonas Björkert on 2023-10-05. -// Copyright © 2023 Jon Fawcett. All rights reserved. -// +// LoopFollow +// DeviceStatus.swift +// Created by Jonas Björkert on 2023-10-05. import Charts import Foundation diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift index c9fc05bf2..e132207a3 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift @@ -1,10 +1,6 @@ -// -// DeviceStatusLoop.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-06-16. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// DeviceStatusLoop.swift +// Created by Jonas Björkert on 2024-06-16. import Charts import Foundation diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift index af2c1f0f3..7443d4b33 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift @@ -1,7 +1,6 @@ -// DeviceStatusOpenAPS.swift // LoopFollow -// Created by Jonas Björkert on 2024-05-19. -// Copyright © 2024 Jon Fawcett. All rights reserved. +// DeviceStatusOpenAPS.swift +// Created by Jonas Björkert on 2024-05-31. import Foundation import HealthKit diff --git a/LoopFollow/Controllers/Nightscout/IAge.swift b/LoopFollow/Controllers/Nightscout/IAge.swift index 39efc9561..d7daa42ce 100644 --- a/LoopFollow/Controllers/Nightscout/IAge.swift +++ b/LoopFollow/Controllers/Nightscout/IAge.swift @@ -1,10 +1,6 @@ -// -// IAge.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-08-05. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// IAge.swift +// Created by Jonas Björkert on 2024-08-05. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/NSProfile.swift b/LoopFollow/Controllers/Nightscout/NSProfile.swift index 45b540dff..3a879e87f 100644 --- a/LoopFollow/Controllers/Nightscout/NSProfile.swift +++ b/LoopFollow/Controllers/Nightscout/NSProfile.swift @@ -1,7 +1,6 @@ -// NSProfile.swift // LoopFollow -// Created by Jonas Björkert on 2024-07-12. -// Copyright © 2024 Jon Fawcett. All rights reserved. +// NSProfile.swift +// Created by Jonas Björkert on 2024-07-15. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/Profile.swift b/LoopFollow/Controllers/Nightscout/Profile.swift index a298c4b1c..7a63dc2cf 100644 --- a/LoopFollow/Controllers/Nightscout/Profile.swift +++ b/LoopFollow/Controllers/Nightscout/Profile.swift @@ -1,10 +1,6 @@ -// -// Profile.swift -// LoopFollow -// -// Created by Jonas Björkert on 2023-10-05. -// Copyright © 2023 Jon Fawcett. All rights reserved. -// +// LoopFollow +// Profile.swift +// Created by Jonas Björkert on 2023-10-05. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/ProfileManager.swift b/LoopFollow/Controllers/Nightscout/ProfileManager.swift index adba5251f..7b786bfbe 100644 --- a/LoopFollow/Controllers/Nightscout/ProfileManager.swift +++ b/LoopFollow/Controllers/Nightscout/ProfileManager.swift @@ -1,7 +1,6 @@ -// ProfileManager.swift // LoopFollow -// Created by Jonas Björkert on 2024-07-12. -// Copyright © 2024 Jon Fawcett. All rights reserved. +// ProfileManager.swift +// Created by Jonas Björkert on 2024-07-15. import Foundation import HealthKit diff --git a/LoopFollow/Controllers/Nightscout/SAge.swift b/LoopFollow/Controllers/Nightscout/SAge.swift index 1ca3d947b..d2f040a47 100644 --- a/LoopFollow/Controllers/Nightscout/SAge.swift +++ b/LoopFollow/Controllers/Nightscout/SAge.swift @@ -1,10 +1,6 @@ -// -// SAge.swift -// LoopFollow -// -// Created by Jonas Björkert on 2023-10-05. -// Copyright © 2023 Jon Fawcett. All rights reserved. -// +// LoopFollow +// SAge.swift +// Created by Jonas Björkert on 2023-10-05. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/Treatments.swift b/LoopFollow/Controllers/Nightscout/Treatments.swift index 2785219a5..0eef90853 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments.swift @@ -1,10 +1,6 @@ -// -// Treatments.swift -// LoopFollow -// -// Created by Jonas Björkert on 2023-10-05. -// Copyright © 2023 Jon Fawcett. All rights reserved. -// +// LoopFollow +// Treatments.swift +// Created by Jonas Björkert on 2023-10-05. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/Treatments/BGCheck.swift b/LoopFollow/Controllers/Nightscout/Treatments/BGCheck.swift index d6d0f8a78..2592dd6a5 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/BGCheck.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/BGCheck.swift @@ -1,10 +1,6 @@ -// -// BGCheck.swift -// LoopFollow -// -// Created by Jonas Björkert on 2023-10-04. -// Copyright © 2023 Jon Fawcett. All rights reserved. -// +// LoopFollow +// BGCheck.swift +// Created by Jonas Björkert on 2023-10-05. import Foundation import UIKit diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Basals.swift b/LoopFollow/Controllers/Nightscout/Treatments/Basals.swift index a611d3a99..49a847bea 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Basals.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Basals.swift @@ -1,10 +1,6 @@ -// -// Basals.swift -// LoopFollow -// -// Created by Jonas Björkert on 2023-10-05. -// Copyright © 2023 Jon Fawcett. All rights reserved. -// +// LoopFollow +// Basals.swift +// Created by Jonas Björkert on 2023-10-05. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Bolus.swift b/LoopFollow/Controllers/Nightscout/Treatments/Bolus.swift index df643539b..3ae15e894 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Bolus.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Bolus.swift @@ -1,10 +1,6 @@ -// -// Bolus.swift -// LoopFollow -// -// Created by Jonas Björkert on 2023-10-05. -// Copyright © 2023 Jon Fawcett. All rights reserved. -// +// LoopFollow +// Bolus.swift +// Created by Jonas Björkert on 2023-10-05. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift b/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift index cb97648ad..650cb3bd3 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift @@ -1,10 +1,6 @@ -// -// Carbs.swift -// LoopFollow -// -// Created by Jonas Björkert on 2023-10-05. -// Copyright © 2023 Jon Fawcett. All rights reserved. -// +// LoopFollow +// Carbs.swift +// Created by Jonas Björkert on 2023-10-05. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/Treatments/InsulinCartridgeChange.swift b/LoopFollow/Controllers/Nightscout/Treatments/InsulinCartridgeChange.swift index e583d0904..3a64fe6c8 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/InsulinCartridgeChange.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/InsulinCartridgeChange.swift @@ -1,10 +1,6 @@ -// -// InsulinCartridgeChange.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-08-05. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// InsulinCartridgeChange.swift +// Created by Jonas Björkert on 2024-08-05. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Notes.swift b/LoopFollow/Controllers/Nightscout/Treatments/Notes.swift index a97ebb6fb..2e1b4d6e0 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Notes.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Notes.swift @@ -1,10 +1,6 @@ -// -// Notes.swift -// LoopFollow -// -// Created by Jonas Björkert on 2023-10-04. -// Copyright © 2023 Jon Fawcett. All rights reserved. -// +// LoopFollow +// Notes.swift +// Created by Jonas Björkert on 2023-10-05. import Foundation import UIKit diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift b/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift index a555b9c19..de00b12c6 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift @@ -1,10 +1,6 @@ -// -// Overrides.swift -// LoopFollow -// -// Created by Jonas Björkert on 2023-10-04. -// Copyright © 2023 Jon Fawcett. All rights reserved. -// +// LoopFollow +// Overrides.swift +// Created by Jonas Björkert on 2023-10-05. import Foundation import UIKit diff --git a/LoopFollow/Controllers/Nightscout/Treatments/ResumePump.swift b/LoopFollow/Controllers/Nightscout/Treatments/ResumePump.swift index 1e2c7a74f..7d1dfb720 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/ResumePump.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/ResumePump.swift @@ -1,10 +1,6 @@ -// -// ResumePump.swift -// LoopFollow -// -// Created by Jonas Björkert on 2023-10-05. -// Copyright © 2023 Jon Fawcett. All rights reserved. -// +// LoopFollow +// ResumePump.swift +// Created by Jonas Björkert on 2023-10-05. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/Treatments/SMB.swift b/LoopFollow/Controllers/Nightscout/Treatments/SMB.swift index 80108cb04..6e9438929 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/SMB.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/SMB.swift @@ -1,10 +1,6 @@ -// -// SMB.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-06-19. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// SMB.swift +// Created by Jonas Björkert on 2023-10-05. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/Treatments/SensorStart.swift b/LoopFollow/Controllers/Nightscout/Treatments/SensorStart.swift index be2d0f174..f22509800 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/SensorStart.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/SensorStart.swift @@ -1,10 +1,6 @@ -// -// SensorStart.swift -// LoopFollow -// -// Created by Jonas Björkert on 2023-10-04. -// Copyright © 2023 Jon Fawcett. All rights reserved. -// +// LoopFollow +// SensorStart.swift +// Created by Jonas Björkert on 2023-10-05. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/Treatments/SiteChange.swift b/LoopFollow/Controllers/Nightscout/Treatments/SiteChange.swift index 459c8e801..7a39cd7de 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/SiteChange.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/SiteChange.swift @@ -1,10 +1,6 @@ -// -// SiteChange.swift -// LoopFollow -// -// Created by Jonas Björkert on 2023-10-06. -// Copyright © 2023 Jon Fawcett. All rights reserved. -// +// LoopFollow +// SiteChange.swift +// Created by Jonas Björkert on 2023-10-06. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/Treatments/SuspendPump.swift b/LoopFollow/Controllers/Nightscout/Treatments/SuspendPump.swift index 35cda12d8..6cef5fa90 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/SuspendPump.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/SuspendPump.swift @@ -1,10 +1,6 @@ -// -// SuspendPump.swift -// LoopFollow -// -// Created by Jonas Björkert on 2023-10-05. -// Copyright © 2023 Jon Fawcett. All rights reserved. -// +// LoopFollow +// SuspendPump.swift +// Created by Jonas Björkert on 2023-10-05. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/Treatments/TemporaryTarget.swift b/LoopFollow/Controllers/Nightscout/Treatments/TemporaryTarget.swift index 38e2e15a8..ec3d3fbf0 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/TemporaryTarget.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/TemporaryTarget.swift @@ -1,10 +1,6 @@ -// -// TemporaryTarget.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-07-26. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// TemporaryTarget.swift +// Created by Jonas Björkert on 2024-07-28. import Foundation import HealthKit diff --git a/LoopFollow/Controllers/SpeakBG.swift b/LoopFollow/Controllers/SpeakBG.swift index 6c34ee5d0..ccaab83d7 100644 --- a/LoopFollow/Controllers/SpeakBG.swift +++ b/LoopFollow/Controllers/SpeakBG.swift @@ -1,3 +1,7 @@ +// LoopFollow +// SpeakBG.swift +// Created by Jonas Björkert on 2025-05-03. + import AVFoundation import CallKit import Foundation diff --git a/LoopFollow/Controllers/Stats.swift b/LoopFollow/Controllers/Stats.swift index 44e0c9621..b09ecb06e 100644 --- a/LoopFollow/Controllers/Stats.swift +++ b/LoopFollow/Controllers/Stats.swift @@ -1,10 +1,6 @@ -// -// Stats.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/23/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// Stats.swift +// Created by Jon Fawcett on 2020-06-23. import Foundation diff --git a/LoopFollow/Controllers/StatsView.swift b/LoopFollow/Controllers/StatsView.swift index 2997fcda4..6cd4d5972 100644 --- a/LoopFollow/Controllers/StatsView.swift +++ b/LoopFollow/Controllers/StatsView.swift @@ -1,10 +1,6 @@ -// -// StatsView.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/23/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// StatsView.swift +// Created by Jon Fawcett on 2020-06-23. import Charts import Foundation diff --git a/LoopFollow/Controllers/Timers.swift b/LoopFollow/Controllers/Timers.swift index adb75e768..ced520c4c 100644 --- a/LoopFollow/Controllers/Timers.swift +++ b/LoopFollow/Controllers/Timers.swift @@ -1,10 +1,6 @@ -// -// Timers.swift -// LoopFollow -// -// Created by Jon Fawcett on 9/3/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// Timers.swift +// Created by Jon Fawcett on 2020-09-03. import Foundation import UIKit diff --git a/LoopFollow/Dexcom/DexcomSettingsView.swift b/LoopFollow/Dexcom/DexcomSettingsView.swift index 7e1ecdd8e..b4c2e63a1 100644 --- a/LoopFollow/Dexcom/DexcomSettingsView.swift +++ b/LoopFollow/Dexcom/DexcomSettingsView.swift @@ -1,10 +1,6 @@ -// -// DexcomSettingsView.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-18. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// DexcomSettingsView.swift +// Created by Jonas Björkert on 2025-01-18. import SwiftUI diff --git a/LoopFollow/Dexcom/DexcomSettingsViewModel.swift b/LoopFollow/Dexcom/DexcomSettingsViewModel.swift index b779c543e..3bac0ce52 100644 --- a/LoopFollow/Dexcom/DexcomSettingsViewModel.swift +++ b/LoopFollow/Dexcom/DexcomSettingsViewModel.swift @@ -1,10 +1,6 @@ -// -// DexcomSettingsViewModel.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-18. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// DexcomSettingsViewModel.swift +// Created by Jonas Björkert on 2025-01-18. import Combine import Foundation diff --git a/LoopFollow/Extensions/Binding+Optional.swift b/LoopFollow/Extensions/Binding+Optional.swift index c76efbd7f..092ac2a28 100644 --- a/LoopFollow/Extensions/Binding+Optional.swift +++ b/LoopFollow/Extensions/Binding+Optional.swift @@ -1,10 +1,6 @@ -// -// Binding+Optional.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-04-21. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// Binding+Optional.swift +// Created by Jonas Björkert on 2025-04-26. import Foundation import SwiftUI diff --git a/LoopFollow/Extensions/EKEventStore+Extensions.swift b/LoopFollow/Extensions/EKEventStore+Extensions.swift index 5233d6ba9..d218e83c3 100644 --- a/LoopFollow/Extensions/EKEventStore+Extensions.swift +++ b/LoopFollow/Extensions/EKEventStore+Extensions.swift @@ -1,10 +1,6 @@ -// -// EKEventStore+Extensions.swift -// LoopFollow -// -// Created by Jonas Björkert on 2023-07-27. -// Copyright © 2023 Jon Fawcett. All rights reserved. -// +// LoopFollow +// EKEventStore+Extensions.swift +// Created by Jonas Björkert on 2023-07-27. import EventKit import Foundation diff --git a/LoopFollow/Extensions/HKQuantity+AnyConvertible.swift b/LoopFollow/Extensions/HKQuantity+AnyConvertible.swift index a32185235..332502d8f 100644 --- a/LoopFollow/Extensions/HKQuantity+AnyConvertible.swift +++ b/LoopFollow/Extensions/HKQuantity+AnyConvertible.swift @@ -1,9 +1,6 @@ -// -// HKQuantity+AnyConvertible.swift -// nightguard -// -// Created by Jonas Björkert on 2024-07-24. -// +// LoopFollow +// HKQuantity+AnyConvertible.swift +// Created by Jonas Björkert on 2024-07-28. import HealthKit diff --git a/LoopFollow/Extensions/HKUnit+Extensions.swift b/LoopFollow/Extensions/HKUnit+Extensions.swift index 81ce8f903..c4d920b54 100644 --- a/LoopFollow/Extensions/HKUnit+Extensions.swift +++ b/LoopFollow/Extensions/HKUnit+Extensions.swift @@ -1,10 +1,6 @@ -// -// HKUnit+Extensions.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-07-15. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// HKUnit+Extensions.swift +// Created by Jonas Björkert on 2024-07-16. import Foundation import HealthKit diff --git a/LoopFollow/Extensions/ShareClientExtension.swift b/LoopFollow/Extensions/ShareClientExtension.swift index 825d70d2e..91e41aad3 100644 --- a/LoopFollow/Extensions/ShareClientExtension.swift +++ b/LoopFollow/Extensions/ShareClientExtension.swift @@ -1,10 +1,6 @@ -// -// ShareClientExtension.swift -// LoopFollow -// -// Created by Jose Paredes on 7/13/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// ShareClientExtension.swift +// Created by Jose Paredes on 2020-07-14. import Foundation import ShareClient diff --git a/LoopFollow/Extensions/UIViewExtension.swift b/LoopFollow/Extensions/UIViewExtension.swift index e93a7b245..db6cf28d5 100644 --- a/LoopFollow/Extensions/UIViewExtension.swift +++ b/LoopFollow/Extensions/UIViewExtension.swift @@ -1,10 +1,6 @@ -// -// UIViewExtension.swift -// LoopFollow -// -// Created by Jose Paredes on 7/16/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// UIViewExtension.swift +// Created by Jose Paredes on 2020-07-17. import Foundation import UIKit diff --git a/LoopFollow/Extensions/UUID+Identifiable.swift b/LoopFollow/Extensions/UUID+Identifiable.swift index f7eccb74a..c82cff480 100644 --- a/LoopFollow/Extensions/UUID+Identifiable.swift +++ b/LoopFollow/Extensions/UUID+Identifiable.swift @@ -1,10 +1,6 @@ -// -// UUID+Identifiable.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-04-21. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// UUID+Identifiable.swift +// Created by Jonas Björkert on 2025-04-26. import Foundation diff --git a/LoopFollow/Helpers/AnyConvertible.swift b/LoopFollow/Helpers/AnyConvertible.swift index 128a922c0..5735ea1b3 100644 --- a/LoopFollow/Helpers/AnyConvertible.swift +++ b/LoopFollow/Helpers/AnyConvertible.swift @@ -1,10 +1,6 @@ -// -// AnyConvertible.swift -// nightguard -// -// Created by Florian Preknya on 1/27/19. -// Copyright © 2019 private. All rights reserved. -// +// LoopFollow +// AnyConvertible.swift +// Created by Jon Fawcett on 2020-06-05. import Foundation diff --git a/LoopFollow/Helpers/AppConstants.swift b/LoopFollow/Helpers/AppConstants.swift index faf8fc9bd..c8b446d43 100644 --- a/LoopFollow/Helpers/AppConstants.swift +++ b/LoopFollow/Helpers/AppConstants.swift @@ -1,10 +1,6 @@ -// -// AppConstants.swift -// scoutwatch -// -// Created by Dirk Hermanns on 26.12.15. -// Copyright © 2015 private. All rights reserved. -// +// LoopFollow +// AppConstants.swift +// Created by Jon Fawcett on 2020-06-05. import Foundation diff --git a/LoopFollow/Helpers/AppVersionManager.swift b/LoopFollow/Helpers/AppVersionManager.swift index 603e902bc..a2695830c 100644 --- a/LoopFollow/Helpers/AppVersionManager.swift +++ b/LoopFollow/Helpers/AppVersionManager.swift @@ -1,10 +1,6 @@ -// -// AppVersionManager.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-05-11. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// AppVersionManager.swift +// Created by Jonas Björkert on 2024-05-11. import Foundation diff --git a/LoopFollow/Helpers/BackgroundTaskAudio.swift b/LoopFollow/Helpers/BackgroundTaskAudio.swift index fbd0889e1..6bed5f430 100755 --- a/LoopFollow/Helpers/BackgroundTaskAudio.swift +++ b/LoopFollow/Helpers/BackgroundTaskAudio.swift @@ -1,9 +1,6 @@ -// -// BackgroundTaskAudio.swift -// -// Created by Yaro on 8/27/16. -// Copyright © 2016 Yaro. All rights reserved. -// +// LoopFollow +// BackgroundTaskAudio.swift +// Created by Jon Fawcett on 2020-06-05. import AVFoundation diff --git a/LoopFollow/Helpers/BuildDetails.swift b/LoopFollow/Helpers/BuildDetails.swift index b8e462df4..561181359 100644 --- a/LoopFollow/Helpers/BuildDetails.swift +++ b/LoopFollow/Helpers/BuildDetails.swift @@ -1,10 +1,6 @@ -// -// BuildDetails.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-03-25. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// BuildDetails.swift +// Created by Jonas Björkert on 2024-03-25. import Foundation diff --git a/LoopFollow/Helpers/Chart.swift b/LoopFollow/Helpers/Chart.swift index 3b81019fa..06e59d9c4 100644 --- a/LoopFollow/Helpers/Chart.swift +++ b/LoopFollow/Helpers/Chart.swift @@ -1,10 +1,6 @@ -// -// Chart.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/3/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// Chart.swift +// Created by Jon Fawcett on 2020-06-05. import Charts import Foundation diff --git a/LoopFollow/Helpers/CycleHelper.swift b/LoopFollow/Helpers/CycleHelper.swift index 7b7d44097..dc3240d79 100644 --- a/LoopFollow/Helpers/CycleHelper.swift +++ b/LoopFollow/Helpers/CycleHelper.swift @@ -1,10 +1,6 @@ -// -// CycleHelper.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-03-01. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// CycleHelper.swift +// Created by Jonas Björkert on 2025-03-01. import Foundation diff --git a/LoopFollow/Helpers/DataStructs.swift b/LoopFollow/Helpers/DataStructs.swift index 5a250f517..983d49752 100644 --- a/LoopFollow/Helpers/DataStructs.swift +++ b/LoopFollow/Helpers/DataStructs.swift @@ -1,10 +1,6 @@ -// -// DataStructs.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/23/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// DataStructs.swift +// Created by Jon Fawcett on 2020-06-23. import Foundation diff --git a/LoopFollow/Helpers/DateTime.swift b/LoopFollow/Helpers/DateTime.swift index 8128c457f..b0bc53041 100644 --- a/LoopFollow/Helpers/DateTime.swift +++ b/LoopFollow/Helpers/DateTime.swift @@ -1,10 +1,6 @@ -// -// DateTime.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/16/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// DateTime.swift +// Created by Jon Fawcett on 2020-06-17. import Foundation diff --git a/LoopFollow/Helpers/DictionaryKeyPath.swift b/LoopFollow/Helpers/DictionaryKeyPath.swift index 512711e28..e080d8fcb 100644 --- a/LoopFollow/Helpers/DictionaryKeyPath.swift +++ b/LoopFollow/Helpers/DictionaryKeyPath.swift @@ -1,3 +1,7 @@ +// LoopFollow +// DictionaryKeyPath.swift +// Created by Jon Fawcett on 2020-06-11. + // For details, see // http://stackoverflow.com/questions/40261857/remove-nested-key-from-dictionary import Foundation diff --git a/LoopFollow/Helpers/GitHubService.swift b/LoopFollow/Helpers/GitHubService.swift index c0337add8..61b00c26f 100644 --- a/LoopFollow/Helpers/GitHubService.swift +++ b/LoopFollow/Helpers/GitHubService.swift @@ -1,10 +1,6 @@ -// -// GitHubService.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-05-11. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// GitHubService.swift +// Created by Jonas Björkert on 2024-05-11. import Foundation diff --git a/LoopFollow/Helpers/Globals.swift b/LoopFollow/Helpers/Globals.swift index 836d61cac..b1b160e11 100644 --- a/LoopFollow/Helpers/Globals.swift +++ b/LoopFollow/Helpers/Globals.swift @@ -1,10 +1,6 @@ -// -// Globals.swift -// LoopFollow -// -// Created by Jon Fawcett on 7/23/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// Globals.swift +// Created by Jon Fawcett on 2020-07-23. import Foundation diff --git a/LoopFollow/Helpers/GlucoseConversion.swift b/LoopFollow/Helpers/GlucoseConversion.swift index ebaee71b2..400f7449f 100644 --- a/LoopFollow/Helpers/GlucoseConversion.swift +++ b/LoopFollow/Helpers/GlucoseConversion.swift @@ -1,10 +1,6 @@ -// -// GlucoseConversion.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-04-28. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// GlucoseConversion.swift +// Created by Jonas Björkert on 2024-04-28. import Foundation diff --git a/LoopFollow/Helpers/Localizer.swift b/LoopFollow/Helpers/Localizer.swift index edeccb4ee..58a7389f1 100644 --- a/LoopFollow/Helpers/Localizer.swift +++ b/LoopFollow/Helpers/Localizer.swift @@ -1,10 +1,6 @@ -// -// Localizer.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/22/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// Localizer.swift +// Created by Jon Fawcett on 2020-06-22. import Foundation import HealthKit diff --git a/LoopFollow/Helpers/Mobileprovision.swift b/LoopFollow/Helpers/Mobileprovision.swift index 5042d2504..47cdffa05 100644 --- a/LoopFollow/Helpers/Mobileprovision.swift +++ b/LoopFollow/Helpers/Mobileprovision.swift @@ -1,3 +1,7 @@ +// LoopFollow +// Mobileprovision.swift +// Created by Jon Fawcett on 2020-10-05. + // // MobileProvision.swift // Fluux.io diff --git a/LoopFollow/Helpers/NightscoutUtils.swift b/LoopFollow/Helpers/NightscoutUtils.swift index 57cc5e3d3..b24c5ee3d 100644 --- a/LoopFollow/Helpers/NightscoutUtils.swift +++ b/LoopFollow/Helpers/NightscoutUtils.swift @@ -1,10 +1,6 @@ -// -// NightscoutUtils.swift -// LoopFollow -// -// Created by Jonas Björkert on 2023-04-09. -// Copyright © 2023 Jon Fawcett. All rights reserved. -// +// LoopFollow +// NightscoutUtils.swift +// Created by bjorkert on 2023-04-09. import Foundation diff --git a/LoopFollow/Helpers/ObservationToken.swift b/LoopFollow/Helpers/ObservationToken.swift index b09c4237c..7c3d7bc43 100644 --- a/LoopFollow/Helpers/ObservationToken.swift +++ b/LoopFollow/Helpers/ObservationToken.swift @@ -1,10 +1,6 @@ -// -// ObservationToken.swift -// nightguard -// -// Created by Florian Preknya on 1/30/19. -// Copyright © 2019 private. All rights reserved. -// +// LoopFollow +// ObservationToken.swift +// Created by Jon Fawcett on 2020-06-05. import Foundation diff --git a/LoopFollow/Helpers/TextFieldWithToolBar.swift b/LoopFollow/Helpers/TextFieldWithToolBar.swift index 003131004..52aa182b8 100644 --- a/LoopFollow/Helpers/TextFieldWithToolBar.swift +++ b/LoopFollow/Helpers/TextFieldWithToolBar.swift @@ -1,10 +1,6 @@ -// -// TextFieldWithToolBar.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-07-27. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// TextFieldWithToolBar.swift +// Created by Jonas Björkert on 2024-07-28. import HealthKit import SwiftUI diff --git a/LoopFollow/Helpers/TimeOfDay.swift b/LoopFollow/Helpers/TimeOfDay.swift index b83cd88af..4c6b408ea 100644 --- a/LoopFollow/Helpers/TimeOfDay.swift +++ b/LoopFollow/Helpers/TimeOfDay.swift @@ -1,10 +1,6 @@ -// -// TimeOfDay.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-04-20. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// TimeOfDay.swift +// Created by Jonas Björkert on 2025-04-26. import Foundation diff --git a/LoopFollow/Helpers/Views/ErrorMessageView.swift b/LoopFollow/Helpers/Views/ErrorMessageView.swift index 1603abe3c..7ba254af9 100644 --- a/LoopFollow/Helpers/Views/ErrorMessageView.swift +++ b/LoopFollow/Helpers/Views/ErrorMessageView.swift @@ -1,10 +1,6 @@ -// -// ErrorMessageView.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-07-31. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// ErrorMessageView.swift +// Created by Jonas Björkert on 2024-07-31. import Foundation import SwiftUI diff --git a/LoopFollow/Helpers/Views/HKQuantityInputView.swift b/LoopFollow/Helpers/Views/HKQuantityInputView.swift index a9a03d953..681ae488e 100644 --- a/LoopFollow/Helpers/Views/HKQuantityInputView.swift +++ b/LoopFollow/Helpers/Views/HKQuantityInputView.swift @@ -1,10 +1,6 @@ -// -// HKQuantityInputView.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-09-17. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// HKQuantityInputView.swift +// Created by Jonas Björkert on 2024-09-17. import Foundation import HealthKit diff --git a/LoopFollow/Helpers/Views/LoadingButtonView.swift b/LoopFollow/Helpers/Views/LoadingButtonView.swift index 9fa7acdf7..9b9585ce6 100644 --- a/LoopFollow/Helpers/Views/LoadingButtonView.swift +++ b/LoopFollow/Helpers/Views/LoadingButtonView.swift @@ -1,10 +1,6 @@ -// -// LoadingButtonView.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-09-17. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// LoadingButtonView.swift +// Created by Jonas Björkert on 2024-09-17. import SwiftUI diff --git a/LoopFollow/Helpers/carbBolusArrays.swift b/LoopFollow/Helpers/carbBolusArrays.swift index 0032b3880..298087b3d 100644 --- a/LoopFollow/Helpers/carbBolusArrays.swift +++ b/LoopFollow/Helpers/carbBolusArrays.swift @@ -1,10 +1,6 @@ -// -// carbBolusArrays.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/17/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// carbBolusArrays.swift +// Created by Jon Fawcett on 2020-06-17. import Foundation diff --git a/LoopFollow/Helpers/isOnPhoneCall.swift b/LoopFollow/Helpers/isOnPhoneCall.swift index c159851c9..33f780cae 100644 --- a/LoopFollow/Helpers/isOnPhoneCall.swift +++ b/LoopFollow/Helpers/isOnPhoneCall.swift @@ -1,10 +1,6 @@ -// -// isOnPhoneCall.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-04-26. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// isOnPhoneCall.swift +// Created by Jonas Björkert on 2025-05-03. import CallKit import Foundation diff --git a/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift index f9cea472c..a220bde5c 100644 --- a/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift +++ b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift @@ -1,10 +1,6 @@ -// -// InfoDisplaySettingsView.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-08-05. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// InfoDisplaySettingsView.swift +// Created by Jonas Björkert on 2024-08-05. import SwiftUI diff --git a/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsViewModel.swift b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsViewModel.swift index 8748e26e3..2e4502124 100644 --- a/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsViewModel.swift +++ b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsViewModel.swift @@ -1,10 +1,6 @@ -// -// InfoDisplaySettingsViewModel.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-08-05. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// InfoDisplaySettingsViewModel.swift +// Created by Jonas Björkert on 2024-08-05. import Foundation import SwiftUI diff --git a/LoopFollow/InfoTable/InfoData.swift b/LoopFollow/InfoTable/InfoData.swift index d8db84c17..57a3939f9 100644 --- a/LoopFollow/InfoTable/InfoData.swift +++ b/LoopFollow/InfoTable/InfoData.swift @@ -1,10 +1,6 @@ -// -// InfoData.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-07-11. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// InfoData.swift +// Created by Jonas Björkert on 2024-07-11. import Foundation diff --git a/LoopFollow/InfoTable/InfoDataSeparator.swift b/LoopFollow/InfoTable/InfoDataSeparator.swift index d34b24023..095351a94 100644 --- a/LoopFollow/InfoTable/InfoDataSeparator.swift +++ b/LoopFollow/InfoTable/InfoDataSeparator.swift @@ -1,10 +1,6 @@ -// -// InfoDataSeparator.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-07-16. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// InfoDataSeparator.swift +// Created by Jonas Björkert on 2024-07-18. import Foundation diff --git a/LoopFollow/InfoTable/InfoManager.swift b/LoopFollow/InfoTable/InfoManager.swift index 720965599..a876d6df0 100644 --- a/LoopFollow/InfoTable/InfoManager.swift +++ b/LoopFollow/InfoTable/InfoManager.swift @@ -1,10 +1,6 @@ -// -// InfoManager.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-07-11. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// InfoManager.swift +// Created by Jonas Björkert on 2024-07-11. import Foundation import HealthKit diff --git a/LoopFollow/InfoTable/InfoType.swift b/LoopFollow/InfoTable/InfoType.swift index 17b64c36f..1d9769554 100644 --- a/LoopFollow/InfoTable/InfoType.swift +++ b/LoopFollow/InfoTable/InfoType.swift @@ -1,10 +1,6 @@ -// -// InfoType.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-07-11. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// InfoType.swift +// Created by Jonas Björkert on 2024-07-11. import Foundation diff --git a/LoopFollow/Log/LogEntry.swift b/LoopFollow/Log/LogEntry.swift index a4d459fed..98eb7ae80 100644 --- a/LoopFollow/Log/LogEntry.swift +++ b/LoopFollow/Log/LogEntry.swift @@ -1,10 +1,6 @@ -// -// LogEntry.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-13. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// LogEntry.swift +// Created by Jonas Björkert on 2025-01-13. import Foundation diff --git a/LoopFollow/Log/LogManager.swift b/LoopFollow/Log/LogManager.swift index 1570a4d8d..88c25bca1 100644 --- a/LoopFollow/Log/LogManager.swift +++ b/LoopFollow/Log/LogManager.swift @@ -1,10 +1,6 @@ -// -// LogManager.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-10. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// LogManager.swift +// Created by Jonas Björkert on 2025-01-13. import Foundation diff --git a/LoopFollow/Log/LogView.swift b/LoopFollow/Log/LogView.swift index eff97efee..8617e69be 100644 --- a/LoopFollow/Log/LogView.swift +++ b/LoopFollow/Log/LogView.swift @@ -1,10 +1,6 @@ -// -// LogView.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-13. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// LogView.swift +// Created by Jonas Björkert on 2025-01-13. import SwiftUI diff --git a/LoopFollow/Log/LogViewModel.swift b/LoopFollow/Log/LogViewModel.swift index f2c54fa5b..afc69e90c 100644 --- a/LoopFollow/Log/LogViewModel.swift +++ b/LoopFollow/Log/LogViewModel.swift @@ -1,10 +1,6 @@ -// -// LogViewModel.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-13. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// LogViewModel.swift +// Created by Jonas Björkert on 2025-01-13. import Combine import Foundation diff --git a/LoopFollow/Log/SearchBar.swift b/LoopFollow/Log/SearchBar.swift index 4efdda0e5..c78f860e3 100644 --- a/LoopFollow/Log/SearchBar.swift +++ b/LoopFollow/Log/SearchBar.swift @@ -1,10 +1,6 @@ -// -// SearchBar.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-13. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// SearchBar.swift +// Created by Jonas Björkert on 2025-01-13. import SwiftUI import UIKit diff --git a/LoopFollow/Metric/CarbMetric.swift b/LoopFollow/Metric/CarbMetric.swift index 3a95a8821..c8e7cda3b 100644 --- a/LoopFollow/Metric/CarbMetric.swift +++ b/LoopFollow/Metric/CarbMetric.swift @@ -1,10 +1,6 @@ -// -// CarbMetric.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-07-17. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// CarbMetric.swift +// Created by Jonas Björkert on 2024-07-18. import Foundation diff --git a/LoopFollow/Metric/InsulinMetric.swift b/LoopFollow/Metric/InsulinMetric.swift index a06d85787..f9a379311 100644 --- a/LoopFollow/Metric/InsulinMetric.swift +++ b/LoopFollow/Metric/InsulinMetric.swift @@ -1,10 +1,6 @@ -// -// InsulinMetric.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-07-17. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// InsulinMetric.swift +// Created by Jonas Björkert on 2024-07-18. import Foundation diff --git a/LoopFollow/Metric/Metric.swift b/LoopFollow/Metric/Metric.swift index 64d9f09b6..babecc9fe 100644 --- a/LoopFollow/Metric/Metric.swift +++ b/LoopFollow/Metric/Metric.swift @@ -1,10 +1,6 @@ -// -// Metric.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-07-17. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// Metric.swift +// Created by Jonas Björkert on 2024-07-18. import Foundation diff --git a/LoopFollow/Nightscout/NightscoutSettingsView.swift b/LoopFollow/Nightscout/NightscoutSettingsView.swift index 6433ccae6..db2343efb 100644 --- a/LoopFollow/Nightscout/NightscoutSettingsView.swift +++ b/LoopFollow/Nightscout/NightscoutSettingsView.swift @@ -1,10 +1,6 @@ -// -// NightscoutSettingsView.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-18. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// NightscoutSettingsView.swift +// Created by Jonas Björkert on 2025-01-18. import SwiftUI diff --git a/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift index e9eb58923..3c6fccc24 100644 --- a/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift +++ b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift @@ -1,10 +1,6 @@ -// -// NightscoutSettingsViewModel.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-18. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// NightscoutSettingsViewModel.swift +// Created by Jonas Björkert on 2025-01-18. import Combine import Foundation diff --git a/LoopFollow/Remote/Loop/LoopNightscoutRemoteView.swift b/LoopFollow/Remote/Loop/LoopNightscoutRemoteView.swift index a35498d40..fcf3a29f7 100644 --- a/LoopFollow/Remote/Loop/LoopNightscoutRemoteView.swift +++ b/LoopFollow/Remote/Loop/LoopNightscoutRemoteView.swift @@ -1,10 +1,6 @@ -// -// LoopNightscoutRemoteView.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-14. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// LoopNightscoutRemoteView.swift +// Created by Jonas Björkert on 2025-01-27. import SwiftUI diff --git a/LoopFollow/Remote/Loop/LoopOverrideView.swift b/LoopFollow/Remote/Loop/LoopOverrideView.swift index ad1ba94ec..ee33c2e90 100644 --- a/LoopFollow/Remote/Loop/LoopOverrideView.swift +++ b/LoopFollow/Remote/Loop/LoopOverrideView.swift @@ -1,10 +1,6 @@ -// -// LoopOverrideView.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-14. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// LoopOverrideView.swift +// Created by Jonas Björkert on 2024-10-09. import HealthKit import SwiftUI diff --git a/LoopFollow/Remote/Loop/LoopOverrideViewModel.swift b/LoopFollow/Remote/Loop/LoopOverrideViewModel.swift index 026150b8e..208524686 100644 --- a/LoopFollow/Remote/Loop/LoopOverrideViewModel.swift +++ b/LoopFollow/Remote/Loop/LoopOverrideViewModel.swift @@ -1,10 +1,6 @@ -// -// LoopOverrideViewModel.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-15. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// LoopOverrideViewModel.swift +// Created by Jonas Björkert on 2025-01-27. import Foundation diff --git a/LoopFollow/Remote/Nightscout/TrioNightscoutRemoteView.swift b/LoopFollow/Remote/Nightscout/TrioNightscoutRemoteView.swift index ebe17cecb..d0fed3488 100644 --- a/LoopFollow/Remote/Nightscout/TrioNightscoutRemoteView.swift +++ b/LoopFollow/Remote/Nightscout/TrioNightscoutRemoteView.swift @@ -1,10 +1,6 @@ -// -// TrioNightscoutRemoteView.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-07-19. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// TrioNightscoutRemoteView.swift +// Created by Jonas Björkert on 2024-07-19. import HealthKit import SwiftUI diff --git a/LoopFollow/Remote/NoRemoteView.swift b/LoopFollow/Remote/NoRemoteView.swift index 27fbcc710..fa6e8d8e7 100644 --- a/LoopFollow/Remote/NoRemoteView.swift +++ b/LoopFollow/Remote/NoRemoteView.swift @@ -1,10 +1,6 @@ -// -// NoRemoteView.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-14. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// NoRemoteView.swift +// Created by Jonas Björkert on 2025-01-27. import SwiftUI diff --git a/LoopFollow/Remote/RemoteType.swift b/LoopFollow/Remote/RemoteType.swift index c906899a3..291263b83 100644 --- a/LoopFollow/Remote/RemoteType.swift +++ b/LoopFollow/Remote/RemoteType.swift @@ -1,10 +1,6 @@ -// -// RemoteType.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-08-18. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// RemoteType.swift +// Created by Jonas Björkert on 2024-09-17. import Foundation diff --git a/LoopFollow/Remote/RemoteViewController.swift b/LoopFollow/Remote/RemoteViewController.swift index 9e0c79f76..4095d76a3 100644 --- a/LoopFollow/Remote/RemoteViewController.swift +++ b/LoopFollow/Remote/RemoteViewController.swift @@ -1,10 +1,6 @@ -// -// RemoteViewController.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-07-19. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// RemoteViewController.swift +// Created by Jonas Björkert on 2024-07-19. import Combine import Foundation diff --git a/LoopFollow/Remote/Settings/RemoteSettingsView.swift b/LoopFollow/Remote/Settings/RemoteSettingsView.swift index 167cbe75c..e02e72a7b 100644 --- a/LoopFollow/Remote/Settings/RemoteSettingsView.swift +++ b/LoopFollow/Remote/Settings/RemoteSettingsView.swift @@ -1,11 +1,6 @@ -// -// RemoteSettingsView.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-08-25. -// Updated on 2024-09-16. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// RemoteSettingsView.swift +// Created by Jonas Björkert on 2024-09-17. import HealthKit import SwiftUI diff --git a/LoopFollow/Remote/Settings/RemoteSettingsViewModel.swift b/LoopFollow/Remote/Settings/RemoteSettingsViewModel.swift index 459331cb4..b966bdc54 100644 --- a/LoopFollow/Remote/Settings/RemoteSettingsViewModel.swift +++ b/LoopFollow/Remote/Settings/RemoteSettingsViewModel.swift @@ -1,10 +1,6 @@ -// -// RemoteSettingsViewModel.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-08-25. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// RemoteSettingsViewModel.swift +// Created by Jonas Björkert on 2024-09-17. import Combine import Foundation diff --git a/LoopFollow/Remote/TRC/BolusView.swift b/LoopFollow/Remote/TRC/BolusView.swift index f8251e8d2..0c1d64401 100644 --- a/LoopFollow/Remote/TRC/BolusView.swift +++ b/LoopFollow/Remote/TRC/BolusView.swift @@ -1,10 +1,6 @@ -// -// BolusView.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-08-25. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// BolusView.swift +// Created by Jonas Björkert on 2024-09-17. import HealthKit import LocalAuthentication diff --git a/LoopFollow/Remote/TRC/MealView.swift b/LoopFollow/Remote/TRC/MealView.swift index a4349b04d..c3dd52298 100644 --- a/LoopFollow/Remote/TRC/MealView.swift +++ b/LoopFollow/Remote/TRC/MealView.swift @@ -1,10 +1,6 @@ -// -// MealView.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-08-25. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// MealView.swift +// Created by Jonas Björkert on 2024-09-17. import HealthKit import LocalAuthentication diff --git a/LoopFollow/Remote/TRC/OverrideView.swift b/LoopFollow/Remote/TRC/OverrideView.swift index c2931ff0c..fef4d8170 100644 --- a/LoopFollow/Remote/TRC/OverrideView.swift +++ b/LoopFollow/Remote/TRC/OverrideView.swift @@ -1,10 +1,6 @@ -// -// OverrideView.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-10-07. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// OverrideView.swift +// Created by Jonas Björkert on 2024-10-09. import HealthKit import SwiftUI diff --git a/LoopFollow/Remote/TRC/PushMessage.swift b/LoopFollow/Remote/TRC/PushMessage.swift index 1de381cce..8c110c7a7 100644 --- a/LoopFollow/Remote/TRC/PushMessage.swift +++ b/LoopFollow/Remote/TRC/PushMessage.swift @@ -1,10 +1,6 @@ -// -// PushMessage.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-08-27. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// PushMessage.swift +// Created by Jonas Björkert on 2024-09-17. import Foundation diff --git a/LoopFollow/Remote/TRC/PushNotificationManager.swift b/LoopFollow/Remote/TRC/PushNotificationManager.swift index d7e3c6e0d..8238d18a2 100644 --- a/LoopFollow/Remote/TRC/PushNotificationManager.swift +++ b/LoopFollow/Remote/TRC/PushNotificationManager.swift @@ -1,10 +1,6 @@ -// -// PushNotificationManager.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-08-27. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// PushNotificationManager.swift +// Created by Jonas Björkert on 2024-09-17. import Foundation import HealthKit diff --git a/LoopFollow/Remote/TRC/TRCCommandType.swift b/LoopFollow/Remote/TRC/TRCCommandType.swift index b4c892737..d391fb5c7 100644 --- a/LoopFollow/Remote/TRC/TRCCommandType.swift +++ b/LoopFollow/Remote/TRC/TRCCommandType.swift @@ -1,10 +1,6 @@ -// -// TRCCommandType.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-10-05. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// TRCCommandType.swift +// Created by Jonas Björkert on 2024-10-05. import Foundation diff --git a/LoopFollow/Remote/TRC/TempTargetView.swift b/LoopFollow/Remote/TRC/TempTargetView.swift index 5dc8f2b24..7fe68617a 100644 --- a/LoopFollow/Remote/TRC/TempTargetView.swift +++ b/LoopFollow/Remote/TRC/TempTargetView.swift @@ -1,10 +1,6 @@ -// -// TempTargetView.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-08-25. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// TempTargetView.swift +// Created by Jonas Björkert on 2024-07-19. import HealthKit import SwiftUI diff --git a/LoopFollow/Remote/TRC/TreatmentResponse.swift b/LoopFollow/Remote/TRC/TreatmentResponse.swift index fbbc40af3..a12f538e8 100644 --- a/LoopFollow/Remote/TRC/TreatmentResponse.swift +++ b/LoopFollow/Remote/TRC/TreatmentResponse.swift @@ -1,10 +1,6 @@ -// -// TreatmentResponse.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-07-24. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// TreatmentResponse.swift +// Created by Jonas Björkert on 2024-07-28. import Foundation diff --git a/LoopFollow/Remote/TRC/TrioNightscoutRemoteController.swift b/LoopFollow/Remote/TRC/TrioNightscoutRemoteController.swift index 62e5eeeb5..944427d17 100644 --- a/LoopFollow/Remote/TRC/TrioNightscoutRemoteController.swift +++ b/LoopFollow/Remote/TRC/TrioNightscoutRemoteController.swift @@ -1,10 +1,6 @@ -// -// TrioNightscoutRemoteController.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-08-26. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// TrioNightscoutRemoteController.swift +// Created by Jonas Björkert on 2024-07-19. import Foundation import HealthKit diff --git a/LoopFollow/Remote/TRC/TrioRemoteControlView.swift b/LoopFollow/Remote/TRC/TrioRemoteControlView.swift index dfc24d2fd..155201470 100644 --- a/LoopFollow/Remote/TRC/TrioRemoteControlView.swift +++ b/LoopFollow/Remote/TRC/TrioRemoteControlView.swift @@ -1,10 +1,6 @@ -// -// TrioRemoteControlView.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-08-25. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// TrioRemoteControlView.swift +// Created by Jonas Björkert on 2024-09-17. import SwiftUI diff --git a/LoopFollow/Remote/TRC/TrioRemoteControlViewModel.swift b/LoopFollow/Remote/TRC/TrioRemoteControlViewModel.swift index 9ba8d0c71..fa4b7a59d 100644 --- a/LoopFollow/Remote/TRC/TrioRemoteControlViewModel.swift +++ b/LoopFollow/Remote/TRC/TrioRemoteControlViewModel.swift @@ -1,10 +1,6 @@ -// -// TrioRemoteControlViewModel.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-08-25. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// TrioRemoteControlViewModel.swift +// Created by Jonas Björkert on 2024-09-17. import Foundation diff --git a/LoopFollow/Remote/TempTargetPreset/TempTargetPreset.swift b/LoopFollow/Remote/TempTargetPreset/TempTargetPreset.swift index 00f6c9de1..7392eb505 100644 --- a/LoopFollow/Remote/TempTargetPreset/TempTargetPreset.swift +++ b/LoopFollow/Remote/TempTargetPreset/TempTargetPreset.swift @@ -1,10 +1,6 @@ -// -// TempTargetPreset.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-07-31. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// TempTargetPreset.swift +// Created by Jonas Björkert on 2024-07-31. import Foundation import HealthKit diff --git a/LoopFollow/Remote/TempTargetPreset/TempTargetPresetManager.swift b/LoopFollow/Remote/TempTargetPreset/TempTargetPresetManager.swift index e0c8baab4..54e068fbf 100644 --- a/LoopFollow/Remote/TempTargetPreset/TempTargetPresetManager.swift +++ b/LoopFollow/Remote/TempTargetPreset/TempTargetPresetManager.swift @@ -1,10 +1,6 @@ -// -// TempTargetPresetManager.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-07-31. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// TempTargetPresetManager.swift +// Created by Jonas Björkert on 2024-07-31. import Combine import Foundation diff --git a/LoopFollow/Settings/AdvancedSettingsView.swift b/LoopFollow/Settings/AdvancedSettingsView.swift index 4cfbb3fda..35fc31be6 100644 --- a/LoopFollow/Settings/AdvancedSettingsView.swift +++ b/LoopFollow/Settings/AdvancedSettingsView.swift @@ -1,10 +1,6 @@ -// -// AdvancedSettingsView.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-23. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// AdvancedSettingsView.swift +// Created by Jonas Björkert on 2025-01-24. import SwiftUI diff --git a/LoopFollow/Settings/AdvancedSettingsViewModel.swift b/LoopFollow/Settings/AdvancedSettingsViewModel.swift index 76caae214..363302470 100644 --- a/LoopFollow/Settings/AdvancedSettingsViewModel.swift +++ b/LoopFollow/Settings/AdvancedSettingsViewModel.swift @@ -1,10 +1,6 @@ -// -// AdvancedSettingsViewModel.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-23. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// AdvancedSettingsViewModel.swift +// Created by Jonas Björkert on 2025-01-24. import Foundation diff --git a/LoopFollow/Snoozer/SnoozerView.swift b/LoopFollow/Snoozer/SnoozerView.swift index 740b04eb4..37988e605 100644 --- a/LoopFollow/Snoozer/SnoozerView.swift +++ b/LoopFollow/Snoozer/SnoozerView.swift @@ -1,10 +1,6 @@ -// -// SnoozerView.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-04-26. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// SnoozerView.swift +// Created by Jonas Björkert on 2025-04-26. import SwiftUI diff --git a/LoopFollow/Snoozer/SnoozerViewController.swift b/LoopFollow/Snoozer/SnoozerViewController.swift index 951845284..09cc35a00 100644 --- a/LoopFollow/Snoozer/SnoozerViewController.swift +++ b/LoopFollow/Snoozer/SnoozerViewController.swift @@ -1,10 +1,6 @@ -// -// SnoozerViewController.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-04-26. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// SnoozerViewController.swift +// Created by Jonas Björkert on 2025-04-26. import Combine import SwiftUI diff --git a/LoopFollow/Snoozer/SnoozerViewModel.swift b/LoopFollow/Snoozer/SnoozerViewModel.swift index cb67a3d3e..4c0ce0e32 100644 --- a/LoopFollow/Snoozer/SnoozerViewModel.swift +++ b/LoopFollow/Snoozer/SnoozerViewModel.swift @@ -1,10 +1,6 @@ -// -// SnoozerViewModel.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-04. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// SnoozerViewModel.swift +// Created by Jonas Björkert on 2025-05-04. import Combine import Foundation diff --git a/LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift b/LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift index 1ec11b071..fc5708cd1 100644 --- a/LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift +++ b/LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift @@ -1,10 +1,6 @@ -// -// ObservableUserDefaultsValue.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-07-24. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// ObservableUserDefaultsValue.swift +// Created by Jonas Björkert on 2024-07-28. import Combine import Foundation diff --git a/LoopFollow/Storage/Framework/ObservableValue.swift b/LoopFollow/Storage/Framework/ObservableValue.swift index e157b9c39..15a529b4e 100644 --- a/LoopFollow/Storage/Framework/ObservableValue.swift +++ b/LoopFollow/Storage/Framework/ObservableValue.swift @@ -1,10 +1,6 @@ -// -// ObservableValue.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-07-25. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// ObservableValue.swift +// Created by Jonas Björkert on 2024-07-28. import Combine import Foundation diff --git a/LoopFollow/Storage/Framework/SecureStorageValue.swift b/LoopFollow/Storage/Framework/SecureStorageValue.swift index 50f3a393a..29ecd0482 100644 --- a/LoopFollow/Storage/Framework/SecureStorageValue.swift +++ b/LoopFollow/Storage/Framework/SecureStorageValue.swift @@ -1,10 +1,6 @@ -// -// SecureStorageValue.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-09-16. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// SecureStorageValue.swift +// Created by Jonas Björkert on 2024-09-17. import Combine import Foundation diff --git a/LoopFollow/Storage/Framework/StorageValue.swift b/LoopFollow/Storage/Framework/StorageValue.swift index a0f62941e..db04ff8c7 100644 --- a/LoopFollow/Storage/Framework/StorageValue.swift +++ b/LoopFollow/Storage/Framework/StorageValue.swift @@ -1,10 +1,6 @@ -// -// StorageValue.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-08-25. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// StorageValue.swift +// Created by Jonas Björkert on 2024-09-17. import Combine import Foundation diff --git a/LoopFollow/Storage/Framework/UserDefaultsValue.swift b/LoopFollow/Storage/Framework/UserDefaultsValue.swift index ff6f851ff..715e5ed1b 100644 --- a/LoopFollow/Storage/Framework/UserDefaultsValue.swift +++ b/LoopFollow/Storage/Framework/UserDefaultsValue.swift @@ -1,10 +1,6 @@ -// -// UserDefaultsValue.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/4/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// UserDefaultsValue.swift +// Created by Jon Fawcett on 2020-06-05. import Foundation diff --git a/LoopFollow/Storage/Framework/UserDefaultsValueGroups.swift b/LoopFollow/Storage/Framework/UserDefaultsValueGroups.swift index ee92cdf25..e8444bc3f 100644 --- a/LoopFollow/Storage/Framework/UserDefaultsValueGroups.swift +++ b/LoopFollow/Storage/Framework/UserDefaultsValueGroups.swift @@ -1,10 +1,6 @@ -// -// UserDefaultsValueGroups.swift -// nightguard -// -// Created by Florian Preknya on 1/29/19. -// Copyright © 2019 private. All rights reserved. -// +// LoopFollow +// UserDefaultsValueGroups.swift +// Created by Jon Fawcett on 2020-06-05. import Foundation diff --git a/LoopFollow/Storage/Observable.swift b/LoopFollow/Storage/Observable.swift index 426344bf5..7eba7202c 100644 --- a/LoopFollow/Storage/Observable.swift +++ b/LoopFollow/Storage/Observable.swift @@ -1,10 +1,6 @@ -// -// Observable.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-07-25. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// Observable.swift +// Created by Jonas Björkert on 2024-07-28. import Foundation import HealthKit diff --git a/LoopFollow/Storage/ObservableUserDefaults.swift b/LoopFollow/Storage/ObservableUserDefaults.swift index 669683cd4..8a24f3662 100644 --- a/LoopFollow/Storage/ObservableUserDefaults.swift +++ b/LoopFollow/Storage/ObservableUserDefaults.swift @@ -1,10 +1,6 @@ -// -// ObservableUserDefaults.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-07-24. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// ObservableUserDefaults.swift +// Created by Jonas Björkert on 2024-07-28. import Combine import Foundation diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index 5102ba1da..c77b3d5fc 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -1,10 +1,6 @@ -// -// Storage.swift -// LoopFollow -// -// Created by Jonas Björkert on 2024-08-25. -// Copyright © 2024 Jon Fawcett. All rights reserved. -// +// LoopFollow +// Storage.swift +// Created by Jonas Björkert on 2024-09-17. import Foundation import HealthKit diff --git a/LoopFollow/Storage/UserDefaults.swift b/LoopFollow/Storage/UserDefaults.swift index 9c346315b..328ca9309 100644 --- a/LoopFollow/Storage/UserDefaults.swift +++ b/LoopFollow/Storage/UserDefaults.swift @@ -1,14 +1,6 @@ -// -// UserDefaults.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/4/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// -// -// -// -// +// LoopFollow +// UserDefaults.swift +// Created by Jon Fawcett on 2020-06-05. import Foundation import HealthKit diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index 886136adb..bb0caffad 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -1,10 +1,6 @@ -// -// AlarmTask.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-12. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// AlarmTask.swift +// Created by Jonas Björkert on 2025-01-13. import Foundation diff --git a/LoopFollow/Task/BGTask.swift b/LoopFollow/Task/BGTask.swift index 66a0939a7..238b2bd41 100644 --- a/LoopFollow/Task/BGTask.swift +++ b/LoopFollow/Task/BGTask.swift @@ -1,10 +1,6 @@ -// -// BGTask.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-11. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// BGTask.swift +// Created by Jonas Björkert on 2025-01-13. import Foundation diff --git a/LoopFollow/Task/CalendarTask.swift b/LoopFollow/Task/CalendarTask.swift index 67386d337..240f56f77 100644 --- a/LoopFollow/Task/CalendarTask.swift +++ b/LoopFollow/Task/CalendarTask.swift @@ -1,10 +1,6 @@ -// -// CalendarTask.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-12. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// CalendarTask.swift +// Created by Jonas Björkert on 2025-01-13. import Foundation diff --git a/LoopFollow/Task/DeviceStatusTask.swift b/LoopFollow/Task/DeviceStatusTask.swift index e023b0b9d..0e25175da 100644 --- a/LoopFollow/Task/DeviceStatusTask.swift +++ b/LoopFollow/Task/DeviceStatusTask.swift @@ -1,10 +1,6 @@ -// -// DeviceStatusTask.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-11. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// DeviceStatusTask.swift +// Created by Jonas Björkert on 2025-01-13. import Foundation diff --git a/LoopFollow/Task/MinAgoTask.swift b/LoopFollow/Task/MinAgoTask.swift index 3c1ac5a4d..f5faab487 100644 --- a/LoopFollow/Task/MinAgoTask.swift +++ b/LoopFollow/Task/MinAgoTask.swift @@ -1,10 +1,6 @@ -// -// MinAgoTask.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-11. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// MinAgoTask.swift +// Created by Jonas Björkert on 2025-01-13. import Foundation import UIKit diff --git a/LoopFollow/Task/ProfileTask.swift b/LoopFollow/Task/ProfileTask.swift index 48b2bad5d..c39083daa 100644 --- a/LoopFollow/Task/ProfileTask.swift +++ b/LoopFollow/Task/ProfileTask.swift @@ -1,10 +1,6 @@ -// -// ProfileTask.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-11. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// ProfileTask.swift +// Created by Jonas Björkert on 2025-01-13. import Foundation diff --git a/LoopFollow/Task/Task.swift b/LoopFollow/Task/Task.swift index 022b6867c..471f15361 100644 --- a/LoopFollow/Task/Task.swift +++ b/LoopFollow/Task/Task.swift @@ -1,10 +1,6 @@ -// -// Task.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-12. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// Task.swift +// Created by Jonas Björkert on 2025-01-13. import Foundation diff --git a/LoopFollow/Task/TaskScheduler.swift b/LoopFollow/Task/TaskScheduler.swift index 7badd6382..ed258e8a0 100644 --- a/LoopFollow/Task/TaskScheduler.swift +++ b/LoopFollow/Task/TaskScheduler.swift @@ -1,10 +1,6 @@ -// -// TaskScheduler.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-10. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// TaskScheduler.swift +// Created by Jonas Björkert on 2025-01-13. import Foundation import UIKit diff --git a/LoopFollow/Task/TreatmentsTask.swift b/LoopFollow/Task/TreatmentsTask.swift index a886bdabc..d735d8beb 100644 --- a/LoopFollow/Task/TreatmentsTask.swift +++ b/LoopFollow/Task/TreatmentsTask.swift @@ -1,10 +1,6 @@ -// -// TreatmentsTask.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-11. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// TreatmentsTask.swift +// Created by Jonas Björkert on 2025-01-13. import Foundation diff --git a/LoopFollow/ViewControllers/AppStateViewController.swift b/LoopFollow/ViewControllers/AppStateViewController.swift index 6dca31a2a..e5778a56f 100644 --- a/LoopFollow/ViewControllers/AppStateViewController.swift +++ b/LoopFollow/ViewControllers/AppStateViewController.swift @@ -1,10 +1,6 @@ -// -// AppStateViewController.swift -// LoopFollow -// -// Created by Jose Paredes on 7/17/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// AppStateViewController.swift +// Created by Jose Paredes on 2020-07-18. import Foundation diff --git a/LoopFollow/ViewControllers/GeneralSettingsViewController.swift b/LoopFollow/ViewControllers/GeneralSettingsViewController.swift index 3a6e66e4a..88a040e90 100644 --- a/LoopFollow/ViewControllers/GeneralSettingsViewController.swift +++ b/LoopFollow/ViewControllers/GeneralSettingsViewController.swift @@ -1,10 +1,6 @@ -// -// GeneralSettingsViewController.swift -// LoopFollow -// -// Created by Jose Paredes on 7/16/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// GeneralSettingsViewController.swift +// Created by Jose Paredes on 2020-07-17. import Eureka import EventKit diff --git a/LoopFollow/ViewControllers/GraphSettingsViewController.swift b/LoopFollow/ViewControllers/GraphSettingsViewController.swift index 086165bf7..92a9bc09d 100644 --- a/LoopFollow/ViewControllers/GraphSettingsViewController.swift +++ b/LoopFollow/ViewControllers/GraphSettingsViewController.swift @@ -1,10 +1,6 @@ -// -// GraphSettingsViewController.swift -// LoopFollow -// -// Created by Jose Paredes on 7/16/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// GraphSettingsViewController.swift +// Created by Jose Paredes on 2020-07-17. import Eureka import EventKit diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 49a5b90ca..9d8042673 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -1,10 +1,6 @@ -// -// MainViewController.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/1/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// MainViewController.swift +// Created by Jon Fawcett on 2020-06-17. import AVFAudio import Charts diff --git a/LoopFollow/ViewControllers/NightScoutViewController.swift b/LoopFollow/ViewControllers/NightScoutViewController.swift index f4ac7dad9..6f88e1c15 100644 --- a/LoopFollow/ViewControllers/NightScoutViewController.swift +++ b/LoopFollow/ViewControllers/NightScoutViewController.swift @@ -1,10 +1,6 @@ -// -// NightScoutViewController.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/1/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// NightScoutViewController.swift +// Created by Jon Fawcett on 2020-06-05. import UIKit import WebKit diff --git a/LoopFollow/ViewControllers/SettingsViewController.swift b/LoopFollow/ViewControllers/SettingsViewController.swift index deae16ae7..a82e8f718 100644 --- a/LoopFollow/ViewControllers/SettingsViewController.swift +++ b/LoopFollow/ViewControllers/SettingsViewController.swift @@ -1,10 +1,6 @@ -// -// SettingsViewController.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/3/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// SettingsViewController.swift +// Created by Jon Fawcett on 2020-06-05. import Eureka import EventKit diff --git a/LoopFollow/ViewControllers/WatchSettingsViewController.swift b/LoopFollow/ViewControllers/WatchSettingsViewController.swift index d6e18ec02..3bcfdc17d 100644 --- a/LoopFollow/ViewControllers/WatchSettingsViewController.swift +++ b/LoopFollow/ViewControllers/WatchSettingsViewController.swift @@ -1,10 +1,6 @@ -// -// WatchSettingsViewController.swift -// LoopFollow -// -// Created by Jose Paredes on 7/16/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// WatchSettingsViewController.swift +// Created by Jose Paredes on 2020-07-17. import Eureka import EventKit diff --git a/LoopFollowTests/AlwaysTrueCondition.swift b/LoopFollowTests/AlwaysTrueCondition.swift index 9a0ce6d04..e1bc33a78 100644 --- a/LoopFollowTests/AlwaysTrueCondition.swift +++ b/LoopFollowTests/AlwaysTrueCondition.swift @@ -1,10 +1,6 @@ -// -// AlwaysTrueCondition.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-04-20. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// AlwaysTrueCondition.swift +// Created by Jonas Björkert on 2025-04-26. @testable import LoopFollow import XCTest diff --git a/LoopFollowTests/BuildExpireConditionTests.swift b/LoopFollowTests/BuildExpireConditionTests.swift index bfc7052b1..6af7b384b 100644 --- a/LoopFollowTests/BuildExpireConditionTests.swift +++ b/LoopFollowTests/BuildExpireConditionTests.swift @@ -1,10 +1,6 @@ -// -// BuildExpireConditionTests.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-04-20. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// +// LoopFollow +// BuildExpireConditionTests.swift +// Created by Jonas Björkert on 2025-04-26. @testable import LoopFollow import XCTest diff --git a/Scripts/swiftformat.sh b/Scripts/swiftformat.sh index e5074ca8f..9d5f1c2a4 100755 --- a/Scripts/swiftformat.sh +++ b/Scripts/swiftformat.sh @@ -12,8 +12,8 @@ assertEnvironment "${SRCROOT}" "Please set SRCROOT to project root folder" unset SDKROOT swift run -c release --package-path BuildTools swiftformat "${SRCROOT}" \ ---enable fileHeader \ ---exclude Pods,Generated,R.generated.swift,fastlane/swift,Dependencies, LoopKit, LibreTransmitter,G7SensorKit,OmniKit, dexcom-share-client-swift,CGMBLEKit,RileyLinkKit,OmniBLE,MinimedKit,TidepoolService +--header "LoopFollow\n{file}\nCreated by {author.name} on {created}." \ +--exclude Pods,Generated,R.generated.swift,fastlane/swift,Dependencies,dexcom-share-client-swift # andOperator,\ # anyObjectProtocol,\ From 17281165272c41123255c68aa9c8c2aeef065633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 17 May 2025 17:00:57 +0200 Subject: [PATCH 069/138] PumpChange --- LoopFollow.xcodeproj/project.pbxproj | 8 ++++ .../AlarmCondition/PumpChangeCondition.swift | 38 +++++++++++++++ LoopFollow/Alarm/AlarmData.swift | 1 + .../Alarm/AlarmEditing/AlarmEditor.swift | 2 + .../Editors/PumpChangeAlarmEditor.swift | 48 +++++++++++++++++++ LoopFollow/Alarm/AlarmManager.swift | 1 + LoopFollow/Task/AlarmTask.swift | 4 +- 7 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 LoopFollow/Alarm/AlarmCondition/PumpChangeCondition.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 59b803085..10dbd0da1 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -145,6 +145,8 @@ DDC6CA3F2DD7C6340060EE25 /* TemporaryAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA3E2DD7C6340060EE25 /* TemporaryAlarmEditor.swift */; }; DDC6CA412DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA402DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift */; }; DDC6CA432DD8CED20060EE25 /* SensorAgeCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA422DD8CED20060EE25 /* SensorAgeCondition.swift */; }; + DDC6CA452DD8D8E60060EE25 /* PumpChangeCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA442DD8D8E60060EE25 /* PumpChangeCondition.swift */; }; + DDC6CA472DD8D9010060EE25 /* PumpChangeAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA462DD8D9010060EE25 /* PumpChangeAlarmEditor.swift */; }; DDC7E5152DBCFA7900EB1127 /* SnoozerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5132DBCE1B900EB1127 /* SnoozerViewController.swift */; }; DDC7E5162DBCFA7F00EB1127 /* SnoozerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5122DBCE1B900EB1127 /* SnoozerView.swift */; }; DDC7E5382DBD887400EB1127 /* isOnPhoneCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5372DBD887400EB1127 /* isOnPhoneCall.swift */; }; @@ -493,6 +495,8 @@ DDC6CA3E2DD7C6340060EE25 /* TemporaryAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryAlarmEditor.swift; sourceTree = ""; }; DDC6CA402DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorAgeAlarmEditor.swift; sourceTree = ""; }; DDC6CA422DD8CED20060EE25 /* SensorAgeCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorAgeCondition.swift; sourceTree = ""; }; + DDC6CA442DD8D8E60060EE25 /* PumpChangeCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpChangeCondition.swift; sourceTree = ""; }; + DDC6CA462DD8D9010060EE25 /* PumpChangeAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpChangeAlarmEditor.swift; sourceTree = ""; }; DDC7E5122DBCE1B900EB1127 /* SnoozerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozerView.swift; sourceTree = ""; }; DDC7E5132DBCE1B900EB1127 /* SnoozerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozerViewController.swift; sourceTree = ""; }; DDC7E5372DBD887400EB1127 /* isOnPhoneCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = isOnPhoneCall.swift; sourceTree = ""; }; @@ -738,6 +742,7 @@ DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = { isa = PBXGroup; children = ( + DDC6CA442DD8D8E60060EE25 /* PumpChangeCondition.swift */, DDC6CA422DD8CED20060EE25 /* SensorAgeCondition.swift */, DDC6CA3C2DD7C6090060EE25 /* TemporaryCondition.swift */, DD7F4C1E2DD6648B00D449E9 /* FastRiseCondition.swift */, @@ -1009,6 +1014,7 @@ DDC7E53F2DBD8A1600EB1127 /* Editors */ = { isa = PBXGroup; children = ( + DDC6CA462DD8D9010060EE25 /* PumpChangeAlarmEditor.swift */, DDC6CA402DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift */, DDC6CA3E2DD7C6340060EE25 /* TemporaryAlarmEditor.swift */, DD7F4C202DD66BB100D449E9 /* FastRiseAlarmEditor.swift */, @@ -1850,6 +1856,7 @@ DD493ADF2ACF22BB009A6922 /* SAge.swift in Sources */, DDC6CA3F2DD7C6340060EE25 /* TemporaryAlarmEditor.swift in Sources */, DDF699992C5AA3060058A8D9 /* TempTargetPresetManager.swift in Sources */, + DDC6CA452DD8D8E60060EE25 /* PumpChangeCondition.swift in Sources */, DD0C0C722C4B000800DBADDF /* TrioNightscoutRemoteView.swift in Sources */, DD493ADB2ACF21A3009A6922 /* Bolus.swift in Sources */, DDF9676E2AD08C6E00C5EB95 /* SiteChange.swift in Sources */, @@ -1899,6 +1906,7 @@ DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */, DD0650EF2DCE96FF004D3B41 /* HighBGCondition.swift in Sources */, DDDF6F452D479AB100884336 /* LoopOverrideView.swift in Sources */, + DDC6CA472DD8D9010060EE25 /* PumpChangeAlarmEditor.swift in Sources */, DD4878132C7B750D0048F05C /* TempTargetView.swift in Sources */, DD0C0C682C48529400DBADDF /* Metric.swift in Sources */, FCC6886D2489909D00A0279D /* AnyConvertible.swift in Sources */, diff --git a/LoopFollow/Alarm/AlarmCondition/PumpChangeCondition.swift b/LoopFollow/Alarm/AlarmCondition/PumpChangeCondition.swift new file mode 100644 index 000000000..7bf6694b0 --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/PumpChangeCondition.swift @@ -0,0 +1,38 @@ +// LoopFollow +// PumpChangeCondition.swift +// Created by Jonas Björkert on 2025-05-17. + +// +// PumpChangeCondition.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-17. +// + +import Foundation + +/// Fires once when we are **≤ threshold hours** away from the Omnipod / +/// cannula 3-day hard-stop. Automatically disables itself after firing. +struct PumpChangeCondition: AlarmCondition { + static let type: AlarmType = .pumpChange + init() {} + + /// Pod lifetime = 3 days = 72 h + private let lifetime: TimeInterval = 3 * 24 * 60 * 60 + + func evaluate(alarm: Alarm, data: AlarmData) -> Bool { + // 0. sanity guards + guard let warnAheadHrs = alarm.threshold, warnAheadHrs > 0 else { return false } + guard let insertTS = data.pumpInsertTime else { return false } + + // convert UNIX timestamp → Date + let insertedAt = Date(timeIntervalSince1970: insertTS) + + // 1. compute “fire-at” moment + let expiry = insertedAt.addingTimeInterval(lifetime) + let trigger = expiry.addingTimeInterval(-warnAheadHrs * 3600) + + // 2. nothing else to track – disableAfterFiring=true handles repeats + return Date() >= trigger + } +} diff --git a/LoopFollow/Alarm/AlarmData.swift b/LoopFollow/Alarm/AlarmData.swift index 010a170df..dc3435e23 100644 --- a/LoopFollow/Alarm/AlarmData.swift +++ b/LoopFollow/Alarm/AlarmData.swift @@ -16,6 +16,7 @@ struct AlarmData: Codable { let recBolus: Double? let COB: Double? let sageInsertTime: TimeInterval? + let pumpInsertTime: TimeInterval? } /* diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index cabe3ce3d..8af8c0316 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -75,6 +75,8 @@ struct AlarmEditor: View { TemporaryAlarmEditor(alarm: $alarm) case .sensorChange: SensorAgeAlarmEditor(alarm: $alarm) + case .pumpChange: + PumpChangeAlarmEditor(alarm: $alarm) /* TODO: add other condition types here */ default: Text("No editor for \(alarm.type.rawValue)") diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift new file mode 100644 index 000000000..296b2eb99 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift @@ -0,0 +1,48 @@ +// LoopFollow +// PumpChangeAlarmEditor.swift +// Created by Jonas Björkert on 2025-05-17. + +// +// PumpChangeAlarmEditor.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-17. +// + +import SwiftUI + +struct PumpChangeAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Form { + InfoBanner( + text: "Alerts once when your pump / cannula is within the time " + + "window you choose below (relative to the 3-day change " + + "limit). After it fires once it disables itself.", + alarmType: alarm.type + ) + + AlarmGeneralSection(alarm: $alarm) + + AlarmStepperSection( + header: "Advance Notice", + footer: "How many hours before the 3-day limit the alert " + + "should fire. Set to 12 hours, for example, to get a " + + "reminder half a day in advance.", + title: "Warn hours", + range: 1 ... 24, + step: 1, + value: Binding( + get: { alarm.threshold ?? 12 }, + set: { alarm.threshold = $0 } + ) + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm, range: 1 ... 24, step: 1) + } + .navigationTitle(alarm.type.rawValue) + } +} diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index c1cfe2727..4fbf2af66 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -28,6 +28,7 @@ class AlarmManager { FastRiseCondition.self, TemporaryCondition.self, SensorAgeCondition.self, + PumpChangeCondition.self, // TODO: add other condition types here ] ) { diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index bb0caffad..0e5032016 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -22,6 +22,7 @@ extension MainViewController { let recBolus = UserDefaultsRepository.deviceRecBolus.value let COB = self.latestCOB?.value let sensorInsertedAt = UserDefaultsRepository.alertSageInsertTime.value + let pumpInsertTime = UserDefaultsRepository.alertCageInsertTime.value let alarmData = AlarmData( bgReadings: self.bgData @@ -38,7 +39,8 @@ extension MainViewController { latestTempTargetEnd: latestTempTargetEnd, recBolus: recBolus, COB: COB, - sageInsertTime: sensorInsertedAt + sageInsertTime: sensorInsertedAt, + pumpInsertTime: pumpInsertTime ) let finalAlarmData: AlarmData From 3627281925a2b9dd71a2abf18feee0fc82950d12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 17 May 2025 18:12:31 +0200 Subject: [PATCH 070/138] PumpInsulin --- LoopFollow.xcodeproj/project.pbxproj | 8 ++++ .../AlarmCondition/PumpVolumeCondition.swift | 26 ++++++++++ LoopFollow/Alarm/AlarmData.swift | 1 + .../Alarm/AlarmEditing/AlarmEditor.swift | 2 + .../Editors/PumpVolumeAlarmEditor.swift | 48 +++++++++++++++++++ LoopFollow/Alarm/AlarmManager.swift | 1 + LoopFollow/Task/AlarmTask.swift | 4 +- 7 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 LoopFollow/Alarm/AlarmCondition/PumpVolumeCondition.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 10dbd0da1..797eadaca 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -147,6 +147,8 @@ DDC6CA432DD8CED20060EE25 /* SensorAgeCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA422DD8CED20060EE25 /* SensorAgeCondition.swift */; }; DDC6CA452DD8D8E60060EE25 /* PumpChangeCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA442DD8D8E60060EE25 /* PumpChangeCondition.swift */; }; DDC6CA472DD8D9010060EE25 /* PumpChangeAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA462DD8D9010060EE25 /* PumpChangeAlarmEditor.swift */; }; + DDC6CA492DD8E47A0060EE25 /* PumpVolumeCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA482DD8E47A0060EE25 /* PumpVolumeCondition.swift */; }; + DDC6CA4B2DD8E4960060EE25 /* PumpVolumeAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA4A2DD8E4960060EE25 /* PumpVolumeAlarmEditor.swift */; }; DDC7E5152DBCFA7900EB1127 /* SnoozerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5132DBCE1B900EB1127 /* SnoozerViewController.swift */; }; DDC7E5162DBCFA7F00EB1127 /* SnoozerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5122DBCE1B900EB1127 /* SnoozerView.swift */; }; DDC7E5382DBD887400EB1127 /* isOnPhoneCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5372DBD887400EB1127 /* isOnPhoneCall.swift */; }; @@ -497,6 +499,8 @@ DDC6CA422DD8CED20060EE25 /* SensorAgeCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorAgeCondition.swift; sourceTree = ""; }; DDC6CA442DD8D8E60060EE25 /* PumpChangeCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpChangeCondition.swift; sourceTree = ""; }; DDC6CA462DD8D9010060EE25 /* PumpChangeAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpChangeAlarmEditor.swift; sourceTree = ""; }; + DDC6CA482DD8E47A0060EE25 /* PumpVolumeCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpVolumeCondition.swift; sourceTree = ""; }; + DDC6CA4A2DD8E4960060EE25 /* PumpVolumeAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpVolumeAlarmEditor.swift; sourceTree = ""; }; DDC7E5122DBCE1B900EB1127 /* SnoozerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozerView.swift; sourceTree = ""; }; DDC7E5132DBCE1B900EB1127 /* SnoozerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozerViewController.swift; sourceTree = ""; }; DDC7E5372DBD887400EB1127 /* isOnPhoneCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = isOnPhoneCall.swift; sourceTree = ""; }; @@ -742,6 +746,7 @@ DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = { isa = PBXGroup; children = ( + DDC6CA482DD8E47A0060EE25 /* PumpVolumeCondition.swift */, DDC6CA442DD8D8E60060EE25 /* PumpChangeCondition.swift */, DDC6CA422DD8CED20060EE25 /* SensorAgeCondition.swift */, DDC6CA3C2DD7C6090060EE25 /* TemporaryCondition.swift */, @@ -1014,6 +1019,7 @@ DDC7E53F2DBD8A1600EB1127 /* Editors */ = { isa = PBXGroup; children = ( + DDC6CA4A2DD8E4960060EE25 /* PumpVolumeAlarmEditor.swift */, DDC6CA462DD8D9010060EE25 /* PumpChangeAlarmEditor.swift */, DDC6CA402DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift */, DDC6CA3E2DD7C6340060EE25 /* TemporaryAlarmEditor.swift */, @@ -1793,6 +1799,7 @@ DDF2C0122BEFB733007A20E6 /* AppVersionManager.swift in Sources */, DD7E19862ACDA59700DBD158 /* BGCheck.swift in Sources */, DD0650F12DCE9A9E004D3B41 /* MissedReadingCondition.swift in Sources */, + DDC6CA4B2DD8E4960060EE25 /* PumpVolumeAlarmEditor.swift in Sources */, DD16AF0F2C99592F00FB655A /* HKQuantityInputView.swift in Sources */, DDFF3D7F2D1414A200BF9D9E /* BLEDevice.swift in Sources */, DD9ACA042D32821400415D8A /* DeviceStatusTask.swift in Sources */, @@ -1862,6 +1869,7 @@ DDF9676E2AD08C6E00C5EB95 /* SiteChange.swift in Sources */, DD13BC772C3FD64E0062313B /* InfoData.swift in Sources */, DD13BC752C3FD6210062313B /* InfoType.swift in Sources */, + DDC6CA492DD8E47A0060EE25 /* PumpVolumeCondition.swift in Sources */, DD1A97142D4294A5000DDC11 /* AdvancedSettingsView.swift in Sources */, DD9ACA0A2D33095600415D8A /* MinAgoTask.swift in Sources */, DD9ED0CC2D35526E000D2A63 /* SearchBar.swift in Sources */, diff --git a/LoopFollow/Alarm/AlarmCondition/PumpVolumeCondition.swift b/LoopFollow/Alarm/AlarmCondition/PumpVolumeCondition.swift new file mode 100644 index 000000000..3e64bc86e --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/PumpVolumeCondition.swift @@ -0,0 +1,26 @@ +// LoopFollow +// PumpVolumeCondition.swift +// Created by Jonas Björkert on 2025-05-17. + +// +// PumpVolumeCondition.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-17. +// + +import Foundation + +/// Fires when the most-recent pump‐reservoir reading is **≤ threshold units**. +/// Re-fires after the user-chosen snooze expires. +struct PumpVolumeCondition: AlarmCondition { + static let type: AlarmType = .pump + init() {} + + func evaluate(alarm: Alarm, data: AlarmData) -> Bool { + guard let threshold = alarm.threshold, threshold > 0 else { return false } + guard let latestVol = data.latestPumpVolume else { return false } + + return latestVol <= threshold + } +} diff --git a/LoopFollow/Alarm/AlarmData.swift b/LoopFollow/Alarm/AlarmData.swift index dc3435e23..a94a3773b 100644 --- a/LoopFollow/Alarm/AlarmData.swift +++ b/LoopFollow/Alarm/AlarmData.swift @@ -17,6 +17,7 @@ struct AlarmData: Codable { let COB: Double? let sageInsertTime: TimeInterval? let pumpInsertTime: TimeInterval? + let latestPumpVolume: Double? } /* diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index 8af8c0316..530c693cd 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -77,6 +77,8 @@ struct AlarmEditor: View { SensorAgeAlarmEditor(alarm: $alarm) case .pumpChange: PumpChangeAlarmEditor(alarm: $alarm) + case .pump: + PumpVolumeAlarmEditor(alarm: $alarm) /* TODO: add other condition types here */ default: Text("No editor for \(alarm.type.rawValue)") diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift new file mode 100644 index 000000000..68a1b90f9 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift @@ -0,0 +1,48 @@ +// LoopFollow +// PumpVolumeAlarmEditor.swift +// Created by Jonas Björkert on 2025-05-17. + +// +// PumpVolumeAlarmEditor.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-17. +// + +import SwiftUI + +struct PumpVolumeAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Form { + InfoBanner( + text: "Alerts when the pump reservoir falls to or below the " + + "unit level you set below.", + alarmType: alarm.type + ) + + AlarmGeneralSection(alarm: $alarm) + + AlarmStepperSection( + header: "Trigger Level", + footer: "An alert fires once the reservoir is at this value " + + "or lower.", + title: "Units ≤", + range: 1 ... 50, + step: 1, + value: Binding( + get: { alarm.threshold ?? 20 }, + set: { alarm.threshold = $0 } + ) + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm, + range: 1 ... 24, + step: 1) + } + .navigationTitle(alarm.type.rawValue) + } +} diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index 4fbf2af66..07e37c590 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -29,6 +29,7 @@ class AlarmManager { TemporaryCondition.self, SensorAgeCondition.self, PumpChangeCondition.self, + PumpVolumeCondition.self, // TODO: add other condition types here ] ) { diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index 0e5032016..7e8ccdacd 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -23,6 +23,7 @@ extension MainViewController { let COB = self.latestCOB?.value let sensorInsertedAt = UserDefaultsRepository.alertSageInsertTime.value let pumpInsertTime = UserDefaultsRepository.alertCageInsertTime.value + let latestPumpVol = self.latestPumpVolume let alarmData = AlarmData( bgReadings: self.bgData @@ -40,7 +41,8 @@ extension MainViewController { recBolus: recBolus, COB: COB, sageInsertTime: sensorInsertedAt, - pumpInsertTime: pumpInsertTime + pumpInsertTime: pumpInsertTime, + latestPumpVolume: latestPumpVol ) let finalAlarmData: AlarmData From 744d9f7896df83c9a82b7d85846ddab3520cd178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 17 May 2025 19:30:06 +0200 Subject: [PATCH 071/138] Add now to alarm evaluators --- LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift | 4 ++-- LoopFollow/Alarm/AlarmCondition/BuildExpireCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/COBCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/FastRiseCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/MissedReadingCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/NotLoopingCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/OverrideEndCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/PumpChangeCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/PumpVolumeCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/RecBolusCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/SensorAgeCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/TempTargetEndCondition.swift | 2 +- .../Alarm/AlarmCondition/TempTargetStartCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/TemporaryCondition.swift | 2 +- 18 files changed, 19 insertions(+), 19 deletions(-) diff --git a/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift b/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift index d7c2a59b6..f9ef020de 100644 --- a/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift @@ -8,7 +8,7 @@ protocol AlarmCondition { static var type: AlarmType { get } init() /// pure, per-alarm logic against `AlarmData` - func evaluate(alarm: Alarm, data: AlarmData) -> Bool + func evaluate(alarm: Alarm, data: AlarmData, now: Date) -> Bool } extension AlarmCondition { @@ -78,6 +78,6 @@ extension AlarmCondition { } // finally, run the type-specific logic - return evaluate(alarm: alarm, data: data) + return evaluate(alarm: alarm, data: data, now: now) } } diff --git a/LoopFollow/Alarm/AlarmCondition/BuildExpireCondition.swift b/LoopFollow/Alarm/AlarmCondition/BuildExpireCondition.swift index 192d98107..81e3d2e28 100644 --- a/LoopFollow/Alarm/AlarmCondition/BuildExpireCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/BuildExpireCondition.swift @@ -8,7 +8,7 @@ struct BuildExpireCondition: AlarmCondition { static let type: AlarmType = .buildExpire init() {} - func evaluate(alarm: Alarm, data: AlarmData) -> Bool { + func evaluate(alarm: Alarm, data: AlarmData, now _: Date) -> Bool { guard let expiry = data.expireDate else { return false } guard let thresholdDays = alarm.threshold else { return false } diff --git a/LoopFollow/Alarm/AlarmCondition/COBCondition.swift b/LoopFollow/Alarm/AlarmCondition/COBCondition.swift index 1812ca66d..08decae0d 100644 --- a/LoopFollow/Alarm/AlarmCondition/COBCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/COBCondition.swift @@ -8,7 +8,7 @@ struct COBCondition: AlarmCondition { static let type: AlarmType = .cob init() {} - func evaluate(alarm: Alarm, data: AlarmData) -> Bool { + func evaluate(alarm: Alarm, data: AlarmData, now _: Date) -> Bool { guard let threshold = alarm.threshold, threshold > 0 else { return false } guard let cob = data.COB, cob >= threshold else { Storage.shared.lastCOBNotified.value = nil diff --git a/LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift b/LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift index cd528be30..6b748ec1e 100644 --- a/LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift @@ -8,7 +8,7 @@ struct FastDropCondition: AlarmCondition { static let type: AlarmType = .fastDrop init() {} - func evaluate(alarm: Alarm, data: AlarmData) -> Bool { + func evaluate(alarm: Alarm, data: AlarmData, now _: Date) -> Bool { // ──────────────────────────────── // 0. sanity checks // ──────────────────────────────── diff --git a/LoopFollow/Alarm/AlarmCondition/FastRiseCondition.swift b/LoopFollow/Alarm/AlarmCondition/FastRiseCondition.swift index a192a2caf..40d81ddff 100644 --- a/LoopFollow/Alarm/AlarmCondition/FastRiseCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/FastRiseCondition.swift @@ -9,7 +9,7 @@ struct FastRiseCondition: AlarmCondition { static let type: AlarmType = .fastRise init() {} - func evaluate(alarm: Alarm, data: AlarmData) -> Bool { + func evaluate(alarm: Alarm, data: AlarmData, now _: Date) -> Bool { guard let rise = alarm.delta, rise > 0, let streak = alarm.monitoringWindow, streak > 0, diff --git a/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift b/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift index c77def6e3..148413b2b 100644 --- a/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift @@ -9,7 +9,7 @@ struct HighBGCondition: AlarmCondition { static let type: AlarmType = .high init() {} - func evaluate(alarm: Alarm, data: AlarmData) -> Bool { + func evaluate(alarm: Alarm, data: AlarmData, now _: Date) -> Bool { // ──────────────────────────────── // 0. get the limit // ──────────────────────────────── diff --git a/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift b/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift index 3b74e1adf..88443e06a 100644 --- a/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift @@ -11,7 +11,7 @@ struct LowBGCondition: AlarmCondition { static let type: AlarmType = .low init() {} - func evaluate(alarm: Alarm, data: AlarmData) -> Bool { + func evaluate(alarm: Alarm, data: AlarmData, now _: Date) -> Bool { // ──────────────────────────────── // 0. sanity checks // ──────────────────────────────── diff --git a/LoopFollow/Alarm/AlarmCondition/MissedReadingCondition.swift b/LoopFollow/Alarm/AlarmCondition/MissedReadingCondition.swift index 7a612aae0..35f1368e1 100644 --- a/LoopFollow/Alarm/AlarmCondition/MissedReadingCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/MissedReadingCondition.swift @@ -9,7 +9,7 @@ struct MissedReadingCondition: AlarmCondition { static let type: AlarmType = .missedReading init() {} - func evaluate(alarm: Alarm, data: AlarmData) -> Bool { + func evaluate(alarm: Alarm, data: AlarmData, now _: Date) -> Bool { // ──────────────────────────────── // 0. sanity checks // ──────────────────────────────── diff --git a/LoopFollow/Alarm/AlarmCondition/NotLoopingCondition.swift b/LoopFollow/Alarm/AlarmCondition/NotLoopingCondition.swift index dbadd1b06..757a6e217 100644 --- a/LoopFollow/Alarm/AlarmCondition/NotLoopingCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/NotLoopingCondition.swift @@ -8,7 +8,7 @@ struct NotLoopingCondition: AlarmCondition { static let type: AlarmType = .notLooping init() {} - func evaluate(alarm: Alarm, data: AlarmData) -> Bool { + func evaluate(alarm: Alarm, data: AlarmData, now _: Date) -> Bool { // ──────────────────────────────── // 0. sanity checks // ──────────────────────────────── diff --git a/LoopFollow/Alarm/AlarmCondition/OverrideEndCondition.swift b/LoopFollow/Alarm/AlarmCondition/OverrideEndCondition.swift index 2960777f8..a3832da7b 100644 --- a/LoopFollow/Alarm/AlarmCondition/OverrideEndCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/OverrideEndCondition.swift @@ -8,7 +8,7 @@ struct OverrideEndCondition: AlarmCondition { static let type: AlarmType = .overrideEnd init() {} - func evaluate(alarm _: Alarm, data: AlarmData) -> Bool { + func evaluate(alarm _: Alarm, data: AlarmData, now _: Date) -> Bool { guard let endTS = data.latestOverrideEnd, endTS > 0 else { return false } guard Date().timeIntervalSince1970 - endTS <= 15 * 60 else { return false } diff --git a/LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift b/LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift index c89b98f01..e3ea27d9a 100644 --- a/LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift @@ -8,7 +8,7 @@ struct OverrideStartCondition: AlarmCondition { static let type: AlarmType = .overrideStart init() {} - func evaluate(alarm _: Alarm, data: AlarmData) -> Bool { + func evaluate(alarm _: Alarm, data: AlarmData, now _: Date) -> Bool { guard let startTS = data.latestOverrideStart, startTS > 0 else { return false } let recent = Date().timeIntervalSince1970 - startTS <= 15 * 60 diff --git a/LoopFollow/Alarm/AlarmCondition/PumpChangeCondition.swift b/LoopFollow/Alarm/AlarmCondition/PumpChangeCondition.swift index 7bf6694b0..cf74101ab 100644 --- a/LoopFollow/Alarm/AlarmCondition/PumpChangeCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/PumpChangeCondition.swift @@ -20,7 +20,7 @@ struct PumpChangeCondition: AlarmCondition { /// Pod lifetime = 3 days = 72 h private let lifetime: TimeInterval = 3 * 24 * 60 * 60 - func evaluate(alarm: Alarm, data: AlarmData) -> Bool { + func evaluate(alarm: Alarm, data: AlarmData, now _: Date) -> Bool { // 0. sanity guards guard let warnAheadHrs = alarm.threshold, warnAheadHrs > 0 else { return false } guard let insertTS = data.pumpInsertTime else { return false } diff --git a/LoopFollow/Alarm/AlarmCondition/PumpVolumeCondition.swift b/LoopFollow/Alarm/AlarmCondition/PumpVolumeCondition.swift index 3e64bc86e..9c16c9936 100644 --- a/LoopFollow/Alarm/AlarmCondition/PumpVolumeCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/PumpVolumeCondition.swift @@ -17,7 +17,7 @@ struct PumpVolumeCondition: AlarmCondition { static let type: AlarmType = .pump init() {} - func evaluate(alarm: Alarm, data: AlarmData) -> Bool { + func evaluate(alarm: Alarm, data: AlarmData, now _: Date) -> Bool { guard let threshold = alarm.threshold, threshold > 0 else { return false } guard let latestVol = data.latestPumpVolume else { return false } diff --git a/LoopFollow/Alarm/AlarmCondition/RecBolusCondition.swift b/LoopFollow/Alarm/AlarmCondition/RecBolusCondition.swift index 1210389bc..3ee5d5987 100644 --- a/LoopFollow/Alarm/AlarmCondition/RecBolusCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/RecBolusCondition.swift @@ -9,7 +9,7 @@ struct RecBolusCondition: AlarmCondition { static let type: AlarmType = .recBolus init() {} - func evaluate(alarm: Alarm, data: AlarmData) -> Bool { + func evaluate(alarm: Alarm, data: AlarmData, now _: Date) -> Bool { // ──────────────────────────────── // 0. sanity checks // ──────────────────────────────── diff --git a/LoopFollow/Alarm/AlarmCondition/SensorAgeCondition.swift b/LoopFollow/Alarm/AlarmCondition/SensorAgeCondition.swift index 9a8d9b88a..2da4d8898 100644 --- a/LoopFollow/Alarm/AlarmCondition/SensorAgeCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/SensorAgeCondition.swift @@ -13,7 +13,7 @@ struct SensorAgeCondition: AlarmCondition { /// Dexcom hard-stop = 10 days = 240 h private let lifetime: TimeInterval = 10 * 24 * 60 * 60 - func evaluate(alarm: Alarm, data: AlarmData) -> Bool { + func evaluate(alarm: Alarm, data: AlarmData, now _: Date) -> Bool { // 0. basic guards guard let warnAheadHrs = alarm.threshold, warnAheadHrs > 0 else { return false } guard let insertTS = data.sageInsertTime else { return false } diff --git a/LoopFollow/Alarm/AlarmCondition/TempTargetEndCondition.swift b/LoopFollow/Alarm/AlarmCondition/TempTargetEndCondition.swift index b16b03890..be5828ea9 100644 --- a/LoopFollow/Alarm/AlarmCondition/TempTargetEndCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/TempTargetEndCondition.swift @@ -9,7 +9,7 @@ struct TempTargetEndCondition: AlarmCondition { static let type: AlarmType = .tempTargetEnd init() {} - func evaluate(alarm _: Alarm, data: AlarmData) -> Bool { + func evaluate(alarm _: Alarm, data: AlarmData, now _: Date) -> Bool { guard let endTS = data.latestTempTargetEnd, endTS > 0 else { return false } guard Date().timeIntervalSince1970 - endTS <= 15 * 60 else { return false } diff --git a/LoopFollow/Alarm/AlarmCondition/TempTargetStartCondition.swift b/LoopFollow/Alarm/AlarmCondition/TempTargetStartCondition.swift index 754541df7..23afb6324 100644 --- a/LoopFollow/Alarm/AlarmCondition/TempTargetStartCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/TempTargetStartCondition.swift @@ -9,7 +9,7 @@ struct TempTargetStartCondition: AlarmCondition { static let type: AlarmType = .tempTargetStart init() {} - func evaluate(alarm _: Alarm, data: AlarmData) -> Bool { + func evaluate(alarm _: Alarm, data: AlarmData, now _: Date) -> Bool { guard let startTS = data.latestTempTargetStart, startTS > 0 else { return false } guard Date().timeIntervalSince1970 - startTS <= 15 * 60 else { return false } diff --git a/LoopFollow/Alarm/AlarmCondition/TemporaryCondition.swift b/LoopFollow/Alarm/AlarmCondition/TemporaryCondition.swift index 489a13807..e1990951a 100644 --- a/LoopFollow/Alarm/AlarmCondition/TemporaryCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/TemporaryCondition.swift @@ -9,7 +9,7 @@ struct TemporaryCondition: AlarmCondition { static let type: AlarmType = .temporary init() {} - func evaluate(alarm: Alarm, data _: AlarmData) -> Bool { + func evaluate(alarm: Alarm, data _: AlarmData, now _: Date) -> Bool { // Needs at least ONE limit guard alarm.belowBG != nil || alarm.aboveBG != nil else { return false } From c55fd403da86a8e2a9c4fe4c300d673343a55bde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 19 May 2025 15:41:47 +0200 Subject: [PATCH 072/138] IOB Alarm --- LoopFollow.xcodeproj/project.pbxproj | 12 +++ .../Alarm/AlarmCondition/IOBCondition.swift | 68 +++++++++++++++ LoopFollow/Alarm/AlarmData.swift | 2 + .../Alarm/AlarmEditing/AlarmEditor.swift | 2 + .../AlarmEditing/Editors/IOBAlarmEditor.swift | 87 +++++++++++++++++++ LoopFollow/Alarm/AlarmManager.swift | 1 + LoopFollow/Alarm/BolusEntry.swift | 17 ++++ LoopFollow/Task/AlarmTask.swift | 5 +- 8 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 LoopFollow/Alarm/AlarmCondition/IOBCondition.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift create mode 100644 LoopFollow/Alarm/BolusEntry.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 797eadaca..554303ceb 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -140,6 +140,9 @@ DDAD162F2D2EF9830084BE10 /* RileyLinkHeartbeatBluetoothDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAD162E2D2EF97C0084BE10 /* RileyLinkHeartbeatBluetoothDevice.swift */; }; DDB0AF522BB1A8BE00AFA48B /* BuildDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB0AF512BB1A8BE00AFA48B /* BuildDetails.swift */; }; DDB0AF552BB1B24A00AFA48B /* BuildDetails.plist in Resources */ = {isa = PBXBuildFile; fileRef = DDB0AF542BB1B24A00AFA48B /* BuildDetails.plist */; }; + DDB9FC7B2DDB573F00EFAA76 /* IOBCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB9FC7A2DDB573F00EFAA76 /* IOBCondition.swift */; }; + DDB9FC7D2DDB575300EFAA76 /* IOBAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB9FC7C2DDB575300EFAA76 /* IOBAlarmEditor.swift */; }; + DDB9FC7F2DDB584500EFAA76 /* BolusEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB9FC7E2DDB584500EFAA76 /* BolusEntry.swift */; }; DDBE3ABD2CB5A961006B37DC /* OverrideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDBE3ABC2CB5A961006B37DC /* OverrideView.swift */; }; DDC6CA3D2DD7C6090060EE25 /* TemporaryCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA3C2DD7C6090060EE25 /* TemporaryCondition.swift */; }; DDC6CA3F2DD7C6340060EE25 /* TemporaryAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA3E2DD7C6340060EE25 /* TemporaryAlarmEditor.swift */; }; @@ -492,6 +495,9 @@ DDB0AF502BB1A84500AFA48B /* capture-build-details.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "capture-build-details.sh"; sourceTree = ""; }; DDB0AF512BB1A8BE00AFA48B /* BuildDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildDetails.swift; sourceTree = ""; }; DDB0AF542BB1B24A00AFA48B /* BuildDetails.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = BuildDetails.plist; sourceTree = ""; }; + DDB9FC7A2DDB573F00EFAA76 /* IOBCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOBCondition.swift; sourceTree = ""; }; + DDB9FC7C2DDB575300EFAA76 /* IOBAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOBAlarmEditor.swift; sourceTree = ""; }; + DDB9FC7E2DDB584500EFAA76 /* BolusEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusEntry.swift; sourceTree = ""; }; DDBE3ABC2CB5A961006B37DC /* OverrideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideView.swift; sourceTree = ""; }; DDC6CA3C2DD7C6090060EE25 /* TemporaryCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryCondition.swift; sourceTree = ""; }; DDC6CA3E2DD7C6340060EE25 /* TemporaryAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryAlarmEditor.swift; sourceTree = ""; }; @@ -746,6 +752,7 @@ DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = { isa = PBXGroup; children = ( + DDB9FC7A2DDB573F00EFAA76 /* IOBCondition.swift */, DDC6CA482DD8E47A0060EE25 /* PumpVolumeCondition.swift */, DDC6CA442DD8D8E60060EE25 /* PumpChangeCondition.swift */, DDC6CA422DD8CED20060EE25 /* SensorAgeCondition.swift */, @@ -1019,6 +1026,7 @@ DDC7E53F2DBD8A1600EB1127 /* Editors */ = { isa = PBXGroup; children = ( + DDB9FC7C2DDB575300EFAA76 /* IOBAlarmEditor.swift */, DDC6CA4A2DD8E4960060EE25 /* PumpVolumeAlarmEditor.swift */, DDC6CA462DD8D9010060EE25 /* PumpChangeAlarmEditor.swift */, DDC6CA402DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift */, @@ -1065,6 +1073,7 @@ DDCF9A7E2D85FCE6004DF4DD /* Alarm */ = { isa = PBXGroup; children = ( + DDB9FC7E2DDB584500EFAA76 /* BolusEntry.swift */, DDC6CA3B2DD7B9050060EE25 /* AlarmType */, DD5DA27B2DC930D6003D44FC /* GlucoseValue.swift */, DD4AFB602DB68BBC00BB593F /* AlarmListView.swift */, @@ -1797,6 +1806,7 @@ DDCF9A8C2D86005E004DF4DD /* AlarmManager.swift in Sources */, FCC6886924898FB100A0279D /* UserDefaultsValueGroups.swift in Sources */, DDF2C0122BEFB733007A20E6 /* AppVersionManager.swift in Sources */, + DDB9FC7B2DDB573F00EFAA76 /* IOBCondition.swift in Sources */, DD7E19862ACDA59700DBD158 /* BGCheck.swift in Sources */, DD0650F12DCE9A9E004D3B41 /* MissedReadingCondition.swift in Sources */, DDC6CA4B2DD8E4960060EE25 /* PumpVolumeAlarmEditor.swift in Sources */, @@ -1874,6 +1884,7 @@ DD9ACA0A2D33095600415D8A /* MinAgoTask.swift in Sources */, DD9ED0CC2D35526E000D2A63 /* SearchBar.swift in Sources */, DDD10EFF2C510C3C00D76A8E /* ObservableUserDefaultsValue.swift in Sources */, + DDB9FC7D2DDB575300EFAA76 /* IOBAlarmEditor.swift in Sources */, DD58171A2D299EF80041FB98 /* DexcomHeartbeatBluetoothDevice.swift in Sources */, DD7F4C0B2DD51C5500D449E9 /* OverrideEndCondition.swift in Sources */, DDFF3D872D14280500BF9D9E /* BackgroundRefreshSettingsViewModel.swift in Sources */, @@ -1887,6 +1898,7 @@ DDCF979624C1443C002C9752 /* GeneralSettingsViewController.swift in Sources */, FCC0FAC224922A22003E610E /* DictionaryKeyPath.swift in Sources */, DD493AD72ACF2139009A6922 /* SuspendPump.swift in Sources */, + DDB9FC7F2DDB584500EFAA76 /* BolusEntry.swift in Sources */, FC9788182485969B00A7906C /* AppDelegate.swift in Sources */, DDD10F072C529DE800D76A8E /* Observable.swift in Sources */, DDFF3D852D14279B00BF9D9E /* BackgroundRefreshSettingsView.swift in Sources */, diff --git a/LoopFollow/Alarm/AlarmCondition/IOBCondition.swift b/LoopFollow/Alarm/AlarmCondition/IOBCondition.swift new file mode 100644 index 000000000..eaf429dca --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/IOBCondition.swift @@ -0,0 +1,68 @@ +// LoopFollow +// IOBCondition.swift +// Created by Jonas Björkert on 2025-05-19. + +// +// IOBCondition.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-17. +// + +import Foundation + +/// Fires when **any** of three rules is true +/// ────────────────────────────────────────── +/// 1. latest IOB ≥ `threshold` +/// 2. within the last `lookbackMinutes`, the **count** of boluses +/// ≥ `monitoringWindow` **and** each bolus ≥ `delta` units +/// 3. within that same window, the **sum** of boluses ≥ `threshold` +struct IOBCondition: AlarmCondition { + static let type: AlarmType = .iob + init() {} + + /// Convenience accessors to keep the code tidy + private struct Params { + let iobMax: Double // alarm.threshold (units) + let minBolus: Double // alarm.delta (units) + let countNeeded: Int // alarm.monitoringWindow (bolus count) + let lookbackMin: Int // alarm.predictiveMinutes (minutes) + + init?(alarm: Alarm) { + guard + let iobMax = alarm.threshold, + let minBolus = alarm.delta, + let count = alarm.monitoringWindow, + let mins = alarm.predictiveMinutes, + iobMax > 0, minBolus > 0, count > 0, mins > 0 + else { return nil } + + self.iobMax = iobMax + self.minBolus = minBolus + countNeeded = count + lookbackMin = mins + } + } + + func evaluate(alarm: Alarm, data: AlarmData, now _: Date) -> Bool { + // ── 0. pull and sanity-check parameters ──────────────────────────── + guard let p = Params(alarm: alarm) else { return false } + + // ── 1. latest IOB alone high enough? ─────────────────────────────── + if let iob = data.IOB, iob >= p.iobMax { return true } + + // ── 2. look at the recent boluses ────────────────────────────────── + let cutoff = Date().addingTimeInterval(-Double(p.lookbackMin) * 60) + + var count = 0 + var total = 0.0 + + for b in data.recentBoluses where b.date >= cutoff && b.units >= p.minBolus { + count += 1 + total += b.units + if count >= p.countNeeded || total >= p.iobMax { return true } + } + + return false + } +} diff --git a/LoopFollow/Alarm/AlarmData.swift b/LoopFollow/Alarm/AlarmData.swift index a94a3773b..d36000698 100644 --- a/LoopFollow/Alarm/AlarmData.swift +++ b/LoopFollow/Alarm/AlarmData.swift @@ -18,6 +18,8 @@ struct AlarmData: Codable { let sageInsertTime: TimeInterval? let pumpInsertTime: TimeInterval? let latestPumpVolume: Double? + let IOB: Double? + let recentBoluses: [BolusEntry] } /* diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index 530c693cd..53017a3a5 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -79,6 +79,8 @@ struct AlarmEditor: View { PumpChangeAlarmEditor(alarm: $alarm) case .pump: PumpVolumeAlarmEditor(alarm: $alarm) + case .iob: + IOBAlarmEditor(alarm: $alarm) /* TODO: add other condition types here */ default: Text("No editor for \(alarm.type.rawValue)") diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift new file mode 100644 index 000000000..4b5a0d7d8 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift @@ -0,0 +1,87 @@ +// LoopFollow +// IOBAlarmEditor.swift +// Created by Jonas Björkert on 2025-05-19. + +// +// IOBAlarmEditor.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-17. +// + +import SwiftUI + +struct IOBAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Form { + InfoBanner( + text: "Alerts when insulin-on-board is high, or when several " + + "boluses in quick succession exceed the limits you set.", + alarmType: alarm.type + ) + + AlarmGeneralSection(alarm: $alarm) + + // ── individual bolus size ── + AlarmStepperSection( + header: "Bolus Size", + footer: "Only boluses equal to or larger than this are counted.", + title: "Bolus ≥", + range: 0.1 ... 20, + step: 0.1, + value: Binding( + get: { alarm.delta ?? 1.0 }, + set: { alarm.delta = $0 } + ) + ) + + // ── number of boluses ── + AlarmStepperSection( + header: "Bolus Count", + footer: "Number of qualifying boluses needed to trigger.", + title: "Count ≥", + range: 1 ... 10, + step: 1, + value: Binding( + get: { Double(alarm.monitoringWindow ?? 2) }, + set: { alarm.monitoringWindow = Int($0) } + ) + ) + + // ── look-back window ── + AlarmStepperSection( + header: "Time Window", + footer: "How far back to look for those boluses.", + title: "Minutes", + range: 5 ... 120, + step: 5, + value: Binding( + get: { Double(alarm.predictiveMinutes ?? 30) }, + set: { alarm.predictiveMinutes = Int($0) } + ) + ) + + // ── absolute IOB limit ── + AlarmStepperSection( + header: "Total IOB", + footer: "Alert if current IOB or total boluses reach this.", + title: "IOB ≥", + range: 1 ... 20, + step: 0.5, + value: Binding( + get: { alarm.threshold ?? 6 }, + set: { alarm.threshold = $0 } + ) + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm, + range: 1 ... 24, + step: 1) // snooze in hours + } + .navigationTitle(alarm.type.rawValue) + } +} diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index 07e37c590..7a73eabef 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -30,6 +30,7 @@ class AlarmManager { SensorAgeCondition.self, PumpChangeCondition.self, PumpVolumeCondition.self, + IOBCondition.self, // TODO: add other condition types here ] ) { diff --git a/LoopFollow/Alarm/BolusEntry.swift b/LoopFollow/Alarm/BolusEntry.swift new file mode 100644 index 000000000..ab708ab99 --- /dev/null +++ b/LoopFollow/Alarm/BolusEntry.swift @@ -0,0 +1,17 @@ +// LoopFollow +// BolusEntry.swift +// Created by Jonas Björkert on 2025-05-19. + +// +// BolusEntry.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-05-19. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// +import Foundation + +struct BolusEntry: Codable { + let units: Double + let date: Date +} diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index 7e8ccdacd..cb06234e4 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -24,6 +24,7 @@ extension MainViewController { let sensorInsertedAt = UserDefaultsRepository.alertSageInsertTime.value let pumpInsertTime = UserDefaultsRepository.alertCageInsertTime.value let latestPumpVol = self.latestPumpVolume + let bolusEntries = self.bolusData.map { BolusEntry(units: $0.value, date: Date(timeIntervalSince1970: $0.date)) } let alarmData = AlarmData( bgReadings: self.bgData @@ -42,7 +43,9 @@ extension MainViewController { COB: COB, sageInsertTime: sensorInsertedAt, pumpInsertTime: pumpInsertTime, - latestPumpVolume: latestPumpVol + latestPumpVolume: latestPumpVol, + IOB: self.latestIOB?.value, + recentBoluses: bolusEntries, ) let finalAlarmData: AlarmData From 267b1e6b0dd5e05b1bf443c07bb420bbbf1158f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 20 May 2025 08:06:02 +0200 Subject: [PATCH 073/138] Battery Alarm --- LoopFollow.xcodeproj/project.pbxproj | 8 ++++ .../AlarmCondition/BatteryCondition.swift | 17 ++++++++ LoopFollow/Alarm/AlarmData.swift | 1 + .../Alarm/AlarmEditing/AlarmEditor.swift | 9 +--- .../Editors/BatteryAlarmEditor.swift | 41 +++++++++++++++++++ LoopFollow/Alarm/AlarmManager.swift | 2 + LoopFollow/Alarm/BolusEntry.swift | 7 ---- LoopFollow/Helpers/Mobileprovision.swift | 1 - LoopFollow/Task/AlarmTask.swift | 2 + 9 files changed, 73 insertions(+), 15 deletions(-) create mode 100644 LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 554303ceb..dad098a24 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -161,6 +161,8 @@ DDC7E5462DBD8A1600EB1127 /* LowBgAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E53E2DBD8A1600EB1127 /* LowBgAlarmEditor.swift */; }; DDC7E5472DBD8A1600EB1127 /* AlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5402DBD8A1600EB1127 /* AlarmEditor.swift */; }; DDC7E5CF2DC77C2000EB1127 /* SnoozerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5CE2DC77C2000EB1127 /* SnoozerViewModel.swift */; }; + DDCC3A4B2DDBB5E4006F1C10 /* BatteryCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A4A2DDBB5E4006F1C10 /* BatteryCondition.swift */; }; + DDCC3A4D2DDBB77C006F1C10 /* BatteryAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A4C2DDBB77C006F1C10 /* BatteryAlarmEditor.swift */; }; DDCF979424C0D380002C9752 /* UIViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979324C0D380002C9752 /* UIViewExtension.swift */; }; DDCF979624C1443C002C9752 /* GeneralSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979524C1443C002C9752 /* GeneralSettingsViewController.swift */; }; DDCF979824C1489C002C9752 /* GraphSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */; }; @@ -516,6 +518,8 @@ DDC7E53E2DBD8A1600EB1127 /* LowBgAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LowBgAlarmEditor.swift; sourceTree = ""; }; DDC7E5402DBD8A1600EB1127 /* AlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmEditor.swift; sourceTree = ""; }; DDC7E5CE2DC77C2000EB1127 /* SnoozerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozerViewModel.swift; sourceTree = ""; }; + DDCC3A4A2DDBB5E4006F1C10 /* BatteryCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryCondition.swift; sourceTree = ""; }; + DDCC3A4C2DDBB77C006F1C10 /* BatteryAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryAlarmEditor.swift; sourceTree = ""; }; DDCF979324C0D380002C9752 /* UIViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtension.swift; sourceTree = ""; }; DDCF979524C1443C002C9752 /* GeneralSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsViewController.swift; sourceTree = ""; }; DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphSettingsViewController.swift; sourceTree = ""; }; @@ -752,6 +756,7 @@ DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = { isa = PBXGroup; children = ( + DDCC3A4A2DDBB5E4006F1C10 /* BatteryCondition.swift */, DDB9FC7A2DDB573F00EFAA76 /* IOBCondition.swift */, DDC6CA482DD8E47A0060EE25 /* PumpVolumeCondition.swift */, DDC6CA442DD8D8E60060EE25 /* PumpChangeCondition.swift */, @@ -1026,6 +1031,7 @@ DDC7E53F2DBD8A1600EB1127 /* Editors */ = { isa = PBXGroup; children = ( + DDCC3A4C2DDBB77C006F1C10 /* BatteryAlarmEditor.swift */, DDB9FC7C2DDB575300EFAA76 /* IOBAlarmEditor.swift */, DDC6CA4A2DD8E4960060EE25 /* PumpVolumeAlarmEditor.swift */, DDC6CA462DD8D9010060EE25 /* PumpChangeAlarmEditor.swift */, @@ -1795,6 +1801,7 @@ DD7F4C252DD7B20700D449E9 /* AlarmType+timeUnit.swift in Sources */, DDC7E5152DBCFA7900EB1127 /* SnoozerViewController.swift in Sources */, DD7F4C072DD5042F00D449E9 /* OverrideStartAlarmEditor.swift in Sources */, + DDCC3A4B2DDBB5E4006F1C10 /* BatteryCondition.swift in Sources */, DDDF6F492D479AF000884336 /* NoRemoteView.swift in Sources */, DD0650ED2DCE9371004D3B41 /* HighBgAlarmEditor.swift in Sources */, DD7F4C172DD63FA700D449E9 /* RecBolusCondition.swift in Sources */, @@ -1879,6 +1886,7 @@ DDF9676E2AD08C6E00C5EB95 /* SiteChange.swift in Sources */, DD13BC772C3FD64E0062313B /* InfoData.swift in Sources */, DD13BC752C3FD6210062313B /* InfoType.swift in Sources */, + DDCC3A4D2DDBB77C006F1C10 /* BatteryAlarmEditor.swift in Sources */, DDC6CA492DD8E47A0060EE25 /* PumpVolumeCondition.swift in Sources */, DD1A97142D4294A5000DDC11 /* AdvancedSettingsView.swift in Sources */, DD9ACA0A2D33095600415D8A /* MinAgoTask.swift in Sources */, diff --git a/LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift b/LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift new file mode 100644 index 000000000..0ced4027f --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift @@ -0,0 +1,17 @@ +// LoopFollow +// BatteryCondition.swift +// Created by Jonas Björkert on 2025-05-19. + +import Foundation + +struct BatteryCondition: AlarmCondition { + static let type: AlarmType = .battery + init() {} + + func evaluate(alarm: Alarm, data: AlarmData, now _: Date) -> Bool { + guard let limit = alarm.threshold, limit > 0 else { return false } + guard let level = data.latestBattery else { return false } + + return level <= limit + } +} diff --git a/LoopFollow/Alarm/AlarmData.swift b/LoopFollow/Alarm/AlarmData.swift index d36000698..b408c6e96 100644 --- a/LoopFollow/Alarm/AlarmData.swift +++ b/LoopFollow/Alarm/AlarmData.swift @@ -20,6 +20,7 @@ struct AlarmData: Codable { let latestPumpVolume: Double? let IOB: Double? let recentBoluses: [BolusEntry] + let latestBattery: Double? } /* diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index 53017a3a5..9a8d2e053 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -2,13 +2,6 @@ // AlarmEditor.swift // Created by Jonas Björkert on 2025-04-26. -// -// AlarmEditor.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-04-21. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// import SwiftUI struct AlarmEditor: View { @@ -81,6 +74,8 @@ struct AlarmEditor: View { PumpVolumeAlarmEditor(alarm: $alarm) case .iob: IOBAlarmEditor(alarm: $alarm) + case .battery: + BatteryAlarmEditor(alarm: $alarm) /* TODO: add other condition types here */ default: Text("No editor for \(alarm.type.rawValue)") diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift new file mode 100644 index 000000000..a5e75da5d --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift @@ -0,0 +1,41 @@ +// LoopFollow +// BatteryAlarmEditor.swift +// Created by Jonas Björkert on 2025-05-19. + +import SwiftUI + +struct BatteryAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Form { + InfoBanner( + text: "Alerts when the phone battery drops below the " + + "percentage you set below.", + alarmType: alarm.type + ) + + AlarmGeneralSection(alarm: $alarm) + + AlarmStepperSection( + header: "Battery Level", + footer: "Alert when remaining charge is equal to or below this.", + title: "Level ≤", + range: 0 ... 100, + step: 5, + unitLabel: "%", + value: Binding( + get: { alarm.threshold ?? 20 }, + set: { alarm.threshold = $0 } + ) + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm, + range: 1 ... 24, + step: 1) + } + .navigationTitle(alarm.type.rawValue) + } +} diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index 7a73eabef..c9494ae8e 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -31,6 +31,8 @@ class AlarmManager { PumpChangeCondition.self, PumpVolumeCondition.self, IOBCondition.self, + BatteryCondition.self, + // TODO: add other condition types here ] ) { diff --git a/LoopFollow/Alarm/BolusEntry.swift b/LoopFollow/Alarm/BolusEntry.swift index ab708ab99..141029ecf 100644 --- a/LoopFollow/Alarm/BolusEntry.swift +++ b/LoopFollow/Alarm/BolusEntry.swift @@ -2,13 +2,6 @@ // BolusEntry.swift // Created by Jonas Björkert on 2025-05-19. -// -// BolusEntry.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-19. -// Copyright © 2025 Jon Fawcett. All rights reserved. -// import Foundation struct BolusEntry: Codable { diff --git a/LoopFollow/Helpers/Mobileprovision.swift b/LoopFollow/Helpers/Mobileprovision.swift index 47cdffa05..b5d2a47f1 100644 --- a/LoopFollow/Helpers/Mobileprovision.swift +++ b/LoopFollow/Helpers/Mobileprovision.swift @@ -6,7 +6,6 @@ // MobileProvision.swift // Fluux.io // -// Created by Mickaël Rémond on 03/11/2018. // Copyright © 2018 ProcessOne. // Distributed under Apache License v2 // diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index cb06234e4..6e38fa18d 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -25,6 +25,7 @@ extension MainViewController { let pumpInsertTime = UserDefaultsRepository.alertCageInsertTime.value let latestPumpVol = self.latestPumpVolume let bolusEntries = self.bolusData.map { BolusEntry(units: $0.value, date: Date(timeIntervalSince1970: $0.date)) } + let latestBattery = UserDefaultsRepository.deviceBatteryLevel.value let alarmData = AlarmData( bgReadings: self.bgData @@ -46,6 +47,7 @@ extension MainViewController { latestPumpVolume: latestPumpVol, IOB: self.latestIOB?.value, recentBoluses: bolusEntries, + latestBattery: latestBattery ) let finalAlarmData: AlarmData From a40d974c9e52afa12707a067f3ccc8694698e069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 20 May 2025 11:28:07 +0200 Subject: [PATCH 074/138] BatteryDropAlarm --- LoopFollow.xcodeproj/project.pbxproj | 20 ++++++- .../AlarmCondition/BatteryCondition.swift | 2 +- .../AlarmCondition/BatteryDropCondition.swift | 36 +++++++++++++ LoopFollow/Alarm/AlarmData.swift | 5 +- .../Alarm/AlarmEditing/AlarmEditor.swift | 2 + .../Editors/BatteryAlarmEditor.swift | 2 +- .../Editors/BatteryDropAlarmEditor.swift | 53 +++++++++++++++++++ LoopFollow/Alarm/AlarmManager.swift | 2 +- .../Alarm/{ => DataStructs}/BolusEntry.swift | 0 .../{ => DataStructs}/GlucoseValue.swift | 0 LoopFollow/Task/AlarmTask.swift | 3 +- 11 files changed, 115 insertions(+), 10 deletions(-) create mode 100644 LoopFollow/Alarm/AlarmCondition/BatteryDropCondition.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift rename LoopFollow/Alarm/{ => DataStructs}/BolusEntry.swift (100%) rename LoopFollow/Alarm/{ => DataStructs}/GlucoseValue.swift (100%) diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index dad098a24..dd76bcebb 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -163,6 +163,8 @@ DDC7E5CF2DC77C2000EB1127 /* SnoozerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC7E5CE2DC77C2000EB1127 /* SnoozerViewModel.swift */; }; DDCC3A4B2DDBB5E4006F1C10 /* BatteryCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A4A2DDBB5E4006F1C10 /* BatteryCondition.swift */; }; DDCC3A4D2DDBB77C006F1C10 /* BatteryAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A4C2DDBB77C006F1C10 /* BatteryAlarmEditor.swift */; }; + DDCC3A4F2DDC5B54006F1C10 /* BatteryDropCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A4E2DDC5B54006F1C10 /* BatteryDropCondition.swift */; }; + DDCC3A542DDC5D62006F1C10 /* BatteryDropAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A532DDC5D62006F1C10 /* BatteryDropAlarmEditor.swift */; }; DDCF979424C0D380002C9752 /* UIViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979324C0D380002C9752 /* UIViewExtension.swift */; }; DDCF979624C1443C002C9752 /* GeneralSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979524C1443C002C9752 /* GeneralSettingsViewController.swift */; }; DDCF979824C1489C002C9752 /* GraphSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */; }; @@ -520,6 +522,8 @@ DDC7E5CE2DC77C2000EB1127 /* SnoozerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozerViewModel.swift; sourceTree = ""; }; DDCC3A4A2DDBB5E4006F1C10 /* BatteryCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryCondition.swift; sourceTree = ""; }; DDCC3A4C2DDBB77C006F1C10 /* BatteryAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryAlarmEditor.swift; sourceTree = ""; }; + DDCC3A4E2DDC5B54006F1C10 /* BatteryDropCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryDropCondition.swift; sourceTree = ""; }; + DDCC3A532DDC5D62006F1C10 /* BatteryDropAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryDropAlarmEditor.swift; sourceTree = ""; }; DDCF979324C0D380002C9752 /* UIViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtension.swift; sourceTree = ""; }; DDCF979524C1443C002C9752 /* GeneralSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsViewController.swift; sourceTree = ""; }; DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphSettingsViewController.swift; sourceTree = ""; }; @@ -756,6 +760,7 @@ DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = { isa = PBXGroup; children = ( + DDCC3A4E2DDC5B54006F1C10 /* BatteryDropCondition.swift */, DDCC3A4A2DDBB5E4006F1C10 /* BatteryCondition.swift */, DDB9FC7A2DDB573F00EFAA76 /* IOBCondition.swift */, DDC6CA482DD8E47A0060EE25 /* PumpVolumeCondition.swift */, @@ -1031,6 +1036,7 @@ DDC7E53F2DBD8A1600EB1127 /* Editors */ = { isa = PBXGroup; children = ( + DDCC3A532DDC5D62006F1C10 /* BatteryDropAlarmEditor.swift */, DDCC3A4C2DDBB77C006F1C10 /* BatteryAlarmEditor.swift */, DDB9FC7C2DDB575300EFAA76 /* IOBAlarmEditor.swift */, DDC6CA4A2DD8E4960060EE25 /* PumpVolumeAlarmEditor.swift */, @@ -1076,12 +1082,20 @@ path = Storage; sourceTree = ""; }; - DDCF9A7E2D85FCE6004DF4DD /* Alarm */ = { + DDCC3A502DDC5BD4006F1C10 /* DataStructs */ = { isa = PBXGroup; children = ( DDB9FC7E2DDB584500EFAA76 /* BolusEntry.swift */, - DDC6CA3B2DD7B9050060EE25 /* AlarmType */, DD5DA27B2DC930D6003D44FC /* GlucoseValue.swift */, + ); + path = DataStructs; + sourceTree = ""; + }; + DDCF9A7E2D85FCE6004DF4DD /* Alarm */ = { + isa = PBXGroup; + children = ( + DDCC3A502DDC5BD4006F1C10 /* DataStructs */, + DDC6CA3B2DD7B9050060EE25 /* AlarmType */, DD4AFB602DB68BBC00BB593F /* AlarmListView.swift */, DDC7E5412DBD8A1600EB1127 /* AlarmEditing */, DD4AFB482DB576C200BB593F /* AlarmSettingsView.swift */, @@ -1811,6 +1825,7 @@ FC16A97A24996673003D6245 /* NightScout.swift in Sources */, DD07B5C929E2F9C400C6A635 /* NightscoutUtils.swift in Sources */, DDCF9A8C2D86005E004DF4DD /* AlarmManager.swift in Sources */, + DDCC3A4F2DDC5B54006F1C10 /* BatteryDropCondition.swift in Sources */, FCC6886924898FB100A0279D /* UserDefaultsValueGroups.swift in Sources */, DDF2C0122BEFB733007A20E6 /* AppVersionManager.swift in Sources */, DDB9FC7B2DDB573F00EFAA76 /* IOBCondition.swift in Sources */, @@ -1884,6 +1899,7 @@ DD0C0C722C4B000800DBADDF /* TrioNightscoutRemoteView.swift in Sources */, DD493ADB2ACF21A3009A6922 /* Bolus.swift in Sources */, DDF9676E2AD08C6E00C5EB95 /* SiteChange.swift in Sources */, + DDCC3A542DDC5D62006F1C10 /* BatteryDropAlarmEditor.swift in Sources */, DD13BC772C3FD64E0062313B /* InfoData.swift in Sources */, DD13BC752C3FD6210062313B /* InfoType.swift in Sources */, DDCC3A4D2DDBB77C006F1C10 /* BatteryAlarmEditor.swift in Sources */, diff --git a/LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift b/LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift index 0ced4027f..9aba1e792 100644 --- a/LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift @@ -1,6 +1,6 @@ // LoopFollow // BatteryCondition.swift -// Created by Jonas Björkert on 2025-05-19. +// Created by Jonas Björkert on 2025-05-20. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/BatteryDropCondition.swift b/LoopFollow/Alarm/AlarmCondition/BatteryDropCondition.swift new file mode 100644 index 000000000..7c30985cf --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/BatteryDropCondition.swift @@ -0,0 +1,36 @@ +// LoopFollow +// BatteryDropCondition.swift +// Created by Jonas Björkert on 2025-05-20. + +import Foundation + +/// Fires when the phone-battery **falls by ≥ Δ % within N minutes**. +/// +/// * `alarm.delta`   ➜ percentage drop (e.g. 5 %) +/// * `alarm.monitoringWindow` ➜ window in minutes (e.g. 15) +/// * Uses `data.batteryHistory`, which must be sorted **oldest → newest**. +struct BatteryDropCondition: AlarmCondition { + static let type: AlarmType = .batteryDrop + init() {} + + func evaluate(alarm: Alarm, data: AlarmData, now _: Date) -> Bool { + // ───── 0. sanity ─────────────────────────────────────────────── + guard + let drop = alarm.delta, drop > 0, + let minutes = alarm.monitoringWindow, minutes > 0, + let latest = data.batteryHistory.last + else { return false } + + // find the sample *closest* to “minutes” ago + let target = latest.timestamp.addingTimeInterval(-Double(minutes) * 60) + + guard let earlier = data.batteryHistory.min(by: { + abs($0.timestamp.timeIntervalSince(target)) < abs($1.timestamp.timeIntervalSince(target)) + }) else { return false } + + // ignore if the earlier level was 100 % (false drop when just unplugged) + guard earlier.batteryLevel < 100 else { return false } + + return (earlier.batteryLevel - latest.batteryLevel) >= drop + } +} diff --git a/LoopFollow/Alarm/AlarmData.swift b/LoopFollow/Alarm/AlarmData.swift index b408c6e96..f7c9a5ee0 100644 --- a/LoopFollow/Alarm/AlarmData.swift +++ b/LoopFollow/Alarm/AlarmData.swift @@ -21,12 +21,9 @@ struct AlarmData: Codable { let IOB: Double? let recentBoluses: [BolusEntry] let latestBattery: Double? + let batteryHistory: [DataStructs.batteryStruct] } /* - // let iob: Double? - // let latestBoluses: [BolusEntry] - // let batteryLevel: Double? // let latestCarbs: [CarbEntry] - // let pumpVolume: Double? */ diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index 9a8d2e053..68e1d6016 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -76,6 +76,8 @@ struct AlarmEditor: View { IOBAlarmEditor(alarm: $alarm) case .battery: BatteryAlarmEditor(alarm: $alarm) + case .batteryDrop: + BatteryDropAlarmEditor(alarm: $alarm) /* TODO: add other condition types here */ default: Text("No editor for \(alarm.type.rawValue)") diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift index a5e75da5d..abc0057d3 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift @@ -1,6 +1,6 @@ // LoopFollow // BatteryAlarmEditor.swift -// Created by Jonas Björkert on 2025-05-19. +// Created by Jonas Björkert on 2025-05-17. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift new file mode 100644 index 000000000..e47424163 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift @@ -0,0 +1,53 @@ +// LoopFollow +// BatteryDropAlarmEditor.swift +// Created by Jonas Björkert on 2025-05-20. + +import SwiftUI + +struct BatteryDropAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Form { + InfoBanner( + text: "Alerts when the phone battery falls by a specified " + + "percentage within a set time window.", + alarmType: alarm.type + ) + + AlarmGeneralSection(alarm: $alarm) + + AlarmStepperSection( + header: "Drop Amount", + footer: "Trigger when charge falls by at least this much.", + title: "Δ %", + range: 5 ... 100, + step: 5, + unitLabel: "%", + value: Binding( + get: { alarm.delta ?? 10 }, + set: { alarm.delta = $0 } + ) + ) + + AlarmStepperSection( + header: "Time Window", + footer: "How far back to look for that drop.", + title: "Minutes", + range: 5 ... 30, + step: 5, + value: Binding( + get: { Double(alarm.monitoringWindow ?? 15) }, + set: { alarm.monitoringWindow = Int($0) } + ) + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm, + range: 1 ... 24, + step: 1) + } + .navigationTitle(alarm.type.rawValue) + } +} diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index c9494ae8e..b370aed06 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -32,7 +32,7 @@ class AlarmManager { PumpVolumeCondition.self, IOBCondition.self, BatteryCondition.self, - + BatteryDropCondition.self, // TODO: add other condition types here ] ) { diff --git a/LoopFollow/Alarm/BolusEntry.swift b/LoopFollow/Alarm/DataStructs/BolusEntry.swift similarity index 100% rename from LoopFollow/Alarm/BolusEntry.swift rename to LoopFollow/Alarm/DataStructs/BolusEntry.swift diff --git a/LoopFollow/Alarm/GlucoseValue.swift b/LoopFollow/Alarm/DataStructs/GlucoseValue.swift similarity index 100% rename from LoopFollow/Alarm/GlucoseValue.swift rename to LoopFollow/Alarm/DataStructs/GlucoseValue.swift diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index 6e38fa18d..0dc3fc88d 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -47,7 +47,8 @@ extension MainViewController { latestPumpVolume: latestPumpVol, IOB: self.latestIOB?.value, recentBoluses: bolusEntries, - latestBattery: latestBattery + latestBattery: latestBattery, + batteryHistory: self.deviceBatteryData ) let finalAlarmData: AlarmData From f06f8f01f83b9665e051c7ade1f384a8fb1a71bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 20 May 2025 15:38:43 +0200 Subject: [PATCH 075/138] MissedBolus --- LoopFollow.xcodeproj/project.pbxproj | 12 +++ .../AlarmCondition/MissedBolusCondition.swift | 66 ++++++++++++++ LoopFollow/Alarm/AlarmData.swift | 5 +- .../Alarm/AlarmEditing/AlarmEditor.swift | 9 +- .../Editors/MissedBolusAlarmEditor.swift | 86 +++++++++++++++++++ LoopFollow/Alarm/AlarmManager.swift | 1 - LoopFollow/Alarm/DataStructs/CarbSample.swift | 10 +++ LoopFollow/Storage/Storage.swift | 1 + LoopFollow/Task/AlarmTask.swift | 4 +- 9 files changed, 184 insertions(+), 10 deletions(-) create mode 100644 LoopFollow/Alarm/AlarmCondition/MissedBolusCondition.swift create mode 100644 LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift create mode 100644 LoopFollow/Alarm/DataStructs/CarbSample.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index dd76bcebb..b6def6eed 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -165,6 +165,9 @@ DDCC3A4D2DDBB77C006F1C10 /* BatteryAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A4C2DDBB77C006F1C10 /* BatteryAlarmEditor.swift */; }; DDCC3A4F2DDC5B54006F1C10 /* BatteryDropCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A4E2DDC5B54006F1C10 /* BatteryDropCondition.swift */; }; DDCC3A542DDC5D62006F1C10 /* BatteryDropAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A532DDC5D62006F1C10 /* BatteryDropAlarmEditor.swift */; }; + DDCC3A562DDC9617006F1C10 /* MissedBolusCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A552DDC9617006F1C10 /* MissedBolusCondition.swift */; }; + 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 */; }; DDCF979624C1443C002C9752 /* GeneralSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979524C1443C002C9752 /* GeneralSettingsViewController.swift */; }; DDCF979824C1489C002C9752 /* GraphSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */; }; @@ -524,6 +527,9 @@ DDCC3A4C2DDBB77C006F1C10 /* BatteryAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryAlarmEditor.swift; sourceTree = ""; }; DDCC3A4E2DDC5B54006F1C10 /* BatteryDropCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryDropCondition.swift; sourceTree = ""; }; DDCC3A532DDC5D62006F1C10 /* BatteryDropAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryDropAlarmEditor.swift; sourceTree = ""; }; + DDCC3A552DDC9617006F1C10 /* MissedBolusCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissedBolusCondition.swift; sourceTree = ""; }; + DDCC3A572DDC9655006F1C10 /* MissedBolusAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissedBolusAlarmEditor.swift; sourceTree = ""; }; + DDCC3A592DDC988F006F1C10 /* CarbSample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbSample.swift; sourceTree = ""; }; DDCF979324C0D380002C9752 /* UIViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtension.swift; sourceTree = ""; }; DDCF979524C1443C002C9752 /* GeneralSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsViewController.swift; sourceTree = ""; }; DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphSettingsViewController.swift; sourceTree = ""; }; @@ -760,6 +766,7 @@ DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = { isa = PBXGroup; children = ( + DDCC3A552DDC9617006F1C10 /* MissedBolusCondition.swift */, DDCC3A4E2DDC5B54006F1C10 /* BatteryDropCondition.swift */, DDCC3A4A2DDBB5E4006F1C10 /* BatteryCondition.swift */, DDB9FC7A2DDB573F00EFAA76 /* IOBCondition.swift */, @@ -1036,6 +1043,7 @@ DDC7E53F2DBD8A1600EB1127 /* Editors */ = { isa = PBXGroup; children = ( + DDCC3A572DDC9655006F1C10 /* MissedBolusAlarmEditor.swift */, DDCC3A532DDC5D62006F1C10 /* BatteryDropAlarmEditor.swift */, DDCC3A4C2DDBB77C006F1C10 /* BatteryAlarmEditor.swift */, DDB9FC7C2DDB575300EFAA76 /* IOBAlarmEditor.swift */, @@ -1085,6 +1093,7 @@ DDCC3A502DDC5BD4006F1C10 /* DataStructs */ = { isa = PBXGroup; children = ( + DDCC3A592DDC988F006F1C10 /* CarbSample.swift */, DDB9FC7E2DDB584500EFAA76 /* BolusEntry.swift */, DD5DA27B2DC930D6003D44FC /* GlucoseValue.swift */, ); @@ -1830,6 +1839,7 @@ DDF2C0122BEFB733007A20E6 /* AppVersionManager.swift in Sources */, DDB9FC7B2DDB573F00EFAA76 /* IOBCondition.swift in Sources */, DD7E19862ACDA59700DBD158 /* BGCheck.swift in Sources */, + DDCC3A5A2DDC988F006F1C10 /* CarbSample.swift in Sources */, DD0650F12DCE9A9E004D3B41 /* MissedReadingCondition.swift in Sources */, DDC6CA4B2DD8E4960060EE25 /* PumpVolumeAlarmEditor.swift in Sources */, DD16AF0F2C99592F00FB655A /* HKQuantityInputView.swift in Sources */, @@ -1841,6 +1851,7 @@ DD0650F92DCFE7BE004D3B41 /* FastDropAlarmEditor.swift in Sources */, DD5817172D2710E90041FB98 /* BLEDeviceSelectionView.swift in Sources */, FC16A97B249966A3003D6245 /* AlarmSound.swift in Sources */, + DDCC3A562DDC9617006F1C10 /* MissedBolusCondition.swift in Sources */, DDBE3ABD2CB5A961006B37DC /* OverrideView.swift in Sources */, DDB0AF522BB1A8BE00AFA48B /* BuildDetails.swift in Sources */, DDA9ACAA2D6A6B8300E6F1A9 /* ContactIncludeOption.swift in Sources */, @@ -1996,6 +2007,7 @@ DD493ADD2ACF21E0009A6922 /* Basals.swift in Sources */, FC16A98124996C07003D6245 /* DateTime.swift in Sources */, FC3CAB022493B6220068A152 /* BackgroundTaskAudio.swift in Sources */, + DDCC3A582DDC9655006F1C10 /* MissedBolusAlarmEditor.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/LoopFollow/Alarm/AlarmCondition/MissedBolusCondition.swift b/LoopFollow/Alarm/AlarmCondition/MissedBolusCondition.swift new file mode 100644 index 000000000..7908a3d88 --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/MissedBolusCondition.swift @@ -0,0 +1,66 @@ +// LoopFollow +// MissedBolusCondition.swift +// Created by Jonas Björkert on 2025-05-20. + +import Foundation + +/// Fires when a carb entry is logged but **no** qualifying bolus is given +/// within the user-defined “delay” (after allowing for a pre-bolus window). +/// • Ignores small-carb treatments, tiny boluses, and low-BG scenarios. +/// • Triggers once per carb entry. +struct MissedBolusCondition: AlarmCondition { + static let type: AlarmType = .missedBolus + init() {} + + func evaluate(alarm: Alarm, data: AlarmData, now: Date) -> Bool { + // ──────────────────────────────── + // 0. pull user settings + // ──────────────────────────────── + guard + let delayMin = alarm.monitoringWindow, delayMin > 0, + let prebolusMin = alarm.predictiveMinutes, + let minBolusU = alarm.delta, // ignore bolus ≤ + let minCarbGr = alarm.threshold, // ignore carbs ≤ + let minBG = alarm.aboveBG // ignore BG ≤ + else { return false } + + // ──────────────────────────────── + // 1. get most-recent carb entry + // ──────────────────────────────── + guard let carb = data.recentCarbs.last else { return false } + + // – must be at least `delayMin` old, but not older than 60 min + guard carb.date > now.addingTimeInterval(-3600), + carb.date < now.addingTimeInterval(-Double(delayMin) * 60) + else { return false } + + // – ignore tiny carbs + guard carb.grams > minCarbGr else { return false } + + // – ignore if BG is low + if let latestBG = data.bgReadings.last, + Double(latestBG.sgv) <= minBG { return false } + + // ──────────────────────────────── + // 2. already alerted for this carb? + // ──────────────────────────────── + if let lastFired = Storage.shared.lastMissedBolusNotified.value, + carb.date <= lastFired { return false } + + // ──────────────────────────────── + // 3. look for a valid bolus + // ──────────────────────────────── + let windowStart = carb.date.addingTimeInterval(-Double(prebolusMin) * 60) + + let hasBolus = data.recentBoluses.contains { bolus in + bolus.date >= windowStart && bolus.units > minBolusU + } + guard !hasBolus else { return false } + + // ──────────────────────────────── + // 4. trigger! + // ──────────────────────────────── + Storage.shared.lastMissedBolusNotified.value = carb.date + return true + } +} diff --git a/LoopFollow/Alarm/AlarmData.swift b/LoopFollow/Alarm/AlarmData.swift index f7c9a5ee0..b10fa64ce 100644 --- a/LoopFollow/Alarm/AlarmData.swift +++ b/LoopFollow/Alarm/AlarmData.swift @@ -22,8 +22,5 @@ struct AlarmData: Codable { let recentBoluses: [BolusEntry] let latestBattery: Double? let batteryHistory: [DataStructs.batteryStruct] + let recentCarbs: [CarbSample] } - -/* - // let latestCarbs: [CarbEntry] - */ diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index 68e1d6016..f5bda1958 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -78,10 +78,11 @@ struct AlarmEditor: View { BatteryAlarmEditor(alarm: $alarm) case .batteryDrop: BatteryDropAlarmEditor(alarm: $alarm) - /* TODO: add other condition types here */ - default: - Text("No editor for \(alarm.type.rawValue)") - .padding() + case .missedBolus: + MissedBolusAlarmEditor(alarm: $alarm) + case .bolus: + // TODO: + MissedBolusAlarmEditor(alarm: $alarm) } } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift new file mode 100644 index 000000000..cdae40e7f --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift @@ -0,0 +1,86 @@ +// LoopFollow +// MissedBolusAlarmEditor.swift +// Created by Jonas Björkert on 2025-05-20. + +import SwiftUI + +struct MissedBolusAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Form { + InfoBanner( + text: "Alerts when carbs are logged but no bolus is delivered " + + "within the delay below. Allows small-carb / treatment " + + "exclusions and pre-bolus detection.", + alarmType: alarm.type + ) + + AlarmGeneralSection(alarm: $alarm) + + AlarmStepperSection( + header: "Delay", + footer: "Minutes to wait after the carb entry before checking " + + "for a bolus.", + title: "Minutes", + range: 5 ... 60, + step: 5, + value: Binding( + get: { Double(alarm.monitoringWindow ?? 15) }, + set: { alarm.monitoringWindow = Int($0) } + ) + ) + + AlarmStepperSection( + header: "Pre-bolus", + footer: "Count boluses given up to this many minutes *before* " + + "the carb entry as valid.", + title: "Minutes", + range: 0 ... 45, + step: 5, + value: Binding( + get: { Double(alarm.predictiveMinutes ?? 15) }, + set: { alarm.predictiveMinutes = Int($0) } + ) + ) + + AlarmStepperSection( + header: "Ignore small boluses", + footer: "Boluses ≤ this size are ignored.", + title: "Units", + range: 0.05 ... 2, + step: 0.05, + value: Binding( + get: { alarm.delta ?? 0.1 }, + set: { alarm.delta = $0 } + ) + ) + + AlarmStepperSection( + header: "Ignore small carbs", + footer: "Carb entries ≤ this amount will not trigger the alarm.", + title: "Grams", + range: 0 ... 15, + step: 1, + value: Binding( + get: { alarm.threshold ?? 4 }, + set: { alarm.threshold = $0 } + ) + ) + + AlarmBGLimitSection( + header: "Ignore low BG", + footer: "Only alert if the current BG is *above* this value.", + toggleText: "Use BG Limit", + pickerTitle: "Alert when BG ≥", + range: 40 ... 140, + value: $alarm.aboveBG + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm, range: 5 ... 60, step: 5) + } + .navigationTitle(alarm.type.rawValue) + } +} diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index b370aed06..94efc6741 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -33,7 +33,6 @@ class AlarmManager { IOBCondition.self, BatteryCondition.self, BatteryDropCondition.self, - // TODO: add other condition types here ] ) { var dict = [AlarmType: AlarmCondition]() diff --git a/LoopFollow/Alarm/DataStructs/CarbSample.swift b/LoopFollow/Alarm/DataStructs/CarbSample.swift new file mode 100644 index 000000000..383d4801b --- /dev/null +++ b/LoopFollow/Alarm/DataStructs/CarbSample.swift @@ -0,0 +1,10 @@ +// LoopFollow +// CarbSample.swift +// Created by Jonas Björkert on 2025-05-20. + +import Foundation + +public struct CarbSample: Codable { + public let grams: Double + public let date: Date +} diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index c77b3d5fc..b9586cd6f 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -55,6 +55,7 @@ class Storage { var lastTempTargetEndNotified = StorageValue(key: "lastTempTargetEndNotified", defaultValue: nil) var lastRecBolusNotified = StorageValue(key: "lastRecBolusNotified", defaultValue: nil) var lastCOBNotified = StorageValue(key: "lastCOBNotified", defaultValue: nil) + var lastMissedBolusNotified = StorageValue(key: "lastMissedBolusNotified", defaultValue: nil) static let shared = Storage() private init() {} diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index 0dc3fc88d..5eadb9bad 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -26,6 +26,7 @@ extension MainViewController { let latestPumpVol = self.latestPumpVolume let bolusEntries = self.bolusData.map { BolusEntry(units: $0.value, date: Date(timeIntervalSince1970: $0.date)) } let latestBattery = UserDefaultsRepository.deviceBatteryLevel.value + let recentCarbs: [CarbSample] = self.carbData.map { CarbSample(grams: $0.value, date: Date(timeIntervalSince1970: $0.date)) } let alarmData = AlarmData( bgReadings: self.bgData @@ -48,7 +49,8 @@ extension MainViewController { IOB: self.latestIOB?.value, recentBoluses: bolusEntries, latestBattery: latestBattery, - batteryHistory: self.deviceBatteryData + batteryHistory: self.deviceBatteryData, + recentCarbs: recentCarbs ) let finalAlarmData: AlarmData From 64b545a0d53fd797bb2c1902b1444a23c8fc1506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 20 May 2025 15:52:30 +0200 Subject: [PATCH 076/138] Cleanup --- LoopFollow/Alarm/Alarm.swift | 7 ++----- LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift | 3 --- LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift | 2 +- LoopFollow/Alarm/AlarmType/AlarmType.swift | 1 - 4 files changed, 3 insertions(+), 10 deletions(-) diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index 002642b90..d11b00f61 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -228,8 +228,6 @@ struct Alarm: Identifiable, Codable, Equatable { soundFile = .indeed case .iob: soundFile = .alertToneRingtone1 - case .bolus: - soundFile = .dholShuffleloop case .cob: soundFile = .alertToneRingtone2 case .high: @@ -286,7 +284,7 @@ extension AlarmType { switch self { case .low, .high, .fastDrop, .fastRise, .missedReading, .temporary: return .glucose - case .iob, .bolus, .cob, .missedBolus, .recBolus: + case .iob, .cob, .missedBolus, .recBolus: return .insulin case .battery, .batteryDrop, .pump, .pumpChange, .sensorChange, .notLooping, .buildExpire: @@ -303,7 +301,7 @@ extension AlarmType { case .fastDrop: return "chevron.down.2" case .fastRise: return "chevron.up.2" case .missedReading: return "wifi.slash" - case .iob, .bolus: return "syringe" + case .iob: return "syringe" case .cob: return "fork.knife" case .missedBolus: return "exclamationmark.arrow.triangle.2.circlepath" case .recBolus: return "bolt.horizontal" @@ -330,7 +328,6 @@ extension AlarmType { case .fastRise: return "Rapid upward BG trend." case .missedReading: return "No CGM data for X minutes." case .iob: return "High insulin-on-board." - case .bolus: return "Large individual bolus." case .cob: return "High carbs-on-board." case .missedBolus: return "Carbs without bolus." case .recBolus: return "Recommended bolus issued." diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index f5bda1958..6d16555f1 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -80,9 +80,6 @@ struct AlarmEditor: View { BatteryDropAlarmEditor(alarm: $alarm) case .missedBolus: MissedBolusAlarmEditor(alarm: $alarm) - case .bolus: - // TODO: - MissedBolusAlarmEditor(alarm: $alarm) } } } diff --git a/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift b/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift index 06b75be13..20ee69084 100644 --- a/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift +++ b/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift @@ -12,7 +12,7 @@ extension AlarmType { return .day case .low, .high, .fastDrop, .fastRise, .missedReading, .notLooping, .missedBolus, - .bolus, .recBolus, + .recBolus, .overrideStart, .overrideEnd, .tempTargetStart, .tempTargetEnd, .temporary: return .minute diff --git a/LoopFollow/Alarm/AlarmType/AlarmType.swift b/LoopFollow/Alarm/AlarmType/AlarmType.swift index 6b0b58c1d..2fd952a22 100644 --- a/LoopFollow/Alarm/AlarmType/AlarmType.swift +++ b/LoopFollow/Alarm/AlarmType/AlarmType.swift @@ -9,7 +9,6 @@ import Foundation enum AlarmType: String, CaseIterable, Codable { case temporary = "Temporary Alert" case iob = "IOB Alert" - case bolus = "Bolus Alert" case cob = "COB Alert" case low = "Low BG Alert" case high = "High BG Alert" From c3b7925a5f60d772b229fb6f72563057725429d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 20 May 2025 20:02:49 +0200 Subject: [PATCH 077/138] Text adjustments --- .../Editors/BatteryAlarmEditor.swift | 11 +++---- .../Editors/BatteryDropAlarmEditor.swift | 14 ++++----- .../Editors/BuildExpireAlarmEditor.swift | 5 ++-- .../AlarmEditing/Editors/COBAlarmEditor.swift | 6 ++-- .../Editors/FastDropAlarmEditor.swift | 6 ++-- .../Editors/FastRiseAlarmEditor.swift | 4 +-- .../Editors/HighBgAlarmEditor.swift | 6 ++-- .../AlarmEditing/Editors/IOBAlarmEditor.swift | 29 +++++++------------ .../Editors/LowBgAlarmEditor.swift | 7 ++--- .../Editors/MissedBolusAlarmEditor.swift | 22 ++++++++------ .../Editors/MissedReadingEditor.swift | 3 +- .../Editors/PumpChangeAlarmEditor.swift | 18 ++++-------- .../Editors/PumpVolumeAlarmEditor.swift | 13 ++------- .../Editors/RecBolusAlarmEditor.swift | 5 ++-- .../Editors/SensorAgeAlarmEditor.swift | 4 +-- .../Editors/TemporaryAlarmEditor.swift | 4 +-- 16 files changed, 69 insertions(+), 88 deletions(-) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift index abc0057d3..5bbb480de 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift @@ -10,8 +10,7 @@ struct BatteryAlarmEditor: View { var body: some View { Form { InfoBanner( - text: "Alerts when the phone battery drops below the " - + "percentage you set below.", + text: "This warns you when the phone’s battery gets low, based on the percentage you choose.", alarmType: alarm.type ) @@ -19,8 +18,8 @@ struct BatteryAlarmEditor: View { AlarmStepperSection( header: "Battery Level", - footer: "Alert when remaining charge is equal to or below this.", - title: "Level ≤", + footer: "This alerts you when the battery drops below this level.", + title: "Battery Below", range: 0 ... 100, step: 5, unitLabel: "%", @@ -32,9 +31,7 @@ struct BatteryAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm) - AlarmSnoozeSection(alarm: $alarm, - range: 1 ... 24, - step: 1) + AlarmSnoozeSection(alarm: $alarm, range: 1 ... 24, step: 1) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift index e47424163..f55846f5b 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift @@ -10,17 +10,16 @@ struct BatteryDropAlarmEditor: View { var body: some View { Form { InfoBanner( - text: "Alerts when the phone battery falls by a specified " - + "percentage within a set time window.", + text: "This warns you if your phone’s battery drops quickly, based on the percentage and time you set.", alarmType: alarm.type ) AlarmGeneralSection(alarm: $alarm) AlarmStepperSection( - header: "Drop Amount", - footer: "Trigger when charge falls by at least this much.", - title: "Δ %", + header: "Battery Drop", + footer: "This alerts you if the battery drops by this much or more.", + title: "Drop Amount", range: 5 ... 100, step: 5, unitLabel: "%", @@ -31,11 +30,12 @@ struct BatteryDropAlarmEditor: View { ) AlarmStepperSection( - header: "Time Window", + header: "Over This Time", footer: "How far back to look for that drop.", - title: "Minutes", + title: "Time Window", range: 5 ... 30, step: 5, + unitLabel: "min", value: Binding( get: { Double(alarm.monitoringWindow ?? 15) }, set: { alarm.monitoringWindow = Int($0) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift index 9ad71c1a9..14c5d51e0 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift @@ -11,13 +11,14 @@ struct BuildExpireAlarmEditor: View { Form { 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." + + "TestFlight or Xcode expiry date. Works with Trio 0.4 and later." ) AlarmGeneralSection(alarm: $alarm) AlarmStepperSection( + header: "Notice Period", footer: "Choose how many days of notice you’d like before the build becomes unusable.", - title: "Expires In", + title: "Days of notice", range: 1 ... 14, step: 1, unitLabel: alarm.type.snoozeTimeUnit.label, diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift index 87f06cc8c..55f34235a 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift @@ -17,9 +17,9 @@ struct COBAlarmEditor: View { AlarmGeneralSection(alarm: $alarm) AlarmStepperSection( - header: "Threshold", - footer: "Alert when COB ≥ this many grams.", - title: "COB", + header: "Carbs on Board Limit", + footer: "Alert when carbs-on-board is above this number.", + title: "Above", range: 1 ... 200, step: 1, unitLabel: "g", diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift index d4bc03e0e..514500985 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift @@ -18,8 +18,8 @@ struct FastDropAlarmEditor: View { AlarmBGSection( header: "Rate of Fall", - footer: "How much the bg must fall to count as a “fast” drop.", - title: "Drop per reading", + footer: "This is how much the glucose must drop to be considered a fast drop.", + title: "Falls by", range: 3 ... 20, value: Binding( get: { alarm.delta ?? 18 }, @@ -31,7 +31,7 @@ struct FastDropAlarmEditor: View { AlarmStepperSection( header: "Consecutive Drops", footer: "Number of drops—each meeting the rate above—required before an alert fires.", - title: "Drops in a row", + title: "Number of Drops", range: 1 ... 3, step: 1, value: Binding( diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift index 04833262e..fc127419d 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift @@ -20,8 +20,8 @@ struct FastRiseAlarmEditor: View { AlarmBGSection( header: "Rate of Rise", - footer: "How much the BG must rise to count as a “fast” rise.", - title: "Rise per reading", + footer: "This is how much the glucose must rise to be considered a fast rise.", + title: "Rises by", range: 3 ... 20, value: Binding( get: { alarm.delta ?? 3 }, diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift index 0fbcfb47c..8cc3c7b30 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift @@ -10,15 +10,15 @@ struct HighBgAlarmEditor: View { var body: some View { Form { InfoBanner( - text: "Alerts when your CGM glucose stays above the limit " + text: "Alerts when 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.", + header: "High Glucose Limit", + footer: "The alert becomes eligible once any reading is at or above this value.", title: "BG", range: 120 ... 350, value: Binding( diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift index 4b5a0d7d8..2c9677fa6 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift @@ -2,13 +2,6 @@ // IOBAlarmEditor.swift // Created by Jonas Björkert on 2025-05-19. -// -// IOBAlarmEditor.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-17. -// - import SwiftUI struct IOBAlarmEditor: View { @@ -24,52 +17,52 @@ struct IOBAlarmEditor: View { AlarmGeneralSection(alarm: $alarm) - // ── individual bolus size ── AlarmStepperSection( - header: "Bolus Size", - footer: "Only boluses equal to or larger than this are counted.", - title: "Bolus ≥", + header: "Boluses Size Limit", + footer: "This counts only boluses larger than this size.", + title: "Above", range: 0.1 ... 20, step: 0.1, + unitLabel: "Units", value: Binding( get: { alarm.delta ?? 1.0 }, set: { alarm.delta = $0 } ) ) - // ── number of boluses ── AlarmStepperSection( header: "Bolus Count", footer: "Number of qualifying boluses needed to trigger.", - title: "Count ≥", + title: "Count", range: 1 ... 10, step: 1, + unitLabel: "Boluses", value: Binding( get: { Double(alarm.monitoringWindow ?? 2) }, set: { alarm.monitoringWindow = Int($0) } ) ) - // ── look-back window ── AlarmStepperSection( header: "Time Window", footer: "How far back to look for those boluses.", - title: "Minutes", + title: "Time", range: 5 ... 120, step: 5, + unitLabel: "min", value: Binding( get: { Double(alarm.predictiveMinutes ?? 30) }, set: { alarm.predictiveMinutes = Int($0) } ) ) - // ── absolute IOB limit ── AlarmStepperSection( - header: "Total IOB", + header: "Insulin On Board", footer: "Alert if current IOB or total boluses reach this.", - title: "IOB ≥", + title: "IOB Above", range: 1 ... 20, step: 0.5, + unitLabel: "Units", value: Binding( get: { alarm.threshold ?? 6 }, set: { alarm.threshold = $0 } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift index dec295a94..2d79f61f8 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift @@ -9,14 +9,13 @@ 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.") + InfoBanner(text: "This warns you if the glucose is too low now or might be soon, based on predictions. Note: predictions is currently not available for Trio.") AlarmGeneralSection(alarm: $alarm) AlarmBGSection( - header: "Threshold", + header: "Low Limit", + footer: "Alert when any reading or prediction is at or below this value.", title: "BG", range: 40 ... 150, value: Binding( diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift index cdae40e7f..e7b0ce1c2 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift @@ -22,9 +22,10 @@ struct MissedBolusAlarmEditor: View { header: "Delay", footer: "Minutes to wait after the carb entry before checking " + "for a bolus.", - title: "Minutes", + title: "Delay", range: 5 ... 60, step: 5, + unitLabel: "min", value: Binding( get: { Double(alarm.monitoringWindow ?? 15) }, set: { alarm.monitoringWindow = Int($0) } @@ -33,11 +34,12 @@ struct MissedBolusAlarmEditor: View { AlarmStepperSection( header: "Pre-bolus", - footer: "Count boluses given up to this many minutes *before* " + + footer: "Count boluses given up to this many minutes before " + "the carb entry as valid.", - title: "Minutes", + title: "Pre-Bolus Time", range: 0 ... 45, step: 5, + unitLabel: "min", value: Binding( get: { Double(alarm.predictiveMinutes ?? 15) }, set: { alarm.predictiveMinutes = Int($0) } @@ -46,10 +48,11 @@ struct MissedBolusAlarmEditor: View { AlarmStepperSection( header: "Ignore small boluses", - footer: "Boluses ≤ this size are ignored.", - title: "Units", + footer: "Boluses below this size are ignored.", + title: "Ignore below", range: 0.05 ... 2, step: 0.05, + unitLabel: "Units", value: Binding( get: { alarm.delta ?? 0.1 }, set: { alarm.delta = $0 } @@ -58,10 +61,11 @@ struct MissedBolusAlarmEditor: View { AlarmStepperSection( header: "Ignore small carbs", - footer: "Carb entries ≤ this amount will not trigger the alarm.", - title: "Grams", + footer: "Carb entries below this amount will not trigger the alarm.", + title: "Below", range: 0 ... 15, step: 1, + unitLabel: "Grams", value: Binding( get: { alarm.threshold ?? 4 }, set: { alarm.threshold = $0 } @@ -70,9 +74,9 @@ struct MissedBolusAlarmEditor: View { AlarmBGLimitSection( header: "Ignore low BG", - footer: "Only alert if the current BG is *above* this value.", + footer: "Only alert if the current BG is above this value.", toggleText: "Use BG Limit", - pickerTitle: "Alert when BG ≥", + pickerTitle: "Above", range: 40 ... 140, value: $alarm.aboveBG ) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift index 7052355cb..b85638c16 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift @@ -9,11 +9,12 @@ struct MissedReadingEditor: View { var body: some View { Form { - InfoBanner(text: "The app notifies you when no CGM reading has been received for the time you choose below.", alarmType: alarm.type) + InfoBanner(text: "This warns you if the glucose monitor stops sending readings for too long..", alarmType: alarm.type) AlarmGeneralSection(alarm: $alarm) AlarmStepperSection( + header: "Reading Delay", footer: "Choose how long the app should wait before alerting.", title: "No reading for", range: 11 ... 121, diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift index 296b2eb99..2b305e977 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift @@ -2,13 +2,6 @@ // PumpChangeAlarmEditor.swift // Created by Jonas Björkert on 2025-05-17. -// -// PumpChangeAlarmEditor.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-17. -// - import SwiftUI struct PumpChangeAlarmEditor: View { @@ -17,9 +10,9 @@ struct PumpChangeAlarmEditor: View { var body: some View { Form { InfoBanner( - text: "Alerts once when your pump / cannula is within the time " + text: "Alerts when the pump / cannula is within the time " + "window you choose below (relative to the 3-day change " - + "limit). After it fires once it disables itself.", + + "limit).", alarmType: alarm.type ) @@ -27,12 +20,11 @@ struct PumpChangeAlarmEditor: View { AlarmStepperSection( header: "Advance Notice", - footer: "How many hours before the 3-day limit the alert " - + "should fire. Set to 12 hours, for example, to get a " - + "reminder half a day in advance.", - title: "Warn hours", + footer: "How many hours before the 3-day limit you’d like a reminder.", + title: "Warning Time", range: 1 ... 24, step: 1, + unitLabel: "Hours", value: Binding( get: { alarm.threshold ?? 12 }, set: { alarm.threshold = $0 } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift index 68a1b90f9..ea5d40dc6 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift @@ -2,13 +2,6 @@ // PumpVolumeAlarmEditor.swift // Created by Jonas Björkert on 2025-05-17. -// -// PumpVolumeAlarmEditor.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-17. -// - import SwiftUI struct PumpVolumeAlarmEditor: View { @@ -17,8 +10,7 @@ struct PumpVolumeAlarmEditor: View { var body: some View { Form { InfoBanner( - text: "Alerts when the pump reservoir falls to or below the " - + "unit level you set below.", + text: "This warns you when the insulin pump is running low on insulin.", alarmType: alarm.type ) @@ -28,9 +20,10 @@ struct PumpVolumeAlarmEditor: View { header: "Trigger Level", footer: "An alert fires once the reservoir is at this value " + "or lower.", - title: "Units ≤", + title: "Reservoir Below", range: 1 ... 50, step: 1, + unitLabel: "Units", value: Binding( get: { alarm.threshold ?? 20 }, set: { alarm.threshold = $0 } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift index dc4366360..996ae9301 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift @@ -19,10 +19,11 @@ struct RecBolusAlarmEditor: View { AlarmStepperSection( header: "Threshold", - footer: "Alert when recommended bolus ≥ this value.", - title: "Units", + footer: "Alert when recommended bolus is above this value.", + title: "More than", range: 0.1 ... 50, step: 0.1, + unitLabel: "Units", value: Binding( get: { alarm.threshold ?? 1.0 }, set: { alarm.threshold = $0 } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift index a55514085..1b516a37b 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift @@ -17,10 +17,10 @@ struct SensorAgeAlarmEditor: View { AlarmGeneralSection(alarm: $alarm) AlarmStepperSection( - header: "Advance warning", + header: "Early Reminder", footer: "Number of hours before the 10-day mark that the alert " + "will fire.", - title: "Hours", + title: "Reminder Time", range: 1 ... 24, step: 1, unitLabel: "hours", diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift index a54721edf..9de2b03b2 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift @@ -23,7 +23,7 @@ struct TemporaryAlarmEditor: View { header: "Low Limit", footer: "Alert if BG is equal to or below this value.", toggleText: "Enable low limit", - pickerTitle: "≤ BG", + pickerTitle: "Below", range: bgRange, value: $alarm.belowBG ) @@ -32,7 +32,7 @@ struct TemporaryAlarmEditor: View { header: "High Limit", footer: "Alert if BG is equal to or above this value.", toggleText: "Enable high limit", - pickerTitle: "≥ BG", + pickerTitle: "Above", range: bgRange, value: $alarm.aboveBG ) From 0e203dd771eb16779b97384f648d134f7b8022da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 20 May 2025 20:41:51 +0200 Subject: [PATCH 078/138] dark theme and small adjustments --- LoopFollow/Alarm/AlarmCondition/OverrideEndCondition.swift | 4 ++-- LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift | 4 ++-- LoopFollow/Alarm/AlarmCondition/TempTargetEndCondition.swift | 4 ++-- .../Alarm/AlarmCondition/TempTargetStartCondition.swift | 4 ++-- LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift | 1 + LoopFollow/Alarm/AlarmListView.swift | 1 + 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/LoopFollow/Alarm/AlarmCondition/OverrideEndCondition.swift b/LoopFollow/Alarm/AlarmCondition/OverrideEndCondition.swift index a3832da7b..ebc463c32 100644 --- a/LoopFollow/Alarm/AlarmCondition/OverrideEndCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/OverrideEndCondition.swift @@ -8,9 +8,9 @@ struct OverrideEndCondition: AlarmCondition { static let type: AlarmType = .overrideEnd init() {} - func evaluate(alarm _: Alarm, data: AlarmData, now _: Date) -> Bool { + func evaluate(alarm _: Alarm, data: AlarmData, now: Date) -> Bool { guard let endTS = data.latestOverrideEnd, endTS > 0 else { return false } - guard Date().timeIntervalSince1970 - endTS <= 15 * 60 else { return false } + guard now.timeIntervalSince1970 - endTS <= 15 * 60 else { return false } let last = Storage.shared.lastOverrideEndNotified.value ?? 0 guard endTS > last else { return false } diff --git a/LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift b/LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift index e3ea27d9a..fd8e4dd24 100644 --- a/LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift @@ -8,10 +8,10 @@ struct OverrideStartCondition: AlarmCondition { static let type: AlarmType = .overrideStart init() {} - func evaluate(alarm _: Alarm, data: AlarmData, now _: Date) -> Bool { + func evaluate(alarm _: Alarm, data: AlarmData, now: Date) -> Bool { guard let startTS = data.latestOverrideStart, startTS > 0 else { return false } - let recent = Date().timeIntervalSince1970 - startTS <= 15 * 60 + let recent = now.timeIntervalSince1970 - startTS <= 15 * 60 guard recent else { return false } let lastNotified = Storage.shared.lastOverrideStartNotified.value ?? 0 diff --git a/LoopFollow/Alarm/AlarmCondition/TempTargetEndCondition.swift b/LoopFollow/Alarm/AlarmCondition/TempTargetEndCondition.swift index be5828ea9..6ec434483 100644 --- a/LoopFollow/Alarm/AlarmCondition/TempTargetEndCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/TempTargetEndCondition.swift @@ -9,9 +9,9 @@ struct TempTargetEndCondition: AlarmCondition { static let type: AlarmType = .tempTargetEnd init() {} - func evaluate(alarm _: Alarm, data: AlarmData, now _: Date) -> Bool { + func evaluate(alarm _: Alarm, data: AlarmData, now: Date) -> Bool { guard let endTS = data.latestTempTargetEnd, endTS > 0 else { return false } - guard Date().timeIntervalSince1970 - endTS <= 15 * 60 else { return false } + guard now.timeIntervalSince1970 - endTS <= 15 * 60 else { return false } let last = Storage.shared.lastTempTargetEndNotified.value ?? 0 guard endTS > last else { return false } diff --git a/LoopFollow/Alarm/AlarmCondition/TempTargetStartCondition.swift b/LoopFollow/Alarm/AlarmCondition/TempTargetStartCondition.swift index 23afb6324..153190225 100644 --- a/LoopFollow/Alarm/AlarmCondition/TempTargetStartCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/TempTargetStartCondition.swift @@ -9,9 +9,9 @@ struct TempTargetStartCondition: AlarmCondition { static let type: AlarmType = .tempTargetStart init() {} - func evaluate(alarm _: Alarm, data: AlarmData, now _: Date) -> Bool { + func evaluate(alarm _: Alarm, data: AlarmData, now: Date) -> Bool { guard let startTS = data.latestTempTargetStart, startTS > 0 else { return false } - guard Date().timeIntervalSince1970 - startTS <= 15 * 60 else { return false } + guard now.timeIntervalSince1970 - startTS <= 15 * 60 else { return false } let last = Storage.shared.lastTempTargetStartNotified.value ?? 0 guard startTS > last else { return false } diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index 6d16555f1..2b3ed7518 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -33,6 +33,7 @@ struct AlarmEditor: View { } } } + .preferredColorScheme(UserDefaultsRepository.forceDarkMode.value ? .dark : nil) } @ViewBuilder diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index d8cab080d..68861824e 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -41,6 +41,7 @@ struct AddAlarmSheet: View { } } } + .preferredColorScheme(UserDefaultsRepository.forceDarkMode.value ? .dark : nil) } } From 58d792681a949d4fe7d5ce23ab360bc7746d819a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 23 May 2025 10:58:21 +0200 Subject: [PATCH 079/138] Unit tests --- LoopFollow.xcodeproj/project.pbxproj | 84 +++++++++---------- .../xcschemes/LoopFollow.xcscheme | 13 ++- LoopFollowTests/AlwaysTrueCondition.swift | 6 -- .../BuildExpireConditionTests.swift | 47 ----------- .../BatteryConditionTests.swift | 38 +++++++++ Tests/AlarmConditions/Helpers.swift | 48 +++++++++++ Tests/Tests.swift | 11 +++ 7 files changed, 148 insertions(+), 99 deletions(-) delete mode 100644 LoopFollowTests/AlwaysTrueCondition.swift delete mode 100644 LoopFollowTests/BuildExpireConditionTests.swift create mode 100644 Tests/AlarmConditions/BatteryConditionTests.swift create mode 100644 Tests/AlarmConditions/Helpers.swift create mode 100644 Tests/Tests.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index b6def6eed..d7458dd61 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ @@ -67,8 +67,6 @@ DD493AE92ACF2445009A6922 /* BGData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD493AE82ACF2445009A6922 /* BGData.swift */; }; DD4AFB3B2DB55CB600BB593F /* TimeOfDay.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB3A2DB55CB600BB593F /* TimeOfDay.swift */; }; DD4AFB3D2DB55D2900BB593F /* AlarmConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB3C2DB55D2900BB593F /* AlarmConfiguration.swift */; }; - DD4AFB422DB5655700BB593F /* BuildExpireConditionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB412DB5652400BB593F /* BuildExpireConditionTests.swift */; }; - DD4AFB432DB5655D00BB593F /* AlwaysTrueCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB402DB5651500BB593F /* AlwaysTrueCondition.swift */; }; DD4AFB492DB576C200BB593F /* AlarmSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB482DB576C200BB593F /* AlarmSettingsView.swift */; }; DD4AFB612DB68BBC00BB593F /* AlarmListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB602DB68BBC00BB593F /* AlarmListView.swift */; }; DD4AFB672DB68C5500BB593F /* UUID+Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4AFB662DB68C5500BB593F /* UUID+Identifiable.swift */; }; @@ -357,7 +355,7 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - DD0247652DB2EB9A00FCADF6 /* PBXContainerItemProxy */ = { + DDCC3ADA2DDE1790006F1C10 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = FC97880C2485969B00A7906C /* Project object */; proxyType = 1; @@ -371,7 +369,6 @@ A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LoopFollow.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmCondition.swift; sourceTree = ""; }; DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildExpireCondition.swift; sourceTree = ""; }; - DD0247612DB2EB9A00FCADF6 /* LoopFollowTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LoopFollowTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DD0650A82DCA8A10004D3B41 /* AlarmBGSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmBGSection.swift; sourceTree = ""; }; DD0650EA2DCE8385004D3B41 /* LowBGCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LowBGCondition.swift; sourceTree = ""; }; DD0650EC2DCE9371004D3B41 /* HighBgAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighBgAlarmEditor.swift; sourceTree = ""; }; @@ -428,8 +425,6 @@ DD493AE82ACF2445009A6922 /* BGData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGData.swift; sourceTree = ""; }; DD4AFB3A2DB55CB600BB593F /* TimeOfDay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeOfDay.swift; sourceTree = ""; }; DD4AFB3C2DB55D2900BB593F /* AlarmConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmConfiguration.swift; sourceTree = ""; }; - DD4AFB402DB5651500BB593F /* AlwaysTrueCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlwaysTrueCondition.swift; sourceTree = ""; }; - DD4AFB412DB5652400BB593F /* BuildExpireConditionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildExpireConditionTests.swift; sourceTree = ""; }; DD4AFB482DB576C200BB593F /* AlarmSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSettingsView.swift; sourceTree = ""; }; DD4AFB602DB68BBC00BB593F /* AlarmListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmListView.swift; sourceTree = ""; }; DD4AFB662DB68C5500BB593F /* UUID+Identifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UUID+Identifiable.swift"; sourceTree = ""; }; @@ -530,6 +525,8 @@ DDCC3A552DDC9617006F1C10 /* MissedBolusCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissedBolusCondition.swift; sourceTree = ""; }; DDCC3A572DDC9655006F1C10 /* MissedBolusAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissedBolusAlarmEditor.swift; sourceTree = ""; }; DDCC3A592DDC988F006F1C10 /* CarbSample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbSample.swift; sourceTree = ""; }; + 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 = ""; }; DDCF979524C1443C002C9752 /* GeneralSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsViewController.swift; sourceTree = ""; }; DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphSettingsViewController.swift; sourceTree = ""; }; @@ -723,8 +720,12 @@ FCFEECA1248857A600402A7F /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + DDCC3AD72DDE1790006F1C10 /* Tests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Tests; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ - DD02475E2DB2EB9A00FCADF6 /* Frameworks */ = { + DDCC3AD32DDE1790006F1C10 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( @@ -747,6 +748,7 @@ 6A5880E0B811AF443B05AB02 /* Frameworks */ = { isa = PBXGroup; children = ( + DDCC3ABF2DDE10B0006F1C10 /* Testing.framework */, FCFEEC9D2486E68E00402A7F /* WebKit.framework */, A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */, FCE537C2249AAB2600F80BF8 /* NotificationCenter.framework */, @@ -792,15 +794,6 @@ path = AlarmCondition; sourceTree = ""; }; - DD02476E2DB4321000FCADF6 /* LoopFollowTests */ = { - isa = PBXGroup; - children = ( - DD4AFB412DB5652400BB593F /* BuildExpireConditionTests.swift */, - DD4AFB402DB5651500BB593F /* AlwaysTrueCondition.swift */, - ); - path = LoopFollowTests; - sourceTree = ""; - }; DD0C0C692C4852A100DBADDF /* Metric */ = { isa = PBXGroup; children = ( @@ -1393,7 +1386,7 @@ FC3AE7B3249E8E0E00AAE1E0 /* LoopFollow.xcdatamodeld */, FC5A5C3C2497B229009C550E /* Config.xcconfig */, FC8DEEE32485D1680075863F /* LoopFollow */, - DD02476E2DB4321000FCADF6 /* LoopFollowTests */, + DDCC3AD72DDE1790006F1C10 /* Tests */, FC9788152485969B00A7906C /* Products */, 8E32230C453C93FDCE59C2B9 /* Pods */, 6A5880E0B811AF443B05AB02 /* Frameworks */, @@ -1404,7 +1397,7 @@ isa = PBXGroup; children = ( FC9788142485969B00A7906C /* Loop Follow.app */, - DD0247612DB2EB9A00FCADF6 /* LoopFollowTests.xctest */, + DDCC3AD62DDE1790006F1C10 /* Tests.xctest */, ); name = Products; sourceTree = ""; @@ -1467,24 +1460,27 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - DD0247602DB2EB9A00FCADF6 /* LoopFollowTests */ = { + DDCC3AD52DDE1790006F1C10 /* Tests */ = { isa = PBXNativeTarget; - buildConfigurationList = DD0247672DB2EB9A00FCADF6 /* Build configuration list for PBXNativeTarget "LoopFollowTests" */; + buildConfigurationList = DDCC3ADC2DDE1790006F1C10 /* Build configuration list for PBXNativeTarget "Tests" */; buildPhases = ( - DD02475D2DB2EB9A00FCADF6 /* Sources */, - DD02475E2DB2EB9A00FCADF6 /* Frameworks */, - DD02475F2DB2EB9A00FCADF6 /* Resources */, + DDCC3AD22DDE1790006F1C10 /* Sources */, + DDCC3AD32DDE1790006F1C10 /* Frameworks */, + DDCC3AD42DDE1790006F1C10 /* Resources */, ); buildRules = ( ); dependencies = ( - DD0247662DB2EB9A00FCADF6 /* PBXTargetDependency */, + DDCC3ADB2DDE1790006F1C10 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + DDCC3AD72DDE1790006F1C10 /* Tests */, ); - name = LoopFollowTests; + name = Tests; packageProductDependencies = ( ); - productName = LoopFollowTests; - productReference = DD0247612DB2EB9A00FCADF6 /* LoopFollowTests.xctest */; + productName = Tests; + productReference = DDCC3AD62DDE1790006F1C10 /* Tests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; FC9788132485969B00A7906C /* LoopFollow */ = { @@ -1521,7 +1517,7 @@ LastUpgradeCheck = 1140; ORGANIZATIONNAME = "Jon Fawcett"; TargetAttributes = { - DD0247602DB2EB9A00FCADF6 = { + DDCC3AD52DDE1790006F1C10 = { CreatedOnToolsVersion = 16.3; TestTargetID = FC9788132485969B00A7906C; }; @@ -1547,13 +1543,13 @@ projectRoot = ""; targets = ( FC9788132485969B00A7906C /* LoopFollow */, - DD0247602DB2EB9A00FCADF6 /* LoopFollowTests */, + DDCC3AD52DDE1790006F1C10 /* Tests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - DD02475F2DB2EB9A00FCADF6 /* Resources */ = { + DDCC3AD42DDE1790006F1C10 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( @@ -1776,12 +1772,10 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - DD02475D2DB2EB9A00FCADF6 /* Sources */ = { + DDCC3AD22DDE1790006F1C10 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DD4AFB432DB5655D00BB593F /* AlwaysTrueCondition.swift in Sources */, - DD4AFB422DB5655700BB593F /* BuildExpireConditionTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2014,10 +2008,10 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - DD0247662DB2EB9A00FCADF6 /* PBXTargetDependency */ = { + DDCC3ADB2DDE1790006F1C10 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = FC9788132485969B00A7906C /* LoopFollow */; - targetProxy = DD0247652DB2EB9A00FCADF6 /* PBXContainerItemProxy */; + targetProxy = DDCC3ADA2DDE1790006F1C10 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ @@ -2041,7 +2035,7 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - DD0247682DB2EB9A00FCADF6 /* Debug */ = { + DDCC3ADD2DDE1790006F1C10 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -2057,8 +2051,8 @@ IPHONEOS_DEPLOYMENT_TARGET = 18.4; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.--unique-id-.LoopFollowTests--app-suffix-.LoopFollowTests"; - PRODUCT_MODULE_NAME = LoopFollowTests; + PRODUCT_BUNDLE_IDENTIFIER = "com.--unique-id-.LoopFollowTests--app-suffix-.Tests"; + PRODUCT_MODULE_NAME = Tests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = NO; @@ -2068,7 +2062,7 @@ }; name = Debug; }; - DD0247692DB2EB9A00FCADF6 /* Release */ = { + DDCC3ADE2DDE1790006F1C10 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -2084,8 +2078,8 @@ IPHONEOS_DEPLOYMENT_TARGET = 18.4; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.--unique-id-.LoopFollowTests--app-suffix-.LoopFollowTests"; - PRODUCT_MODULE_NAME = LoopFollowTests; + PRODUCT_BUNDLE_IDENTIFIER = "com.--unique-id-.LoopFollowTests--app-suffix-.Tests"; + PRODUCT_MODULE_NAME = Tests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; @@ -2263,11 +2257,11 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - DD0247672DB2EB9A00FCADF6 /* Build configuration list for PBXNativeTarget "LoopFollowTests" */ = { + DDCC3ADC2DDE1790006F1C10 /* Build configuration list for PBXNativeTarget "Tests" */ = { isa = XCConfigurationList; buildConfigurations = ( - DD0247682DB2EB9A00FCADF6 /* Debug */, - DD0247692DB2EB9A00FCADF6 /* Release */, + DDCC3ADD2DDE1790006F1C10 /* Debug */, + DDCC3ADE2DDE1790006F1C10 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/LoopFollow.xcworkspace/xcshareddata/xcschemes/LoopFollow.xcscheme b/LoopFollow.xcworkspace/xcshareddata/xcschemes/LoopFollow.xcscheme index e8ee3d9e4..19ef472c3 100644 --- a/LoopFollow.xcworkspace/xcshareddata/xcschemes/LoopFollow.xcscheme +++ b/LoopFollow.xcworkspace/xcshareddata/xcschemes/LoopFollow.xcscheme @@ -34,12 +34,23 @@ parallelizable = "YES"> + + + + threshold") + func ignoresAbove() { + let alarm = Alarm.battery(threshold: 20) + let data = AlarmData.withBattery(55) + #expect(!cond.evaluate(alarm: alarm, data: data, now: .init())) + } + + @Test("#does NOT fire if no battery reading") + func ignoresMissingReading() { + let alarm = Alarm.battery(threshold: 20) + let data = AlarmData.withBattery(nil) + #expect(!cond.evaluate(alarm: alarm, data: data, now: .init())) + } + + @Test("#does NOT fire if threshold is nil / zero") + func ignoresBadConfig() { + let alarm = Alarm.battery(threshold: nil) + let data = AlarmData.withBattery(5) + #expect(!cond.evaluate(alarm: alarm, data: data, now: .init())) + } +} diff --git a/Tests/AlarmConditions/Helpers.swift b/Tests/AlarmConditions/Helpers.swift new file mode 100644 index 000000000..e20c9cf85 --- /dev/null +++ b/Tests/AlarmConditions/Helpers.swift @@ -0,0 +1,48 @@ +// LoopFollow +// Helpers.swift +// Created by Jonas Björkert on 2025-05-21. + +// Tests/AlarmConditions/Helpers.swift +import Foundation +@testable import LoopFollow +import Testing + +@testable import LoopFollow +import Testing + +// MARK: - Alarm helpers + +extension Alarm { + static func battery(threshold: Double?) -> Self { + var alarm = Alarm(type: .battery) + alarm.threshold = threshold + return alarm + } +} + +// MARK: - AlarmData helpers + +extension AlarmData { + static func withBattery(_ level: Double?) -> Self { + AlarmData( + bgReadings: [], + predictionData: [], + expireDate: nil, + lastLoopTime: nil, + latestOverrideStart: nil, + latestOverrideEnd: nil, + latestTempTargetStart: nil, + latestTempTargetEnd: nil, + recBolus: nil, + COB: nil, + sageInsertTime: nil, + pumpInsertTime: nil, + latestPumpVolume: nil, + IOB: nil, + recentBoluses: [], + latestBattery: level, + batteryHistory: [], + recentCarbs: [] + ) + } +} diff --git a/Tests/Tests.swift b/Tests/Tests.swift new file mode 100644 index 000000000..c4d2f3273 --- /dev/null +++ b/Tests/Tests.swift @@ -0,0 +1,11 @@ +// LoopFollow +// Tests.swift +// Created by Jonas Björkert on 2025-05-21. + +import Testing + +struct Tests { + @Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + } +} From f03a2a6a5a2e5644d9526a6dd6dac44f374615e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 24 May 2025 13:53:28 +0200 Subject: [PATCH 080/138] Acknowledge --- LoopFollow/Alarm/Alarm.swift | 3 ++ .../Components/AlarmSnoozeSection.swift | 8 +++-- .../Editors/FastDropAlarmEditor.swift | 6 +--- .../Editors/FastRiseAlarmEditor.swift | 2 +- .../Editors/OverrideEndAlarmEditor.swift | 6 +--- .../Editors/OverrideStartAlarmEditor.swift | 6 +--- .../Editors/TempTargetEndAlarmEditor.swift | 2 +- .../Editors/TempTargetStartAlarmEditor.swift | 2 +- .../Editors/TemporaryAlarmEditor.swift | 2 -- LoopFollow/Alarm/AlarmManager.swift | 8 +++-- .../Alarm/AlarmType/AlarmType+timeUnit.swift | 8 +++-- LoopFollow/Snoozer/SnoozerView.swift | 30 ++++++++++--------- LoopFollow/Snoozer/SnoozerViewModel.swift | 3 ++ 13 files changed, 44 insertions(+), 42 deletions(-) diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index d11b00f61..dd17a040a 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -268,6 +268,9 @@ struct Alarm: Identifiable, Codable, Equatable { repeatSoundOption = .never case .temporary: soundFile = .indeed + snoozeDuration = 0 + aboveBG = 180 + belowBG = 70 } } } diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift index 2167838bd..4d32b82f6 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift @@ -42,9 +42,11 @@ struct AlarmSnoozeSection: View { Section( header: Text("SNOOZE"), footer: Text( - "“Default Snooze” controls how long the alert stays quiet after " - + "you press Snooze. Toggle “Snoozed” to mute this alarm right now " - + "until the time below." + """ + “Default Snooze” controls the default value for how long the alert stays quiet after you press Snooze. \ + \(range.contains(0) ? "A snooze duration of 0 means the alarm is acknowledged (silenced), and will alert again next time the condition applies, without time limitation. " : "")\ + Toggle “Snoozed” to mute this alarm right now. + """ ) ) { Stepper( diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift index 514500985..2e2211d94 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift @@ -51,11 +51,7 @@ struct FastDropAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm) - AlarmSnoozeSection( - alarm: $alarm, - range: 5 ... 60, - step: 5 - ) + AlarmSnoozeSection(alarm: $alarm, range: 0 ... 60, step: 5) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift index fc127419d..d3a6ef296 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift @@ -54,7 +54,7 @@ struct FastRiseAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm) - AlarmSnoozeSection(alarm: $alarm, range: 5 ... 60, step: 5) + AlarmSnoozeSection(alarm: $alarm, range: 0 ... 60, step: 5) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift index 1dc6a0dd0..6f81f7379 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift @@ -15,11 +15,7 @@ struct OverrideEndAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm, hideRepeat: true) - AlarmSnoozeSection( - alarm: $alarm, - range: 10 ... 60, - step: 5 - ) + AlarmSnoozeSection(alarm: $alarm, range: 0 ... 60, step: 5) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift index 6915810bb..9fb1922c2 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift @@ -18,11 +18,7 @@ struct OverrideStartAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm, hideRepeat: true) - AlarmSnoozeSection( - alarm: $alarm, - range: 10 ... 60, - step: 5 - ) + AlarmSnoozeSection(alarm: $alarm, range: 0 ... 60, step: 5) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift index 28ed1282c..08df29c97 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift @@ -15,7 +15,7 @@ struct TempTargetEndAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm, hideRepeat: true) - AlarmSnoozeSection(alarm: $alarm, range: 10 ... 60, step: 5) + AlarmSnoozeSection(alarm: $alarm, range: 0 ... 60, step: 5) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift index d710c1367..5c3c5d738 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift @@ -15,7 +15,7 @@ struct TempTargetStartAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm, hideRepeat: true) - AlarmSnoozeSection(alarm: $alarm, range: 10 ... 60, step: 5) + AlarmSnoozeSection(alarm: $alarm, range: 0 ... 60, step: 5) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift index 9de2b03b2..d7dc5722f 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift @@ -37,7 +37,6 @@ struct TemporaryAlarmEditor: View { value: $alarm.aboveBG ) - // Validation: ensure at least one limit is on if alarm.belowBG == nil && alarm.aboveBG == nil { Text("⚠️ Please enable at least one limit.") .foregroundColor(.red) @@ -45,7 +44,6 @@ struct TemporaryAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm) - AlarmSnoozeSection(alarm: $alarm, range: 5 ... 60, step: 5) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index 94efc6741..f2c26fb73 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -159,9 +159,11 @@ class AlarmManager { if let idx = alarms.firstIndex(where: { $0.id == alarmID }) { let alarm = alarms[idx] let units = snoozeUnits ?? alarm.snoozeDuration - let snoozeSeconds = Double(units) * alarm.type.snoozeTimeUnit.seconds - alarms[idx].snoozedUntil = Date().addingTimeInterval(snoozeSeconds) - Storage.shared.alarms.value = alarms + if units > 0 { + let snoozeSeconds = Double(units) * alarm.type.snoozeTimeUnit.seconds + alarms[idx].snoozedUntil = Date().addingTimeInterval(snoozeSeconds) + Storage.shared.alarms.value = alarms + } stopAlarm() } } diff --git a/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift b/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift index 20ee69084..72f41cf43 100644 --- a/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift +++ b/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift @@ -14,17 +14,19 @@ extension AlarmType { .missedReading, .notLooping, .missedBolus, .recBolus, .overrideStart, .overrideEnd, .tempTargetStart, - .tempTargetEnd, .temporary: + .tempTargetEnd: return .minute case .battery, .batteryDrop, .sensorChange, .pumpChange, .cob, .iob, .pump: return .hour + case .temporary: + return .none } } } enum TimeUnit { - case minute, hour, day + case minute, hour, day, none /// How many seconds in one “unit” var seconds: TimeInterval { @@ -32,6 +34,7 @@ enum TimeUnit { case .minute: return 60 case .hour: return 60 * 60 case .day: return 60 * 60 * 24 + case .none: return 0 } } @@ -41,6 +44,7 @@ enum TimeUnit { case .minute: return "min" case .hour: return "hours" case .day: return "days" + case .none: return "none" } } } diff --git a/LoopFollow/Snoozer/SnoozerView.swift b/LoopFollow/Snoozer/SnoozerView.swift index 37988e605..49a7b9978 100644 --- a/LoopFollow/Snoozer/SnoozerView.swift +++ b/LoopFollow/Snoozer/SnoozerView.swift @@ -94,24 +94,26 @@ struct SnoozerView: View { Divider() // snooze controls - HStack { - VStack(alignment: .leading, spacing: 4) { - Text("Snooze for") - .font(.headline) - Text("\(vm.snoozeUnits) \(vm.timeUnitLabel)") - .font(.title3).bold() + if alarm.type.snoozeTimeUnit != .none { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text("Snooze for") + .font(.headline) + Text("\(vm.snoozeUnits) \(vm.timeUnitLabel)") + .font(.title3).bold() + } + Spacer() + Stepper("", value: $vm.snoozeUnits, + in: 1 ... (alarm.type.snoozeTimeUnit == .day ? 30 : + alarm.type.snoozeTimeUnit == .hour ? 24 : 60), + step: alarm.type.snoozeTimeUnit == .minute ? 5 : 1) + .labelsHidden() } - Spacer() - Stepper("", value: $vm.snoozeUnits, - in: 1 ... (alarm.type.snoozeTimeUnit == .day ? 30 : - alarm.type.snoozeTimeUnit == .hour ? 24 : 60), - step: alarm.type.snoozeTimeUnit == .minute ? 5 : 1) - .labelsHidden() + .padding(.horizontal, 24) } - .padding(.horizontal, 24) Button(action: vm.snoozeTapped) { - Text("Snooze") + Text(vm.snoozeUnits == 0 ? "Acknowledge" : "Snooze") .font(.title2).bold() .frame(maxWidth: .infinity, minHeight: 50) .background(Color.accentColor) diff --git a/LoopFollow/Snoozer/SnoozerViewModel.swift b/LoopFollow/Snoozer/SnoozerViewModel.swift index 4c0ce0e32..798b9b480 100644 --- a/LoopFollow/Snoozer/SnoozerViewModel.swift +++ b/LoopFollow/Snoozer/SnoozerViewModel.swift @@ -27,6 +27,9 @@ final class SnoozerViewModel: ObservableObject { } } .store(in: &cancellables) + if let alarm = activeAlarm { + snoozeUnits = alarm.snoozeDuration + } } func snoozeTapped() { From 00e7fa9e0771189d4f3fb6131b33714f78e93736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 24 May 2025 21:25:25 +0200 Subject: [PATCH 081/138] Acknowledge --- LoopFollow.xcodeproj/project.pbxproj | 8 +++ .../AlarmCondition/PumpVolumeCondition.swift | 7 --- .../Components/AlarmSnoozeSection.swift | 8 ++- .../Editors/BatteryAlarmEditor.swift | 2 +- .../Editors/BatteryDropAlarmEditor.swift | 4 +- .../Editors/BuildExpireAlarmEditor.swift | 6 +-- .../AlarmEditing/Editors/COBAlarmEditor.swift | 6 +-- .../Editors/FastDropAlarmEditor.swift | 2 +- .../Editors/FastRiseAlarmEditor.swift | 2 +- .../Editors/HighBgAlarmEditor.swift | 6 +-- .../AlarmEditing/Editors/IOBAlarmEditor.swift | 4 +- .../Editors/LowBgAlarmEditor.swift | 6 +-- .../Editors/MissedBolusAlarmEditor.swift | 2 +- .../Editors/MissedReadingEditor.swift | 6 +-- .../Editors/NotLoopingAlarmEditor.swift | 6 +-- .../Editors/OverrideEndAlarmEditor.swift | 2 +- .../Editors/OverrideStartAlarmEditor.swift | 2 +- .../Editors/PumpChangeAlarmEditor.swift | 2 +- .../Editors/PumpVolumeAlarmEditor.swift | 4 +- .../Editors/RecBolusAlarmEditor.swift | 2 +- .../Editors/SensorAgeAlarmEditor.swift | 4 +- .../Editors/TempTargetEndAlarmEditor.swift | 2 +- .../Editors/TempTargetStartAlarmEditor.swift | 2 +- .../Alarm/AlarmType/AlarmType+Snooze.swift | 49 +++++++++++++++++++ .../AlarmType/AlarmType+canAcknowledge.swift | 20 ++++++++ .../Alarm/AlarmType/AlarmType+timeUnit.swift | 21 -------- LoopFollow/Snoozer/SnoozerView.swift | 5 +- 27 files changed, 102 insertions(+), 88 deletions(-) create mode 100644 LoopFollow/Alarm/AlarmType/AlarmType+Snooze.swift create mode 100644 LoopFollow/Alarm/AlarmType/AlarmType+canAcknowledge.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index d7458dd61..64afc3d65 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -20,6 +20,8 @@ DD0650F72DCFDA26004D3B41 /* InfoBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650F62DCFDA26004D3B41 /* InfoBanner.swift */; }; DD0650F92DCFE7BE004D3B41 /* FastDropAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0650F82DCFE7BE004D3B41 /* FastDropAlarmEditor.swift */; }; DD07B5C929E2F9C400C6A635 /* NightscoutUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */; }; + DD0B9D562DE1EC8A0090C337 /* AlarmType+Snooze.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0B9D552DE1EC8A0090C337 /* AlarmType+Snooze.swift */; }; + DD0B9D582DE1F3B20090C337 /* AlarmType+canAcknowledge.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0B9D572DE1F3B20090C337 /* AlarmType+canAcknowledge.swift */; }; DD0C0C602C415B9D00DBADDF /* ProfileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */; }; DD0C0C622C4175FD00DBADDF /* NSProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C612C4175FD00DBADDF /* NSProfile.swift */; }; DD0C0C642C45A59400DBADDF /* HKUnit+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C632C45A59400DBADDF /* HKUnit+Extensions.swift */; }; @@ -379,6 +381,8 @@ DD0650F62DCFDA26004D3B41 /* InfoBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoBanner.swift; sourceTree = ""; }; DD0650F82DCFE7BE004D3B41 /* FastDropAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastDropAlarmEditor.swift; sourceTree = ""; }; DD07B5C829E2F9C400C6A635 /* NightscoutUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutUtils.swift; sourceTree = ""; }; + DD0B9D552DE1EC8A0090C337 /* AlarmType+Snooze.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlarmType+Snooze.swift"; sourceTree = ""; }; + DD0B9D572DE1F3B20090C337 /* AlarmType+canAcknowledge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlarmType+canAcknowledge.swift"; sourceTree = ""; }; DD0C0C5F2C415B9D00DBADDF /* ProfileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileManager.swift; sourceTree = ""; }; DD0C0C612C4175FD00DBADDF /* NSProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSProfile.swift; sourceTree = ""; }; DD0C0C632C45A59400DBADDF /* HKUnit+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HKUnit+Extensions.swift"; sourceTree = ""; }; @@ -1002,6 +1006,8 @@ DDCF9A812D85FD14004DF4DD /* AlarmType.swift */, DD7F4C222DD7A62200D449E9 /* AlarmType+SortDirection.swift */, DD7F4C242DD7B20700D449E9 /* AlarmType+timeUnit.swift */, + DD0B9D552DE1EC8A0090C337 /* AlarmType+Snooze.swift */, + DD0B9D572DE1F3B20090C337 /* AlarmType+canAcknowledge.swift */, ); path = AlarmType; sourceTree = ""; @@ -1786,6 +1792,7 @@ DD7F4BA32DD3C8A900D449E9 /* AlarmBGPicker.swift in Sources */, DD5334292C6166A500062F9D /* InfoDisplaySettingsView.swift in Sources */, DD48781E2C7DAF2F0048F05C /* PushNotificationManager.swift in Sources */, + DD0B9D582DE1F3B20090C337 /* AlarmType+canAcknowledge.swift in Sources */, DD2C2E562D3C3917006413A5 /* DexcomSettingsView.swift in Sources */, DD7F4BC52DD3CE0700D449E9 /* AlarmBGLimitSection.swift in Sources */, DD7F4B9F2DD1F92700D449E9 /* AlarmActiveSection.swift in Sources */, @@ -1824,6 +1831,7 @@ DD7F4C172DD63FA700D449E9 /* RecBolusCondition.swift in Sources */, FCC6886F2489A53800A0279D /* AppConstants.swift in Sources */, DD7E19842ACDA50C00DBD158 /* Overrides.swift in Sources */, + DD0B9D562DE1EC8A0090C337 /* AlarmType+Snooze.swift in Sources */, DDF2C0102BEFA991007A20E6 /* GitHubService.swift in Sources */, FC16A97A24996673003D6245 /* NightScout.swift in Sources */, DD07B5C929E2F9C400C6A635 /* NightscoutUtils.swift in Sources */, diff --git a/LoopFollow/Alarm/AlarmCondition/PumpVolumeCondition.swift b/LoopFollow/Alarm/AlarmCondition/PumpVolumeCondition.swift index 9c16c9936..e01f4d3a0 100644 --- a/LoopFollow/Alarm/AlarmCondition/PumpVolumeCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/PumpVolumeCondition.swift @@ -2,13 +2,6 @@ // PumpVolumeCondition.swift // Created by Jonas Björkert on 2025-05-17. -// -// PumpVolumeCondition.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-05-17. -// - import Foundation /// Fires when the most-recent pump‐reservoir reading is **≤ threshold units**. diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift index 4d32b82f6..6fda8e6cf 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift @@ -6,8 +6,6 @@ import SwiftUI struct AlarmSnoozeSection: View { @Binding var alarm: Alarm - let range: ClosedRange - let step: Int private var unitLabel: String { alarm.type.snoozeTimeUnit.label } @@ -44,15 +42,15 @@ struct AlarmSnoozeSection: View { footer: Text( """ “Default Snooze” controls the default value for how long the alert stays quiet after you press Snooze. \ - \(range.contains(0) ? "A snooze duration of 0 means the alarm is acknowledged (silenced), and will alert again next time the condition applies, without time limitation. " : "")\ + "A snooze duration of 0 means the alarm is acknowledged (silenced), and will alert again next time the condition applies, without time limitation. " \ Toggle “Snoozed” to mute this alarm right now. """ ) ) { Stepper( value: defaultSnoozeBinding, - in: range, - step: step + in: alarm.type.snoozeRange, + step: alarm.type.snoozeStep ) { HStack { Text("Default Snooze:") diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift index 5bbb480de..e85f6c8a7 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift @@ -31,7 +31,7 @@ struct BatteryAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm) - AlarmSnoozeSection(alarm: $alarm, range: 1 ... 24, step: 1) + AlarmSnoozeSection(alarm: $alarm) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift index f55846f5b..676e27b80 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift @@ -44,9 +44,7 @@ struct BatteryDropAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm) - AlarmSnoozeSection(alarm: $alarm, - range: 1 ... 24, - step: 1) + AlarmSnoozeSection(alarm: $alarm) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift index 14c5d51e0..44d0b96d3 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift @@ -30,11 +30,7 @@ struct BuildExpireAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm) - AlarmSnoozeSection( - alarm: $alarm, - range: 1 ... 14, - step: 1 - ) + AlarmSnoozeSection(alarm: $alarm) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift index 55f34235a..5dd514d62 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift @@ -32,11 +32,7 @@ struct COBAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm) - AlarmSnoozeSection( - alarm: $alarm, - range: 1 ... 6, - step: 1 - ) + AlarmSnoozeSection(alarm: $alarm) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift index 2e2211d94..595164dd4 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift @@ -51,7 +51,7 @@ struct FastDropAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm) - AlarmSnoozeSection(alarm: $alarm, range: 0 ... 60, step: 5) + AlarmSnoozeSection(alarm: $alarm) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift index d3a6ef296..479eef046 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift @@ -54,7 +54,7 @@ struct FastRiseAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm) - AlarmSnoozeSection(alarm: $alarm, range: 0 ... 60, step: 5) + AlarmSnoozeSection(alarm: $alarm) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift index 8cc3c7b30..6c31fd530 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift @@ -43,11 +43,7 @@ struct HighBgAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm) - AlarmSnoozeSection( - alarm: $alarm, - range: 10 ... 120, - step: 5 - ) + AlarmSnoozeSection(alarm: $alarm) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift index 2c9677fa6..d6d85b44a 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift @@ -71,9 +71,7 @@ struct IOBAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm) - AlarmSnoozeSection(alarm: $alarm, - range: 1 ... 24, - step: 1) // snooze in hours + AlarmSnoozeSection(alarm: $alarm) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift index 2d79f61f8..0439db0fa 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift @@ -55,11 +55,7 @@ struct LowBgAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm) - AlarmSnoozeSection( - alarm: $alarm, - range: 5 ... 30, - step: 5 - ) + AlarmSnoozeSection(alarm: $alarm) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift index e7b0ce1c2..64cefb58d 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift @@ -83,7 +83,7 @@ struct MissedBolusAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm) - AlarmSnoozeSection(alarm: $alarm, range: 5 ... 60, step: 5) + AlarmSnoozeSection(alarm: $alarm) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift index b85638c16..525bcf715 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift @@ -28,11 +28,7 @@ struct MissedReadingEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm) - AlarmSnoozeSection( - alarm: $alarm, - range: 10 ... 180, - step: 5 - ) + AlarmSnoozeSection(alarm: $alarm) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift index 6150958ec..f58d068e6 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift @@ -32,11 +32,7 @@ struct NotLoopingAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm) - AlarmSnoozeSection( - alarm: $alarm, - range: 10 ... 120, - step: 5 - ) + AlarmSnoozeSection(alarm: $alarm) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift index 6f81f7379..9882c845a 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift @@ -15,7 +15,7 @@ struct OverrideEndAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm, hideRepeat: true) - AlarmSnoozeSection(alarm: $alarm, range: 0 ... 60, step: 5) + AlarmSnoozeSection(alarm: $alarm) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift index 9fb1922c2..f63c54782 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift @@ -18,7 +18,7 @@ struct OverrideStartAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm, hideRepeat: true) - AlarmSnoozeSection(alarm: $alarm, range: 0 ... 60, step: 5) + AlarmSnoozeSection(alarm: $alarm) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift index 2b305e977..4d7f47ac7 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift @@ -33,7 +33,7 @@ struct PumpChangeAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm) - AlarmSnoozeSection(alarm: $alarm, range: 1 ... 24, step: 1) + AlarmSnoozeSection(alarm: $alarm) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift index ea5d40dc6..0a0bb5378 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift @@ -32,9 +32,7 @@ struct PumpVolumeAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm) - AlarmSnoozeSection(alarm: $alarm, - range: 1 ... 24, - step: 1) + AlarmSnoozeSection(alarm: $alarm) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift index 996ae9301..f31692c15 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift @@ -32,7 +32,7 @@ struct RecBolusAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm) - AlarmSnoozeSection(alarm: $alarm, range: 5 ... 60, step: 5) + AlarmSnoozeSection(alarm: $alarm) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift index 1b516a37b..f2282ad42 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift @@ -32,9 +32,7 @@ struct SensorAgeAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm) - AlarmSnoozeSection(alarm: $alarm, - range: 1 ... 24, - step: 1) + AlarmSnoozeSection(alarm: $alarm) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift index 08df29c97..d72021a2a 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift @@ -15,7 +15,7 @@ struct TempTargetEndAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm, hideRepeat: true) - AlarmSnoozeSection(alarm: $alarm, range: 0 ... 60, step: 5) + AlarmSnoozeSection(alarm: $alarm) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift index 5c3c5d738..445c46636 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift @@ -15,7 +15,7 @@ struct TempTargetStartAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm, hideRepeat: true) - AlarmSnoozeSection(alarm: $alarm, range: 0 ... 60, step: 5) + AlarmSnoozeSection(alarm: $alarm) } .navigationTitle(alarm.type.rawValue) } diff --git a/LoopFollow/Alarm/AlarmType/AlarmType+Snooze.swift b/LoopFollow/Alarm/AlarmType/AlarmType+Snooze.swift new file mode 100644 index 000000000..e3e15bede --- /dev/null +++ b/LoopFollow/Alarm/AlarmType/AlarmType+Snooze.swift @@ -0,0 +1,49 @@ +// LoopFollow +// AlarmType+Snooze.swift +// Created by Jonas Björkert on 2025-05-24. + +import Foundation + +extension AlarmType { + /// What “unit” we use for snoozeDuration for this alarmType. + var snoozeTimeUnit: TimeUnit { + switch self { + case .buildExpire: + return .day + case .low, .high, .fastDrop, .fastRise, + .missedReading, .notLooping, .missedBolus, + .recBolus, + .overrideStart, .overrideEnd, .tempTargetStart, + .tempTargetEnd: + return .minute + case .battery, .batteryDrop, .sensorChange, .pumpChange, .cob, .iob, + .pump: + return .hour + case .temporary: + return .none + } + } + + /// Valid values you may pick in the UI (`Stepper`, `Picker`, etc.). + /// The *lower* bound may be 0 if you want “Acknowledge”. + var snoozeRange: ClosedRange { + switch snoozeTimeUnit { + case .minute: + return (canAcknowledge ? 0 : 5) ... 120 + case .hour: + return (canAcknowledge ? 0 : 1) ... 24 + case .day: + return (canAcknowledge ? 0 : 1) ... 10 + case .none: + return 0 ... 0 + } + } + + /// How much the value should grow/shrink when you tap the `Stepper`. + var snoozeStep: Int { + switch snoozeTimeUnit { + case .minute: return 5 + default: return 1 + } + } +} diff --git a/LoopFollow/Alarm/AlarmType/AlarmType+canAcknowledge.swift b/LoopFollow/Alarm/AlarmType/AlarmType+canAcknowledge.swift new file mode 100644 index 000000000..efab6d2a1 --- /dev/null +++ b/LoopFollow/Alarm/AlarmType/AlarmType+canAcknowledge.swift @@ -0,0 +1,20 @@ +// LoopFollow +// AlarmType+canAcknowledge.swift +// Created by Jonas Björkert on 2025-05-24. + +import Foundation + +extension AlarmType { + /// True if this alarm may be silenced with an “Acknowledge” by settings snooze time to 0 + var canAcknowledge: Bool { + switch self { + // These are alarms that typically has a "memory", they will only alarm once and acknowledge them is fine + case .low, .high, .fastDrop, .fastRise, .temporary, .cob, .missedBolus, .recBolus, .overrideStart, .overrideEnd, .tempTargetStart, .tempTargetEnd: + return true + // These are alarms without memory, if they only are acknowledged - they would alarm again immediately + case + .batteryDrop, .missedReading, .notLooping, .battery, .buildExpire, .iob, .sensorChange, .pumpChange, .pump: + return false + } + } +} diff --git a/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift b/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift index 72f41cf43..19ae85a93 100644 --- a/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift +++ b/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift @@ -4,27 +4,6 @@ import Foundation -extension AlarmType { - /// What “unit” we use for snoozeDuration for this alarmType. - var snoozeTimeUnit: TimeUnit { - switch self { - case .buildExpire: - return .day - case .low, .high, .fastDrop, .fastRise, - .missedReading, .notLooping, .missedBolus, - .recBolus, - .overrideStart, .overrideEnd, .tempTargetStart, - .tempTargetEnd: - return .minute - case .battery, .batteryDrop, .sensorChange, .pumpChange, .cob, .iob, - .pump: - return .hour - case .temporary: - return .none - } - } -} - enum TimeUnit { case minute, hour, day, none diff --git a/LoopFollow/Snoozer/SnoozerView.swift b/LoopFollow/Snoozer/SnoozerView.swift index 49a7b9978..98cde2fed 100644 --- a/LoopFollow/Snoozer/SnoozerView.swift +++ b/LoopFollow/Snoozer/SnoozerView.swift @@ -104,9 +104,8 @@ struct SnoozerView: View { } Spacer() Stepper("", value: $vm.snoozeUnits, - in: 1 ... (alarm.type.snoozeTimeUnit == .day ? 30 : - alarm.type.snoozeTimeUnit == .hour ? 24 : 60), - step: alarm.type.snoozeTimeUnit == .minute ? 5 : 1) + in: alarm.type.snoozeRange, + step: alarm.type.snoozeStep) .labelsHidden() } .padding(.horizontal, 24) From 6fdd02093c89883742b783bfc8c46b0277ad4abb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 24 May 2025 21:55:51 +0200 Subject: [PATCH 082/138] Range adjustments --- LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift | 2 +- LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift index 595164dd4..c53ba74d3 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift @@ -20,7 +20,7 @@ struct FastDropAlarmEditor: View { header: "Rate of Fall", footer: "This is how much the glucose must drop to be considered a fast drop.", title: "Falls by", - range: 3 ... 20, + range: 3 ... 54, value: Binding( get: { alarm.delta ?? 18 }, set: { alarm.delta = $0 } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift index 479eef046..e0aec3a86 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift @@ -22,7 +22,7 @@ struct FastRiseAlarmEditor: View { header: "Rate of Rise", footer: "This is how much the glucose must rise to be considered a fast rise.", title: "Rises by", - range: 3 ... 20, + range: 3 ... 54, value: Binding( get: { alarm.delta ?? 3 }, set: { alarm.delta = $0 } From e7bcd9f3fbe9b929181263b4be36a1c45fa923c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sun, 25 May 2025 12:43:56 +0200 Subject: [PATCH 083/138] Override and temp target alarm fix --- LoopFollow/Task/AlarmTask.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index 5eadb9bad..3a2df4fb7 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -15,10 +15,11 @@ extension MainViewController { func alarmTaskAction() { DispatchQueue.main.async { - let latestOverrideStart = self.overrideGraphData.last?.date - let latestOverrideEnd = self.overrideGraphData.last?.endDate - let latestTempTargetStart = self.tempTargetGraphData.last?.date - let latestTempTargetEnd = self.tempTargetGraphData.last?.endDate + let now = Date().timeIntervalSince1970 + let latestOverrideStart = self.overrideGraphData.last { $0.date <= now }?.date + let latestOverrideEnd = self.overrideGraphData.last { $0.endDate <= now }?.endDate + let latestTempTargetStart = self.tempTargetGraphData.last { $0.date <= now }?.date + let latestTempTargetEnd = self.tempTargetGraphData.last { $0.endDate <= now }?.endDate let recBolus = UserDefaultsRepository.deviceRecBolus.value let COB = self.latestCOB?.value let sensorInsertedAt = UserDefaultsRepository.alertSageInsertTime.value From 55054f5f6af1a335b07199f71030e5d143911139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 26 May 2025 13:26:27 +0200 Subject: [PATCH 084/138] Snoozer emoji --- LoopFollow.xcodeproj/project.pbxproj | 24 +- .../Alarm/AlarmEditing/AlarmEditor.swift | 2 +- .../Components/AlarmBGLimitSection.swift | 2 +- .../Components/AlarmBGSection.swift | 2 +- LoopFollow/Alarm/AlarmListView.swift | 2 +- LoopFollow/Application/AppDelegate.swift | 7 +- LoopFollow/Application/SceneDelegate.swift | 28 +- .../Controllers/AppStateController.swift | 16 - ...t => MainViewController+updateStats.swift} | 6 +- .../Controllers/Nightscout/BGData.swift | 6 +- .../Controllers/Nightscout/DeviceStatus.swift | 2 +- LoopFollow/Controllers/SpeakBG.swift | 20 +- LoopFollow/Controllers/Stats.swift | 2 +- .../Views/BGPicker.swift} | 23 +- LoopFollow/Settings/GeneralSettingsView.swift | 116 ++++++ LoopFollow/Snoozer/SnoozerView.swift | 59 ++- LoopFollow/Storage/Observable.swift | 3 +- LoopFollow/Storage/Storage.swift | 24 ++ LoopFollow/Storage/UserDefaults.swift | 25 -- .../GeneralSettingsViewController.swift | 344 ------------------ .../GraphSettingsViewController.swift | 2 +- .../ViewControllers/MainViewController.swift | 213 +++++++++-- .../NightScoutViewController.swift | 2 +- .../SettingsViewController.swift | 42 ++- .../WatchSettingsViewController.swift | 2 +- 25 files changed, 452 insertions(+), 522 deletions(-) rename LoopFollow/Controllers/{StatsView.swift => MainViewController+updateStats.swift} (94%) rename LoopFollow/{Alarm/AlarmEditing/Components/AlarmBGPicker.swift => Helpers/Views/BGPicker.swift} (63%) create mode 100644 LoopFollow/Settings/GeneralSettingsView.swift delete mode 100644 LoopFollow/ViewControllers/GeneralSettingsViewController.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 64afc3d65..71dd7653b 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -98,7 +98,6 @@ DD7F4B9D2DD1EAE500D449E9 /* AlarmAudioSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4B9C2DD1EAE500D449E9 /* AlarmAudioSection.swift */; }; DD7F4B9F2DD1F92700D449E9 /* AlarmActiveSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4B9E2DD1F92700D449E9 /* AlarmActiveSection.swift */; }; DD7F4BA12DD2193F00D449E9 /* AlarmSnoozeSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4BA02DD2193F00D449E9 /* AlarmSnoozeSection.swift */; }; - DD7F4BA32DD3C8A900D449E9 /* AlarmBGPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4BA22DD3C8A900D449E9 /* AlarmBGPicker.swift */; }; DD7F4BC52DD3CE0700D449E9 /* AlarmBGLimitSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4BC42DD3CE0700D449E9 /* AlarmBGLimitSection.swift */; }; DD7F4BC72DD473A600D449E9 /* FastDropCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4BC62DD473A600D449E9 /* FastDropCondition.swift */; }; DD7F4C032DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C022DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift */; }; @@ -120,6 +119,8 @@ DD7F4C232DD7A62200D449E9 /* AlarmType+SortDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C222DD7A62200D449E9 /* AlarmType+SortDirection.swift */; }; DD7F4C252DD7B20700D449E9 /* AlarmType+timeUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4C242DD7B20700D449E9 /* AlarmType+timeUnit.swift */; }; DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */; }; + DD8316182DE3633D004467AA /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8316172DE3633D004467AA /* GeneralSettingsView.swift */; }; + DD8316442DE47CA9004467AA /* BGPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8316432DE47CA9004467AA /* BGPicker.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 */; }; @@ -169,7 +170,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 */; }; - DDCF979624C1443C002C9752 /* GeneralSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979524C1443C002C9752 /* GeneralSettingsViewController.swift */; }; DDCF979824C1489C002C9752 /* GraphSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */; }; DDCF979A24C14DB4002C9752 /* WatchSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979924C14DB4002C9752 /* WatchSettingsViewController.swift */; }; DDCF979E24C2382A002C9752 /* AppStateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979D24C2382A002C9752 /* AppStateController.swift */; }; @@ -211,7 +211,7 @@ FC16A97F249969E2003D6245 /* Graphs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC16A97E249969E2003D6245 /* Graphs.swift */; }; FC16A98124996C07003D6245 /* DateTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC16A98024996C07003D6245 /* DateTime.swift */; }; FC1BDD2B24A22650001B652C /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC1BDD2A24A22650001B652C /* Stats.swift */; }; - FC1BDD2D24A23204001B652C /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC1BDD2C24A23204001B652C /* StatsView.swift */; }; + FC1BDD2D24A23204001B652C /* MainViewController+updateStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC1BDD2C24A23204001B652C /* MainViewController+updateStats.swift */; }; FC1BDD3224A2585C001B652C /* DataStructs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC1BDD2E24A232A3001B652C /* DataStructs.swift */; }; FC3AE7B5249E8E0E00AAE1E0 /* LoopFollow.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FC3AE7B3249E8E0E00AAE1E0 /* LoopFollow.xcdatamodeld */; }; FC3CAB022493B6220068A152 /* BackgroundTaskAudio.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC688592489554800A0279D /* BackgroundTaskAudio.swift */; }; @@ -458,7 +458,6 @@ DD7F4B9C2DD1EAE500D449E9 /* AlarmAudioSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmAudioSection.swift; sourceTree = ""; }; DD7F4B9E2DD1F92700D449E9 /* AlarmActiveSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmActiveSection.swift; sourceTree = ""; }; DD7F4BA02DD2193F00D449E9 /* AlarmSnoozeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSnoozeSection.swift; sourceTree = ""; }; - DD7F4BA22DD3C8A900D449E9 /* AlarmBGPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmBGPicker.swift; sourceTree = ""; }; DD7F4BC42DD3CE0700D449E9 /* AlarmBGLimitSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmBGLimitSection.swift; sourceTree = ""; }; DD7F4BC62DD473A600D449E9 /* FastDropCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastDropCondition.swift; sourceTree = ""; }; DD7F4C022DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotLoopingAlarmEditor.swift; sourceTree = ""; }; @@ -480,6 +479,8 @@ DD7F4C222DD7A62200D449E9 /* AlarmType+SortDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlarmType+SortDirection.swift"; sourceTree = ""; }; DD7F4C242DD7B20700D449E9 /* AlarmType+timeUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlarmType+timeUnit.swift"; sourceTree = ""; }; DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EKEventStore+Extensions.swift"; sourceTree = ""; }; + DD8316172DE3633D004467AA /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = ""; }; + DD8316432DE47CA9004467AA /* BGPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGPicker.swift; sourceTree = ""; }; DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnipodDashHeartbeatBluetoothTransmitter.swift; sourceTree = ""; }; DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseConversion.swift; sourceTree = ""; }; DD98F54324BCEFEE0007425A /* ShareClientExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareClientExtension.swift; sourceTree = ""; }; @@ -532,7 +533,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 = ""; }; - DDCF979524C1443C002C9752 /* GeneralSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsViewController.swift; sourceTree = ""; }; DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphSettingsViewController.swift; sourceTree = ""; }; DDCF979924C14DB4002C9752 /* WatchSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchSettingsViewController.swift; sourceTree = ""; }; DDCF979D24C2382A002C9752 /* AppStateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateController.swift; sourceTree = ""; }; @@ -574,7 +574,7 @@ FC16A97E249969E2003D6245 /* Graphs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Graphs.swift; sourceTree = ""; }; FC16A98024996C07003D6245 /* DateTime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTime.swift; sourceTree = ""; }; FC1BDD2A24A22650001B652C /* Stats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stats.swift; sourceTree = ""; }; - FC1BDD2C24A23204001B652C /* StatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsView.swift; sourceTree = ""; }; + FC1BDD2C24A23204001B652C /* MainViewController+updateStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainViewController+updateStats.swift"; sourceTree = ""; }; FC1BDD2E24A232A3001B652C /* DataStructs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStructs.swift; sourceTree = ""; }; FC3AE7B4249E8E0E00AAE1E0 /* LoopFollow.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LoopFollow.xcdatamodel; sourceTree = ""; }; FC5A5C3C2497B229009C550E /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; @@ -839,6 +839,7 @@ children = ( DD1A97152D4294B2000DDC11 /* AdvancedSettingsViewModel.swift */, DD1A97132D4294A4000DDC11 /* AdvancedSettingsView.swift */, + DD8316172DE3633D004467AA /* GeneralSettingsView.swift */, ); path = Settings; sourceTree = ""; @@ -1026,7 +1027,6 @@ isa = PBXGroup; children = ( DD7F4BC42DD3CE0700D449E9 /* AlarmBGLimitSection.swift */, - DD7F4BA22DD3C8A900D449E9 /* AlarmBGPicker.swift */, DD7F4BA02DD2193F00D449E9 /* AlarmSnoozeSection.swift */, DD7F4B9E2DD1F92700D449E9 /* AlarmActiveSection.swift */, DD7F4B9C2DD1EAE500D449E9 /* AlarmAudioSection.swift */, @@ -1162,6 +1162,7 @@ DDF6999C2C5AAA4C0058A8D9 /* Views */ = { isa = PBXGroup; children = ( + DD8316432DE47CA9004467AA /* BGPicker.swift */, DDF6999D2C5AAA640058A8D9 /* ErrorMessageView.swift */, DD16AF0E2C99592F00FB655A /* HKQuantityInputView.swift */, DD16AF102C997B4600FB655A /* LoadingButtonView.swift */, @@ -1214,7 +1215,7 @@ FC16A97C24996747003D6245 /* SpeakBG.swift */, FC16A97E249969E2003D6245 /* Graphs.swift */, FC1BDD2A24A22650001B652C /* Stats.swift */, - FC1BDD2C24A23204001B652C /* StatsView.swift */, + FC1BDD2C24A23204001B652C /* MainViewController+updateStats.swift */, FCA2DDE52501095000254A8C /* Timers.swift */, DD608A0B2C27415C00F91132 /* BackgroundAlertManager.swift */, ); @@ -1456,7 +1457,6 @@ FC97881B2485969B00A7906C /* MainViewController.swift */, FC97881D2485969B00A7906C /* NightScoutViewController.swift */, FCFEECA1248857A600402A7F /* SettingsViewController.swift */, - DDCF979524C1443C002C9752 /* GeneralSettingsViewController.swift */, DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */, DDCF979924C14DB4002C9752 /* WatchSettingsViewController.swift */, ); @@ -1789,7 +1789,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DD7F4BA32DD3C8A900D449E9 /* AlarmBGPicker.swift in Sources */, DD5334292C6166A500062F9D /* InfoDisplaySettingsView.swift in Sources */, DD48781E2C7DAF2F0048F05C /* PushNotificationManager.swift in Sources */, DD0B9D582DE1F3B20090C337 /* AlarmType+canAcknowledge.swift in Sources */, @@ -1859,6 +1858,7 @@ DDA9ACAA2D6A6B8300E6F1A9 /* ContactIncludeOption.swift in Sources */, DD7F4C1D2DD650D500D449E9 /* COBAlarmEditor.swift in Sources */, DD0C0C622C4175FD00DBADDF /* NSProfile.swift in Sources */, + DD8316182DE3633D004467AA /* GeneralSettingsView.swift in Sources */, DD58171E2D299FCA0041FB98 /* BluetoothDeviceDelegate.swift in Sources */, DDE69ED22C7256260013EAEC /* RemoteType.swift in Sources */, DDD10F032C518A6500D76A8E /* TreatmentResponse.swift in Sources */, @@ -1932,7 +1932,7 @@ DD6A935E2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift in Sources */, DD493AD52ACF2109009A6922 /* ResumePump.swift in Sources */, DD85E9952D739CFE001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift in Sources */, - DDCF979624C1443C002C9752 /* GeneralSettingsViewController.swift in Sources */, + DD8316442DE47CA9004467AA /* BGPicker.swift in Sources */, FCC0FAC224922A22003E610E /* DictionaryKeyPath.swift in Sources */, DD493AD72ACF2139009A6922 /* SuspendPump.swift in Sources */, DDB9FC7F2DDB584500EFAA76 /* BolusEntry.swift in Sources */, @@ -1980,7 +1980,7 @@ DDA9ACAC2D6B317100E6F1A9 /* ContactType.swift in Sources */, DDDF6F432D479A9900884336 /* LoopNightscoutRemoteView.swift in Sources */, DDD10F052C529DA200D76A8E /* ObservableValue.swift in Sources */, - FC1BDD2D24A23204001B652C /* StatsView.swift in Sources */, + FC1BDD2D24A23204001B652C /* MainViewController+updateStats.swift in Sources */, DD4878102C7B74BF0048F05C /* TrioRemoteControlView.swift in Sources */, DD7F4C152DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift in Sources */, DD2C2E542D3C37DC006413A5 /* DexcomSettingsViewModel.swift in Sources */, diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index 2b3ed7518..58ecdeb1f 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -33,7 +33,7 @@ struct AlarmEditor: View { } } } - .preferredColorScheme(UserDefaultsRepository.forceDarkMode.value ? .dark : nil) + .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) } @ViewBuilder diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift index 174230bf4..fb1f0055e 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift @@ -53,7 +53,7 @@ struct AlarmBGLimitSection: View { Toggle(toggleText, isOn: isOn) if isOn.wrappedValue { - AlarmBGPicker( + BGPicker( title: pickerTitle, range: range, value: pickerValue diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift index 444694447..16066bcab 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift @@ -31,7 +31,7 @@ struct AlarmBGSection: View { header: header.map(Text.init), footer: footer.map(Text.init) ) { - AlarmBGPicker( + BGPicker( title: title, range: range, value: $value diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index 68861824e..12d25bafd 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -41,7 +41,7 @@ struct AddAlarmSheet: View { } } } - .preferredColorScheme(UserDefaultsRepository.forceDarkMode.value ? .dark : nil) + .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) } } diff --git a/LoopFollow/Application/AppDelegate.swift b/LoopFollow/Application/AppDelegate.swift index d3f213c50..a1f02a910 100644 --- a/LoopFollow/Application/AppDelegate.swift +++ b/LoopFollow/Application/AppDelegate.swift @@ -53,14 +53,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // MARK: UISceneSession Lifecycle func application(_: UIApplication, willFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // set "prevent screen lock" to ON when the app is started for the first time - if !UserDefaultsRepository.screenlockSwitchState.exists { - UserDefaultsRepository.screenlockSwitchState.value = true - } - // set the "prevent screen lock" option when the app is started // This method doesn't seem to be working anymore. Added to view controllers as solution offered on SO - UIApplication.shared.isIdleTimerDisabled = UserDefaultsRepository.screenlockSwitchState.value + UIApplication.shared.isIdleTimerDisabled = Storage.shared.screenlockSwitchState.value return true } diff --git a/LoopFollow/Application/SceneDelegate.swift b/LoopFollow/Application/SceneDelegate.swift index d45572b4d..30b42bca4 100644 --- a/LoopFollow/Application/SceneDelegate.swift +++ b/LoopFollow/Application/SceneDelegate.swift @@ -37,10 +37,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { vc.appStateController = appStateController } } - - // Register the SceneDelegate as an observer for the "toggleSpeakBG" notification, which will be triggered when the user toggles the "Speak BG" feature in General Settings. This helps ensure that the Quick Action is updated according to the current setting. - NotificationCenter.default.addObserver(self, selector: #selector(handleToggleSpeakBGEvent), name: NSNotification.Name("toggleSpeakBG"), object: nil) - updateQuickActions() } func sceneDidDisconnect(_: UIScene) { @@ -48,8 +44,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // This occurs shortly after the scene enters the background, or when its session is discarded. // Release any resources associated with this scene that can be re-created the next time the scene connects. // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). - - NotificationCenter.default.removeObserver(self, name: NSNotification.Name("toggleSpeakBG"), object: nil) } func sceneDidBecomeActive(_: UIScene) { @@ -76,29 +70,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { (UIApplication.shared.delegate as? AppDelegate)?.saveContext() } - // Update the Home Screen Quick Action for toggling the "Speak BG" feature based on the current setting in UserDefaultsRepository. This function uses UIApplicationShortcutItem to create a 3D touch action for controlling the feature. - func updateQuickActions() { - let iconName = UserDefaultsRepository.speakBG.value ? "pause.circle.fill" : "play.circle.fill" - let iconTemplate = UIApplicationShortcutIcon(systemImageName: iconName) - - let shortcut = UIApplicationShortcutItem(type: Bundle.main.bundleIdentifier! + ".toggleSpeakBG", - localizedTitle: "Speak BG", - localizedSubtitle: nil, - icon: iconTemplate, - userInfo: nil) - UIApplication.shared.shortcutItems = [shortcut] - } - // Handle the UIApplicationShortcutItem when the user taps on the Home Screen Quick Action. This function toggles the "Speak BG" setting in UserDefaultsRepository, speaks the current state (on/off) using AVSpeechSynthesizer, and updates the Quick Action appearance. func handleShortcutItem(_ shortcutItem: UIApplicationShortcutItem) { if let bundleIdentifier = Bundle.main.bundleIdentifier { let expectedType = bundleIdentifier + ".toggleSpeakBG" if shortcutItem.type == expectedType { - UserDefaultsRepository.speakBG.value.toggle() - let message = UserDefaultsRepository.speakBG.value ? "BG Speak is now on" : "BG Speak is now off" + Storage.shared.speakBG.value.toggle() + let message = Storage.shared.speakBG.value ? "BG Speak is now on" : "BG Speak is now off" let utterance = AVSpeechUtterance(string: message) synthesizer.speak(utterance) - updateQuickActions() } } } @@ -107,8 +87,4 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func windowScene(_: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler _: @escaping (Bool) -> Void) { handleShortcutItem(shortcutItem) } - - @objc func handleToggleSpeakBGEvent() { - updateQuickActions() - } } diff --git a/LoopFollow/Controllers/AppStateController.swift b/LoopFollow/Controllers/AppStateController.swift index e412ec9d3..a27245878 100644 --- a/LoopFollow/Controllers/AppStateController.swift +++ b/LoopFollow/Controllers/AppStateController.swift @@ -30,22 +30,6 @@ enum ChartSettingsChangeEnum: Int { case show90MinLineChanged = 32768 } -// General Settings Flags -enum GeneralSettingsChangeEnum: Int { - case colorBGTextChange = 1 - case speakBGChange = 2 - case appBadgeChange = 16 - case dimScreenWhenIdleChange = 32 - case forceDarkModeChang = 64 - case persistentNotificationChange = 128 - case persistentNotificationLastBGTimeChange = 256 - case screenlockSwitchStateChange = 512 - case showStatsChange = 1024 - case showSmallGraphChange = 2048 - case useIFCCChange = 4096 - case showDisplayNameChange = 8192 -} - class AppStateController { // add app states & methods here diff --git a/LoopFollow/Controllers/StatsView.swift b/LoopFollow/Controllers/MainViewController+updateStats.swift similarity index 94% rename from LoopFollow/Controllers/StatsView.swift rename to LoopFollow/Controllers/MainViewController+updateStats.swift index 6cd4d5972..605417d47 100644 --- a/LoopFollow/Controllers/StatsView.swift +++ b/LoopFollow/Controllers/MainViewController+updateStats.swift @@ -1,5 +1,5 @@ // LoopFollow -// StatsView.swift +// MainViewController+updateStats.swift // Created by Jon Fawcett on 2020-06-23. import Charts @@ -27,7 +27,7 @@ extension MainViewController { statsInRangePercent.text = String(format: "%.1f%", stats.percentRange) + "%" statsHighPercent.text = String(format: "%.1f%", stats.percentHigh) + "%" statsAvgBG.text = Localizer.toDisplayUnits(String(format: "%.0f%", stats.avgBG)) - if UserDefaultsRepository.useIFCC.value { + if Storage.shared.useIFCC.value { statsEstA1C.text = String(format: "%.0f%", stats.a1C) } else { statsEstA1C.text = String(format: "%.1f%", stats.a1C) @@ -38,7 +38,7 @@ extension MainViewController { } } - func createStatsPie(pieData: [DataStructs.pieData]) { + fileprivate func createStatsPie(pieData: [DataStructs.pieData]) { statsPieChart.legend.enabled = false statsPieChart.drawEntryLabelsEnabled = false statsPieChart.drawHoleEnabled = false diff --git a/LoopFollow/Controllers/Nightscout/BGData.swift b/LoopFollow/Controllers/Nightscout/BGData.swift index e98ec84b9..ce32d3ff9 100644 --- a/LoopFollow/Controllers/Nightscout/BGData.swift +++ b/LoopFollow/Controllers/Nightscout/BGData.swift @@ -208,7 +208,7 @@ extension MainViewController { } func updateServerText(with serverText: String? = nil) { - if UserDefaultsRepository.showDisplayName.value, let displayName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String { + if Storage.shared.showDisplayName.value, let displayName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String { self.serverText.text = displayName } else if let serverText = serverText { self.serverText.text = serverText @@ -238,8 +238,8 @@ extension MainViewController { // Set BGText with the latest BG value self.setBGTextColor() - Observable.shared.bgText.value = Localizer - .toDisplayUnits(String(latestBG)) + Observable.shared.bgText.value = Localizer.toDisplayUnits(String(latestBG)) + Observable.shared.bg.value = latestBG // Direction handling if let directionBG = entries[latestEntryIndex].direction { diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift index 7aac5c966..0817b818d 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift @@ -67,7 +67,7 @@ extension MainViewController { LoopStatusLabel.textAlignment = .right LoopStatusLabel.font = UIFont.systemFont(ofSize: 17) - if UserDefaultsRepository.forceDarkMode.value { + if Storage.shared.forceDarkMode.value { LoopStatusLabel.textColor = UIColor.white } else { LoopStatusLabel.textColor = UIColor.black diff --git a/LoopFollow/Controllers/SpeakBG.swift b/LoopFollow/Controllers/SpeakBG.swift index ccaab83d7..05db62a2d 100644 --- a/LoopFollow/Controllers/SpeakBG.swift +++ b/LoopFollow/Controllers/SpeakBG.swift @@ -8,17 +8,17 @@ import Foundation extension MainViewController { func evaluateSpeakConditions(currentValue: Int, previousValue: Int) { - if !UserDefaultsRepository.speakBG.value { + guard Storage.shared.speakBG.value else { return } - let always = UserDefaultsRepository.speakBGAlways.value - let lowThreshold = UserDefaultsRepository.speakLowBGLimit.value - let fastDropDelta = UserDefaultsRepository.speakFastDropDelta.value - let highThreshold = UserDefaultsRepository.speakHighBGLimit.value - let speakLowBG = UserDefaultsRepository.speakLowBG.value - let speakProactiveLowBG = UserDefaultsRepository.speakProactiveLowBG.value - let speakHighBG = UserDefaultsRepository.speakHighBG.value + let always = Storage.shared.speakBGAlways.value + let lowThreshold = Storage.shared.speakLowBGLimit.value + let fastDropDelta = Storage.shared.speakFastDropDelta.value + let highThreshold = Storage.shared.speakHighBGLimit.value + let speakLowBG = Storage.shared.speakLowBG.value + let speakProactiveLowBG = Storage.shared.speakProactiveLowBG.value + let speakHighBG = Storage.shared.speakHighBG.value // Speak always if always { @@ -42,7 +42,7 @@ extension MainViewController { // * next predictive value is low // * fast drop occurs below high if speakProactiveLowBG { - let predictiveTrigger = !predictionData.isEmpty && Float(predictionData.first!.sgv) <= lowThreshold + let predictiveTrigger = !predictionData.isEmpty && Double(predictionData.first!.sgv) <= lowThreshold if predictiveTrigger || currentValue <= Int(lowThreshold) || previousValue <= Int(lowThreshold) || @@ -147,7 +147,7 @@ extension MainViewController { let bloodGlucoseDifference = currentValue - previousValue - let preferredLanguage = UserDefaultsRepository.speakLanguage.value + let preferredLanguage = Storage.shared.speakLanguage.value let voiceLanguageCode = LanguageVoiceMapping.voiceLanguageCode(forAppLanguage: preferredLanguage) let texts = AnnouncementTexts.forLanguage(preferredLanguage) diff --git a/LoopFollow/Controllers/Stats.swift b/LoopFollow/Controllers/Stats.swift index b09ecb06e..f1408dd87 100644 --- a/LoopFollow/Controllers/Stats.swift +++ b/LoopFollow/Controllers/Stats.swift @@ -66,7 +66,7 @@ class StatsData { stdDev = stdDev * Float(GlucoseConversion.mgDlToMmolL) } - if UserDefaultsRepository.useIFCC.value { + if Storage.shared.useIFCC.value { a1C = (((46.7 + Float(avgBG)) / 28.7) - 2.152) / 0.09148 } else { a1C = (46.7 + Float(avgBG)) / 28.7 diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGPicker.swift b/LoopFollow/Helpers/Views/BGPicker.swift similarity index 63% rename from LoopFollow/Alarm/AlarmEditing/Components/AlarmBGPicker.swift rename to LoopFollow/Helpers/Views/BGPicker.swift index 5b51f0a02..3460df072 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGPicker.swift +++ b/LoopFollow/Helpers/Views/BGPicker.swift @@ -1,15 +1,18 @@ // LoopFollow -// AlarmBGPicker.swift -// Created by Jonas Björkert on 2025-05-14. +// BGPicker.swift +// Created by Jonas Björkert on 2025-05-26. import HealthKit import SwiftUI -struct AlarmBGPicker: View { +/// Lets the user pick a BG-related number (mg/dL or mmol/L) inside any form row. +struct BGPicker: View { let title: String let range: ClosedRange @Binding var value: Double + // MARK: – Helpers + private var unit: HKUnit { UserDefaultsRepository.getPreferredUnit() } private var allValues: [Double] { @@ -19,27 +22,27 @@ struct AlarmBGPicker: View { let upper = floor((range.upperBound / 18) / step) * step return stride(from: lower, through: upper, by: step).map { $0 * 18 } } else { - return Array(stride(from: range.lowerBound, through: range.upperBound, by: 1)) + return Array(stride(from: range.lowerBound, + through: range.upperBound, + by: 1)) } } private var snappedValue: Binding { Binding( - get: { - allValues.min(by: { abs($0 - value) < abs($1 - value) }) ?? value - }, + get: { allValues.min(by: { abs($0 - value) < abs($1 - value) }) ?? value }, set: { value = $0 } ) } var body: some View { - Picker(selection: snappedValue, - label: HStack { Text(title) }) - { + Picker(selection: snappedValue) { ForEach(allValues, id: \.self) { v in Text("\(Localizer.formatQuantity(v)) \(unit.localizedShortUnitString)") .tag(v) } + } label: { + Text(title) } } } diff --git a/LoopFollow/Settings/GeneralSettingsView.swift b/LoopFollow/Settings/GeneralSettingsView.swift new file mode 100644 index 000000000..361a51a52 --- /dev/null +++ b/LoopFollow/Settings/GeneralSettingsView.swift @@ -0,0 +1,116 @@ +// LoopFollow +// GeneralSettingsView.swift +// Created by Jonas Björkert on 2025-05-25. + +import SwiftUI + +struct GeneralSettingsView: View { + @Environment(\.presentationMode) var presentationMode + + @ObservedObject var colorBGText = Storage.shared.colorBGText + @ObservedObject var appBadge = Storage.shared.appBadge + @ObservedObject var forceDarkMode = Storage.shared.forceDarkMode + @ObservedObject var showStats = Storage.shared.showStats + @ObservedObject var useIFCC = Storage.shared.useIFCC + @ObservedObject var showSmallGraph = Storage.shared.showSmallGraph + @ObservedObject var screenlockSwitchState = Storage.shared.screenlockSwitchState + @ObservedObject var showDisplayName = Storage.shared.showDisplayName + @ObservedObject var snoozerEmoji = Storage.shared.snoozerEmoji + + // Speak-BG settings + @ObservedObject var speakBG = Storage.shared.speakBG + @ObservedObject var speakBGAlways = Storage.shared.speakBGAlways + @ObservedObject var speakLanguage = Storage.shared.speakLanguage + @ObservedObject var speakLowBG = Storage.shared.speakLowBG + @ObservedObject var speakProactiveLowBG = Storage.shared.speakProactiveLowBG + @ObservedObject var speakLowBGLimit = Storage.shared.speakLowBGLimit + @ObservedObject var speakFastDropDelta = Storage.shared.speakFastDropDelta + @ObservedObject var speakHighBG = Storage.shared.speakHighBG + @ObservedObject var speakHighBGLimit = Storage.shared.speakHighBGLimit + + var body: some View { + NavigationStack { + Form { + Section("App Settings") { + Toggle("Display App Badge", isOn: $appBadge.value) + } + + Section("Display") { + Toggle("Force Dark Mode (restart app)", isOn: $forceDarkMode.value) + Toggle("Display Stats", isOn: $showStats.value) + Toggle("Use IFCC A1C", isOn: $useIFCC.value) + Toggle("Display Small Graph", isOn: $showSmallGraph.value) + Toggle("Color BG Text", isOn: $colorBGText.value) + Toggle("Keep Screen Active", isOn: $screenlockSwitchState.value) + Toggle("Show Display Name", isOn: $showDisplayName.value) + Toggle("Snoozer emoji", isOn: $snoozerEmoji.value) + } + + Section("Speak BG") { + Toggle("Speak BG", isOn: $speakBG.value.animation()) + + if speakBG.value { + Picker("Language", selection: $speakLanguage.value) { + Text("English").tag("en") + Text("Italian").tag("it") + Text("Slovak").tag("sk") + Text("Swedish").tag("sv") + } + + Toggle("Always", isOn: $speakBGAlways.value.animation()) + + if !speakBGAlways.value { + Toggle("Low", isOn: $speakLowBG.value.animation()) + .onChange(of: speakLowBG.value) { newValue in + if newValue { + speakProactiveLowBG.value = false + } + } + + Toggle("Proactive Low", isOn: $speakProactiveLowBG.value.animation()) + .onChange(of: speakProactiveLowBG.value) { newValue in + if newValue { + speakLowBG.value = false + } + } + + if speakLowBG.value || speakProactiveLowBG.value { + BGPicker( + title: "Low BG Limit", + range: 40 ... 108, + value: $speakLowBGLimit.value + ) + } + + if speakProactiveLowBG.value { + BGPicker( + title: "Fast Drop Delta", + range: 3 ... 20, + value: $speakFastDropDelta.value + ) + } + + Toggle("High", isOn: $speakHighBG.value.animation()) + + if speakHighBG.value { + BGPicker( + title: "High BG Limit", + range: 140 ... 300, + value: $speakHighBGLimit.value + ) + } + } + } + } + } + .navigationTitle("General Settings") + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Done") { + presentationMode.wrappedValue.dismiss() + } + } + } + } + } +} diff --git a/LoopFollow/Snoozer/SnoozerView.swift b/LoopFollow/Snoozer/SnoozerView.swift index 98cde2fed..b4fc5ec96 100644 --- a/LoopFollow/Snoozer/SnoozerView.swift +++ b/LoopFollow/Snoozer/SnoozerView.swift @@ -13,6 +13,8 @@ struct SnoozerView: View { @ObservedObject var directionText = Observable.shared.directionText @ObservedObject var deltaText = Observable.shared.deltaText @ObservedObject var bgStale = Observable.shared.bgStale + @ObservedObject var bg = Observable.shared.bg + @ObservedObject var snoozerEmoji = Storage.shared.snoozerEmoji var body: some View { GeometryReader { geo in @@ -128,17 +130,62 @@ struct SnoozerView: View { .animation(.spring(), value: vm.activeAlarm != nil) } else { TimelineView(.periodic(from: .now, by: 1)) { context in - Text(context.date, format: - Date.FormatStyle(date: .omitted, time: .shortened)) - .font(.system(size: 70)) - .minimumScaleFactor(0.5) - .foregroundColor(.white) - .frame(height: 78) + VStack(spacing: 4) { + if snoozerEmoji.value { + Text(bgEmoji) + .font(.system(size: 128)) + .minimumScaleFactor(0.5) + } + + Text(context.date, format: Date.FormatStyle(date: .omitted, time: .shortened)) + .font(.system(size: 70)) + .minimumScaleFactor(0.5) + .foregroundColor(.white) + .frame(height: 78) + } } Spacer() } } } + + private var bgEmoji: String { + guard let bg = bg.value, !bgStale.value else { + return "🤷" + } + + if UserDefaultsRepository.getPreferredUnit() == .millimolesPerLiter, Localizer.removePeriodAndCommaForBadge(bgText.value) == "55" { + return "🦄" + } + + if UserDefaultsRepository.getPreferredUnit() == .milligramsPerDeciliter, bg == 100 { + return "🦄" + } + + switch bg { + case ..<40: return "❌" + case ..<55: return "🥶" + case ..<73: return "😱" + case ..<98: return "😊" + case ..<102: return "🥇" + case ..<109: return "😎" + case ..<127: return "🥳" + case ..<145: return "🤔" + case ..<163: return "😳" + case ..<181: return "😵‍💫" + case ..<199: return "🎃" + case ..<217: return "🙀" + case ..<235: return "🔥" + case ..<253: return "😬" + case ..<271: return "😡" + case ..<289: return "🤬" + case ..<307: return "🥵" + case ..<325: return "🫣" + case ..<343: return "😩" + case ..<361: return "🤯" + default: return "👿" + } + } } private extension View { diff --git a/LoopFollow/Storage/Observable.swift b/LoopFollow/Storage/Observable.swift index 7eba7202c..324a8cf45 100644 --- a/LoopFollow/Storage/Observable.swift +++ b/LoopFollow/Storage/Observable.swift @@ -18,8 +18,9 @@ class Observable { var minAgoText = ObservableValue(default: "?? min ago") var bgText = ObservableValue(default: "BG") + var bg = ObservableValue(default: nil) var bgStale = ObservableValue(default: true) - var bgTextColor = ObservableValue(default: .yellow) + var bgTextColor = ObservableValue(default: .primary) var directionText = ObservableValue(default: "-") var deltaText = ObservableValue(default: "+0") diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index b9586cd6f..5035f8816 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -4,6 +4,7 @@ import Foundation import HealthKit +import UIKit /* Observable persistant storage @@ -57,6 +58,29 @@ class Storage { var lastCOBNotified = StorageValue(key: "lastCOBNotified", defaultValue: nil) var lastMissedBolusNotified = StorageValue(key: "lastMissedBolusNotified", defaultValue: nil) + // General Settings [BEGIN] + var appBadge = StorageValue(key: "appBadge", defaultValue: true) + var colorBGText = StorageValue(key: "colorBGText", defaultValue: true) + var forceDarkMode = StorageValue(key: "forceDarkMode", defaultValue: true) + var showStats = StorageValue(key: "showStats", defaultValue: true) + var useIFCC = StorageValue(key: "useIFCC", defaultValue: false) + var showSmallGraph = StorageValue(key: "showSmallGraph", defaultValue: true) + var screenlockSwitchState = StorageValue(key: "screenlockSwitchState", defaultValue: true) + var showDisplayName = StorageValue(key: "showDisplayName", defaultValue: false) + var snoozerEmoji = StorageValue(key: "snoozerEmoji", defaultValue: false) + + var speakBG = StorageValue(key: "speakBG", defaultValue: false) + var speakBGAlways = StorageValue(key: "speakBGAlways", defaultValue: true) + var speakLowBG = StorageValue(key: "speakLowBG", defaultValue: false) + var speakProactiveLowBG = StorageValue(key: "speakProactiveLowBG", defaultValue: false) + var speakFastDropDelta = StorageValue(key: "speakFastDropDelta", defaultValue: 10.0) + var speakLowBGLimit = StorageValue(key: "speakLowBGLimit", defaultValue: 72.0) + var speakHighBGLimit = StorageValue(key: "speakHighBGLimit", defaultValue: 180.0) + var speakHighBG = StorageValue(key: "speakHighBG", defaultValue: false) + var speakLanguage = StorageValue(key: "speakLanguage", defaultValue: "en") + + // General Settings [END] + static let shared = Storage() private init() {} } diff --git a/LoopFollow/Storage/UserDefaults.swift b/LoopFollow/Storage/UserDefaults.swift index 328ca9309..dafc58db6 100644 --- a/LoopFollow/Storage/UserDefaults.swift +++ b/LoopFollow/Storage/UserDefaults.swift @@ -98,37 +98,12 @@ class UserDefaultsRepository { static let highLine = UserDefaultsValue(key: "highLine", default: 180.0) static let smallGraphHeight = UserDefaultsValue(key: "smallGraphHeight", default: 40) - // General Settings - static let colorBGText = UserDefaultsValue(key: "colorBGText", default: true) - static let showStats = UserDefaultsValue(key: "showStats", default: true) - static let useIFCC = UserDefaultsValue(key: "useIFCC", default: false) - static let showSmallGraph = UserDefaultsValue(key: "showSmallGraph", default: true) - static let speakBG = UserDefaultsValue(key: "speakBG", default: false) - static let speakBGAlways = UserDefaultsValue(key: "speakBGAlways", default: true) - static let speakLowBG = UserDefaultsValue(key: "speakLowBG", default: false) - static let speakProactiveLowBG = UserDefaultsValue(key: "speakProactiveLowBG", default: false) - static let speakFastDropDelta = UserDefaultsValue(key: "speakFastDropDelta", default: 10.0) - static let speakLowBGLimit = UserDefaultsValue(key: "speakLowBGLimit", default: 72.0) - static let speakHighBGLimit = UserDefaultsValue(key: "speakHighBGLimit", default: 180.0) - static let speakHighBG = UserDefaultsValue(key: "speakHighBG", default: false) - static let speakLanguage = UserDefaultsValue(key: "speakLanguage", default: "en") - static let showDisplayName = UserDefaultsValue(key: "showDisplayName", default: false) - // Deprecated, used to detect if backgroundRefresh was set to off. TODO: Remove in the beginning of 2026 static let backgroundRefresh = UserDefaultsValue(key: "backgroundRefresh", default: true) - static let appBadge = UserDefaultsValue(key: "appBadge", default: true) static let dimScreenWhenIdle = UserDefaultsValue(key: "dimScreenWhenIdle", default: 0) - static let forceDarkMode = UserDefaultsValue(key: "forceDarkMode", default: true) static let persistentNotification = UserDefaultsValue(key: "persistentNotification", default: false) static let persistentNotificationLastBGTime = UserDefaultsValue(key: "persistentNotificationLastBGTime", default: 0) - static let screenlockSwitchState = UserDefaultsValue( - key: "screenlockSwitchState", - default: UIApplication.shared.isIdleTimerDisabled, - onChange: { screenlock in - UIApplication.shared.isIdleTimerDisabled = screenlock - } - ) // Advanced Settings // static let onlyDownloadBG = UserDefaultsValue(key: "onlyDownloadBG", default: false) diff --git a/LoopFollow/ViewControllers/GeneralSettingsViewController.swift b/LoopFollow/ViewControllers/GeneralSettingsViewController.swift deleted file mode 100644 index 88a040e90..000000000 --- a/LoopFollow/ViewControllers/GeneralSettingsViewController.swift +++ /dev/null @@ -1,344 +0,0 @@ -// LoopFollow -// GeneralSettingsViewController.swift -// Created by Jose Paredes on 2020-07-17. - -import Eureka -import EventKit -import EventKitUI -import Foundation - -class GeneralSettingsViewController: FormViewController { - var appStateController: AppStateController? - - override func viewDidLoad() { - super.viewDidLoad() - - if UserDefaultsRepository.forceDarkMode.value { - overrideUserInterfaceStyle = .dark - } - buildGeneralSettings() - - // Register the GeneralSettingsViewController as an observer for the UIApplication.willEnterForegroundNotification, which will be triggered when the app enters the foreground. This helps ensure that the "Speak BG" switch in the General Settings is updated according to the current setting. - NotificationCenter.default.addObserver(self, selector: #selector(handleAppWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil) - } - - private func buildGeneralSettings() { - form - +++ Section("App Settings") - <<< SwitchRow("appBadge") { row in - row.title = "Display App Badge" - row.tag = "appBadge" - row.value = UserDefaultsRepository.appBadge.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.appBadge.value = value - // Force main screen update - // guard let mainScreen = self?.tabBarController!.viewControllers?[0] as? MainViewController else { return } - // mainScreen.nightscoutLoader(forceLoad: true) - - // set the appstate to indicate settings change and flags - if let appState = self!.appStateController { - appState.generalSettingsChanged = true - appState.generalSettingsChanges |= GeneralSettingsChangeEnum.appBadgeChange.rawValue - } - } - <<< SwitchRow("persistentNotification") { row in - row.title = "Persistent Notification" - row.value = UserDefaultsRepository.persistentNotification.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.persistentNotification.value = value - } - - +++ Section("Display Settings") - <<< SwitchRow("forceDarkMode") { row in - row.title = "Force Dark Mode (Restart App)" - row.value = UserDefaultsRepository.forceDarkMode.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.forceDarkMode.value = value - } - <<< SwitchRow("showStats") { row in - row.title = "Display Stats" - row.value = UserDefaultsRepository.showStats.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.showStats.value = value - - // set the appstate to indicate settings change and flags - if let appState = self!.appStateController { - appState.generalSettingsChanged = true - appState.generalSettingsChanges |= GeneralSettingsChangeEnum.showStatsChange.rawValue - } - } - <<< SwitchRow("useIFCC") { row in - row.title = "Use IFCC A1C" - row.value = UserDefaultsRepository.useIFCC.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.useIFCC.value = value - - // set the appstate to indicate settings change and flags - if let appState = self!.appStateController { - appState.generalSettingsChanged = true - appState.generalSettingsChanges |= GeneralSettingsChangeEnum.useIFCCChange.rawValue - } - } - <<< SwitchRow("showSmallGraph") { row in - row.title = "Display Small Graph" - row.value = UserDefaultsRepository.showSmallGraph.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.showSmallGraph.value = value - - // set the appstate to indicate settings change and flags - if let appState = self!.appStateController { - appState.generalSettingsChanged = true - appState.generalSettingsChanges |= GeneralSettingsChangeEnum.showSmallGraphChange.rawValue - } - } - <<< SwitchRow("colorBGText") { row in - row.title = "Color Main BG Text" - row.value = UserDefaultsRepository.colorBGText.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.colorBGText.value = value - // Force main screen update - // guard let mainScreen = self?.tabBarController!.viewControllers?[0] as? MainViewController else { return } - // mainScreen.setBGTextColor() - - // set the appstate to indicate settings change and flags - if let appState = self!.appStateController { - appState.generalSettingsChanged = true - appState.generalSettingsChanges |= GeneralSettingsChangeEnum.colorBGTextChange.rawValue - } - } - - <<< SwitchRow("screenlockSwitchState") { row in - row.title = "Keep Screen Active" - row.value = UserDefaultsRepository.screenlockSwitchState.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.screenlockSwitchState.value = value - } - - <<< SwitchRow("showDisplayName") { row in - row.title = "Show Display Name" - row.value = UserDefaultsRepository.showDisplayName.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.showDisplayName.value = value - - if let appState = self!.appStateController { - appState.generalSettingsChanged = true - appState.generalSettingsChanges |= GeneralSettingsChangeEnum.showDisplayNameChange.rawValue - } - } - - +++ Section("Speak BG Settings") - <<< SwitchRow("speakBG") { row in - row.title = "Speak BG" - row.value = UserDefaultsRepository.speakBG.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.speakBG.value = value - self?.updateSpeakBGSettingsVisibility() - } - - <<< PushRow("speakLanguage") { row in - row.title = "Speaking Language" - row.options = ["en", "it", "sk", "sv"] - row.value = UserDefaultsRepository.speakLanguage.value - row.displayValueFor = { value in - switch value { - case "en": return "English" - case "it": return "Italian" - case "sk": return "Slovak" - case "sv": return "Swedish" - default: return "Unknown" - } - } - row.hidden = Condition.function(["speakBG"]) { form in - let speakBGRow: SwitchRow! = form.rowBy(tag: "speakBG") - return !(speakBGRow.value ?? false) - } - row.presentationMode = PresentationMode.presentModally( - controllerProvider: ControllerProvider.callback { - SelectorViewController>> { _ in } - }, - onDismiss: { vc in - vc.dismiss(animated: true) - } - ) - }.onChange { row in - guard let value = row.value else { return } - UserDefaultsRepository.speakLanguage.value = value - } - - <<< SwitchRow("speakBGAlways") { row in - row.title = "Always" - row.value = UserDefaultsRepository.speakBGAlways.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.speakBGAlways.value = value - self?.updateSpeakBGSettingsVisibility() - } - - <<< SwitchRow("speakLowBG") { row in - row.title = "Low" - row.value = UserDefaultsRepository.speakLowBG.value - }.onChange { [weak self] row in - self?.handleLowProactiveLowToggle(row: row, opposingRowTag: "speakProactiveLowBG") - } - - <<< SwitchRow("speakProactiveLowBG") { row in - row.title = "Proactive Low" - row.value = UserDefaultsRepository.speakProactiveLowBG.value - }.onChange { [weak self] row in - self?.handleLowProactiveLowToggle(row: row, opposingRowTag: "speakLowBG") - } - - <<< StepperRow("speakLowBGLimit") { row in - row.title = "Low BG Limit" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 40 - row.cell.stepper.maximumValue = 108 - row.value = Double(UserDefaultsRepository.speakLowBGLimit.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return Localizer.toDisplayUnits(String(value)) - } - // Visibility depends on either 'speakLowBG' or 'speakProactiveLowBG' being true - row.hidden = Condition.function(["speakLowBG", "speakProactiveLowBG", "speakBG", "speakBGAlways"]) { form in - let speakBGRow: SwitchRow! = form.rowBy(tag: "speakBG") - let speakBGAlwaysRow: SwitchRow! = form.rowBy(tag: "speakBGAlways") - let speakLowBGRow: SwitchRow! = form.rowBy(tag: "speakLowBG") - let speakProactiveLowBGRow: SwitchRow! = form.rowBy(tag: "speakProactiveLowBG") - return !(speakLowBGRow.value ?? false) && !(speakProactiveLowBGRow.value ?? false) || !(speakBGRow.value ?? true) || (speakBGAlwaysRow.value ?? false) - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.speakLowBGLimit.value = Float(value) - } - - <<< StepperRow("speakFastDropDelta") { row in - row.title = "Fast Drop Delta" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 3 - row.cell.stepper.maximumValue = 20 - row.value = Double(UserDefaultsRepository.speakFastDropDelta.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return Localizer.toDisplayUnits(String(value)) - } - // Visibility depends on 'speakProactiveLowBG' being true - row.hidden = Condition.function(["speakProactiveLowBG", "speakBG", "speakBGAlways"]) { form in - let speakBGRow: SwitchRow! = form.rowBy(tag: "speakBG") - let speakBGAlwaysRow: SwitchRow! = form.rowBy(tag: "speakBGAlways") - let speakProactiveLowBGRow: SwitchRow! = form.rowBy(tag: "speakProactiveLowBG") - return !(speakProactiveLowBGRow.value ?? false) || !(speakBGRow.value ?? true) || (speakBGAlwaysRow.value ?? false) - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.speakFastDropDelta.value = Float(value) - } - - <<< SwitchRow("speakHighBG") { row in - row.title = "High" - row.value = UserDefaultsRepository.speakHighBG.value - }.onChange { row in - guard let value = row.value else { return } - UserDefaultsRepository.speakHighBG.value = value - } - - <<< StepperRow("speakHighBGLimit") { row in - row.title = "High BG Limit" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 140 - row.cell.stepper.maximumValue = 300 - row.value = Double(UserDefaultsRepository.speakHighBGLimit.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return Localizer.toDisplayUnits(String(value)) - } - // Visibility depends on 'speakHighBG' or 'speakProactiveLowBG' being true - row.hidden = Condition.function(["speakHighBG", "speakProactiveLowBG", "speakBG", "speakBGAlways"]) { form in - let speakBGRow: SwitchRow! = form.rowBy(tag: "speakBG") - let speakBGAlwaysRow: SwitchRow! = form.rowBy(tag: "speakBGAlways") - let speakHighBGRow: SwitchRow! = form.rowBy(tag: "speakHighBG") - let speakProactiveLowBGRow: SwitchRow! = form.rowBy(tag: "speakProactiveLowBG") - return !(speakHighBGRow.value ?? false) && !(speakProactiveLowBGRow.value ?? false) || !(speakBGRow.value ?? true) || (speakBGAlwaysRow.value ?? false) - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.speakHighBGLimit.value = Float(value) - } - - +++ ButtonRow { - $0.title = "DONE" - }.onCellSelection { _, _ in - self.dismiss(animated: true, completion: nil) - } - - // Call to update initial visibility based on current settings - updateSpeakBGSettingsVisibility() - } - - func updateSpeakBGSettingsVisibility() { - let alwaysOn = UserDefaultsRepository.speakBGAlways.value - let speakBGOn = UserDefaultsRepository.speakBG.value - - // Determine visibility for "Always", "Low", "Proactive Low", and "High" based on "Speak BG" and "Always" - let shouldHideAlways = !speakBGOn - let shouldHideSettings = alwaysOn || !speakBGOn - - form.rowBy(tag: "speakBGAlways")?.hidden = Condition(booleanLiteral: shouldHideAlways) - form.rowBy(tag: "speakBGAlways")?.evaluateHidden() - - for tag in ["speakLowBG", "speakProactiveLowBG", "speakHighBG"] { - if let row = form.rowBy(tag: tag) { - row.hidden = Condition(booleanLiteral: shouldHideSettings) - row.evaluateHidden() - row.updateCell() - } - } - } - - func handleLowProactiveLowToggle(row: BaseRow, opposingRowTag: String) { - guard let switchRow = row as? SwitchRow, let value = switchRow.value else { return } - - // Update the UserDefaults value for the current row. - if row.tag == "speakLowBG" { - UserDefaultsRepository.speakLowBG.value = value - } else if row.tag == "speakProactiveLowBG" { - UserDefaultsRepository.speakProactiveLowBG.value = value - } - - // If the current switch is being turned ON, turn the opposing switch OFF. - if value { - if let opposingRow = form.rowBy(tag: opposingRowTag) as? SwitchRow { - opposingRow.value = false - opposingRow.updateCell() - - // Update the UserDefaults value for the opposing row. - if opposingRowTag == "speakLowBG" { - UserDefaultsRepository.speakLowBG.value = false - } else if opposingRowTag == "speakProactiveLowBG" { - UserDefaultsRepository.speakProactiveLowBG.value = false - } - } - } - } - - // Update the "Speak BG" SwitchRow value in the General Settings when the app enters the foreground. This ensures that the switch reflects the current setting in UserDefaultsRepository, even if it was changed using the Home Screen Quick Action while the app was in the background. - @objc func handleAppWillEnterForeground() { - if let row = form.rowBy(tag: "speakBG") as? SwitchRow { - row.value = UserDefaultsRepository.speakBG.value - row.updateCell() - } - } - - deinit { - NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil) - } -} diff --git a/LoopFollow/ViewControllers/GraphSettingsViewController.swift b/LoopFollow/ViewControllers/GraphSettingsViewController.swift index 92a9bc09d..fe89f93df 100644 --- a/LoopFollow/ViewControllers/GraphSettingsViewController.swift +++ b/LoopFollow/ViewControllers/GraphSettingsViewController.swift @@ -12,7 +12,7 @@ class GraphSettingsViewController: FormViewController { override func viewDidLoad() { super.viewDidLoad() - if UserDefaultsRepository.forceDarkMode.value { + if Storage.shared.forceDarkMode.value { overrideUserInterfaceStyle = .dark } diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 9d8042673..0868d13be 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -106,7 +106,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele let store = EKEventStore() // Stores the time of the last speech announcement to prevent repeated announcements. - // This is a temporary safeguard until the issue with multiple calls to speakBG is fixed. var lastSpeechTime: Date? var autoScrollPauseUntil: Date? = nil @@ -132,6 +131,111 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele UserDefaultsRepository.backgroundRefresh.value = true } + // Remove this in a year later than the release of the new Alarms [BEGIN] + let legacyColorBGText = UserDefaultsValue(key: "colorBGText", default: true) + if legacyColorBGText.exists { + Storage.shared.colorBGText.value = legacyColorBGText.value + legacyColorBGText.setNil(key: "colorBGText") + } + + let legacyAppBadge = UserDefaultsValue(key: "appBadge", default: true) + if legacyAppBadge.exists { + Storage.shared.appBadge.value = legacyAppBadge.value + legacyAppBadge.setNil(key: "appBadge") + } + + let legacyForceDarkMode = UserDefaultsValue(key: "forceDarkMode", default: true) + if legacyForceDarkMode.exists { + Storage.shared.forceDarkMode.value = legacyForceDarkMode.value + legacyForceDarkMode.setNil(key: "forceDarkMode") + } + + let legacyShowStats = UserDefaultsValue(key: "showStats", default: true) + if legacyShowStats.exists { + Storage.shared.showStats.value = legacyShowStats.value + legacyShowStats.setNil(key: "showStats") + } + + let legacyUseIFCC = UserDefaultsValue(key: "useIFCC", default: false) + if legacyUseIFCC.exists { + Storage.shared.useIFCC.value = legacyUseIFCC.value + legacyUseIFCC.setNil(key: "useIFCC") + } + + let legacyShowSmallGraph = UserDefaultsValue(key: "showSmallGraph", default: true) + if legacyShowSmallGraph.exists { + Storage.shared.showSmallGraph.value = legacyShowSmallGraph.value + legacyShowSmallGraph.setNil(key: "showSmallGraph") + } + + let legacyScreenlockSwitchState = UserDefaultsValue(key: "screenlockSwitchState", default: true) + if legacyScreenlockSwitchState.exists { + Storage.shared.screenlockSwitchState.value = legacyScreenlockSwitchState.value + legacyScreenlockSwitchState.setNil(key: "screenlockSwitchState") + } + + let legacyShowDisplayName = UserDefaultsValue(key: "showDisplayName", default: false) + if legacyShowDisplayName.exists { + Storage.shared.showDisplayName.value = legacyShowDisplayName.value + legacyShowDisplayName.setNil(key: "showDisplayName") + } + + let legacySpeakBG = UserDefaultsValue(key: "speakBG", default: false) + if legacySpeakBG.exists { + Storage.shared.speakBG.value = legacySpeakBG.value + legacySpeakBG.setNil(key: "speakBG") + } + + let legacySpeakBGAlways = UserDefaultsValue(key: "speakBGAlways", default: true) + if legacySpeakBGAlways.exists { + Storage.shared.speakBGAlways.value = legacySpeakBGAlways.value + legacySpeakBGAlways.setNil(key: "speakBGAlways") + } + + let legacySpeakLowBG = UserDefaultsValue(key: "speakLowBG", default: false) + if legacySpeakLowBG.exists { + Storage.shared.speakLowBG.value = legacySpeakLowBG.value + legacySpeakLowBG.setNil(key: "speakLowBG") + } + + let legacySpeakProactiveLowBG = UserDefaultsValue(key: "speakProactiveLowBG", default: false) + if legacySpeakProactiveLowBG.exists { + Storage.shared.speakProactiveLowBG.value = legacySpeakProactiveLowBG.value + legacySpeakProactiveLowBG.setNil(key: "speakProactiveLowBG") + } + + let legacySpeakFastDropDelta = UserDefaultsValue(key: "speakFastDropDelta", default: 10.0) + if legacySpeakFastDropDelta.exists { + Storage.shared.speakFastDropDelta.value = Double(legacySpeakFastDropDelta.value) + legacySpeakFastDropDelta.setNil(key: "speakFastDropDelta") + } + + let legacySpeakLowBGLimit = UserDefaultsValue(key: "speakLowBGLimit", default: 72.0) + if legacySpeakLowBGLimit.exists { + Storage.shared.speakLowBGLimit.value = Double(legacySpeakLowBGLimit.value) + legacySpeakLowBGLimit.setNil(key: "speakLowBGLimit") + } + + let legacySpeakHighBGLimit = UserDefaultsValue(key: "speakHighBGLimit", default: 180.0) + if legacySpeakHighBGLimit.exists { + Storage.shared.speakHighBGLimit.value = Double(legacySpeakHighBGLimit.value) + legacySpeakHighBGLimit.setNil(key: "speakHighBGLimit") + } + + let legacySpeakHighBG = UserDefaultsValue(key: "speakHighBG", default: false) + if legacySpeakHighBG.exists { + Storage.shared.speakHighBG.value = legacySpeakHighBG.value + legacySpeakHighBG.setNil(key: "speakHighBG") + } + + let legacySpeakLanguage = UserDefaultsValue(key: "speakLanguage", default: "en") + if legacySpeakLanguage.exists { + Storage.shared.speakLanguage.value = legacySpeakLanguage.value + legacySpeakLanguage.setNil(key: "speakLanguage") + } + + // Remove this in a year later than the release of the new Alarms [END] + // Ensure alertNotLooping has a minimum value of 16. if UserDefaultsRepository.alertNotLooping.value < 16 { UserDefaultsRepository.alertNotLooping.value = 16 @@ -157,13 +261,13 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele dexShare = ShareClient(username: shareUserName, password: sharePassword, shareServer: shareServer) // setup show/hide small graph and stats - BGChartFull.isHidden = !UserDefaultsRepository.showSmallGraph.value - statsView.isHidden = !UserDefaultsRepository.showStats.value + BGChartFull.isHidden = !Storage.shared.showSmallGraph.value + statsView.isHidden = !Storage.shared.showStats.value BGChart.delegate = self BGChartFull.delegate = self - if UserDefaultsRepository.forceDarkMode.value { + if Storage.shared.forceDarkMode.value { overrideUserInterfaceStyle = .dark tabBarController?.overrideUserInterfaceStyle = .dark } @@ -236,6 +340,70 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele self?.tabBarController?.selectedIndex = 2 } .store(in: &cancellables) + + Storage.shared.colorBGText.$value + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.setBGTextColor() + } + .store(in: &cancellables) + + Storage.shared.showStats.$value + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.statsView.isHidden = !Storage.shared.showStats.value + } + .store(in: &cancellables) + + Storage.shared.useIFCC.$value + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.updateStats() + } + .store(in: &cancellables) + + Storage.shared.showSmallGraph.$value + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.BGChartFull.isHidden = !Storage.shared.showSmallGraph.value + } + .store(in: &cancellables) + + Storage.shared.screenlockSwitchState.$value + .receive(on: DispatchQueue.main) + .sink { newValue in + UIApplication.shared.isIdleTimerDisabled = newValue + } + .store(in: &cancellables) + + Storage.shared.showDisplayName.$value + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.updateServerText() + } + .store(in: &cancellables) + + Storage.shared.speakBG.$value + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.updateQuickActions() + } + .store(in: &cancellables) + + updateQuickActions() + } + + // Update the Home Screen Quick Action for toggling the "Speak BG" feature based on the current speakBG setting. + func updateQuickActions() { + let iconName = Storage.shared.speakBG.value ? "pause.circle.fill" : "play.circle.fill" + let iconTemplate = UIApplicationShortcutIcon(systemImageName: iconName) + + let shortcut = UIApplicationShortcutItem(type: Bundle.main.bundleIdentifier! + ".toggleSpeakBG", + localizedTitle: "Speak BG", + localizedSubtitle: nil, + icon: iconTemplate, + userInfo: nil) + UIApplication.shared.shortcutItems = [shortcut] } deinit { @@ -294,7 +462,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele override func viewWillAppear(_: Bool) { // set screen lock - UIApplication.shared.isIdleTimerDisabled = UserDefaultsRepository.screenlockSwitchState.value + UIApplication.shared.isIdleTimerDisabled = Storage.shared.screenlockSwitchState.value // check the app state if let appState = appStateController { @@ -312,33 +480,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele appState.chartSettingsChanges = 0 } if appState.generalSettingsChanged { - // settings for appBadge changed - if appState.generalSettingsChanges & GeneralSettingsChangeEnum.appBadgeChange.rawValue != 0 {} - - // settings for textcolor changed - if appState.generalSettingsChanges & GeneralSettingsChangeEnum.colorBGTextChange.rawValue != 0 { - setBGTextColor() - } - - // settings for showStats changed - if appState.generalSettingsChanges & GeneralSettingsChangeEnum.showStatsChange.rawValue != 0 { - statsView.isHidden = !UserDefaultsRepository.showStats.value - } - - // settings for useIFCC changed - if appState.generalSettingsChanges & GeneralSettingsChangeEnum.useIFCCChange.rawValue != 0 { - updateStats() - } - - // settings for showSmallGraph changed - if appState.generalSettingsChanges & GeneralSettingsChangeEnum.showSmallGraphChange.rawValue != 0 { - BGChartFull.isHidden = !UserDefaultsRepository.showSmallGraph.value - } - - if appState.generalSettingsChanges & GeneralSettingsChangeEnum.showDisplayNameChange.rawValue != 0 { - updateServerText() - } - // reset the app state appState.generalSettingsChanged = false appState.generalSettingsChanges = 0 @@ -394,7 +535,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele @objc func appCameToForeground() { // reset screenlock state if needed - UIApplication.shared.isIdleTimerDisabled = UserDefaultsRepository.screenlockSwitchState.value + UIApplication.shared.isIdleTimerDisabled = Storage.shared.screenlockSwitchState.value if Storage.shared.backgroundRefreshType.value == .silentTune { backgroundTask.stopBackgroundTask() @@ -503,7 +644,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } func updateBadge(val: Int) { - if UserDefaultsRepository.appBadge.value { + if Storage.shared.appBadge.value { let latestBG = String(val) UIApplication.shared.applicationIconBadgeNumber = Int(Localizer.removePeriodAndCommaForBadge(Localizer.toDisplayUnits(latestBG))) ?? val } else { @@ -515,7 +656,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele if bgData.count > 0 { let latestBG = bgData[bgData.count - 1].sgv var color = NSUIColor.label - if UserDefaultsRepository.colorBGText.value { + if Storage.shared.colorBGText.value { if Float(latestBG) >= UserDefaultsRepository.highLine.value { color = NSUIColor.systemYellow Observable.shared.bgTextColor.value = .yellow @@ -526,6 +667,8 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele color = NSUIColor.systemGreen Observable.shared.bgTextColor.value = .green } + } else { + Observable.shared.bgTextColor.value = .primary } BGText.textColor = color diff --git a/LoopFollow/ViewControllers/NightScoutViewController.swift b/LoopFollow/ViewControllers/NightScoutViewController.swift index 6f88e1c15..eb4f5f1e9 100644 --- a/LoopFollow/ViewControllers/NightScoutViewController.swift +++ b/LoopFollow/ViewControllers/NightScoutViewController.swift @@ -12,7 +12,7 @@ class NightscoutViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - if UserDefaultsRepository.forceDarkMode.value { + if Storage.shared.forceDarkMode.value { overrideUserInterfaceStyle = .dark } diff --git a/LoopFollow/ViewControllers/SettingsViewController.swift b/LoopFollow/ViewControllers/SettingsViewController.swift index a82e8f718..1c635271f 100644 --- a/LoopFollow/ViewControllers/SettingsViewController.swift +++ b/LoopFollow/ViewControllers/SettingsViewController.swift @@ -38,7 +38,7 @@ class SettingsViewController: FormViewController, NightscoutSettingsViewModelDel override func viewDidLoad() { super.viewDidLoad() - if UserDefaultsRepository.forceDarkMode.value { + if Storage.shared.forceDarkMode.value { overrideUserInterfaceStyle = .dark } @@ -98,11 +98,10 @@ class SettingsViewController: FormViewController, NightscoutSettingsViewModelDel $0.title = "General Settings" $0.presentationMode = .show( controllerProvider: .callback(builder: { - let controller = GeneralSettingsViewController() - controller.appStateController = self.appStateController - return controller - } - ), onDismiss: nil + self.presentGeneralSettings() + return UIViewController() + }), + onDismiss: nil ) } <<< ButtonRow("graphSettings") { @@ -283,7 +282,7 @@ class SettingsViewController: FormViewController, NightscoutSettingsViewModelDel let hostingController = UIHostingController(rootView: settingsView) hostingController.modalPresentationStyle = .formSheet - if UserDefaultsRepository.forceDarkMode.value { + if Storage.shared.forceDarkMode.value { hostingController.overrideUserInterfaceStyle = .dark } @@ -296,7 +295,7 @@ class SettingsViewController: FormViewController, NightscoutSettingsViewModelDel let hostingController = UIHostingController(rootView: settingsView) hostingController.modalPresentationStyle = .formSheet - if UserDefaultsRepository.forceDarkMode.value { + if Storage.shared.forceDarkMode.value { hostingController.overrideUserInterfaceStyle = .dark } @@ -308,7 +307,7 @@ class SettingsViewController: FormViewController, NightscoutSettingsViewModelDel let hostingController = UIHostingController(rootView: settingsView) hostingController.modalPresentationStyle = .formSheet - if UserDefaultsRepository.forceDarkMode.value { + if Storage.shared.forceDarkMode.value { hostingController.overrideUserInterfaceStyle = .dark } @@ -320,7 +319,7 @@ class SettingsViewController: FormViewController, NightscoutSettingsViewModelDel let hostingController = UIHostingController(rootView: settingsView) hostingController.modalPresentationStyle = .formSheet - if UserDefaultsRepository.forceDarkMode.value { + if Storage.shared.forceDarkMode.value { hostingController.overrideUserInterfaceStyle = .dark } @@ -333,7 +332,7 @@ class SettingsViewController: FormViewController, NightscoutSettingsViewModelDel let hostingController = UIHostingController(rootView: contactSettingsView) hostingController.modalPresentationStyle = .formSheet - if UserDefaultsRepository.forceDarkMode.value { + if Storage.shared.forceDarkMode.value { hostingController.overrideUserInterfaceStyle = .dark } @@ -346,7 +345,7 @@ class SettingsViewController: FormViewController, NightscoutSettingsViewModelDel let hostingController = UIHostingController(rootView: settingsView) hostingController.modalPresentationStyle = .formSheet - if UserDefaultsRepository.forceDarkMode.value { + if Storage.shared.forceDarkMode.value { hostingController.overrideUserInterfaceStyle = .dark } @@ -359,7 +358,7 @@ class SettingsViewController: FormViewController, NightscoutSettingsViewModelDel let hostingController = UIHostingController(rootView: logView) hostingController.modalPresentationStyle = .formSheet - if UserDefaultsRepository.forceDarkMode.value { + if Storage.shared.forceDarkMode.value { hostingController.overrideUserInterfaceStyle = .dark } @@ -374,7 +373,7 @@ class SettingsViewController: FormViewController, NightscoutSettingsViewModelDel let hostingController = UIHostingController(rootView: view) hostingController.modalPresentationStyle = .formSheet - if UserDefaultsRepository.forceDarkMode.value { + if Storage.shared.forceDarkMode.value { hostingController.overrideUserInterfaceStyle = .dark } @@ -391,7 +390,7 @@ class SettingsViewController: FormViewController, NightscoutSettingsViewModelDel let hostingController = UIHostingController(rootView: settingsView) hostingController.modalPresentationStyle = .formSheet - if UserDefaultsRepository.forceDarkMode.value { + if Storage.shared.forceDarkMode.value { hostingController.overrideUserInterfaceStyle = .dark } @@ -404,13 +403,24 @@ class SettingsViewController: FormViewController, NightscoutSettingsViewModelDel let hostingController = UIHostingController(rootView: view) hostingController.modalPresentationStyle = .formSheet - if UserDefaultsRepository.forceDarkMode.value { + if Storage.shared.forceDarkMode.value { hostingController.overrideUserInterfaceStyle = .dark } present(hostingController, animated: true, completion: nil) } + func presentGeneralSettings() { + let view = GeneralSettingsView() + let hostingController = UIHostingController(rootView: view) + hostingController.modalPresentationStyle = .formSheet + + if Storage.shared.forceDarkMode.value { + hostingController.overrideUserInterfaceStyle = .dark + } + present(hostingController, animated: true) + } + private func shareLogs() { let logFilesToShare = LogManager.shared.logFilesForTodayAndYesterday() diff --git a/LoopFollow/ViewControllers/WatchSettingsViewController.swift b/LoopFollow/ViewControllers/WatchSettingsViewController.swift index 3bcfdc17d..19c55c4f6 100644 --- a/LoopFollow/ViewControllers/WatchSettingsViewController.swift +++ b/LoopFollow/ViewControllers/WatchSettingsViewController.swift @@ -12,7 +12,7 @@ class WatchSettingsViewController: FormViewController { override func viewDidLoad() { super.viewDidLoad() - if UserDefaultsRepository.forceDarkMode.value { + if Storage.shared.forceDarkMode.value { overrideUserInterfaceStyle = .dark } From d51d91c2435e406ba68e24dac8adda6144f0cb42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 26 May 2025 13:31:19 +0200 Subject: [PATCH 085/138] Remove persistentNotification --- LoopFollow/Storage/UserDefaults.swift | 2 -- LoopFollow/ViewControllers/MainViewController.swift | 10 ---------- 2 files changed, 12 deletions(-) diff --git a/LoopFollow/Storage/UserDefaults.swift b/LoopFollow/Storage/UserDefaults.swift index dafc58db6..0f188c67e 100644 --- a/LoopFollow/Storage/UserDefaults.swift +++ b/LoopFollow/Storage/UserDefaults.swift @@ -102,8 +102,6 @@ class UserDefaultsRepository { static let backgroundRefresh = UserDefaultsValue(key: "backgroundRefresh", default: true) static let dimScreenWhenIdle = UserDefaultsValue(key: "dimScreenWhenIdle", default: 0) - static let persistentNotification = UserDefaultsValue(key: "persistentNotification", default: false) - static let persistentNotificationLastBGTime = UserDefaultsValue(key: "persistentNotificationLastBGTime", default: 0) // Advanced Settings // static let onlyDownloadBG = UserDefaultsValue(key: "onlyDownloadBG", default: false) diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 0868d13be..649edf323 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -787,16 +787,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } } - func persistentNotification(bgTime: TimeInterval) { - if UserDefaultsRepository.persistentNotification.value && bgTime > UserDefaultsRepository.persistentNotificationLastBGTime.value && bgData.count > 0 { - /* TODO: - guard let snoozer = self.tabBarController!.viewControllers?[2] as? SnoozeViewController else { return } - snoozer.sendNotification(self, bgVal: Localizer.toDisplayUnits(String(bgData[bgData.count - 1].sgv)), directionVal: Observable.shared.directionText.value, deltaVal: latestDeltaString, minAgoVal: Observable.shared.minAgoText.value, alertLabelVal: "Latest BG") */ - } - } - - // General Notifications - func sendGeneralNotification(_: Any, title: String, subtitle: String, body: String, timer: TimeInterval) { let content = UNMutableNotificationContent() content.title = title From 236c153c2fd10a91e180faad25d4d6a7aac35575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 26 May 2025 13:56:58 +0200 Subject: [PATCH 086/138] Code cleanup --- .../Controllers/AppStateController.swift | 30 +------------------ .../GraphSettingsViewController.swift | 9 ------ .../ViewControllers/MainViewController.swift | 21 ++----------- 3 files changed, 3 insertions(+), 57 deletions(-) diff --git a/LoopFollow/Controllers/AppStateController.swift b/LoopFollow/Controllers/AppStateController.swift index a27245878..1355a6d3a 100644 --- a/LoopFollow/Controllers/AppStateController.swift +++ b/LoopFollow/Controllers/AppStateController.swift @@ -10,37 +10,9 @@ import Foundation // Setup App States to comminicate between views -// Graph Setup Flags -enum ChartSettingsChangeEnum: Int { - case chartScaleXChanged = 1 - case showDotsChanged = 2 - case showLinesChanged = 4 - case offsetCarbsBolusChanged = 8 - case hoursToLoadChanged = 16 - case predictionToLoadChanged = 32 - case minBasalScaleChanged = 64 - case minBGScaleChanged = 128 - case overrideDisplayLocationChanged = 256 - case lowLineChanged = 512 - case highLineChanged = 1024 - case smallGraphHeight = 2048 - case showDIALinesChanged = 4096 - case showMidnightLinesChanged = 8192 - case show30MinLineChanged = 16384 - case show90MinLineChanged = 32768 -} - class AppStateController { // add app states & methods here - // General Settings States - var generalSettingsChanged: Bool = false - var generalSettingsChanges: Int = 0 - // Chart Settings State - var chartSettingsChanged: Bool = false // settings change has ocurred - var chartSettingsChanges: Int = 0 // what settings have changed - - // Info Data Settings State; no need for flags - var infoDataSettingsChanged: Bool = false + var chartSettingsChanged: Bool = false } diff --git a/LoopFollow/ViewControllers/GraphSettingsViewController.swift b/LoopFollow/ViewControllers/GraphSettingsViewController.swift index fe89f93df..430578f1d 100644 --- a/LoopFollow/ViewControllers/GraphSettingsViewController.swift +++ b/LoopFollow/ViewControllers/GraphSettingsViewController.swift @@ -79,7 +79,6 @@ class GraphSettingsViewController: FormViewController { // tell main screen that grap needs updating if let appState = self!.appStateController { appState.chartSettingsChanged = true - appState.chartSettingsChanges |= ChartSettingsChangeEnum.showDotsChanged.rawValue } } <<< SwitchRow("switchRowLines") { row in @@ -94,7 +93,6 @@ class GraphSettingsViewController: FormViewController { if let appState = self!.appStateController { appState.chartSettingsChanged = true - appState.chartSettingsChanges |= ChartSettingsChangeEnum.showLinesChanged.rawValue } } <<< SwitchRow("showValues") { row in @@ -121,7 +119,6 @@ class GraphSettingsViewController: FormViewController { // tell main screen that graph needs updating if let appState = self!.appStateController { appState.chartSettingsChanged = true - appState.chartSettingsChanges |= ChartSettingsChangeEnum.showDIALinesChanged.rawValue } } <<< SwitchRow("show30MinLine") { row in @@ -134,7 +131,6 @@ class GraphSettingsViewController: FormViewController { // Tell the main screen that graph needs updating if let appState = self!.appStateController { appState.chartSettingsChanged = true - appState.chartSettingsChanges |= ChartSettingsChangeEnum.show30MinLineChanged.rawValue } } <<< SwitchRow("show90MinLine") { row in @@ -147,7 +143,6 @@ class GraphSettingsViewController: FormViewController { // Tell the main screen that graph needs updating if let appState = self!.appStateController { appState.chartSettingsChanged = true - appState.chartSettingsChanges |= ChartSettingsChangeEnum.show90MinLineChanged.rawValue } } <<< SwitchRow("smallGraphTreatments") { row in @@ -173,7 +168,6 @@ class GraphSettingsViewController: FormViewController { if let appState = self!.appStateController { appState.chartSettingsChanged = true - appState.chartSettingsChanges |= ChartSettingsChangeEnum.smallGraphHeight.rawValue } } <<< StepperRow("predictionToLoad") { row in @@ -231,7 +225,6 @@ class GraphSettingsViewController: FormViewController { // tell main screen to update if let appState = self!.appStateController { appState.chartSettingsChanged = true - appState.chartSettingsChanges |= ChartSettingsChangeEnum.lowLineChanged.rawValue } } <<< StepperRow("highLine") { row in @@ -254,7 +247,6 @@ class GraphSettingsViewController: FormViewController { // let app state know of the change if let appState = self!.appStateController { appState.chartSettingsChanged = true - appState.chartSettingsChanges |= ChartSettingsChangeEnum.highLineChanged.rawValue } } <<< StepperRow("downloadDays") { row in @@ -282,7 +274,6 @@ class GraphSettingsViewController: FormViewController { // tell main screen that graph needs updating if let appState = self!.appStateController { appState.chartSettingsChanged = true - appState.chartSettingsChanges |= ChartSettingsChangeEnum.showMidnightLinesChanged.rawValue } } diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 649edf323..832de420a 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -467,31 +467,14 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele // check the app state if let appState = appStateController { if appState.chartSettingsChanged { - // can look at settings flags to be more fine tuned updateBGGraphSettings() - if ChartSettingsChangeEnum.smallGraphHeight.rawValue != 0 { - smallGraphHeightConstraint.constant = CGFloat(UserDefaultsRepository.smallGraphHeight.value) - view.layoutIfNeeded() - } + smallGraphHeightConstraint.constant = CGFloat(UserDefaultsRepository.smallGraphHeight.value) + view.layoutIfNeeded() // reset the app state appState.chartSettingsChanged = false - appState.chartSettingsChanges = 0 - } - if appState.generalSettingsChanged { - // reset the app state - appState.generalSettingsChanged = false - appState.generalSettingsChanges = 0 } - if appState.infoDataSettingsChanged { - infoTable.reloadData() - - // reset - appState.infoDataSettingsChanged = false - } - - // add more processing of the app state } } From 880b9b5abc2deec9bf36402eb844c6fb108ee288 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 26 May 2025 19:32:36 +0200 Subject: [PATCH 087/138] Graph Settings --- LoopFollow.xcodeproj/project.pbxproj | 24 +- LoopFollow/Application/SceneDelegate.swift | 16 - .../Controllers/AppStateController.swift | 18 -- LoopFollow/Controllers/Graphs.swift | 168 +++++----- .../MainViewController+updateStats.swift | 2 +- .../Controllers/Nightscout/BGData.swift | 8 +- .../Nightscout/DeviceStatusLoop.swift | 2 +- .../Nightscout/DeviceStatusOpenAPS.swift | 4 +- .../Controllers/Nightscout/Profile.swift | 4 +- .../Controllers/Nightscout/Treatments.swift | 2 +- .../Nightscout/Treatments/Carbs.swift | 3 +- .../Nightscout/Treatments/Overrides.swift | 4 +- .../Treatments/TemporaryTarget.swift | 2 +- LoopFollow/Controllers/Stats.swift | 4 +- .../BinaryFloatingPoint+localized.swift | 13 + .../Helpers/Views/SettingsStepperRow.swift | 43 +++ LoopFollow/Settings/GraphSettingsView.swift | 152 ++++++++++ LoopFollow/Storage/Observable.swift | 2 + LoopFollow/Storage/Storage+Migrate.swift | 180 +++++++++++ LoopFollow/Storage/Storage.swift | 21 +- LoopFollow/Storage/UserDefaults.swift | 19 -- .../GraphSettingsViewController.swift | 286 ------------------ .../ViewControllers/MainViewController.swift | 140 +-------- .../NightScoutViewController.swift | 2 - .../SettingsViewController.swift | 23 +- .../WatchSettingsViewController.swift | 2 - 26 files changed, 555 insertions(+), 589 deletions(-) delete mode 100644 LoopFollow/Controllers/AppStateController.swift create mode 100644 LoopFollow/Helpers/BinaryFloatingPoint+localized.swift create mode 100644 LoopFollow/Helpers/Views/SettingsStepperRow.swift create mode 100644 LoopFollow/Settings/GraphSettingsView.swift create mode 100644 LoopFollow/Storage/Storage+Migrate.swift delete mode 100644 LoopFollow/ViewControllers/GraphSettingsViewController.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 71dd7653b..c33929758 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -121,6 +121,10 @@ DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */; }; DD8316182DE3633D004467AA /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8316172DE3633D004467AA /* GeneralSettingsView.swift */; }; DD8316442DE47CA9004467AA /* BGPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8316432DE47CA9004467AA /* BGPicker.swift */; }; + DD8316462DE49B09004467AA /* GraphSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8316452DE49B09004467AA /* GraphSettingsView.swift */; }; + 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 */; }; 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 */; }; @@ -170,9 +174,7 @@ 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 */; }; - DDCF979824C1489C002C9752 /* GraphSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */; }; DDCF979A24C14DB4002C9752 /* WatchSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979924C14DB4002C9752 /* WatchSettingsViewController.swift */; }; - DDCF979E24C2382A002C9752 /* AppStateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979D24C2382A002C9752 /* AppStateController.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 */; }; @@ -481,6 +483,10 @@ DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EKEventStore+Extensions.swift"; sourceTree = ""; }; DD8316172DE3633D004467AA /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = ""; }; DD8316432DE47CA9004467AA /* BGPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGPicker.swift; sourceTree = ""; }; + DD8316452DE49B09004467AA /* GraphSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphSettingsView.swift; sourceTree = ""; }; + DD8316472DE49EE5004467AA /* Storage+Migrate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+Migrate.swift"; sourceTree = ""; }; + DD8316492DE4C504004467AA /* SettingsStepperRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsStepperRow.swift; sourceTree = ""; }; + DD83164B2DE4DB3A004467AA /* BinaryFloatingPoint+localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BinaryFloatingPoint+localized.swift"; sourceTree = ""; }; DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnipodDashHeartbeatBluetoothTransmitter.swift; sourceTree = ""; }; DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseConversion.swift; sourceTree = ""; }; DD98F54324BCEFEE0007425A /* ShareClientExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareClientExtension.swift; sourceTree = ""; }; @@ -533,9 +539,7 @@ 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 = ""; }; - DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphSettingsViewController.swift; sourceTree = ""; }; DDCF979924C14DB4002C9752 /* WatchSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchSettingsViewController.swift; sourceTree = ""; }; - DDCF979D24C2382A002C9752 /* AppStateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateController.swift; sourceTree = ""; }; DDCF9A7F2D85FD09004DF4DD /* Alarm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alarm.swift; sourceTree = ""; }; DDCF9A812D85FD14004DF4DD /* AlarmType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmType.swift; sourceTree = ""; }; DDCF9A872D85FD33004DF4DD /* AlarmData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmData.swift; sourceTree = ""; }; @@ -837,6 +841,7 @@ DD1A97122D429495000DDC11 /* Settings */ = { isa = PBXGroup; children = ( + DD8316452DE49B09004467AA /* GraphSettingsView.swift */, DD1A97152D4294B2000DDC11 /* AdvancedSettingsViewModel.swift */, DD1A97132D4294A4000DDC11 /* AdvancedSettingsView.swift */, DD8316172DE3633D004467AA /* GeneralSettingsView.swift */, @@ -1085,6 +1090,7 @@ DDD10F002C510C6B00D76A8E /* ObservableUserDefaults.swift */, DDD10F062C529DE800D76A8E /* Observable.swift */, DD4878042C7B2C970048F05C /* Storage.swift */, + DD8316472DE49EE5004467AA /* Storage+Migrate.swift */, ); path = Storage; sourceTree = ""; @@ -1162,6 +1168,7 @@ DDF6999C2C5AAA4C0058A8D9 /* Views */ = { isa = PBXGroup; children = ( + DD8316492DE4C504004467AA /* SettingsStepperRow.swift */, DD8316432DE47CA9004467AA /* BGPicker.swift */, DDF6999D2C5AAA640058A8D9 /* ErrorMessageView.swift */, DD16AF0E2C99592F00FB655A /* HKQuantityInputView.swift */, @@ -1209,7 +1216,6 @@ isa = PBXGroup; children = ( DD7E19802ACDA0EA00DBD158 /* Nightscout */, - DDCF979D24C2382A002C9752 /* AppStateController.swift */, FC7CE589248ABEA3001F83B8 /* AlarmSound.swift */, FC16A97924996673003D6245 /* NightScout.swift */, FC16A97C24996747003D6245 /* SpeakBG.swift */, @@ -1425,6 +1431,7 @@ FCC688542489367300A0279D /* Helpers */ = { isa = PBXGroup; children = ( + DD83164B2DE4DB3A004467AA /* BinaryFloatingPoint+localized.swift */, DD4AFB3A2DB55CB600BB593F /* TimeOfDay.swift */, DD7B0D432D730A320063DCB6 /* CycleHelper.swift */, DDF6999C2C5AAA4C0058A8D9 /* Views */, @@ -1457,7 +1464,6 @@ FC97881B2485969B00A7906C /* MainViewController.swift */, FC97881D2485969B00A7906C /* NightScoutViewController.swift */, FCFEECA1248857A600402A7F /* SettingsViewController.swift */, - DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */, DDCF979924C14DB4002C9752 /* WatchSettingsViewController.swift */, ); path = ViewControllers; @@ -1812,20 +1818,19 @@ DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */, FCC6886724898F8000A0279D /* UserDefaultsValue.swift in Sources */, DD7F4C092DD504A700D449E9 /* OverrideStartCondition.swift in Sources */, - DDCF979E24C2382A002C9752 /* AppStateController.swift in Sources */, DDEF503F2D32754F00999A5D /* ProfileTask.swift in Sources */, DD7F4C052DD4BBE200D449E9 /* NotLoopingCondition.swift in Sources */, DD5334212C60EBEE00062F9D /* InsulinCartridgeChange.swift in Sources */, DD7F4B9D2DD1EAE500D449E9 /* AlarmAudioSection.swift in Sources */, FC97881E2485969B00A7906C /* NightScoutViewController.swift in Sources */, DD608A0A2C23593900F91132 /* SMB.swift in Sources */, - DDCF979824C1489C002C9752 /* GraphSettingsViewController.swift in Sources */, FC3AE7B5249E8E0E00AAE1E0 /* LoopFollow.xcdatamodeld in Sources */, DD7F4C252DD7B20700D449E9 /* AlarmType+timeUnit.swift in Sources */, DDC7E5152DBCFA7900EB1127 /* SnoozerViewController.swift in Sources */, DD7F4C072DD5042F00D449E9 /* OverrideStartAlarmEditor.swift in Sources */, DDCC3A4B2DDBB5E4006F1C10 /* BatteryCondition.swift in Sources */, DDDF6F492D479AF000884336 /* NoRemoteView.swift in Sources */, + DD83164A2DE4C504004467AA /* SettingsStepperRow.swift in Sources */, DD0650ED2DCE9371004D3B41 /* HighBgAlarmEditor.swift in Sources */, DD7F4C172DD63FA700D449E9 /* RecBolusCondition.swift in Sources */, FCC6886F2489A53800A0279D /* AppConstants.swift in Sources */, @@ -1958,6 +1963,7 @@ DD98F54424BCEFEE0007425A /* ShareClientExtension.swift in Sources */, DD9ED0CE2D35587A000D2A63 /* LogEntry.swift in Sources */, DD0C0C602C415B9D00DBADDF /* ProfileManager.swift in Sources */, + DD83164C2DE4DB3A004467AA /* BinaryFloatingPoint+localized.swift in Sources */, DD4AFB3D2DB55D2900BB593F /* AlarmConfiguration.swift in Sources */, DDF699962C5582290058A8D9 /* TextFieldWithToolBar.swift in Sources */, DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */, @@ -1985,6 +1991,7 @@ DD7F4C152DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift in Sources */, DD2C2E542D3C37DC006413A5 /* DexcomSettingsViewModel.swift in Sources */, FCE537BC249A4D7D00F80BF8 /* carbBolusArrays.swift in Sources */, + DD8316462DE49B09004467AA /* GraphSettingsView.swift in Sources */, DD5334232C60ED3600062F9D /* IAge.swift in Sources */, FCD2A27D24C9D044009F7B7B /* Globals.swift in Sources */, DD0C0C642C45A59400DBADDF /* HKUnit+Extensions.swift in Sources */, @@ -2000,6 +2007,7 @@ DD16AF0D2C98485400FB655A /* SecureStorageValue.swift in Sources */, DDC7E5CF2DC77C2000EB1127 /* SnoozerViewModel.swift in Sources */, DD2C2E4F2D3B8AF1006413A5 /* NightscoutSettingsView.swift in Sources */, + DD8316482DE49EE5004467AA /* Storage+Migrate.swift in Sources */, DD1A97162D4294B3000DDC11 /* AdvancedSettingsViewModel.swift in Sources */, FCA2DDE62501095000254A8C /* Timers.swift in Sources */, DD2C2E512D3B8B0C006413A5 /* NightscoutSettingsViewModel.swift in Sources */, diff --git a/LoopFollow/Application/SceneDelegate.swift b/LoopFollow/Application/SceneDelegate.swift index 30b42bca4..aaa016503 100644 --- a/LoopFollow/Application/SceneDelegate.swift +++ b/LoopFollow/Application/SceneDelegate.swift @@ -9,8 +9,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? let synthesizer = AVSpeechSynthesizer() - let appStateController = AppStateController() - func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. @@ -23,20 +21,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { else { return } - - // set the main controllers' connection to the app sate - // other controllers that need to know app state are setup programatically - for i in 0 ..< viewControllers.count { - if let vc = viewControllers[i] as? MainViewController { - vc.appStateController = appStateController - } - if let vc = viewControllers[i] as? NightscoutViewController { - vc.appStateController = appStateController - } - if let vc = viewControllers[i] as? SettingsViewController { - vc.appStateController = appStateController - } - } } func sceneDidDisconnect(_: UIScene) { diff --git a/LoopFollow/Controllers/AppStateController.swift b/LoopFollow/Controllers/AppStateController.swift deleted file mode 100644 index 1355a6d3a..000000000 --- a/LoopFollow/Controllers/AppStateController.swift +++ /dev/null @@ -1,18 +0,0 @@ -// LoopFollow -// AppStateController.swift -// Created by Jose Paredes on 2020-07-18. - -import Foundation - -// App Sate used used to changes to the app view controllers (Settings, for example) -// Recommended way of utilizing is when viewVillAppear(..) is called, -// look in the app state to see if further action must be t - -// Setup App States to comminicate between views - -class AppStateController { - // add app states & methods here - - // Chart Settings State - var chartSettingsChanged: Bool = false -} diff --git a/LoopFollow/Controllers/Graphs.swift b/LoopFollow/Controllers/Graphs.swift index b511e11ce..61e7f3783 100644 --- a/LoopFollow/Controllers/Graphs.swift +++ b/LoopFollow/Controllers/Graphs.swift @@ -240,7 +240,7 @@ extension MainViewController { func createGraph() { // Create the BG Graph Data let bgChartEntry = [ChartDataEntry]() - let maxBG: Float = UserDefaultsRepository.minBGScale.value + let maxBG = Storage.shared.minBGScale.value // Setup BG line details let lineBG = LineChartDataSet(entries: bgChartEntry, label: "") @@ -251,12 +251,12 @@ extension MainViewController { lineBG.highlightEnabled = true lineBG.drawValuesEnabled = false - if UserDefaultsRepository.showLines.value { + if Storage.shared.showLines.value { lineBG.lineWidth = 2 } else { lineBG.lineWidth = 0 } - if UserDefaultsRepository.showDots.value { + if Storage.shared.showDots.value { lineBG.drawCirclesEnabled = true } else { lineBG.drawCirclesEnabled = false @@ -275,12 +275,12 @@ extension MainViewController { linePrediction.highlightEnabled = true linePrediction.drawValuesEnabled = false - if UserDefaultsRepository.showLines.value { + if Storage.shared.showLines.value { linePrediction.lineWidth = 2 } else { linePrediction.lineWidth = 0 } - if UserDefaultsRepository.showDots.value { + if Storage.shared.showDots.value { linePrediction.drawCirclesEnabled = true } else { linePrediction.drawCirclesEnabled = false @@ -290,7 +290,7 @@ extension MainViewController { // create Basal graph data let chartEntry = [ChartDataEntry]() - let maxBasal = UserDefaultsRepository.minBasalScale.value + let maxBasal = Storage.shared.minBasalScale.value let lineBasal = LineChartDataSet(entries: chartEntry, label: "") lineBasal.setDrawHighlightIndicators(false) lineBasal.setColor(NSUIColor.systemBlue, alpha: 0.5) @@ -322,7 +322,7 @@ extension MainViewController { lineBolus.drawCirclesEnabled = true lineBolus.drawFilledEnabled = false - if UserDefaultsRepository.showValues.value { + if Storage.shared.showValues.value { lineBolus.drawValuesEnabled = true lineBolus.highlightEnabled = false } else { @@ -348,7 +348,7 @@ extension MainViewController { lineCarbs.drawCirclesEnabled = true lineCarbs.drawFilledEnabled = false - if UserDefaultsRepository.showValues.value { + if Storage.shared.showValues.value { lineCarbs.drawValuesEnabled = true lineCarbs.highlightEnabled = false } else { @@ -469,12 +469,12 @@ extension MainViewController { COBlinePrediction.highlightEnabled = true COBlinePrediction.drawValuesEnabled = false - if UserDefaultsRepository.showLines.value { + if Storage.shared.showLines.value { COBlinePrediction.lineWidth = 2 } else { COBlinePrediction.lineWidth = 0 } - if UserDefaultsRepository.showDots.value { + if Storage.shared.showDots.value { COBlinePrediction.drawCirclesEnabled = true } else { COBlinePrediction.drawCirclesEnabled = false @@ -493,12 +493,12 @@ extension MainViewController { IOBlinePrediction.highlightEnabled = true IOBlinePrediction.drawValuesEnabled = false - if UserDefaultsRepository.showLines.value { + if Storage.shared.showLines.value { IOBlinePrediction.lineWidth = 2 } else { IOBlinePrediction.lineWidth = 0 } - if UserDefaultsRepository.showDots.value { + if Storage.shared.showDots.value { IOBlinePrediction.drawCirclesEnabled = true } else { IOBlinePrediction.drawCirclesEnabled = false @@ -517,12 +517,12 @@ extension MainViewController { UAMlinePrediction.highlightEnabled = true UAMlinePrediction.drawValuesEnabled = false - if UserDefaultsRepository.showLines.value { + if Storage.shared.showLines.value { UAMlinePrediction.lineWidth = 2 } else { UAMlinePrediction.lineWidth = 0 } - if UserDefaultsRepository.showDots.value { + if Storage.shared.showDots.value { UAMlinePrediction.drawCirclesEnabled = true } else { UAMlinePrediction.drawCirclesEnabled = false @@ -541,12 +541,12 @@ extension MainViewController { ZTlinePrediction.highlightEnabled = true ZTlinePrediction.drawValuesEnabled = false - if UserDefaultsRepository.showLines.value { + if Storage.shared.showLines.value { ZTlinePrediction.lineWidth = 2 } else { ZTlinePrediction.lineWidth = 0 } - if UserDefaultsRepository.showDots.value { + if Storage.shared.showDots.value { ZTlinePrediction.drawCirclesEnabled = true } else { ZTlinePrediction.drawCirclesEnabled = false @@ -570,7 +570,7 @@ extension MainViewController { lineSmb.drawCirclesEnabled = false lineSmb.drawFilledEnabled = false - if UserDefaultsRepository.showValues.value { + if Storage.shared.showValues.value { lineSmb.drawValuesEnabled = true lineSmb.highlightEnabled = false } else { @@ -624,13 +624,13 @@ extension MainViewController { // Add lower red line based on low alert value let ll = ChartLimitLine() - ll.limit = Double(UserDefaultsRepository.lowLine.value) + ll.limit = Storage.shared.lowLine.value ll.lineColor = NSUIColor.systemRed.withAlphaComponent(0.5) BGChart.rightAxis.addLimitLine(ll) // Add upper yellow line based on low alert value let ul = ChartLimitLine() - ul.limit = Double(UserDefaultsRepository.highLine.value) + ul.limit = Storage.shared.highLine.value ul.lineColor = NSUIColor.systemYellow.withAlphaComponent(0.5) BGChart.rightAxis.addLimitLine(ul) @@ -689,7 +689,7 @@ extension MainViewController { ul.lineWidth = 1 BGChart.xAxis.addLimitLine(ul) - if UserDefaultsRepository.show30MinLine.value { + if Storage.shared.show30MinLine.value { let ul2 = ChartLimitLine() ul2.limit = Double(dateTimeUtils.getNowTimeIntervalUTC().advanced(by: -30 * 60)) ul2.lineColor = NSUIColor.systemBlue.withAlphaComponent(0.5) @@ -697,7 +697,7 @@ extension MainViewController { BGChart.xAxis.addLimitLine(ul2) } - if UserDefaultsRepository.showDIALines.value { + if Storage.shared.showDIALines.value { for i in 1 ..< 7 { let ul = ChartLimitLine() ul.limit = Double(dateTimeUtils.getNowTimeIntervalUTC() - Double(i * 60 * 60)) @@ -710,7 +710,7 @@ extension MainViewController { } } - if UserDefaultsRepository.show90MinLine.value { + if Storage.shared.show90MinLine.value { let ul3 = ChartLimitLine() ul3.limit = Double(dateTimeUtils.getNowTimeIntervalUTC().advanced(by: -90 * 60)) ul3.lineColor = NSUIColor.systemOrange.withAlphaComponent(0.5) @@ -721,9 +721,9 @@ extension MainViewController { func createMidnightLines() { // Draw a line at midnight: useful when showing multiple days of data - if UserDefaultsRepository.showMidnightLines.value { + if Storage.shared.showMidnightLines.value { var midnightTimeInterval = dateTimeUtils.getTimeIntervalMidnightToday() - let graphHours = 24 * UserDefaultsRepository.downloadDays.value + let graphHours = 24 * Storage.shared.downloadDays.value let graphStart = dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) while midnightTimeInterval > graphStart { // Large chart @@ -752,14 +752,14 @@ extension MainViewController { let dataIndexPrediction = 1 let lineBG = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet let linePrediction = BGChart.lineData!.dataSets[dataIndexPrediction] as! LineChartDataSet - if UserDefaultsRepository.showLines.value { + if Storage.shared.showLines.value { lineBG.lineWidth = 2 linePrediction.lineWidth = 2 } else { lineBG.lineWidth = 0 linePrediction.lineWidth = 0 } - if UserDefaultsRepository.showDots.value { + if Storage.shared.showDots.value { lineBG.drawCirclesEnabled = true linePrediction.drawCirclesEnabled = true } else { @@ -774,13 +774,13 @@ extension MainViewController { // Add lower red line based on low alert value let ll = ChartLimitLine() - ll.limit = Double(UserDefaultsRepository.lowLine.value) + ll.limit = Storage.shared.lowLine.value ll.lineColor = NSUIColor.systemRed.withAlphaComponent(0.5) BGChart.rightAxis.addLimitLine(ll) // Add upper yellow line based on low alert value let ul = ChartLimitLine() - ul.limit = Double(UserDefaultsRepository.highLine.value) + ul.limit = Storage.shared.highLine.value ul.lineColor = NSUIColor.systemYellow.withAlphaComponent(0.5) BGChart.rightAxis.addLimitLine(ul) @@ -802,22 +802,22 @@ extension MainViewController { let smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet mainChart.removeAll(keepingCapacity: false) smallChart.removeAll(keepingCapacity: false) - let maxBGOffset: Float = 50 + let maxBGOffset: Double = 50 var colors = [NSUIColor]() - topBG = UserDefaultsRepository.minBGScale.value + topBG = Storage.shared.minBGScale.value for i in 0 ..< entries.count { - if Float(entries[i].sgv) > topBG - maxBGOffset { - topBG = Float(entries[i].sgv) + maxBGOffset + if Double(entries[i].sgv) > topBG - maxBGOffset { + topBG = Double(entries[i].sgv) + maxBGOffset } let value = ChartDataEntry(x: Double(entries[i].date), y: Double(entries[i].sgv), data: formatPillText(line1: Localizer.toDisplayUnits(String(entries[i].sgv)), time: entries[i].date)) mainChart.append(value) smallChart.append(value) - if Double(entries[i].sgv) >= Double(UserDefaultsRepository.highLine.value) { + if Double(entries[i].sgv) >= Storage.shared.highLine.value { colors.append(NSUIColor.systemYellow) - } else if Double(entries[i].sgv) <= Double(UserDefaultsRepository.lowLine.value) { + } else if Double(entries[i].sgv) <= Storage.shared.lowLine.value { colors.append(NSUIColor.systemRed) } else { colors.append(NSUIColor.systemGreen) @@ -877,17 +877,17 @@ extension MainViewController { smallChart.clear() var colors = [NSUIColor]() - let maxBGOffset: Float = 20 + let maxBGOffset: Double = 20 - topPredictionBG = UserDefaultsRepository.minBGScale.value + topPredictionBG = Storage.shared.minBGScale.value for i in 0 ..< predictionData.count { var predictionVal = Double(predictionData[i].sgv) - if Float(predictionVal) > topPredictionBG - maxBGOffset { - topPredictionBG = Float(predictionVal) + maxBGOffset + if Double(predictionVal) > topPredictionBG - maxBGOffset { + topPredictionBG = predictionVal + maxBGOffset } if i == 0 { - if UserDefaultsRepository.showDots.value { + if Storage.shared.showDots.value { colors.append((color ?? NSUIColor.systemPurple).withAlphaComponent(0.0)) } else { colors.append((color ?? NSUIColor.systemPurple).withAlphaComponent(1.0)) @@ -930,12 +930,12 @@ extension MainViewController { var dataIndex = 2 BGChart.lineData?.dataSets[dataIndex].clear() BGChartFull.lineData?.dataSets[dataIndex].clear() - var maxBasal = UserDefaultsRepository.minBasalScale.value + var maxBasal = Storage.shared.minBasalScale.value var maxBasalSmall = 0.0 for i in 0 ..< basalData.count { let value = ChartDataEntry(x: Double(basalData[i].date), y: Double(basalData[i].basalRate), data: formatPillText(line1: String(basalData[i].basalRate), time: basalData[i].date)) BGChart.data?.dataSets[dataIndex].addEntry(value) - if UserDefaultsRepository.smallGraphTreatments.value { + if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(value) } if basalData[i].basalRate > maxBasal { @@ -953,7 +953,7 @@ extension MainViewController { BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() - if UserDefaultsRepository.smallGraphTreatments.value { + if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() @@ -967,7 +967,7 @@ extension MainViewController { for i in 0 ..< basalScheduleData.count { let value = ChartDataEntry(x: Double(basalScheduleData[i].date), y: Double(basalScheduleData[i].basalRate)) BGChart.data?.dataSets[dataIndex].addEntry(value) - if UserDefaultsRepository.smallGraphTreatments.value { + if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(value) } } @@ -975,7 +975,7 @@ extension MainViewController { BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() - if UserDefaultsRepository.smallGraphTreatments.value { + if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() @@ -1010,12 +1010,12 @@ extension MainViewController { } // skip if outside of visible area - let graphHours = 24 * UserDefaultsRepository.downloadDays.value + let graphHours = 24 * Storage.shared.downloadDays.value if dateTimeStamp < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue } let dot = ChartDataEntry(x: Double(dateTimeStamp), y: Double(bolusData[i].sgv), data: formatter.string(from: NSNumber(value: bolusData[i].value))) mainChart.addEntry(dot) - if UserDefaultsRepository.smallGraphTreatments.value { + if Storage.shared.smallGraphTreatments.value { smallChart.addEntry(dot) } } @@ -1040,7 +1040,7 @@ extension MainViewController { BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() - if UserDefaultsRepository.smallGraphTreatments.value { + if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() @@ -1082,12 +1082,12 @@ extension MainViewController { dateTimeStamp = dateTimeStamp - 150 } - let graphHours = 24 * UserDefaultsRepository.downloadDays.value + let graphHours = 24 * Storage.shared.downloadDays.value if dateTimeStamp < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue } let dot = ChartDataEntry(x: Double(dateTimeStamp), y: Double(smbData[i].sgv), data: formatter.string(from: NSNumber(value: smbData[i].value))) mainChart.addEntry(dot) - if UserDefaultsRepository.smallGraphTreatments.value { + if Storage.shared.smallGraphTreatments.value { smallChart.addEntry(dot) } } @@ -1095,7 +1095,7 @@ extension MainViewController { BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() - if UserDefaultsRepository.smallGraphTreatments.value { + if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() @@ -1106,8 +1106,8 @@ extension MainViewController { var dataIndex = 4 var mainChart = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet var smallChart = BGChartFull.lineData!.dataSets[dataIndex] as! LineChartDataSet - mainChart.clear() - smallChart.clear() + mainChart.removeAll(keepingCapacity: true) + smallChart.removeAll(keepingCapacity: true) var colors = [NSUIColor]() for i in 0 ..< carbData.count { @@ -1119,7 +1119,7 @@ extension MainViewController { var valueString: String = formatter.string(from: NSNumber(value: carbData[i].value))! var hours = 3 - if carbData[i].absorptionTime > 0, UserDefaultsRepository.showAbsorption.value { + if carbData[i].absorptionTime > 0, Storage.shared.showAbsorption.value { hours = carbData[i].absorptionTime / 60 valueString += " " + String(hours) + "h" } @@ -1131,7 +1131,7 @@ extension MainViewController { colors.append(NSUIColor.systemOrange.withAlphaComponent(1.0)) // skip if outside of visible area - let graphHours = 24 * UserDefaultsRepository.downloadDays.value + let graphHours = 24 * Storage.shared.downloadDays.value if dateTimeStamp < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue } if carbShift { @@ -1140,7 +1140,7 @@ extension MainViewController { let dot = ChartDataEntry(x: Double(dateTimeStamp), y: Double(carbData[i].sgv), data: valueString) BGChart.data?.dataSets[dataIndex].addEntry(dot) - if UserDefaultsRepository.smallGraphTreatments.value { + if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(dot) } } @@ -1165,7 +1165,7 @@ extension MainViewController { BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() - if UserDefaultsRepository.smallGraphTreatments.value { + if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() @@ -1184,12 +1184,12 @@ extension MainViewController { formatter.minimumIntegerDigits = 1 // skip if outside of visible area - let graphHours = 24 * UserDefaultsRepository.downloadDays.value + let graphHours = 24 * Storage.shared.downloadDays.value if bgCheckData[i].date < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue } let value = ChartDataEntry(x: Double(bgCheckData[i].date), y: Double(bgCheckData[i].sgv), data: formatPillText(line1: Localizer.toDisplayUnits(String(bgCheckData[i].sgv)), time: bgCheckData[i].date)) BGChart.data?.dataSets[dataIndex].addEntry(value) - if UserDefaultsRepository.smallGraphTreatments.value { + if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(value) } } @@ -1197,7 +1197,7 @@ extension MainViewController { BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() - if UserDefaultsRepository.smallGraphTreatments.value { + if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() @@ -1211,12 +1211,12 @@ extension MainViewController { let thisData = suspendGraphData for i in 0 ..< thisData.count { // skip if outside of visible area - let graphHours = 24 * UserDefaultsRepository.downloadDays.value + let graphHours = 24 * Storage.shared.downloadDays.value if thisData[i].date < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue } let value = ChartDataEntry(x: Double(thisData[i].date), y: Double(thisData[i].sgv), data: formatPillText(line1: "Suspend Pump", time: thisData[i].date)) BGChart.data?.dataSets[dataIndex].addEntry(value) - if UserDefaultsRepository.smallGraphTreatments.value { + if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(value) } } @@ -1224,7 +1224,7 @@ extension MainViewController { BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() - if UserDefaultsRepository.smallGraphTreatments.value { + if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() @@ -1238,12 +1238,12 @@ extension MainViewController { let thisData = resumeGraphData for i in 0 ..< thisData.count { // skip if outside of visible area - let graphHours = 24 * UserDefaultsRepository.downloadDays.value + let graphHours = 24 * Storage.shared.downloadDays.value if thisData[i].date < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue } let value = ChartDataEntry(x: Double(thisData[i].date), y: Double(thisData[i].sgv), data: formatPillText(line1: "Resume Pump", time: thisData[i].date)) BGChart.data?.dataSets[dataIndex].addEntry(value) - if UserDefaultsRepository.smallGraphTreatments.value { + if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(value) } } @@ -1251,7 +1251,7 @@ extension MainViewController { BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() - if UserDefaultsRepository.smallGraphTreatments.value { + if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() @@ -1265,12 +1265,12 @@ extension MainViewController { let thisData = sensorStartGraphData for i in 0 ..< thisData.count { // skip if outside of visible area - let graphHours = 24 * UserDefaultsRepository.downloadDays.value + let graphHours = 24 * Storage.shared.downloadDays.value if thisData[i].date < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue } let value = ChartDataEntry(x: Double(thisData[i].date), y: Double(thisData[i].sgv), data: formatPillText(line1: "Start Sensor", time: thisData[i].date)) BGChart.data?.dataSets[dataIndex].addEntry(value) - if UserDefaultsRepository.smallGraphTreatments.value { + if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(value) } } @@ -1278,7 +1278,7 @@ extension MainViewController { BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() - if UserDefaultsRepository.smallGraphTreatments.value { + if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() @@ -1292,12 +1292,12 @@ extension MainViewController { let thisData = noteGraphData for i in 0 ..< thisData.count { // skip if outside of visible area - let graphHours = 24 * UserDefaultsRepository.downloadDays.value + let graphHours = 24 * Storage.shared.downloadDays.value if thisData[i].date < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { continue } let value = ChartDataEntry(x: Double(thisData[i].date), y: Double(thisData[i].sgv), data: formatPillText(line1: thisData[i].note, time: thisData[i].date)) BGChart.data?.dataSets[dataIndex].addEntry(value) - if UserDefaultsRepository.smallGraphTreatments.value { + if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(value) } } @@ -1305,7 +1305,7 @@ extension MainViewController { BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() - if UserDefaultsRepository.smallGraphTreatments.value { + if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() @@ -1316,7 +1316,7 @@ extension MainViewController { let entries = bgData var bgChartEntry = [ChartDataEntry]() var colors = [NSUIColor]() - var maxBG: Float = UserDefaultsRepository.minBGScale.value + var maxBG = Storage.shared.minBGScale.value let lineBG = LineChartDataSet(entries: bgChartEntry, label: "") @@ -1346,7 +1346,7 @@ extension MainViewController { // create Basal graph data var chartEntry = [ChartDataEntry]() - var maxBasal = UserDefaultsRepository.minBasalScale.value + var maxBasal = Storage.shared.minBasalScale.value let lineBasal = LineChartDataSet(entries: chartEntry, label: "") lineBasal.setDrawHighlightIndicators(false) lineBasal.setColor(NSUIColor.systemBlue, alpha: 0.5) @@ -1651,27 +1651,27 @@ extension MainViewController { // Shift dots 30 seconds to create an empty 0 space between consecutive temps let preStartDot = ChartDataEntry(x: Double(thisItem.date), y: yBottom, data: labelText) BGChart.data?.dataSets[dataIndex].addEntry(preStartDot) - if UserDefaultsRepository.smallGraphTreatments.value { + if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(preStartDot) } let startDot = ChartDataEntry(x: Double(thisItem.date + 1), y: yTop, data: labelText) BGChart.data?.dataSets[dataIndex].addEntry(startDot) - if UserDefaultsRepository.smallGraphTreatments.value { + if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(startDot) } // End Dot let endDot = ChartDataEntry(x: Double(thisItem.endDate - 2), y: yTop, data: labelText) BGChart.data?.dataSets[dataIndex].addEntry(endDot) - if UserDefaultsRepository.smallGraphTreatments.value { + if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(endDot) } // Post end dot let postEndDot = ChartDataEntry(x: Double(thisItem.endDate - 1), y: yBottom, data: labelText) BGChart.data?.dataSets[dataIndex].addEntry(postEndDot) - if UserDefaultsRepository.smallGraphTreatments.value { + if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].addEntry(postEndDot) } } @@ -1679,7 +1679,7 @@ extension MainViewController { BGChart.data?.dataSets[dataIndex].notifyDataSetChanged() BGChart.data?.notifyDataChanged() BGChart.notifyDataSetChanged() - if UserDefaultsRepository.smallGraphTreatments.value { + if Storage.shared.smallGraphTreatments.value { BGChartFull.data?.dataSets[dataIndex].notifyDataSetChanged() BGChartFull.data?.notifyDataChanged() BGChartFull.notifyDataSetChanged() @@ -1704,7 +1704,7 @@ extension MainViewController { func addEntryToCharts(entry: ChartDataEntry, chart: LineChartDataSet, smallChart: LineChartDataSet?) { chart.addEntry(entry) - if UserDefaultsRepository.smallGraphTreatments.value, let smallChart = smallChart { + if Storage.shared.smallGraphTreatments.value, let smallChart = smallChart { smallChart.addEntry(entry) } } @@ -1722,7 +1722,7 @@ extension MainViewController { mainChartDataSet.clear() var smallChartDataSet: LineChartDataSet? - if UserDefaultsRepository.smallGraphTreatments.value, + if Storage.shared.smallGraphTreatments.value, let smallChartData = BGChartFull.lineData, smallChartData.dataSets.count > dataIndex, let smallDataSet = smallChartData.dataSets[dataIndex] as? LineChartDataSet @@ -1871,16 +1871,16 @@ extension MainViewController { smallChart.clear() var colors = [NSUIColor]() - let maxBGOffset: Float = 20 + let maxBGOffset: Double = 20 for i in 0 ..< predictionData.count { let predictionVal = Double(predictionData[i].sgv) - if Float(predictionVal) > topPredictionBG - maxBGOffset { - topPredictionBG = Float(predictionVal) + maxBGOffset + if predictionVal > topPredictionBG - maxBGOffset { + topPredictionBG = predictionVal + maxBGOffset } if i == 0 { - if UserDefaultsRepository.showDots.value { + if Storage.shared.showDots.value { colors.append(color.withAlphaComponent(0.0)) } else { colors.append(color.withAlphaComponent(1.0)) diff --git a/LoopFollow/Controllers/MainViewController+updateStats.swift b/LoopFollow/Controllers/MainViewController+updateStats.swift index 605417d47..ca0a1eac2 100644 --- a/LoopFollow/Controllers/MainViewController+updateStats.swift +++ b/LoopFollow/Controllers/MainViewController+updateStats.swift @@ -10,7 +10,7 @@ extension MainViewController { func updateStats() { if bgData.count > 0 { var lastDayOfData = bgData - let graphHours = 24 * UserDefaultsRepository.downloadDays.value + let graphHours = 24 * Storage.shared.downloadDays.value // If we loaded more than 1 day of data, only use the last day for the stats if graphHours > 24 { let oneDayAgo = dateTimeUtils.getTimeIntervalNHoursAgo(N: 24) diff --git a/LoopFollow/Controllers/Nightscout/BGData.swift b/LoopFollow/Controllers/Nightscout/BGData.swift index ce32d3ff9..cc799fa02 100644 --- a/LoopFollow/Controllers/Nightscout/BGData.swift +++ b/LoopFollow/Controllers/Nightscout/BGData.swift @@ -10,7 +10,7 @@ extension MainViewController { func webLoadDexShare() { // Dexcom Share only returns 24 hrs of data as of now // Requesting more just for consistency with NS - let graphHours = 24 * UserDefaultsRepository.downloadDays.value + let graphHours = 24 * Storage.shared.downloadDays.value let count = graphHours * 12 dexShare?.fetchData(count) { err, result in if let error = err { @@ -52,8 +52,8 @@ extension MainViewController { var parameters: [String: String] = [:] let utcISODateFormatter = ISO8601DateFormatter() - let date = Calendar.current.date(byAdding: .day, value: -1 * UserDefaultsRepository.downloadDays.value, to: Date())! - parameters["count"] = "\(UserDefaultsRepository.downloadDays.value * 2 * 24 * 60 / 5)" + let date = Calendar.current.date(byAdding: .day, value: -1 * Storage.shared.downloadDays.value, to: Date())! + parameters["count"] = "\(Storage.shared.downloadDays.value * 2 * 24 * 60 / 5)" parameters["find[dateString][$gte]"] = utcISODateFormatter.string(from: date) // Exclude 'cal' entries @@ -118,7 +118,7 @@ extension MainViewController { /// Processes incoming BG data. func ProcessDexBGData(data: [ShareGlucoseData], sourceName: String) { - let graphHours = 24 * UserDefaultsRepository.downloadDays.value + let graphHours = 24 * Storage.shared.downloadDays.value guard !data.isEmpty else { LogManager.shared.log(category: .nightscout, message: "No bg data received. Skipping processing.", limitIdentifier: "No bg data received. Skipping processing.") diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift index e132207a3..0a5376ac4 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift @@ -69,7 +69,7 @@ extension MainViewController { if UserDefaultsRepository.downloadPrediction.value, previousLastLoopTime < lastLoopTime { predictionData.removeAll() var predictionTime = lastLoopTime - let toLoad = Int(UserDefaultsRepository.predictionToLoad.value * 12) + let toLoad = Int(Storage.shared.predictionToLoad.value * 12) var i = 0 while i <= toLoad { if i < prediction.count { diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift index 7443d4b33..4b8c72455 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift @@ -181,7 +181,7 @@ extension MainViewController { let predictioncolor = UIColor.systemGray PredictionLabel.textColor = predictioncolor - topPredictionBG = UserDefaultsRepository.minBGScale.value + topPredictionBG = Storage.shared.minBGScale.value if let predbgdata = predBGsData { let predictionTypes: [(type: String, colorName: String, dataIndex: Int)] = [ ("ZT", "ZT", 12), @@ -197,7 +197,7 @@ extension MainViewController { var predictionData = [ShareGlucoseData]() if let graphdata = predbgdata[type] as? [Double] { var predictionTime = updatedTime ?? Date().timeIntervalSince1970 - let toLoad = Int(UserDefaultsRepository.predictionToLoad.value * 12) + let toLoad = Int(Storage.shared.predictionToLoad.value * 12) for i in 0 ... toLoad { if i < graphdata.count { diff --git a/LoopFollow/Controllers/Nightscout/Profile.swift b/LoopFollow/Controllers/Nightscout/Profile.swift index 7a63dc2cf..fdc444ce5 100644 --- a/LoopFollow/Controllers/Nightscout/Profile.swift +++ b/LoopFollow/Controllers/Nightscout/Profile.swift @@ -36,7 +36,7 @@ extension MainViewController { var basalSegments: [DataStructs.basalProfileSegment] = [] - let graphHours = 24 * UserDefaultsRepository.downloadDays.value + let graphHours = 24 * Storage.shared.downloadDays.value // Build scheduled basal segments from right to left by // moving pointers to the current midnight and current basal var midnight = dateTimeUtils.getTimeIntervalMidnightToday() @@ -69,7 +69,7 @@ extension MainViewController { var firstPass = true // Runs the scheduled basal to the end of the prediction line - var predictionEndTime = dateTimeUtils.getNowTimeIntervalUTC() + (3600 * UserDefaultsRepository.predictionToLoad.value) + var predictionEndTime = dateTimeUtils.getNowTimeIntervalUTC() + (3600 * Storage.shared.predictionToLoad.value) basalScheduleData.removeAll() for i in 0 ..< basalSegments.count { diff --git a/LoopFollow/Controllers/Nightscout/Treatments.swift b/LoopFollow/Controllers/Nightscout/Treatments.swift index 0eef90853..aba121c37 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments.swift @@ -10,7 +10,7 @@ extension MainViewController { func WebLoadNSTreatments() { if !UserDefaultsRepository.downloadTreatments.value { return } - let startTimeString = dateTimeUtils.getDateTimeString(addingDays: -1 * UserDefaultsRepository.downloadDays.value) + let startTimeString = dateTimeUtils.getDateTimeString(addingDays: -1 * Storage.shared.downloadDays.value) let currentTimeString = dateTimeUtils.getDateTimeString(addingHours: 6) let parameters: [String: String] = [ "find[created_at][$gte]": startTimeString, diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift b/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift index 650cb3bd3..3bb3e0b38 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift @@ -39,7 +39,7 @@ extension MainViewController { offset = bolusTime.offset ? 70 : 20 } - if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (3600 * UserDefaultsRepository.predictionToLoad.value)) { + if dateTimeStamp < (dateTimeUtils.getNowTimeIntervalUTC() + (3600 * Storage.shared.predictionToLoad.value)) { // Make the dot let dot = carbGraphStruct(value: Double(carbs), date: Double(dateTimeStamp), sgv: Int(sgv.sgv + Double(offset)), absorptionTime: absorptionTime) carbData.append(dot) @@ -55,7 +55,6 @@ extension MainViewController { var totalCarbs = 0.0 let calendar = Calendar.current - let now = Date() for entry in entries { var carbDate = "" diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift b/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift index 1d4c90336..83fcacb42 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift @@ -21,8 +21,8 @@ extension MainViewController { } let now = Date().timeIntervalSince1970 - let maxEndDate = now + UserDefaultsRepository.predictionToLoad.value * 3600 - let graphHorizon = dateTimeUtils.getTimeIntervalNHoursAgo(N: 24 * UserDefaultsRepository.downloadDays.value) + let maxEndDate = now + Storage.shared.predictionToLoad.value * 3600 + let graphHorizon = dateTimeUtils.getTimeIntervalNHoursAgo(N: 24 * Storage.shared.downloadDays.value) for i in 0 ..< sorted.count { let e = sorted[i] diff --git a/LoopFollow/Controllers/Nightscout/Treatments/TemporaryTarget.swift b/LoopFollow/Controllers/Nightscout/Treatments/TemporaryTarget.swift index ec3d3fbf0..3e13df678 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/TemporaryTarget.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/TemporaryTarget.swift @@ -17,7 +17,7 @@ extension MainViewController { guard let parsedDate = NightscoutUtils.parseDate(dateStr) else { continue } var dateTimeStamp = parsedDate.timeIntervalSince1970 - let graphHours = 24 * UserDefaultsRepository.downloadDays.value + let graphHours = 24 * Storage.shared.downloadDays.value if dateTimeStamp < dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) { dateTimeStamp = dateTimeUtils.getTimeIntervalNHoursAgo(N: graphHours) } diff --git a/LoopFollow/Controllers/Stats.swift b/LoopFollow/Controllers/Stats.swift index f1408dd87..10d74a0fa 100644 --- a/LoopFollow/Controllers/Stats.swift +++ b/LoopFollow/Controllers/Stats.swift @@ -27,9 +27,9 @@ class StatsData { for i in 0 ..< bgData.count { // Set low/range/high counts for pie chart and %'s - if Float(bgData[i].sgv) <= UserDefaultsRepository.lowLine.value { + if Double(bgData[i].sgv) <= Storage.shared.lowLine.value { countLow += 1 - } else if Float(bgData[i].sgv) >= UserDefaultsRepository.highLine.value { + } else if Double(bgData[i].sgv) >= Storage.shared.highLine.value { countHigh += 1 } else { countRange += 1 diff --git a/LoopFollow/Helpers/BinaryFloatingPoint+localized.swift b/LoopFollow/Helpers/BinaryFloatingPoint+localized.swift new file mode 100644 index 000000000..f0d58cb14 --- /dev/null +++ b/LoopFollow/Helpers/BinaryFloatingPoint+localized.swift @@ -0,0 +1,13 @@ +// LoopFollow +// BinaryFloatingPoint+localized.swift +// Created by Jonas Björkert on 2025-05-26. + +import Foundation + +extension BinaryFloatingPoint { + func localized(maxFractionDigits: Int) -> String { + let style = FloatingPointFormatStyle() + .precision(.fractionLength(0 ... maxFractionDigits)) + return style.format(self) + } +} diff --git a/LoopFollow/Helpers/Views/SettingsStepperRow.swift b/LoopFollow/Helpers/Views/SettingsStepperRow.swift new file mode 100644 index 000000000..5f84d7d0c --- /dev/null +++ b/LoopFollow/Helpers/Views/SettingsStepperRow.swift @@ -0,0 +1,43 @@ +// LoopFollow +// SettingsStepperRow.swift +// Created by Jonas Björkert on 2025-05-26. + +import SwiftUI + +struct SettingsStepperRow: View + where Value: Strideable & Comparable, + Value.Stride: SignedNumeric & Comparable +{ + let title: String + let range: ClosedRange + let step: Value.Stride + + private let format: (Value) -> String + + @Binding private var value: Value + + init( + title: String, + range: ClosedRange, + step: Value.Stride, + value: Binding, + format: @escaping (Value) -> String = { "\($0)" } + ) { + self.title = title + self.range = range + self.step = step + _value = value + self.format = format + } + + var body: some View { + Stepper(value: $value, in: range, step: step) { + HStack { + Text(title) + Spacer() + Text(format(value)) + .foregroundColor(.secondary) + } + } + } +} diff --git a/LoopFollow/Settings/GraphSettingsView.swift b/LoopFollow/Settings/GraphSettingsView.swift new file mode 100644 index 000000000..c3922de56 --- /dev/null +++ b/LoopFollow/Settings/GraphSettingsView.swift @@ -0,0 +1,152 @@ +// LoopFollow +// GraphSettingsView.swift +// Created by Jonas Björkert on 2025-05-26. + +import SwiftUI + +struct GraphSettingsView: View { + @Environment(\.presentationMode) private var presentationMode + + // ── Stored settings ────────────────────────────────────────────────────── + @ObservedObject private var showDots = Storage.shared.showDots + @ObservedObject private var showLines = Storage.shared.showLines + @ObservedObject private var showValues = Storage.shared.showValues + @ObservedObject private var showAbsorption = Storage.shared.showAbsorption + @ObservedObject private var showDIALines = Storage.shared.showDIALines + @ObservedObject private var show30MinLine = Storage.shared.show30MinLine + @ObservedObject private var show90MinLine = Storage.shared.show90MinLine + @ObservedObject private var showMidnightLines = Storage.shared.showMidnightLines + @ObservedObject private var smallGraphTreatments = Storage.shared.smallGraphTreatments + + @ObservedObject private var smallGraphHeight = Storage.shared.smallGraphHeight + @ObservedObject private var predictionToLoad = Storage.shared.predictionToLoad + @ObservedObject private var minBasalScale = Storage.shared.minBasalScale + @ObservedObject private var minBGScale = Storage.shared.minBGScale + @ObservedObject private var lowLine = Storage.shared.lowLine + @ObservedObject private var highLine = Storage.shared.highLine + @ObservedObject private var downloadDays = Storage.shared.downloadDays + + // ───────────────────────────────────────────────────────────────────────── + private var nightscoutEnabled: Bool { IsNightscoutEnabled() } + + var body: some View { + NavigationStack { + Form { + // ── Graph Display ──────────────────────────────────────────── + Section("Graph Display") { + Toggle("Display Dots", isOn: $showDots.value) + .onChange(of: showDots.value) { _ in markDirty() } + + Toggle("Display Lines", isOn: $showLines.value) + .onChange(of: showLines.value) { _ in markDirty() } + + if nightscoutEnabled { + Toggle("Show DIA Lines", isOn: $showDIALines.value) + .onChange(of: showDIALines.value) { _ in markDirty() } + + Toggle("Show −30 min Line", isOn: $show30MinLine.value) + .onChange(of: show30MinLine.value) { _ in markDirty() } + + Toggle("Show −90 min Line", isOn: $show90MinLine.value) + .onChange(of: show90MinLine.value) { _ in markDirty() } + } + + Toggle("Show Midnight Lines", isOn: $showMidnightLines.value) + .onChange(of: showMidnightLines.value) { _ in markDirty() } + } + + // ── Treatments ─────────────────────────────────────────────── + if nightscoutEnabled { + Section("Treatments") { + Toggle("Show Carb/Bolus Values", isOn: $showValues.value) + Toggle("Show Carb Absorption", isOn: $showAbsorption.value) + Toggle("Treatments on Small Graph", + isOn: $smallGraphTreatments.value) + } + } + + // ── Small Graph ────────────────────────────────────────────── + Section("Small Graph") { + SettingsStepperRow( + title: "Height", + range: 40 ... 80, + step: 5, + value: $smallGraphHeight.value, + format: { "\(Int($0)) pt" } + ) + .onChange(of: smallGraphHeight.value) { _ in markDirty() } + } + + // ── Prediction ─────────────────────────────────────────────── + if nightscoutEnabled { + Section("Prediction") { + SettingsStepperRow( + title: "Hours of Prediction", + range: 0 ... 6, + step: 0.25, + value: $predictionToLoad.value, + format: { "\($0.localized(maxFractionDigits: 2)) h" } + ) + } + } + + // ── Basal / BG scale ───────────────────────────────────────── + if nightscoutEnabled { + Section("Basal / BG Scale") { + SettingsStepperRow( + title: "Min Basal", + range: 0.5 ... 20, + step: 0.5, + value: $minBasalScale.value, + format: { "\($0.localized(maxFractionDigits: 1)) U/h" } + ) + + BGPicker( + title: "Min BG Scale", + range: 40 ... 400, + value: $minBGScale.value + ) + .onChange(of: minBGScale.value) { _ in markDirty() } + } + } + + // ── Target lines ───────────────────────────────────────────── + Section("Target Lines") { + BGPicker(title: "Low BG Line", + range: 40 ... 120, + value: $lowLine.value) + .onChange(of: lowLine.value) { _ in markDirty() } + + BGPicker(title: "High BG Line", + range: 120 ... 400, + value: $highLine.value) + .onChange(of: highLine.value) { _ in markDirty() } + } + + // ── History window ─────────────────────────────────────────── + if nightscoutEnabled { + Section("History") { + SettingsStepperRow( + title: "Show Days Back", + range: 1 ... 4, + step: 1, + value: $downloadDays.value, + format: { "\(Int($0)) d" } + ) + } + } + } + .navigationTitle("Graph Settings") + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Done") { presentationMode.wrappedValue.dismiss() } + } + } + } + } + + /// Marks the chart as needing a redraw + private func markDirty() { + Observable.shared.chartSettingsChanged.value = true + } +} diff --git a/LoopFollow/Storage/Observable.swift b/LoopFollow/Storage/Observable.swift index 324a8cf45..d42e793f7 100644 --- a/LoopFollow/Storage/Observable.swift +++ b/LoopFollow/Storage/Observable.swift @@ -28,5 +28,7 @@ class Observable { var debug = ObservableValue(default: false) + var chartSettingsChanged = ObservableValue(default: false) + private init() {} } diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift new file mode 100644 index 000000000..125156bdd --- /dev/null +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -0,0 +1,180 @@ +// LoopFollow +// Storage+Migrate.swift +// Created by Jonas Björkert on 2025-05-26. + +import Foundation + +extension Storage { + func migrate() { + // Helper: 1-to-1 type ----------------------------------------------------------------- + func move( + _ legacy: @autoclosure () -> UserDefaultsValue, + into newValue: StorageValue + ) { + let item = legacy() + guard item.exists else { return } + newValue.value = item.value + item.setNil(key: item.key) + } + + // Helper: Float → Double ------------------------------------------------------------ + func moveFloatToDouble( + _ legacy: @autoclosure () -> UserDefaultsValue, + into newValue: StorageValue + ) { + let item = legacy() + guard item.exists else { return } + newValue.value = Double(item.value) + item.setNil(key: item.key) + } + + if !UserDefaultsRepository.backgroundRefresh.value { + Storage.shared.backgroundRefreshType.value = .none + UserDefaultsRepository.backgroundRefresh.value = true + } + + // Remove this in a year later than the release of the new Alarms [BEGIN] + let legacyColorBGText = UserDefaultsValue(key: "colorBGText", default: true) + if legacyColorBGText.exists { + Storage.shared.colorBGText.value = legacyColorBGText.value + legacyColorBGText.setNil(key: "colorBGText") + } + + let legacyAppBadge = UserDefaultsValue(key: "appBadge", default: true) + if legacyAppBadge.exists { + Storage.shared.appBadge.value = legacyAppBadge.value + legacyAppBadge.setNil(key: "appBadge") + } + + let legacyForceDarkMode = UserDefaultsValue(key: "forceDarkMode", default: true) + if legacyForceDarkMode.exists { + Storage.shared.forceDarkMode.value = legacyForceDarkMode.value + legacyForceDarkMode.setNil(key: "forceDarkMode") + } + + let legacyShowStats = UserDefaultsValue(key: "showStats", default: true) + if legacyShowStats.exists { + Storage.shared.showStats.value = legacyShowStats.value + legacyShowStats.setNil(key: "showStats") + } + + let legacyUseIFCC = UserDefaultsValue(key: "useIFCC", default: false) + if legacyUseIFCC.exists { + Storage.shared.useIFCC.value = legacyUseIFCC.value + legacyUseIFCC.setNil(key: "useIFCC") + } + + let legacyShowSmallGraph = UserDefaultsValue(key: "showSmallGraph", default: true) + if legacyShowSmallGraph.exists { + Storage.shared.showSmallGraph.value = legacyShowSmallGraph.value + legacyShowSmallGraph.setNil(key: "showSmallGraph") + } + + let legacyScreenlockSwitchState = UserDefaultsValue(key: "screenlockSwitchState", default: true) + if legacyScreenlockSwitchState.exists { + Storage.shared.screenlockSwitchState.value = legacyScreenlockSwitchState.value + legacyScreenlockSwitchState.setNil(key: "screenlockSwitchState") + } + + let legacyShowDisplayName = UserDefaultsValue(key: "showDisplayName", default: false) + if legacyShowDisplayName.exists { + Storage.shared.showDisplayName.value = legacyShowDisplayName.value + legacyShowDisplayName.setNil(key: "showDisplayName") + } + + let legacySpeakBG = UserDefaultsValue(key: "speakBG", default: false) + if legacySpeakBG.exists { + Storage.shared.speakBG.value = legacySpeakBG.value + legacySpeakBG.setNil(key: "speakBG") + } + + let legacySpeakBGAlways = UserDefaultsValue(key: "speakBGAlways", default: true) + if legacySpeakBGAlways.exists { + Storage.shared.speakBGAlways.value = legacySpeakBGAlways.value + legacySpeakBGAlways.setNil(key: "speakBGAlways") + } + + let legacySpeakLowBG = UserDefaultsValue(key: "speakLowBG", default: false) + if legacySpeakLowBG.exists { + Storage.shared.speakLowBG.value = legacySpeakLowBG.value + legacySpeakLowBG.setNil(key: "speakLowBG") + } + + let legacySpeakProactiveLowBG = UserDefaultsValue(key: "speakProactiveLowBG", default: false) + if legacySpeakProactiveLowBG.exists { + Storage.shared.speakProactiveLowBG.value = legacySpeakProactiveLowBG.value + legacySpeakProactiveLowBG.setNil(key: "speakProactiveLowBG") + } + + let legacySpeakFastDropDelta = UserDefaultsValue(key: "speakFastDropDelta", default: 10.0) + if legacySpeakFastDropDelta.exists { + Storage.shared.speakFastDropDelta.value = Double(legacySpeakFastDropDelta.value) + legacySpeakFastDropDelta.setNil(key: "speakFastDropDelta") + } + + let legacySpeakLowBGLimit = UserDefaultsValue(key: "speakLowBGLimit", default: 72.0) + if legacySpeakLowBGLimit.exists { + Storage.shared.speakLowBGLimit.value = Double(legacySpeakLowBGLimit.value) + legacySpeakLowBGLimit.setNil(key: "speakLowBGLimit") + } + + let legacySpeakHighBGLimit = UserDefaultsValue(key: "speakHighBGLimit", default: 180.0) + if legacySpeakHighBGLimit.exists { + Storage.shared.speakHighBGLimit.value = Double(legacySpeakHighBGLimit.value) + legacySpeakHighBGLimit.setNil(key: "speakHighBGLimit") + } + + let legacySpeakHighBG = UserDefaultsValue(key: "speakHighBG", default: false) + if legacySpeakHighBG.exists { + Storage.shared.speakHighBG.value = legacySpeakHighBG.value + legacySpeakHighBG.setNil(key: "speakHighBG") + } + + let legacySpeakLanguage = UserDefaultsValue(key: "speakLanguage", default: "en") + if legacySpeakLanguage.exists { + Storage.shared.speakLanguage.value = legacySpeakLanguage.value + legacySpeakLanguage.setNil(key: "speakLanguage") + } + + // ── General (done earlier, but safe to repeat) ── + move(UserDefaultsValue(key: "colorBGText", default: true), into: Storage.shared.colorBGText) + move(UserDefaultsValue(key: "appBadge", default: true), into: appBadge) + move(UserDefaultsValue(key: "forceDarkMode", default: false), into: forceDarkMode) + move(UserDefaultsValue(key: "showStats", default: true), into: showStats) + move(UserDefaultsValue(key: "useIFCC", default: false), into: useIFCC) + move(UserDefaultsValue(key: "showSmallGraph", default: true), into: showSmallGraph) + move(UserDefaultsValue(key: "screenlockSwitchState", default: false), into: screenlockSwitchState) + move(UserDefaultsValue(key: "showDisplayName", default: false), into: showDisplayName) + + // ── Speak-BG ── + move(UserDefaultsValue(key: "speakBG", default: false), into: speakBG) + move(UserDefaultsValue(key: "speakBGAlways", default: true), into: speakBGAlways) + move(UserDefaultsValue(key: "speakLowBG", default: false), into: speakLowBG) + move(UserDefaultsValue(key: "speakProactiveLowBG", default: false), into: speakProactiveLowBG) + move(UserDefaultsValue(key: "speakHighBG", default: false), into: speakHighBG) + moveFloatToDouble(UserDefaultsValue(key: "speakLowBGLimit", default: 72.0), into: speakLowBGLimit) + moveFloatToDouble(UserDefaultsValue(key: "speakHighBGLimit", default: 180.0), into: speakHighBGLimit) + moveFloatToDouble(UserDefaultsValue(key: "speakFastDropDelta", default: 10.0), into: speakFastDropDelta) + move(UserDefaultsValue(key: "speakLanguage", default: "en"), into: speakLanguage) + + // ── Graph ── + move(UserDefaultsValue(key: "showDots", default: true), into: showDots) + move(UserDefaultsValue(key: "showLines", default: true), into: showLines) + move(UserDefaultsValue(key: "showValues", default: true), into: showValues) + move(UserDefaultsValue(key: "showAbsorption", default: true), into: showAbsorption) + move(UserDefaultsValue(key: "showDIAMarkers", default: true), into: showDIALines) + move(UserDefaultsValue(key: "show30MinLine", default: false), into: show30MinLine) + move(UserDefaultsValue(key: "show90MinLine", default: false), into: show90MinLine) + move(UserDefaultsValue(key: "showMidnightMarkers", default: false), into: showMidnightLines) + move(UserDefaultsValue(key: "smallGraphTreatments", default: true), into: smallGraphTreatments) + + move(UserDefaultsValue(key: "smallGraphHeight", default: 40), into: smallGraphHeight) + move(UserDefaultsValue(key: "predictionToLoad", default: 1.0), into: predictionToLoad) + move(UserDefaultsValue(key: "minBasalScale", default: 5.0), into: minBasalScale) + moveFloatToDouble(UserDefaultsValue(key: "minBGScale", default: 250.0), into: minBGScale) + moveFloatToDouble(UserDefaultsValue(key: "lowLine", default: 70.0), into: lowLine) + moveFloatToDouble(UserDefaultsValue(key: "highLine", default: 180.0), into: highLine) + move(UserDefaultsValue(key: "downloadDays", default: 1), into: downloadDays) + // Remove this in a year later than the release of the new Alarms [END] + } +} diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index 5035f8816..84933405e 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -78,9 +78,28 @@ class Storage { var speakHighBGLimit = StorageValue(key: "speakHighBGLimit", defaultValue: 180.0) var speakHighBG = StorageValue(key: "speakHighBG", defaultValue: false) var speakLanguage = StorageValue(key: "speakLanguage", defaultValue: "en") - // General Settings [END] + // Graph Settings [BEGIN] + var showDots = StorageValue(key: "showDots", defaultValue: true) + var showLines = StorageValue(key: "showLines", defaultValue: true) + var showValues = StorageValue(key: "showValues", defaultValue: true) + var showAbsorption = StorageValue(key: "showAbsorption", defaultValue: true) + var showDIALines = StorageValue(key: "showDIAMarkers", defaultValue: true) + var show30MinLine = StorageValue(key: "show30MinLine", defaultValue: false) + var show90MinLine = StorageValue(key: "show90MinLine", defaultValue: false) + var showMidnightLines = StorageValue(key: "showMidnightMarkers", defaultValue: false) + var smallGraphTreatments = StorageValue(key: "smallGraphTreatments", defaultValue: true) + + var smallGraphHeight = StorageValue(key: "smallGraphHeight", defaultValue: 40) + var predictionToLoad = StorageValue(key: "predictionToLoad", defaultValue: 1.0) + var minBasalScale = StorageValue(key: "minBasalScale", defaultValue: 5.0) + var minBGScale = StorageValue(key: "minBGScale", defaultValue: 250.0) + var lowLine = StorageValue(key: "lowLine", defaultValue: 70.0) + var highLine = StorageValue(key: "highLine", defaultValue: 180.0) + var downloadDays = StorageValue(key: "downloadDays", defaultValue: 1) + // Graph Settings [END] + static let shared = Storage() private init() {} } diff --git a/LoopFollow/Storage/UserDefaults.swift b/LoopFollow/Storage/UserDefaults.swift index 0f188c67e..74fee21f1 100644 --- a/LoopFollow/Storage/UserDefaults.swift +++ b/LoopFollow/Storage/UserDefaults.swift @@ -81,30 +81,12 @@ class UserDefaultsRepository { // Graph Settings static let chartScaleX = UserDefaultsValue(key: "chartScaleX", default: 18.0) - static let showDots = UserDefaultsValue(key: "showDots", default: true) - static let smallGraphTreatments = UserDefaultsValue(key: "smallGraphTreatments", default: true) - static let showValues = UserDefaultsValue(key: "showValues", default: true) - static let showAbsorption = UserDefaultsValue(key: "showAbsorption", default: true) - static let showLines = UserDefaultsValue(key: "showLines", default: true) static let hoursToLoad = UserDefaultsValue(key: "hoursToLoad", default: 24) - static let predictionToLoad = UserDefaultsValue(key: "predictionToLoad", default: 1) - static let minBasalScale = UserDefaultsValue(key: "minBasalScale", default: 5.0) - static let minBGScale = UserDefaultsValue(key: "minBGScale", default: 250.0) - static let showDIALines = UserDefaultsValue(key: "showDIAMarkers", default: true) - static let show30MinLine = UserDefaultsValue(key: "show30MinLine", default: false) - static let show90MinLine = UserDefaultsValue(key: "show90MinLine", default: false) - static let showMidnightLines = UserDefaultsValue(key: "showMidnightMarkers", default: false) - static let lowLine = UserDefaultsValue(key: "lowLine", default: 70.0) - static let highLine = UserDefaultsValue(key: "highLine", default: 180.0) - static let smallGraphHeight = UserDefaultsValue(key: "smallGraphHeight", default: 40) // Deprecated, used to detect if backgroundRefresh was set to off. TODO: Remove in the beginning of 2026 static let backgroundRefresh = UserDefaultsValue(key: "backgroundRefresh", default: true) - static let dimScreenWhenIdle = UserDefaultsValue(key: "dimScreenWhenIdle", default: 0) - // Advanced Settings - // static let onlyDownloadBG = UserDefaultsValue(key: "onlyDownloadBG", default: false) static let downloadTreatments = UserDefaultsValue(key: "downloadTreatments", default: true) static let downloadPrediction = UserDefaultsValue(key: "downloadPrediction", default: true) static let graphOtherTreatments = UserDefaultsValue(key: "graphOtherTreatments", default: true) @@ -112,7 +94,6 @@ class UserDefaultsRepository { static let graphBolus = UserDefaultsValue(key: "graphBolus", default: true) static let graphCarbs = UserDefaultsValue(key: "graphCarbs", default: true) static let bgUpdateDelay = UserDefaultsValue(key: "bgUpdateDelay", default: 10) - static let downloadDays = UserDefaultsValue(key: "downloadDays", default: 1) // Watch Calendar Settings static let calendarIdentifier = UserDefaultsValue(key: "calendarIdentifier", default: "") diff --git a/LoopFollow/ViewControllers/GraphSettingsViewController.swift b/LoopFollow/ViewControllers/GraphSettingsViewController.swift deleted file mode 100644 index 430578f1d..000000000 --- a/LoopFollow/ViewControllers/GraphSettingsViewController.swift +++ /dev/null @@ -1,286 +0,0 @@ -// LoopFollow -// GraphSettingsViewController.swift -// Created by Jose Paredes on 2020-07-17. - -import Eureka -import EventKit -import EventKitUI -import Foundation - -class GraphSettingsViewController: FormViewController { - var appStateController: AppStateController? - - override func viewDidLoad() { - super.viewDidLoad() - if Storage.shared.forceDarkMode.value { - overrideUserInterfaceStyle = .dark - } - - buildGraphSettings() - - showHideNSDetails() - } - - func showHideNSDetails() { - var isHidden = false - var isEnabled = true - if !IsNightscoutEnabled() { - isHidden = true - isEnabled = false - } - - if let row1 = form.rowBy(tag: "predictionToLoad") as? StepperRow { - row1.hidden = .function(["hide"]) { _ in - isHidden - } - row1.evaluateHidden() - } - if let row2 = form.rowBy(tag: "smallGraphTreatments") as? SwitchRow { - row2.hidden = .function(["hide"]) { _ in - isHidden - } - row2.evaluateHidden() - } - if let row3 = form.rowBy(tag: "minBasalScale") as? StepperRow { - row3.hidden = .function(["hide"]) { _ in - isHidden - } - row3.evaluateHidden() - } - - if let row4 = form.rowBy(tag: "showValues") as? SwitchRow { - row4.hidden = .function(["hide"]) { _ in - isHidden - } - row4.evaluateHidden() - } - if let row5 = form.rowBy(tag: "showAbsorption") as? SwitchRow { - row5.hidden = .function(["hide"]) { _ in - isHidden - } - row5.evaluateHidden() - } - } - - private func buildGraphSettings() { - form - +++ Section("Graph Settings") - - <<< SwitchRow("switchRowDots") { row in - row.title = "Display Dots" - row.value = UserDefaultsRepository.showDots.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.showDots.value = value - // Force main screen update - // guard let mainScreen = self?.tabBarController!.viewControllers?[0] as? MainViewController else { return } - // mainScreen.updateBGGraphSettings() - - // tell main screen that grap needs updating - if let appState = self!.appStateController { - appState.chartSettingsChanged = true - } - } - <<< SwitchRow("switchRowLines") { row in - row.title = "Display Lines" - row.value = UserDefaultsRepository.showLines.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.showLines.value = value - // Force main screen update - // guard let mainScreen = self?.tabBarController!.viewControllers?[0] as? MainViewController else { return } - // mainScreen.updateBGGraphSettings() - - if let appState = self!.appStateController { - appState.chartSettingsChanged = true - } - } - <<< SwitchRow("showValues") { row in - row.title = "Show Carb/Bolus Values" - row.value = UserDefaultsRepository.showValues.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.showValues.value = value - } - <<< SwitchRow("showAbsorption") { row in - row.title = "Show Carb Absorption" - row.value = UserDefaultsRepository.showAbsorption.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.showAbsorption.value = value - } - <<< SwitchRow("showDIAMarkers") { row in - row.title = "Show DIA Lines" - row.value = UserDefaultsRepository.showDIALines.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.showDIALines.value = value - - // tell main screen that graph needs updating - if let appState = self!.appStateController { - appState.chartSettingsChanged = true - } - } - <<< SwitchRow("show30MinLine") { row in - row.title = "Show -30 min line" - row.value = UserDefaultsRepository.show30MinLine.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.show30MinLine.value = value - - // Tell the main screen that graph needs updating - if let appState = self!.appStateController { - appState.chartSettingsChanged = true - } - } - <<< SwitchRow("show90MinLine") { row in - row.title = "Show -90 min line" - row.value = UserDefaultsRepository.show90MinLine.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.show90MinLine.value = value - - // Tell the main screen that graph needs updating - if let appState = self!.appStateController { - appState.chartSettingsChanged = true - } - } - <<< SwitchRow("smallGraphTreatments") { row in - row.title = "Treatments on Small Graph" - row.value = UserDefaultsRepository.smallGraphTreatments.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.smallGraphTreatments.value = value - } - <<< StepperRow("smallGraphHeight") { row in - row.title = "Small Graph Height" - row.cell.stepper.stepValue = 5 - row.cell.stepper.minimumValue = 40 - row.cell.stepper.maximumValue = 80 - row.value = Double(UserDefaultsRepository.smallGraphHeight.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.smallGraphHeight.value = Int(value) - - if let appState = self!.appStateController { - appState.chartSettingsChanged = true - } - } - <<< StepperRow("predictionToLoad") { row in - row.title = "Hours of Prediction" - row.cell.stepper.stepValue = 0.25 - row.cell.stepper.minimumValue = 0.0 - row.cell.stepper.maximumValue = 6.0 - row.value = Double(UserDefaultsRepository.predictionToLoad.value) - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.predictionToLoad.value = value - } - <<< StepperRow("minBGScale") { row in - row.title = "Min BG Scale" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = Double(UserDefaultsRepository.highLine.value) - row.cell.stepper.maximumValue = 400 - row.value = Double(UserDefaultsRepository.minBGScale.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return Localizer.toDisplayUnits(String(value)) - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.minBGScale.value = Float(value) - } - - <<< StepperRow("minBasalScale") { row in - row.title = "Min Basal Scale" - row.cell.stepper.stepValue = 0.5 - row.cell.stepper.minimumValue = 0.5 - row.cell.stepper.maximumValue = 20 - row.value = Double(UserDefaultsRepository.minBasalScale.value) - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.minBasalScale.value = value - } - <<< StepperRow("lowLine") { row in - row.title = "Low BG Display Value" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 40 - row.cell.stepper.maximumValue = 120 - row.value = Double(UserDefaultsRepository.lowLine.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return Localizer.toDisplayUnits(String(value)) - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.lowLine.value = Float(value) - // Force main screen update - // guard let mainScreen = self?.tabBarController!.viewControllers?[0] as? MainViewController else { return } - // mainScreen.updateBGGraphSettings() - - // tell main screen to update - if let appState = self!.appStateController { - appState.chartSettingsChanged = true - } - } - <<< StepperRow("highLine") { row in - row.title = "High BG Display Value" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 120 - row.cell.stepper.maximumValue = 400 - row.value = Double(UserDefaultsRepository.highLine.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return Localizer.toDisplayUnits(String(value)) - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.highLine.value = Float(value) - // Force main screen update - // guard let mainScreen = self?.tabBarController!.viewControllers?[0] as? MainViewController else { return } - // mainScreen.updateBGGraphSettings() - - // let app state know of the change - if let appState = self!.appStateController { - appState.chartSettingsChanged = true - } - } - <<< StepperRow("downloadDays") { row in - // NS supports up to 4 days - row.title = "Show Days Back" - row.cell.stepper.stepValue = 1 - row.cell.stepper.minimumValue = 1 - row.cell.stepper.maximumValue = 4 - row.value = Double(UserDefaultsRepository.downloadDays.value) - row.displayValueFor = { value in - guard let value = value else { return nil } - return "\(Int(value))" - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.downloadDays.value = Int(value) - } - <<< SwitchRow("showMidnightMarkers") { row in - row.title = "Show Midnight Lines" - row.value = UserDefaultsRepository.showMidnightLines.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.showMidnightLines.value = value - - // tell main screen that graph needs updating - if let appState = self!.appStateController { - appState.chartSettingsChanged = true - } - } - - +++ ButtonRow { - $0.title = "DONE" - }.onCellSelection { _, _ in - self.dismiss(animated: true, completion: nil) - } - } -} diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 832de420a..f95ab8e5e 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -43,8 +43,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele let speechSynthesizer = AVSpeechSynthesizer() - var appStateController: AppStateController? - // Variables for BG Charts var firstGraphLoad: Bool = true var currentOverride = 1.0 @@ -89,8 +87,8 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele var lastOverrideStartTime: TimeInterval = 0 var lastOverrideEndTime: TimeInterval = 0 - var topBG: Float = UserDefaultsRepository.minBGScale.value - var topPredictionBG: Float = UserDefaultsRepository.minBGScale.value + var topBG: Double = Storage.shared.minBGScale.value + var topPredictionBG: Double = Storage.shared.minBGScale.value var lastOverrideAlarm: TimeInterval = 0 @@ -125,116 +123,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele Storage.shared.remoteType.value = .none } - // Migration of UserDefaultsRepository -> Storage handling - if !UserDefaultsRepository.backgroundRefresh.value { - Storage.shared.backgroundRefreshType.value = .none - UserDefaultsRepository.backgroundRefresh.value = true - } - - // Remove this in a year later than the release of the new Alarms [BEGIN] - let legacyColorBGText = UserDefaultsValue(key: "colorBGText", default: true) - if legacyColorBGText.exists { - Storage.shared.colorBGText.value = legacyColorBGText.value - legacyColorBGText.setNil(key: "colorBGText") - } - - let legacyAppBadge = UserDefaultsValue(key: "appBadge", default: true) - if legacyAppBadge.exists { - Storage.shared.appBadge.value = legacyAppBadge.value - legacyAppBadge.setNil(key: "appBadge") - } - - let legacyForceDarkMode = UserDefaultsValue(key: "forceDarkMode", default: true) - if legacyForceDarkMode.exists { - Storage.shared.forceDarkMode.value = legacyForceDarkMode.value - legacyForceDarkMode.setNil(key: "forceDarkMode") - } - - let legacyShowStats = UserDefaultsValue(key: "showStats", default: true) - if legacyShowStats.exists { - Storage.shared.showStats.value = legacyShowStats.value - legacyShowStats.setNil(key: "showStats") - } - - let legacyUseIFCC = UserDefaultsValue(key: "useIFCC", default: false) - if legacyUseIFCC.exists { - Storage.shared.useIFCC.value = legacyUseIFCC.value - legacyUseIFCC.setNil(key: "useIFCC") - } - - let legacyShowSmallGraph = UserDefaultsValue(key: "showSmallGraph", default: true) - if legacyShowSmallGraph.exists { - Storage.shared.showSmallGraph.value = legacyShowSmallGraph.value - legacyShowSmallGraph.setNil(key: "showSmallGraph") - } - - let legacyScreenlockSwitchState = UserDefaultsValue(key: "screenlockSwitchState", default: true) - if legacyScreenlockSwitchState.exists { - Storage.shared.screenlockSwitchState.value = legacyScreenlockSwitchState.value - legacyScreenlockSwitchState.setNil(key: "screenlockSwitchState") - } - - let legacyShowDisplayName = UserDefaultsValue(key: "showDisplayName", default: false) - if legacyShowDisplayName.exists { - Storage.shared.showDisplayName.value = legacyShowDisplayName.value - legacyShowDisplayName.setNil(key: "showDisplayName") - } - - let legacySpeakBG = UserDefaultsValue(key: "speakBG", default: false) - if legacySpeakBG.exists { - Storage.shared.speakBG.value = legacySpeakBG.value - legacySpeakBG.setNil(key: "speakBG") - } - - let legacySpeakBGAlways = UserDefaultsValue(key: "speakBGAlways", default: true) - if legacySpeakBGAlways.exists { - Storage.shared.speakBGAlways.value = legacySpeakBGAlways.value - legacySpeakBGAlways.setNil(key: "speakBGAlways") - } - - let legacySpeakLowBG = UserDefaultsValue(key: "speakLowBG", default: false) - if legacySpeakLowBG.exists { - Storage.shared.speakLowBG.value = legacySpeakLowBG.value - legacySpeakLowBG.setNil(key: "speakLowBG") - } - - let legacySpeakProactiveLowBG = UserDefaultsValue(key: "speakProactiveLowBG", default: false) - if legacySpeakProactiveLowBG.exists { - Storage.shared.speakProactiveLowBG.value = legacySpeakProactiveLowBG.value - legacySpeakProactiveLowBG.setNil(key: "speakProactiveLowBG") - } - - let legacySpeakFastDropDelta = UserDefaultsValue(key: "speakFastDropDelta", default: 10.0) - if legacySpeakFastDropDelta.exists { - Storage.shared.speakFastDropDelta.value = Double(legacySpeakFastDropDelta.value) - legacySpeakFastDropDelta.setNil(key: "speakFastDropDelta") - } - - let legacySpeakLowBGLimit = UserDefaultsValue(key: "speakLowBGLimit", default: 72.0) - if legacySpeakLowBGLimit.exists { - Storage.shared.speakLowBGLimit.value = Double(legacySpeakLowBGLimit.value) - legacySpeakLowBGLimit.setNil(key: "speakLowBGLimit") - } - - let legacySpeakHighBGLimit = UserDefaultsValue(key: "speakHighBGLimit", default: 180.0) - if legacySpeakHighBGLimit.exists { - Storage.shared.speakHighBGLimit.value = Double(legacySpeakHighBGLimit.value) - legacySpeakHighBGLimit.setNil(key: "speakHighBGLimit") - } - - let legacySpeakHighBG = UserDefaultsValue(key: "speakHighBG", default: false) - if legacySpeakHighBG.exists { - Storage.shared.speakHighBG.value = legacySpeakHighBG.value - legacySpeakHighBG.setNil(key: "speakHighBG") - } - - let legacySpeakLanguage = UserDefaultsValue(key: "speakLanguage", default: "en") - if legacySpeakLanguage.exists { - Storage.shared.speakLanguage.value = legacySpeakLanguage.value - legacySpeakLanguage.setNil(key: "speakLanguage") - } - - // Remove this in a year later than the release of the new Alarms [END] + Storage.shared.migrate() // Ensure alertNotLooping has a minimum value of 16. if UserDefaultsRepository.alertNotLooping.value < 16 { @@ -252,7 +141,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele infoManager = InfoManager(tableView: infoTable) - smallGraphHeightConstraint.constant = CGFloat(UserDefaultsRepository.smallGraphHeight.value) + smallGraphHeightConstraint.constant = CGFloat(Storage.shared.smallGraphHeight.value) view.layoutIfNeeded() let shareUserName = UserDefaultsRepository.shareUserName.value @@ -461,20 +350,15 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } override func viewWillAppear(_: Bool) { - // set screen lock UIApplication.shared.isIdleTimerDisabled = Storage.shared.screenlockSwitchState.value - // check the app state - if let appState = appStateController { - if appState.chartSettingsChanged { - updateBGGraphSettings() + if Observable.shared.chartSettingsChanged.value { + updateBGGraphSettings() - smallGraphHeightConstraint.constant = CGFloat(UserDefaultsRepository.smallGraphHeight.value) - view.layoutIfNeeded() + smallGraphHeightConstraint.constant = CGFloat(Storage.shared.smallGraphHeight.value) + view.layoutIfNeeded() - // reset the app state - appState.chartSettingsChanged = false - } + Observable.shared.chartSettingsChanged.value = false } } @@ -640,10 +524,10 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele let latestBG = bgData[bgData.count - 1].sgv var color = NSUIColor.label if Storage.shared.colorBGText.value { - if Float(latestBG) >= UserDefaultsRepository.highLine.value { + if Double(latestBG) >= Storage.shared.highLine.value { color = NSUIColor.systemYellow Observable.shared.bgTextColor.value = .yellow - } else if Float(latestBG) <= UserDefaultsRepository.lowLine.value { + } else if Double(latestBG) <= Storage.shared.lowLine.value { color = NSUIColor.systemRed Observable.shared.bgTextColor.value = .red } else { @@ -796,7 +680,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } func calculateMaxBgGraphValue() -> Float { - return max(topBG, topPredictionBG) + return max(Float(topBG), Float(topPredictionBG)) } func loadDebugData() { diff --git a/LoopFollow/ViewControllers/NightScoutViewController.swift b/LoopFollow/ViewControllers/NightScoutViewController.swift index eb4f5f1e9..869508bb2 100644 --- a/LoopFollow/ViewControllers/NightScoutViewController.swift +++ b/LoopFollow/ViewControllers/NightScoutViewController.swift @@ -8,8 +8,6 @@ import WebKit class NightscoutViewController: UIViewController { @IBOutlet var webView: WKWebView! - var appStateController: AppStateController? - override func viewDidLoad() { super.viewDidLoad() if Storage.shared.forceDarkMode.value { diff --git a/LoopFollow/ViewControllers/SettingsViewController.swift b/LoopFollow/ViewControllers/SettingsViewController.swift index 1c635271f..07a853903 100644 --- a/LoopFollow/ViewControllers/SettingsViewController.swift +++ b/LoopFollow/ViewControllers/SettingsViewController.swift @@ -10,7 +10,6 @@ import UIKit class SettingsViewController: FormViewController, NightscoutSettingsViewModelDelegate { var tokenRow: TextRow? - var appStateController: AppStateController? var statusLabelRow: LabelRow! func showHideNSDetails() { @@ -108,11 +107,10 @@ class SettingsViewController: FormViewController, NightscoutSettingsViewModelDel $0.title = "Graph Settings" $0.presentationMode = .show( controllerProvider: .callback(builder: { - let controller = GraphSettingsViewController() - controller.appStateController = self.appStateController - return controller - } - ), onDismiss: nil + self.presentGraphSettings() + return UIViewController() + }), + onDismiss: nil ) } <<< ButtonRow("informationDisplaySettings") { @@ -165,7 +163,6 @@ class SettingsViewController: FormViewController, NightscoutSettingsViewModelDel $0.presentationMode = .show( controllerProvider: .callback(builder: { let controller = WatchSettingsViewController() - controller.appStateController = self.appStateController return controller } ), onDismiss: nil @@ -421,6 +418,18 @@ class SettingsViewController: FormViewController, NightscoutSettingsViewModelDel present(hostingController, animated: true) } + func presentGraphSettings() { + let view = GraphSettingsView() + let host = UIHostingController(rootView: view) + host.modalPresentationStyle = .formSheet + + if Storage.shared.forceDarkMode.value { + host.overrideUserInterfaceStyle = .dark + } + + present(host, animated: true) + } + private func shareLogs() { let logFilesToShare = LogManager.shared.logFilesForTodayAndYesterday() diff --git a/LoopFollow/ViewControllers/WatchSettingsViewController.swift b/LoopFollow/ViewControllers/WatchSettingsViewController.swift index 19c55c4f6..0b85e6e17 100644 --- a/LoopFollow/ViewControllers/WatchSettingsViewController.swift +++ b/LoopFollow/ViewControllers/WatchSettingsViewController.swift @@ -8,8 +8,6 @@ import EventKitUI import Foundation class WatchSettingsViewController: FormViewController { - var appStateController: AppStateController? - override func viewDidLoad() { super.viewDidLoad() if Storage.shared.forceDarkMode.value { From 59935e15e33b5505c8dd9230c8ea695a53da3aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 26 May 2025 20:04:58 +0200 Subject: [PATCH 088/138] Calendar settings --- LoopFollow.xcodeproj/project.pbxproj | 8 +- LoopFollow/Settings/WatchSettingsView.swift | 123 +++++++++++++++ LoopFollow/Storage/Storage+Migrate.swift | 6 + LoopFollow/Storage/Storage.swift | 7 + LoopFollow/Storage/UserDefaults.swift | 8 - LoopFollow/Task/CalendarTask.swift | 4 +- .../ViewControllers/MainViewController.swift | 13 +- .../SettingsViewController.swift | 12 +- .../WatchSettingsViewController.swift | 147 ------------------ 9 files changed, 156 insertions(+), 172 deletions(-) create mode 100644 LoopFollow/Settings/WatchSettingsView.swift delete mode 100644 LoopFollow/ViewControllers/WatchSettingsViewController.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index c33929758..5792e5392 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/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 = ""; }; DD8316492DE4C504004467AA /* SettingsStepperRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsStepperRow.swift; sourceTree = ""; }; DD83164B2DE4DB3A004467AA /* BinaryFloatingPoint+localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BinaryFloatingPoint+localized.swift"; sourceTree = ""; }; + DD83164D2DE4E093004467AA /* WatchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchSettingsView.swift; sourceTree = ""; }; DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnipodDashHeartbeatBluetoothTransmitter.swift; sourceTree = ""; }; DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseConversion.swift; sourceTree = ""; }; DD98F54324BCEFEE0007425A /* ShareClientExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareClientExtension.swift; sourceTree = ""; }; @@ -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 = ""; }; - DDCF979924C14DB4002C9752 /* WatchSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchSettingsViewController.swift; sourceTree = ""; }; DDCF9A7F2D85FD09004DF4DD /* Alarm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alarm.swift; sourceTree = ""; }; DDCF9A812D85FD14004DF4DD /* AlarmType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmType.swift; sourceTree = ""; }; DDCF9A872D85FD33004DF4DD /* AlarmData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmData.swift; sourceTree = ""; }; @@ -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 = ""; @@ -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 */, diff --git a/LoopFollow/Settings/WatchSettingsView.swift b/LoopFollow/Settings/WatchSettingsView.swift new file mode 100644 index 000000000..a3416716d --- /dev/null +++ b/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 = "" + } + } +} diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index 125156bdd..8f1096af3 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -176,5 +176,11 @@ extension Storage { moveFloatToDouble(UserDefaultsValue(key: "highLine", default: 180.0), into: highLine) move(UserDefaultsValue(key: "downloadDays", default: 1), into: downloadDays) // Remove this in a year later than the release of the new Alarms [END] + + // ── Watch / Calendar ──────────────────────────────────────────────── + move(UserDefaultsValue(key: "writeCalendarEvent", default: false), into: writeCalendarEvent) + move(UserDefaultsValue(key: "calendarIdentifier", default: ""), into: calendarIdentifier) + move(UserDefaultsValue(key: "watchLine1", default: "%BG% %DIRECTION% %DELTA% %MINAGO%"), into: watchLine1) + move(UserDefaultsValue(key: "watchLine2", default: "C:%COB% I:%IOB% B:%BASAL%"), into: watchLine2) } } diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index 84933405e..a1d257f02 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -100,6 +100,13 @@ class Storage { var downloadDays = StorageValue(key: "downloadDays", defaultValue: 1) // Graph Settings [END] + // Calendar entries [BEGIN] + var writeCalendarEvent = StorageValue(key: "writeCalendarEvent", defaultValue: false) + var calendarIdentifier = StorageValue(key: "calendarIdentifier", defaultValue: "") + var watchLine1 = StorageValue(key: "watchLine1", defaultValue: "%BG% %DIRECTION% %DELTA% %MINAGO%") + var watchLine2 = StorageValue(key: "watchLine2", defaultValue: "C:%COB% I:%IOB% B:%BASAL%") + // Calendar entries [END] + static let shared = Storage() private init() {} } diff --git a/LoopFollow/Storage/UserDefaults.swift b/LoopFollow/Storage/UserDefaults.swift index 74fee21f1..347b57795 100644 --- a/LoopFollow/Storage/UserDefaults.swift +++ b/LoopFollow/Storage/UserDefaults.swift @@ -95,14 +95,6 @@ class UserDefaultsRepository { static let graphCarbs = UserDefaultsValue(key: "graphCarbs", default: true) static let bgUpdateDelay = UserDefaultsValue(key: "bgUpdateDelay", default: 10) - // Watch Calendar Settings - static let calendarIdentifier = UserDefaultsValue(key: "calendarIdentifier", default: "") - static let savedEventID = UserDefaultsValue(key: "savedEventID", default: "") - static let lastCalendarStartDate = UserDefaultsValue(key: "lastCalendarStartDate", default: nil) - static let writeCalendarEvent = UserDefaultsValue(key: "writeCalendarEvent", default: false) - static let watchLine1 = UserDefaultsValue(key: "watchLine1", default: "%BG% %DIRECTION% %DELTA% %MINAGO%") - static let watchLine2 = UserDefaultsValue(key: "watchLine2", default: "C:%COB% I:%IOB% B:%BASAL%") - // Alarm Settings static let systemOutputVolume = UserDefaultsValue(key: "systemOutputVolume", default: 0.5) static let fadeInTimeInterval = UserDefaultsValue(key: "fadeInTimeInterval", default: 0) diff --git a/LoopFollow/Task/CalendarTask.swift b/LoopFollow/Task/CalendarTask.swift index 240f56f77..6758f9507 100644 --- a/LoopFollow/Task/CalendarTask.swift +++ b/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() } diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index f95ab8e5e..54ff29370 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/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") } diff --git a/LoopFollow/ViewControllers/SettingsViewController.swift b/LoopFollow/ViewControllers/SettingsViewController.swift index 07a853903..99084c95e 100644 --- a/LoopFollow/ViewControllers/SettingsViewController.swift +++ b/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") { diff --git a/LoopFollow/ViewControllers/WatchSettingsViewController.swift b/LoopFollow/ViewControllers/WatchSettingsViewController.swift deleted file mode 100644 index 0b85e6e17..000000000 --- a/LoopFollow/ViewControllers/WatchSettingsViewController.swift +++ /dev/null @@ -1,147 +0,0 @@ -// LoopFollow -// WatchSettingsViewController.swift -// Created by Jose Paredes on 2020-07-17. - -import Eureka -import EventKit -import EventKitUI -import Foundation - -class WatchSettingsViewController: FormViewController { - override func viewDidLoad() { - super.viewDidLoad() - if Storage.shared.forceDarkMode.value { - overrideUserInterfaceStyle = .dark - } - - let eventStore = EKEventStore() - eventStore.requestCalendarAccess { [weak self] granted, _ in - guard let self = self else { return } - - DispatchQueue.main.async { - // Update the form based on the calendar access status - self.buildWatchSettings(hasCalendarAccess: granted) - self.showHideNSDetails() - } - } - } - - func showHideNSDetails() { - var isHidden = false - var isEnabled = true - if !IsNightscoutEnabled() { - isHidden = true - isEnabled = false - } - - let tmpArr = ["IOB", "COB", "BASAL", "LOOP", "OVERRIDE"] - for i in 0 ..< tmpArr.count { - if let row1 = form.rowBy(tag: tmpArr[i]) as? LabelRow { - row1.hidden = .function(["hide"]) { _ in - isHidden - } - row1.evaluateHidden() - } - } - } - - private func buildWatchSettings(hasCalendarAccess: Bool) { - struct cal { - var title: String - var identifier: String - } - - // array of calendars - let store = EKEventStore() - let ekCalendars = store.calendars(for: EKEntityType.event) - var calendars: [cal] = [] - for i in 0 ..< ekCalendars.count { - let item = cal(title: ekCalendars[i].title, identifier: ekCalendars[i].calendarIdentifier) - calendars.append(item) - } - - form - +++ Section(header: "Save BG to Calendar", footer: "Add the Apple calendar complication to your Apple Watch face or Carplay to see BG readings. Create a new calendar called 'Follow' and modify the calendar settings in the iPhone Watch/Carplay App to only display the Follow calendar on your watch or car. It is important to use a new calendar because this will delete other events on the same calendar. Edit Line 1 and Line 2 to be displayed using variables below that will be replaced by the values. Other text entered will not be replaced") - <<< LabelRow { - $0.title = "Calendar Access Denied" - $0.hidden = Condition.function(["hide"]) { _ in hasCalendarAccess } - }.cellUpdate { cell, _ in - cell.textLabel?.textColor = .red - } - <<< SwitchRow("writeCalendarEvent") { row in - row.title = "Save BG to Calendar" - row.value = UserDefaultsRepository.writeCalendarEvent.value - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.writeCalendarEvent.value = value - } - <<< PickerInputRow("calendarIdentifier") { row in - row.title = "Calendar" - row.options = calendars.map { $0.identifier } - row.value = UserDefaultsRepository.calendarIdentifier.value - row.displayValueFor = { value in - guard let value = value else { return nil } - let matching = calendars - .flatMap { $0 } - .filter { $0.identifier.range(of: value) != nil || $0.title.range(of: value) != nil } - if matching.count > 0 { - return "\(String(matching[0].title))" - } else { - return " - " - } - } - }.onChange { [weak self] row in - guard let value = row.value else { return } - UserDefaultsRepository.calendarIdentifier.value = value - } - <<< TextRow("watchLine1") { row in - row.title = "Line 1" - row.value = UserDefaultsRepository.watchLine1.value - }.onChange { row in - guard let value = row.value else { return } - UserDefaultsRepository.watchLine1.value = value - } - <<< TextRow("watchLine2") { row in - row.title = "Line 2" - row.value = UserDefaultsRepository.watchLine2.value - }.onChange { row in - guard let value = row.value else { return } - UserDefaultsRepository.watchLine2.value = value - } - - +++ Section(header: "Available Variables", footer: "") - <<< LabelRow("BG") { row in - row.title = "%BG% : Blood Glucose Reading" - } - <<< LabelRow("DIRECTION") { row in - row.title = "%DIRECTION% : Dexcom Trend Arrow" - } - <<< LabelRow("DELTA") { row in - row.title = "%DELTA% : +/- From Last Reading" - } - <<< LabelRow("IOB") { row in - row.title = "%IOB% : Insulin on Board" - } - <<< LabelRow("COB") { row in - row.title = "%COB% : Carbs on Board" - } - <<< LabelRow("BASAL") { row in - row.title = "%BASAL% : Current Basal u/hr" - } - <<< LabelRow("LOOP") { row in - row.title = "%LOOP% : Loop Status Symbol" - } - <<< LabelRow("OVERRIDE") { row in - row.title = "%OVERRIDE% : Active Override %" - } - <<< LabelRow("MINAGO") { row in - row.title = "%MINAGO% : Only displays for old readings" - } - - +++ ButtonRow { - $0.title = "DONE" - }.onCellSelection { _, _ in - self.dismiss(animated: true, completion: nil) - } - } -} From 6e728719dfd0ef9761fb21c7bca786ea08a03b0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 26 May 2025 20:54:50 +0200 Subject: [PATCH 089/138] SettingsMenu --- LoopFollow.xcodeproj/project.pbxproj | 4 + LoopFollow/Helpers/AppVersionManager.swift | 8 + LoopFollow/Settings/SettingsMenuView.swift | 260 ++++++++++ .../ViewControllers/MainViewController.swift | 15 +- .../SettingsViewController.swift | 451 +----------------- 5 files changed, 301 insertions(+), 437 deletions(-) create mode 100644 LoopFollow/Settings/SettingsMenuView.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 5792e5392..357f7246a 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -126,6 +126,7 @@ 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 */; }; + DD8316502DE4E635004467AA /* SettingsMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD83164F2DE4E635004467AA /* SettingsMenuView.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 */; }; @@ -488,6 +489,7 @@ DD8316492DE4C504004467AA /* SettingsStepperRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsStepperRow.swift; sourceTree = ""; }; DD83164B2DE4DB3A004467AA /* BinaryFloatingPoint+localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BinaryFloatingPoint+localized.swift"; sourceTree = ""; }; DD83164D2DE4E093004467AA /* WatchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchSettingsView.swift; sourceTree = ""; }; + DD83164F2DE4E635004467AA /* SettingsMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsMenuView.swift; sourceTree = ""; }; DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnipodDashHeartbeatBluetoothTransmitter.swift; sourceTree = ""; }; DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseConversion.swift; sourceTree = ""; }; DD98F54324BCEFEE0007425A /* ShareClientExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareClientExtension.swift; sourceTree = ""; }; @@ -841,6 +843,7 @@ DD1A97122D429495000DDC11 /* Settings */ = { isa = PBXGroup; children = ( + DD83164F2DE4E635004467AA /* SettingsMenuView.swift */, DD83164D2DE4E093004467AA /* WatchSettingsView.swift */, DD8316452DE49B09004467AA /* GraphSettingsView.swift */, DD1A97152D4294B2000DDC11 /* AdvancedSettingsViewModel.swift */, @@ -2005,6 +2008,7 @@ DD7F4C132DD51FD500D449E9 /* TempTargetEndCondition.swift in Sources */, DDD10F012C510C6B00D76A8E /* ObservableUserDefaults.swift in Sources */, DD16AF0D2C98485400FB655A /* SecureStorageValue.swift in Sources */, + DD8316502DE4E635004467AA /* SettingsMenuView.swift in Sources */, DDC7E5CF2DC77C2000EB1127 /* SnoozerViewModel.swift in Sources */, DD2C2E4F2D3B8AF1006413A5 /* NightscoutSettingsView.swift in Sources */, DD8316482DE49EE5004467AA /* Storage+Migrate.swift in Sources */, diff --git a/LoopFollow/Helpers/AppVersionManager.swift b/LoopFollow/Helpers/AppVersionManager.swift index a2695830c..c42a9c2d3 100644 --- a/LoopFollow/Helpers/AppVersionManager.swift +++ b/LoopFollow/Helpers/AppVersionManager.swift @@ -7,6 +7,14 @@ import Foundation class AppVersionManager { private let githubService = GitHubService() + func checkForNewVersionAsync() async -> (latest: String?, isNewer: Bool, isBlacklisted: Bool) { + await withCheckedContinuation { cont in + checkForNewVersion { latest, newer, blacklisted in + cont.resume(returning: (latest, newer, blacklisted)) + } + } + } + /// Checks for the availability of a new app version and if the current version is blacklisted. /// - Parameter completion: Returns latest version, a boolean for newer version existence, and blacklist status. /// Usage: `versionManager.checkForNewVersion { latestVersion, isNewer, isBlacklisted in ... }` diff --git a/LoopFollow/Settings/SettingsMenuView.swift b/LoopFollow/Settings/SettingsMenuView.swift new file mode 100644 index 000000000..d3734f54f --- /dev/null +++ b/LoopFollow/Settings/SettingsMenuView.swift @@ -0,0 +1,260 @@ +// LoopFollow +// SettingsMenuView.swift +// Created by Jonas Björkert on 2025-05-26. + +import SwiftUI + +struct SettingsMenuView: View { + // MARK: – Call-backs ----------------------------------------------------- + + let onNightscoutVisibilityChange: (_ enabled: Bool) -> Void + + // MARK: – Local state ---------------------------------------------------- + + @State private var sheet: Sheet? + @State private var latestVersion: String? + @State private var versionTint: Color = .secondary + + // MARK: – Body ----------------------------------------------------------- + + var body: some View { + NavigationStack { + List { + // ────────────── Data settings ────────────── + dataSection + + // ────────────── App settings ────────────── + Section("App Settings") { + navRow(title: "Background Refresh Settings", + icon: "arrow.clockwise", + destination: .backgroundRefresh) + + navRow(title: "General Settings", + icon: "gearshape", + destination: .general) + + navRow(title: "Graph Settings", + icon: "chart.xyaxis.line", + destination: .graph) + + if IsNightscoutEnabled() { + navRow(title: "Information Display Settings", + icon: "info.circle", + destination: .infoDisplay) + } + } + + // ────────────── Alarms ────────────── + Section { + navRow(title: "Alarms", + icon: "bell", + destination: .alarmsList) + + navRow(title: "Alarm Settings", + icon: "bell.badge", + destination: .alarmSettings) + } + + // ────────────── Integrations ────────────── + Section("Integrations") { + navRow(title: "Calendar", + icon: "calendar", + destination: .calendar) + + navRow(title: "Contact", + icon: "envelope", + destination: .contact) + } + + // ────────────── Advanced / Logs ────────────── + Section("Advanced Settings") { + navRow(title: "Advanced Settings", + icon: "exclamationmark.shield", + destination: .advanced) + } + + Section("Logging") { + navRow(title: "View Log", + icon: "doc.text.magnifyingglass", + destination: .viewLog) + + Button { + shareLogs() + } label: { + Label("Share Logs", systemImage: "square.and.arrow.up") + } + .accessibilityIdentifier("ShareLogsButton") + } + + // ────────────── Community ────────────── + Section("Community") { + Link(destination: URL(string: "https://www.facebook.com/groups/loopfollowlnl")!) { + Label("LoopFollow Facebook Group", systemImage: "person.3") + } + } + + // ────────────── Build info ────────────── + buildInfoSection + } + .navigationTitle("Settings") + } + .task { await refreshVersionInfo() } + .sheet(item: $sheet) { $0.destination } + } + + // MARK: – Section builders ---------------------------------------------- + + /// “Data Settings” + @ViewBuilder + private var dataSection: some View { + Section("Data Settings") { + Picker("Units", + selection: Binding( + get: { UserDefaultsRepository.units.value }, + set: { UserDefaultsRepository.units.value = $0 } + )) { + Text("mg/dL").tag("mg/dL") + Text("mmol/L").tag("mmol/L") + } + .pickerStyle(.segmented) + + navRow(title: "Nightscout Settings", + icon: "antenna.radiowaves.left.and.right", + destination: .nightscout) + + navRow(title: "Dexcom Settings", + icon: "waveform.path.ecg", + destination: .dexcom) + } + .onAppear { + onNightscoutVisibilityChange(IsNightscoutEnabled()) + } + } + + /// version / build info + @ViewBuilder + private var buildInfoSection: some View { + let build = BuildDetails.default + let ver = AppVersionManager().version() + + Section("Build Information") { + keyValue("Version", ver, tint: versionTint) + keyValue("Latest version", latestVersion ?? "Fetching…") + + if !(build.isMacApp() || build.isSimulatorBuild()) { + keyValue(build.expirationHeaderString, + dateTimeUtils.formattedDate(from: build.calculateExpirationDate())) + } + keyValue("Built", dateTimeUtils.formattedDate(from: build.buildDate())) + keyValue("Branch", build.branchAndSha) + } + } + + // MARK: – Row helpers ---------------------------------------------------- + + /// Standard row with icon, chevron and sheet presentation + private func navRow(title: String, + icon: String, + destination: Sheet) -> some View + { + NavigationLink { + destination.destination + } label: { + Label(title, systemImage: icon) + } + } + + /// Simple key-value row + @ViewBuilder + private func keyValue(_ key: String, _ value: String, tint: Color = .secondary) -> some View { + HStack { + Text(key) + Spacer() + Text(value).foregroundColor(tint) + } + } + + // MARK: – Version check -------------------------------------------------- + + private func refreshVersionInfo() async { + let manager = AppVersionManager() + let (latest, newer, blacklisted) = await manager.checkForNewVersionAsync() + latestVersion = latest ?? "Unknown" + + // match old colour logic + let current = manager.version() + versionTint = blacklisted ? .red : + newer ? .orange : + latest == current ? .green : .secondary + } + + // MARK: – Share logs ----------------------------------------------------- + + private func shareLogs() { + let files = LogManager.shared.logFilesForTodayAndYesterday() + guard !files.isEmpty else { + UIApplication.shared.topMost?.presentSimpleAlert( + title: "No Logs Available", + message: "There are no logs to share." + ) + return + } + let avc = UIActivityViewController(activityItems: files, applicationActivities: nil) + UIApplication.shared.topMost?.present(avc, animated: true) + } +} + +// MARK: – Sheet routing identical to earlier ------------------------------- + +private enum Sheet: Identifiable { + case nightscout, dexcom + case backgroundRefresh + case general, graph + case infoDisplay + case alarmsList, alarmSettings + case remote + case calendar, contact + case advanced + case viewLog + + var id: Int { hashValue } + + @ViewBuilder + var destination: some View { + switch self { + case .nightscout: NightscoutSettingsView(viewModel: .init()) + case .dexcom: DexcomSettingsView(viewModel: .init()) + case .backgroundRefresh: BackgroundRefreshSettingsView(viewModel: .init()) + case .general: GeneralSettingsView() + case .graph: GraphSettingsView() + case .infoDisplay: InfoDisplaySettingsView(viewModel: .init()) + case .alarmsList: AlarmListView() + case .alarmSettings: AlarmSettingsView() + case .remote: RemoteSettingsView(viewModel: .init()) + case .calendar: WatchSettingsView() + case .contact: ContactSettingsView(viewModel: .init()) + case .advanced: AdvancedSettingsView(viewModel: .init()) + case .viewLog: LogView(viewModel: .init()) + } + } +} + +import UIKit + +extension UIApplication { + var topMost: UIViewController? { + guard var top = keyWindow?.rootViewController else { return nil } + while let presented = top.presentedViewController { + top = presented + } + return top + } +} + +extension UIViewController { + func presentSimpleAlert(title: String, message: String) { + let a = UIAlertController(title: title, message: message, preferredStyle: .alert) + a.addAction(UIAlertAction(title: "OK", style: .default)) + present(a, animated: true) + } +} diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 54ff29370..e84286c5c 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -587,7 +587,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } let direction = bgDirectionGraphic(bgData[bgData.count - 1].direction ?? "") - var eventStartDate = Date(timeIntervalSince1970: bgData[bgData.count - 1].date) + let eventStartDate = Date(timeIntervalSince1970: bgData[bgData.count - 1].date) var eventEndDate = eventStartDate.addingTimeInterval(60 * 10) var eventTitle = Storage.shared.watchLine1.value if Storage.shared.watchLine2.value.count > 1 { @@ -621,12 +621,12 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele eventTitle = eventTitle.replacingOccurrences(of: "%BASAL%", with: basal) // Delete Events from last 2 hours and 2 hours in future - var deleteStartDate = Date().addingTimeInterval(-60 * 60 * 2) - var deleteEndDate = Date().addingTimeInterval(60 * 60 * 2) + let deleteStartDate = Date().addingTimeInterval(-60 * 60 * 2) + let deleteEndDate = Date().addingTimeInterval(60 * 60 * 2) // guard solves for some ios upgrades removing the calendar 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]? + let predicate2 = store.predicateForEvents(withStart: deleteStartDate, end: deleteEndDate, calendars: [deleteCalendar]) + let eVDelete = store.events(matching: predicate2) as [EKEvent]? if eVDelete != nil { for i in eVDelete! { do { @@ -638,7 +638,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } // Write New Event - var event = EKEvent(eventStore: store) + let event = EKEvent(eventStore: store) event.title = eventTitle event.startDate = eventStartDate event.endDate = eventEndDate @@ -649,7 +649,8 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele lastCalDate = bgData[bgData.count - 1].date } catch { - LogManager.shared.log(category: .calendar, message: "Error storing to the calendar") + let msg = "Error storing to calendar: \(error.localizedDescription) (\(error))" + LogManager.shared.log(category: .calendar, message: msg) } } diff --git a/LoopFollow/ViewControllers/SettingsViewController.swift b/LoopFollow/ViewControllers/SettingsViewController.swift index 99084c95e..101af0ac1 100644 --- a/LoopFollow/ViewControllers/SettingsViewController.swift +++ b/LoopFollow/ViewControllers/SettingsViewController.swift @@ -2,449 +2,40 @@ // SettingsViewController.swift // Created by Jon Fawcett on 2020-06-05. -import Eureka -import EventKit -import EventKitUI import SwiftUI import UIKit -class SettingsViewController: FormViewController, NightscoutSettingsViewModelDelegate { - var tokenRow: TextRow? - var statusLabelRow: LabelRow! +final class SettingsViewController: UIViewController { + // MARK: Stored properties - func showHideNSDetails() { - var isHidden = false - var isEnabled = true - if !IsNightscoutEnabled() { - isHidden = true - isEnabled = false - } - - if let row1 = form.rowBy(tag: "informationDisplaySettings") as? ButtonRow { - row1.hidden = .function(["hide"]) { _ in - isHidden - } - row1.evaluateHidden() - } + private var host: UIHostingController! - if IsNightscoutEnabled() { - isEnabled = true - } - - guard let nightscoutTab = tabBarController?.tabBar.items![3] else { return } - nightscoutTab.isEnabled = isEnabled - } + // MARK: Life-cycle override func viewDidLoad() { super.viewDidLoad() - if Storage.shared.forceDarkMode.value { - overrideUserInterfaceStyle = .dark - } - - let buildDetails = BuildDetails.default - let formattedBuildDate = dateTimeUtils.formattedDate(from: buildDetails.buildDate()) - let branchAndSha = buildDetails.branchAndSha - let expiration = dateTimeUtils.formattedDate(from: buildDetails.calculateExpirationDate()) - let expirationHeaderString = buildDetails.expirationHeaderString - let versionManager = AppVersionManager() - let version = versionManager.version() - let isMacApp = buildDetails.isMacApp() - let isSimulatorBuild = buildDetails.isSimulatorBuild() - - form - +++ Section(header: "Data Settings", footer: "") - <<< SegmentedRow("units") { row in - row.title = "Units" - row.options = ["mg/dL", "mmol/L"] - row.value = UserDefaultsRepository.units.value - }.onChange { row in - guard let value = row.value else { return } - UserDefaultsRepository.units.value = value - } - <<< ButtonRow("nightscout") { - $0.title = "Nightscout Settings" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - self.presentNightscoutSettingsView() - return UIViewController() - }), onDismiss: nil - ) - } - <<< ButtonRow("dexcom") { - $0.title = "Dexcom Settings" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - self.presentDexcomSettingsView() - return UIViewController() - }), onDismiss: nil - ) - } - - +++ Section("App Settings") - - <<< ButtonRow("backgroundRefreshSettings") { - $0.title = "Background Refresh Settings" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - self.presentBackgroundRefreshSettings() - return UIViewController() - }), - onDismiss: nil - ) - } - - <<< ButtonRow { - $0.title = "General Settings" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - self.presentGeneralSettings() - return UIViewController() - }), - onDismiss: nil - ) - } - <<< ButtonRow("graphSettings") { - $0.title = "Graph Settings" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - self.presentGraphSettings() - return UIViewController() - }), - onDismiss: nil - ) - } - <<< ButtonRow("informationDisplaySettings") { - $0.title = "Information Display Settings" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - self.presentInfoDisplaySettings() - return UIViewController() - } - ), onDismiss: nil - ) - } - - <<< ButtonRow("alarmsList") { - $0.title = "Alarms" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - self.presentAlarmList() - return UIViewController() - }), - onDismiss: nil - ) - } - - <<< ButtonRow("alarmsSettings") { - $0.title = "Alarm Settings" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - self.presentAlarmSettings() - return UIViewController() - }), - onDismiss: nil - ) - } - - <<< ButtonRow("remoteSettings") { - $0.title = "Remote Settings" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - self.presentRemoteSettings() - return UIViewController() - }), - onDismiss: nil - ) - } - - +++ Section("Integrations") - <<< ButtonRow { - $0.title = "Calendar" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - let host = UIHostingController(rootView: WatchSettingsView()) - host.modalPresentationStyle = .formSheet - if Storage.shared.forceDarkMode.value { - host.overrideUserInterfaceStyle = .dark - } - return host - }), - onDismiss: nil - ) - } - <<< ButtonRow("contact") { - $0.title = "Contact" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - self.presentContactSettings() - return UIViewController() - } - ), onDismiss: nil - ) - } - +++ Section("Advanced Settings") - <<< ButtonRow { - $0.title = "Advanced Settings" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - self.presentAdvancedSettingsView() - return UIViewController() - }), onDismiss: nil - ) - } - - +++ Section("Logging") - <<< ButtonRow("viewlog") { - $0.title = "View Log" - $0.presentationMode = .show( - controllerProvider: .callback(builder: { - self.presentLogView() - return UIViewController() - }), onDismiss: nil - ) - } - <<< ButtonRow("shareLogs") { - $0.title = "Share Logs" - $0.cellSetup { cell, _ in - cell.accessibilityIdentifier = "ShareLogsButton" - } - $0.onCellSelection { [weak self] _, _ in - self?.shareLogs() - } - } - - +++ Section("Build Information") - <<< LabelRow { - $0.title = "Version" - $0.value = version - $0.tag = "currentVersionRow" - } - <<< LabelRow { - $0.title = "Latest version" - $0.value = "Fetching..." - $0.tag = "latestVersionRow" - } - <<< LabelRow { - $0.title = expirationHeaderString - $0.value = expiration - $0.hidden = Condition(booleanLiteral: isMacApp || isSimulatorBuild) - } - <<< LabelRow { - $0.title = "Built" - $0.value = formattedBuildDate - } - <<< LabelRow { - $0.title = "Branch" - $0.value = branchAndSha - } - - showHideNSDetails() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - refreshVersionInfo() - } - - func refreshVersionInfo() { - let versionManager = AppVersionManager() - versionManager.checkForNewVersion { latestVersion, isNewer, isBlacklisted in - DispatchQueue.main.async { - if let currentVersionRow = self.form.rowBy(tag: "currentVersionRow") as? LabelRow { - currentVersionRow.cell.detailTextLabel?.textColor = self.getColor(isBlacklisted: isBlacklisted, isNewer: isNewer, isCurrent: latestVersion == versionManager.version()) - currentVersionRow.updateCell() - } - - if let latestVersionRow = self.form.rowBy(tag: "latestVersionRow") as? LabelRow { - latestVersionRow.value = latestVersion ?? "Unknown" - latestVersionRow.updateCell() - } - } - } - } - - private func getColor(isBlacklisted: Bool, isNewer: Bool, isCurrent: Bool) -> UIColor { - if isBlacklisted { - return .red - } else if isNewer { - return .orange - } else if isCurrent { - return .green - } else { - return .secondaryLabel - } - } - - func presentInfoDisplaySettings() { - let viewModel = InfoDisplaySettingsViewModel() - let settingsView = InfoDisplaySettingsView(viewModel: viewModel) - - let hostingController = UIHostingController(rootView: settingsView) - hostingController.modalPresentationStyle = .formSheet - - if Storage.shared.forceDarkMode.value { - hostingController.overrideUserInterfaceStyle = .dark - } - - present(hostingController, animated: true, completion: nil) - } - - func presentRemoteSettings() { - let viewModel = RemoteSettingsViewModel() - let settingsView = RemoteSettingsView(viewModel: viewModel) - let hostingController = UIHostingController(rootView: settingsView) - hostingController.modalPresentationStyle = .formSheet - - if Storage.shared.forceDarkMode.value { - hostingController.overrideUserInterfaceStyle = .dark - } - - present(hostingController, animated: true, completion: nil) - } - - func presentAlarmSettings() { - let settingsView = AlarmSettingsView() - let hostingController = UIHostingController(rootView: settingsView) - hostingController.modalPresentationStyle = .formSheet - - if Storage.shared.forceDarkMode.value { - hostingController.overrideUserInterfaceStyle = .dark - } - - present(hostingController, animated: true, completion: nil) - } - - func presentAlarmList() { - let settingsView = AlarmListView() - let hostingController = UIHostingController(rootView: settingsView) - hostingController.modalPresentationStyle = .formSheet - - if Storage.shared.forceDarkMode.value { - hostingController.overrideUserInterfaceStyle = .dark - } - - present(hostingController, animated: true, completion: nil) - } - - func presentContactSettings() { - let viewModel = ContactSettingsViewModel() - let contactSettingsView = ContactSettingsView(viewModel: viewModel) - let hostingController = UIHostingController(rootView: contactSettingsView) - hostingController.modalPresentationStyle = .formSheet - - if Storage.shared.forceDarkMode.value { - hostingController.overrideUserInterfaceStyle = .dark - } - - present(hostingController, animated: true, completion: nil) - } - - func presentBackgroundRefreshSettings() { - let viewModel = BackgroundRefreshSettingsViewModel() - let settingsView = BackgroundRefreshSettingsView(viewModel: viewModel) - let hostingController = UIHostingController(rootView: settingsView) - hostingController.modalPresentationStyle = .formSheet - - if Storage.shared.forceDarkMode.value { - hostingController.overrideUserInterfaceStyle = .dark - } - - present(hostingController, animated: true, completion: nil) - } - - func presentLogView() { - let viewModel = LogViewModel() - let logView = LogView(viewModel: viewModel) - let hostingController = UIHostingController(rootView: logView) - hostingController.modalPresentationStyle = .formSheet - - if Storage.shared.forceDarkMode.value { - hostingController.overrideUserInterfaceStyle = .dark - } - - present(hostingController, animated: true, completion: nil) - } - - func presentNightscoutSettingsView() { - let viewModel = NightscoutSettingsViewModel() - viewModel.delegate = self - - let view = NightscoutSettingsView(viewModel: viewModel) - let hostingController = UIHostingController(rootView: view) - hostingController.modalPresentationStyle = .formSheet - - if Storage.shared.forceDarkMode.value { - hostingController.overrideUserInterfaceStyle = .dark - } - - present(hostingController, animated: true, completion: nil) - } - - func nightscoutSettingsDidFinish() { - showHideNSDetails() - } - - func presentDexcomSettingsView() { - let viewModel = DexcomSettingsViewModel() - let settingsView = DexcomSettingsView(viewModel: viewModel) - let hostingController = UIHostingController(rootView: settingsView) - hostingController.modalPresentationStyle = .formSheet - - if Storage.shared.forceDarkMode.value { - hostingController.overrideUserInterfaceStyle = .dark - } - - present(hostingController, animated: true, completion: nil) - } - - func presentAdvancedSettingsView() { - let viewModel = AdvancedSettingsViewModel() - let view = AdvancedSettingsView(viewModel: viewModel) - let hostingController = UIHostingController(rootView: view) - hostingController.modalPresentationStyle = .formSheet - - if Storage.shared.forceDarkMode.value { - hostingController.overrideUserInterfaceStyle = .dark - } - - present(hostingController, animated: true, completion: nil) - } - func presentGeneralSettings() { - let view = GeneralSettingsView() - let hostingController = UIHostingController(rootView: view) - hostingController.modalPresentationStyle = .formSheet - - if Storage.shared.forceDarkMode.value { - hostingController.overrideUserInterfaceStyle = .dark - } - present(hostingController, animated: true) - } - - func presentGraphSettings() { - let view = GraphSettingsView() - let host = UIHostingController(rootView: view) - host.modalPresentationStyle = .formSheet + // Build SwiftUI menu + host = UIHostingController( + rootView: SettingsMenuView { [weak self] nightscoutEnabled in + self?.tabBarController?.tabBar.items?[3].isEnabled = nightscoutEnabled + }) + // Dark-mode override if Storage.shared.forceDarkMode.value { host.overrideUserInterfaceStyle = .dark } - present(host, animated: true) - } - - private func shareLogs() { - let logFilesToShare = LogManager.shared.logFilesForTodayAndYesterday() - - if !logFilesToShare.isEmpty { - let activityViewController = UIActivityViewController(activityItems: logFilesToShare, applicationActivities: nil) - activityViewController.popoverPresentationController?.sourceView = view - present(activityViewController, animated: true, completion: nil) - } else { - let alert = UIAlertController(title: "No Logs Available", message: "There are no logs to share.", preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "OK", style: .default)) - present(alert, animated: true, completion: nil) - } + // Embed + addChild(host) + host.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(host.view) + NSLayoutConstraint.activate([ + host.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + host.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + host.view.topAnchor.constraint(equalTo: view.topAnchor), + host.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + host.didMove(toParent: self) } } From 75977b07fcd621aa3732f5762085182a027ceece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 27 May 2025 07:59:11 +0200 Subject: [PATCH 090/138] Removed Eureka --- Podfile | 1 - Podfile.lock | 8 - Pods/Eureka/LICENSE | 21 - Pods/Eureka/README.md | 1343 ----------------- Pods/Eureka/Source/Core/BaseRow.swift | 294 ---- Pods/Eureka/Source/Core/Cell.swift | 174 --- Pods/Eureka/Source/Core/CellType.swift | 81 - Pods/Eureka/Source/Core/Core.swift | 1151 -------------- Pods/Eureka/Source/Core/Form.swift | 453 ------ .../Eureka/Source/Core/HeaderFooterView.swift | 162 -- Pods/Eureka/Source/Core/Helpers.swift | 79 - Pods/Eureka/Source/Core/InlineRowType.swift | 147 -- .../Source/Core/NavigationAccessoryView.swift | 155 -- Pods/Eureka/Source/Core/Operators.swift | 161 -- .../Eureka/Source/Core/PresenterRowType.swift | 57 - Pods/Eureka/Source/Core/Row.swift | 203 --- .../Source/Core/RowControllerType.swift | 35 - Pods/Eureka/Source/Core/RowProtocols.swift | 62 - Pods/Eureka/Source/Core/RowType.swift | 258 ---- Pods/Eureka/Source/Core/Section.swift | 598 -------- .../Source/Core/SelectableRowType.swift | 32 - .../Source/Core/SelectableSection.swift | 155 -- Pods/Eureka/Source/Core/SwipeActions.swift | 152 -- Pods/Eureka/Source/Core/Validation.swift | 99 -- Pods/Eureka/Source/Rows/ActionSheetRow.swift | 96 -- Pods/Eureka/Source/Rows/AlertRow.swift | 66 - Pods/Eureka/Source/Rows/ButtonRow.swift | 104 -- .../Source/Rows/ButtonRowWithPresent.swift | 88 -- Pods/Eureka/Source/Rows/CheckRow.swift | 83 - .../Source/Rows/Common/AlertOptionsRow.swift | 41 - .../Source/Rows/Common/DateFieldRow.swift | 144 -- .../Rows/Common/DateInlineFieldRow.swift | 80 - .../Source/Rows/Common/DecimalFormatter.swift | 59 - Pods/Eureka/Source/Rows/Common/FieldRow.swift | 494 ------ .../Common/GenericMultipleSelectorRow.swift | 86 -- .../Source/Rows/Common/OptionsRow.swift | 37 - .../Eureka/Source/Rows/Common/Protocols.swift | 35 - .../Source/Rows/Common/SelectorRow.swift | 87 -- .../MultipleSelectorViewController.swift | 149 -- .../Controllers/SelectorAlertController.swift | 80 - .../Controllers/SelectorViewController.swift | 232 --- Pods/Eureka/Source/Rows/DateInlineRow.swift | 214 --- Pods/Eureka/Source/Rows/DatePickerRow.swift | 156 -- Pods/Eureka/Source/Rows/DateRow.swift | 99 -- .../Source/Rows/DoublePickerInputRow.swift | 112 -- Pods/Eureka/Source/Rows/DoublePickerRow.swift | 129 -- Pods/Eureka/Source/Rows/FieldsRow.swift | 399 ----- Pods/Eureka/Source/Rows/LabelRow.swift | 61 - .../Source/Rows/MultipleSelectorRow.swift | 38 - Pods/Eureka/Source/Rows/PickerInlineRow.swift | 202 --- Pods/Eureka/Source/Rows/PickerInputRow.swift | 159 -- Pods/Eureka/Source/Rows/PickerRow.swift | 137 -- .../Source/Rows/PopoverSelectorRow.swift | 55 - Pods/Eureka/Source/Rows/PushRow.swift | 42 - Pods/Eureka/Source/Rows/SegmentedRow.swift | 194 --- .../Rows/SelectableRows/ListCheckRow.swift | 70 - Pods/Eureka/Source/Rows/SliderRow.swift | 175 --- Pods/Eureka/Source/Rows/StepperRow.swift | 157 -- Pods/Eureka/Source/Rows/SwitchRow.swift | 81 - Pods/Eureka/Source/Rows/TextAreaRow.swift | 336 ----- .../Source/Rows/TriplePickerInputRow.swift | 158 -- Pods/Eureka/Source/Rows/TriplePickerRow.swift | 176 --- .../Source/Validations/RuleClosure.swift | 43 - .../Eureka/Source/Validations/RuleEmail.swift | 33 - .../Source/Validations/RuleEqualsToRow.swift | 55 - .../Source/Validations/RuleLength.swift | 84 -- .../Eureka/Source/Validations/RuleRange.swift | 109 -- .../Source/Validations/RuleRegExp.swift | 61 - .../Source/Validations/RuleRequired.swift | 43 - Pods/Eureka/Source/Validations/RuleURL.swift | 54 - Pods/Local Podspecs/Eureka.podspec.json | 29 - Pods/Manifest.lock | 8 - Pods/Pods.xcodeproj/project.pbxproj | 476 +----- .../Eureka/Eureka-Info.plist | 26 - .../Eureka/Eureka-dummy.m | 5 - .../Eureka/Eureka-prefix.pch | 12 - .../Eureka/Eureka-umbrella.h | 16 - .../Eureka/Eureka.debug.xcconfig | 15 - .../Eureka/Eureka.modulemap | 6 - .../Eureka/Eureka.release.xcconfig | 15 - .../Pods-LoopFollow-acknowledgements.markdown | 25 - .../Pods-LoopFollow-acknowledgements.plist | 31 - ...ow-frameworks-Debug-input-files.xcfilelist | 1 - ...w-frameworks-Debug-output-files.xcfilelist | 1 - ...-frameworks-Release-input-files.xcfilelist | 1 - ...frameworks-Release-output-files.xcfilelist | 1 - .../Pods-LoopFollow-frameworks.sh | 2 - .../Pods-LoopFollow.debug.xcconfig | 6 +- .../Pods-LoopFollow.release.xcconfig | 6 +- 89 files changed, 19 insertions(+), 12137 deletions(-) delete mode 100644 Pods/Eureka/LICENSE delete mode 100644 Pods/Eureka/README.md delete mode 100644 Pods/Eureka/Source/Core/BaseRow.swift delete mode 100644 Pods/Eureka/Source/Core/Cell.swift delete mode 100644 Pods/Eureka/Source/Core/CellType.swift delete mode 100644 Pods/Eureka/Source/Core/Core.swift delete mode 100644 Pods/Eureka/Source/Core/Form.swift delete mode 100644 Pods/Eureka/Source/Core/HeaderFooterView.swift delete mode 100644 Pods/Eureka/Source/Core/Helpers.swift delete mode 100644 Pods/Eureka/Source/Core/InlineRowType.swift delete mode 100644 Pods/Eureka/Source/Core/NavigationAccessoryView.swift delete mode 100644 Pods/Eureka/Source/Core/Operators.swift delete mode 100644 Pods/Eureka/Source/Core/PresenterRowType.swift delete mode 100644 Pods/Eureka/Source/Core/Row.swift delete mode 100644 Pods/Eureka/Source/Core/RowControllerType.swift delete mode 100644 Pods/Eureka/Source/Core/RowProtocols.swift delete mode 100644 Pods/Eureka/Source/Core/RowType.swift delete mode 100644 Pods/Eureka/Source/Core/Section.swift delete mode 100644 Pods/Eureka/Source/Core/SelectableRowType.swift delete mode 100644 Pods/Eureka/Source/Core/SelectableSection.swift delete mode 100644 Pods/Eureka/Source/Core/SwipeActions.swift delete mode 100644 Pods/Eureka/Source/Core/Validation.swift delete mode 100644 Pods/Eureka/Source/Rows/ActionSheetRow.swift delete mode 100644 Pods/Eureka/Source/Rows/AlertRow.swift delete mode 100644 Pods/Eureka/Source/Rows/ButtonRow.swift delete mode 100644 Pods/Eureka/Source/Rows/ButtonRowWithPresent.swift delete mode 100644 Pods/Eureka/Source/Rows/CheckRow.swift delete mode 100644 Pods/Eureka/Source/Rows/Common/AlertOptionsRow.swift delete mode 100644 Pods/Eureka/Source/Rows/Common/DateFieldRow.swift delete mode 100644 Pods/Eureka/Source/Rows/Common/DateInlineFieldRow.swift delete mode 100644 Pods/Eureka/Source/Rows/Common/DecimalFormatter.swift delete mode 100644 Pods/Eureka/Source/Rows/Common/FieldRow.swift delete mode 100644 Pods/Eureka/Source/Rows/Common/GenericMultipleSelectorRow.swift delete mode 100644 Pods/Eureka/Source/Rows/Common/OptionsRow.swift delete mode 100644 Pods/Eureka/Source/Rows/Common/Protocols.swift delete mode 100644 Pods/Eureka/Source/Rows/Common/SelectorRow.swift delete mode 100644 Pods/Eureka/Source/Rows/Controllers/MultipleSelectorViewController.swift delete mode 100644 Pods/Eureka/Source/Rows/Controllers/SelectorAlertController.swift delete mode 100644 Pods/Eureka/Source/Rows/Controllers/SelectorViewController.swift delete mode 100644 Pods/Eureka/Source/Rows/DateInlineRow.swift delete mode 100644 Pods/Eureka/Source/Rows/DatePickerRow.swift delete mode 100644 Pods/Eureka/Source/Rows/DateRow.swift delete mode 100644 Pods/Eureka/Source/Rows/DoublePickerInputRow.swift delete mode 100644 Pods/Eureka/Source/Rows/DoublePickerRow.swift delete mode 100644 Pods/Eureka/Source/Rows/FieldsRow.swift delete mode 100644 Pods/Eureka/Source/Rows/LabelRow.swift delete mode 100644 Pods/Eureka/Source/Rows/MultipleSelectorRow.swift delete mode 100644 Pods/Eureka/Source/Rows/PickerInlineRow.swift delete mode 100644 Pods/Eureka/Source/Rows/PickerInputRow.swift delete mode 100644 Pods/Eureka/Source/Rows/PickerRow.swift delete mode 100644 Pods/Eureka/Source/Rows/PopoverSelectorRow.swift delete mode 100644 Pods/Eureka/Source/Rows/PushRow.swift delete mode 100644 Pods/Eureka/Source/Rows/SegmentedRow.swift delete mode 100644 Pods/Eureka/Source/Rows/SelectableRows/ListCheckRow.swift delete mode 100644 Pods/Eureka/Source/Rows/SliderRow.swift delete mode 100644 Pods/Eureka/Source/Rows/StepperRow.swift delete mode 100644 Pods/Eureka/Source/Rows/SwitchRow.swift delete mode 100644 Pods/Eureka/Source/Rows/TextAreaRow.swift delete mode 100644 Pods/Eureka/Source/Rows/TriplePickerInputRow.swift delete mode 100644 Pods/Eureka/Source/Rows/TriplePickerRow.swift delete mode 100644 Pods/Eureka/Source/Validations/RuleClosure.swift delete mode 100644 Pods/Eureka/Source/Validations/RuleEmail.swift delete mode 100644 Pods/Eureka/Source/Validations/RuleEqualsToRow.swift delete mode 100644 Pods/Eureka/Source/Validations/RuleLength.swift delete mode 100644 Pods/Eureka/Source/Validations/RuleRange.swift delete mode 100644 Pods/Eureka/Source/Validations/RuleRegExp.swift delete mode 100644 Pods/Eureka/Source/Validations/RuleRequired.swift delete mode 100644 Pods/Eureka/Source/Validations/RuleURL.swift delete mode 100644 Pods/Local Podspecs/Eureka.podspec.json delete mode 100644 Pods/Target Support Files/Eureka/Eureka-Info.plist delete mode 100644 Pods/Target Support Files/Eureka/Eureka-dummy.m delete mode 100644 Pods/Target Support Files/Eureka/Eureka-prefix.pch delete mode 100644 Pods/Target Support Files/Eureka/Eureka-umbrella.h delete mode 100644 Pods/Target Support Files/Eureka/Eureka.debug.xcconfig delete mode 100644 Pods/Target Support Files/Eureka/Eureka.modulemap delete mode 100644 Pods/Target Support Files/Eureka/Eureka.release.xcconfig diff --git a/Podfile b/Podfile index a53ba817e..fb7fcce76 100644 --- a/Podfile +++ b/Podfile @@ -2,7 +2,6 @@ target 'LoopFollow' do use_frameworks! pod 'Charts' - pod 'Eureka', :git => 'https://github.com/xmartlabs/Eureka.git' pod 'ShareClient', :git => 'https://github.com/loopandlearn/dexcom-share-client-swift.git', :branch => 'loopfollow' end diff --git a/Podfile.lock b/Podfile.lock index 82e0343dc..9a7875837 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -3,13 +3,11 @@ PODS: - Charts/Core (= 4.1.0) - Charts/Core (4.1.0): - SwiftAlgorithms (~> 1.0) - - Eureka (5.3.6) - ShareClient (1.2) - SwiftAlgorithms (1.0.0) DEPENDENCIES: - Charts - - Eureka (from `https://github.com/xmartlabs/Eureka.git`) - ShareClient (from `https://github.com/loopandlearn/dexcom-share-client-swift.git`, branch `loopfollow`) SPEC REPOS: @@ -18,23 +16,17 @@ SPEC REPOS: - SwiftAlgorithms EXTERNAL SOURCES: - Eureka: - :git: https://github.com/xmartlabs/Eureka.git ShareClient: :branch: loopfollow :git: https://github.com/loopandlearn/dexcom-share-client-swift.git CHECKOUT OPTIONS: - Eureka: - :commit: 044e31674d319c8edb19d993a5f8ea4e24641542 - :git: https://github.com/xmartlabs/Eureka.git ShareClient: :commit: d7a3323a014d41827ee177a92d528786f9f09e75 :git: https://github.com/loopandlearn/dexcom-share-client-swift.git SPEC CHECKSUMS: Charts: ce0768268078eee0336f122c3c4ca248e4e204c5 - Eureka: 28ad9dec6286cd7cd601fdf8e8df39bb7356a8f4 ShareClient: 60b911c95e73b0ea9c5aad6d194a9c6b5f34b741 SwiftAlgorithms: 38dda4731d19027fdeee1125f973111bf3386b53 diff --git a/Pods/Eureka/LICENSE b/Pods/Eureka/LICENSE deleted file mode 100644 index 910ab05c3..000000000 --- a/Pods/Eureka/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2019 XMARTLABS - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/Pods/Eureka/README.md b/Pods/Eureka/README.md deleted file mode 100644 index c1501de32..000000000 --- a/Pods/Eureka/README.md +++ /dev/null @@ -1,1343 +0,0 @@ -![Eureka: Elegant form builder in Swift](Eureka.jpg) - -

-Build status -Platform iOS -Swift 5 compatible -Carthage compatible -CocoaPods compatible -License: MIT -codebeat badge -

- -Made with ❤️ by [XMARTLABS](http://xmartlabs.com). This is the re-creation of [XLForm] in Swift. - -[简体中文](Documentation/README_CN.md) - -## Overview - - - - - - - -
- - - - - -
- -## Contents - -* [Requirements] -* [Usage] - + [How to create a Form] - + [Getting row values] - + [Operators] - + [Using the callbacks] - + [Section Header and Footer] - + [Dynamically hide and show rows (or sections)] - + [List sections] - + [Multivalued sections] - + [Validations] - + [Swipe Actions] -* [Custom rows] - + [Basic custom rows] - + [Custom inline rows] - + [Custom presenter rows] -* [Row catalog] -* [Installation] -* [FAQ] - -**For more information look at [our blog post] that introduces *Eureka*.** - -## Requirements (for latest release) - -* Xcode 11+ -* Swift 5.0+ - -### Example project - -You can clone and run the Example project to see examples of most of Eureka's features. - - - - - - -
- - - -
- -## Usage - -### How to create a form -By extending `FormViewController` you can then simply add sections and rows to the `form` variable. - -```swift -import Eureka - -class MyFormViewController: FormViewController { - - override func viewDidLoad() { - super.viewDidLoad() - form +++ Section("Section1") - <<< TextRow(){ row in - row.title = "Text Row" - row.placeholder = "Enter text here" - } - <<< PhoneRow(){ - $0.title = "Phone Row" - $0.placeholder = "And numbers here" - } - +++ Section("Section2") - <<< DateRow(){ - $0.title = "Date Row" - $0.value = Date(timeIntervalSinceReferenceDate: 0) - } - } -} -``` - -In the example we create two sections with standard rows, the result is this: - -
-Screenshot of Custom Cells -
- -You could create a form by just setting up the `form` property by yourself without extending from `FormViewController` but this method is typically more convenient. - -#### Configuring the keyboard navigation accesory - -To change the behaviour of this you should set the navigation options of your controller. The `FormViewController` has a `navigationOptions` variable which is an enum and can have one or more of the following values: - -- **disabled**: no view at all -- **enabled**: enable view at the bottom -- **stopDisabledRow**: if the navigation should stop when the next row is disabled -- **skipCanNotBecomeFirstResponderRow**: if the navigation should skip the rows that return false to `canBecomeFirstResponder()` - -The default value is `enabled & skipCanNotBecomeFirstResponderRow` - -To enable smooth scrolling to off-screen rows, enable it via the `animateScroll` property. By default, the `FormViewController` jumps immediately between rows when the user hits the next or previous buttons in the keyboard navigation accesory, including when the next row is off screen. - -To set the amount of space between the keyboard and the highlighted row following a navigation event, set the `rowKeyboardSpacing` property. By default, when the form scrolls to an offscreen view no space will be left between the top of the keyboard and the bottom of the row. - -```swift -class MyFormViewController: FormViewController { - - override func viewDidLoad() { - super.viewDidLoad() - form = ... - - // Enables the navigation accessory and stops navigation when a disabled row is encountered - navigationOptions = RowNavigationOptions.Enabled.union(.StopDisabledRow) - // Enables smooth scrolling on navigation to off-screen rows - animateScroll = true - // Leaves 20pt of space between the keyboard and the highlighted row after scrolling to an off screen row - rowKeyboardSpacing = 20 - } -} -``` - -If you want to change the whole navigation accessory view, you will have to override the `navigationAccessoryView` variable in your subclass of `FormViewController`. - - -### Getting row values - -The `Row` object holds a ***value*** of a specific type. -For example, a `SwitchRow` holds a `Bool` value, while a `TextRow` holds a `String` value. - -```swift -// Get the value of a single row -let row: TextRow? = form.rowBy(tag: "MyRowTag") -let value = row.value - -// Get the value of all rows which have a Tag assigned -// The dictionary contains the 'rowTag':value pairs. -let valuesDictionary = form.values() -``` - -### Operators - -Eureka includes custom operators to make form creation easy: - -#### +++       Add a section -```swift -form +++ Section() - -// Chain it to add multiple Sections -form +++ Section("First Section") +++ Section("Another Section") - -// Or use it with rows and get a blank section for free -form +++ TextRow() - +++ TextRow() // Each row will be on a separate section -``` - -#### <<<       Insert a row - -```swift -form +++ Section() - <<< TextRow() - <<< DateRow() - -// Or implicitly create the Section -form +++ TextRow() - <<< DateRow() -``` - -#### +=        Append an array - -```swift -// Append Sections into a Form -form += [Section("A"), Section("B"), Section("C")] - -// Append Rows into a Section -section += [TextRow(), DateRow()] -``` - -### Using the callbacks - -Eureka includes callbacks to change the appearance and behavior of a row. - -#### Understanding Row and Cell - -A `Row` is an abstraction Eureka uses which holds a **value** and contains the view `Cell`. The `Cell` manages the view and subclasses `UITableViewCell`. - -Here is an example: - -```swift -let row = SwitchRow("SwitchRow") { row in // initializer - row.title = "The title" - }.onChange { row in - row.title = (row.value ?? false) ? "The title expands when on" : "The title" - row.updateCell() - }.cellSetup { cell, row in - cell.backgroundColor = .lightGray - }.cellUpdate { cell, row in - cell.textLabel?.font = .italicSystemFont(ofSize: 18.0) - } -``` - -Screenshot of Disabled Row - -#### Callbacks list - -* **onChange()** - - Called when the value of a row changes. You might be interested in adjusting some parameters here or even make some other rows appear or disappear. - -* **onCellSelection()** - - Called each time the user taps on the row and it gets selected. Note that this will also get called for disabled rows so you should start your code inside this callback with something like `guard !row.isDisabled else { return }` - -* **cellSetup()** - - Called only once when the cell is first configured. Set permanent settings here. - -* **cellUpdate()** - - Called each time the cell appears on screen. You can change the appearance here using variables that may not be present on cellSetup(). - -* **onCellHighlightChanged()** - - Called whenever the cell or any subview become or resign the first responder. - -* **onRowValidationChanged()** - - Called whenever the the validation errors associated with a row changes. - -* **onExpandInlineRow()** - - Called before expanding the inline row. Applies to rows conforming `InlineRowType` protocol. - -* **onCollapseInlineRow()** - - Called before collapsing the inline row. Applies to rows conforming `InlineRowType` protocol. - -* **onPresent()** - - Called by a row just before presenting another view controller. Applies to rows conforming `PresenterRowType` protocol. Use it to set up the presented controller. - - -### Section Header and Footer - -You can set a title `String` or a custom `View` as the header or footer of a `Section`. - -#### String title -```swift -Section("Title") - -Section(header: "Title", footer: "Footer Title") - -Section(footer: "Footer Title") -``` - -#### Custom view -You can use a Custom View from a `.xib` file: - -```swift -Section() { section in - var header = HeaderFooterView(.nibFile(name: "MyHeaderNibFile", bundle: nil)) - - // Will be called every time the header appears on screen - header.onSetupView = { view, _ in - // Commonly used to setup texts inside the view - // Don't change the view hierarchy or size here! - } - section.header = header -} -``` - -Or a custom `UIView` created programmatically - -```swift -Section(){ section in - var header = HeaderFooterView(.class) - header.height = {100} - header.onSetupView = { view, _ in - view.backgroundColor = .red - } - section.header = header -} -``` -Or just build the view with a Callback -```swift -Section(){ section in - section.header = { - var header = HeaderFooterView(.callback({ - let view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) - view.backgroundColor = .red - return view - })) - header.height = { 100 } - return header - }() -} -``` - -### Dynamically hide and show rows (or sections) - -Screenshot of Hidden Rows - -In this case we are hiding and showing whole sections. - -To accomplish this each row has a `hidden` variable of optional type `Condition` which can be set using a function or `NSPredicate`. - - -#### Hiding using a function condition - -Using the `function` case of `Condition`: -```swift -Condition.function([String], (Form)->Bool) -``` -The array of `String` to pass should contain the tags of the rows this row depends on. Each time the value of any of those rows changes the function is reevaluated. -The function then takes the `Form` and returns a `Bool` indicating whether the row should be hidden or not. This the most powerful way of setting up the `hidden` property as it has no explicit limitations of what can be done. - -```swift -form +++ Section() - <<< SwitchRow("switchRowTag"){ - $0.title = "Show message" - } - <<< LabelRow(){ - - $0.hidden = Condition.function(["switchRowTag"], { form in - return !((form.rowBy(tag: "switchRowTag") as? SwitchRow)?.value ?? false) - }) - $0.title = "Switch is on!" - } -``` - -Screenshot of Hidden Rows - -```swift -public enum Condition { - case function([String], (Form)->Bool) - case predicate(NSPredicate) -} -``` - -#### Hiding using an NSPredicate - -The `hidden` variable can also be set with a NSPredicate. In the predicate string you can reference values of other rows by their tags to determine if a row should be hidden or visible. -This will only work if the values of the rows the predicate has to check are NSObjects (String and Int will work as they are bridged to their ObjC counterparts, but enums won't work). -Why could it then be useful to use predicates when they are more limited? Well, they can be much simpler, shorter and readable than functions. Look at this example: - -```swift -$0.hidden = Condition.predicate(NSPredicate(format: "$switchTag == false")) -``` - -And we can write it even shorter since `Condition` conforms to `ExpressibleByStringLiteral`: - -```swift -$0.hidden = "$switchTag == false" -``` - -*Note: we will substitute the value of the row whose tag is 'switchTag' instead of '$switchTag'* - -For all of this to work, **all of the implicated rows must have a tag** as the tag will identify them. - -We can also hide a row by doing: -```swift -$0.hidden = true -``` -as `Condition` conforms to `ExpressibleByBooleanLiteral`. - -Not setting the `hidden` variable will leave the row always visible. - -If you manually set the hidden (or disabled) condition after the form has been displayed you may have to call `row.evaluateHidden()` to force Eureka to reevaluate the new condition. -See [this FAQ section](https://github.com/xmartlabs/Eureka#row-does-not-update-after-changing-hidden-or-disabled-condition) for more info. - -##### Sections -For sections this works just the same. That means we can set up section `hidden` property to show/hide it dynamically. - -##### Disabling rows -To disable rows, each row has an `disabled` variable which is also an optional `Condition` type property. This variable also works the same as the `hidden` variable so that it requires the rows to have a tag. - -Note that if you want to disable a row permanently you can also set `disabled` variable to `true`. - -### List Sections - -To display a list of options, Eureka includes a special section called `SelectableSection`. -When creating one you need to pass the type of row to use in the options and the `selectionType`. -The `selectionType` is an enum which can be either `multipleSelection` or `singleSelection(enableDeselection: Bool)` where the `enableDeselection` parameter determines if the selected rows can be deselected or not. - -```swift -form +++ SelectableSection>("Where do you live", selectionType: .singleSelection(enableDeselection: true)) - -let continents = ["Africa", "Antarctica", "Asia", "Australia", "Europe", "North America", "South America"] -for option in continents { - form.last! <<< ListCheckRow(option){ listRow in - listRow.title = option - listRow.selectableValue = option - listRow.value = nil - } -} -``` - -##### What kind of rows can be used? - -To create such a section you have to create a row that conforms the `SelectableRowType` protocol. - -```swift -public protocol SelectableRowType : RowType { - var selectableValue : Value? { get set } -} -``` - -This `selectableValue` is where the value of the row will be permanently stored. The `value` variable will be used to determine if the row is selected or not, being 'selectableValue' if selected or nil otherwise. -Eureka includes the `ListCheckRow` which is used for example. In the custom rows of the Examples project you can also find the `ImageCheckRow`. - -##### Getting the selected rows - -To easily get the selected row/s of a `SelectableSection` there are two methods: `selectedRow()` and `selectedRows()` which can be called to get the selected row in case it is a `SingleSelection` section or all the selected rows if it is a `MultipleSelection` section. - -##### Grouping options in sections - -Additionally you can setup list of options to be grouped by sections using following properties of `SelectorViewController`: - -- `sectionKeyForValue` - a closure that should return key for particular row value. This key is later used to break options by sections. - -- `sectionHeaderTitleForKey` - a closure that returns header title for a section for particular key. By default returns the key itself. - -- `sectionFooterTitleForKey` - a closure that returns footer title for a section for particular key. - -### Multivalued Sections - -Eureka supports multiple values for a certain field (such as telephone numbers in a contact) by using Multivalued sections. It allows us to easily create insertable, deletable and reorderable sections. - -Screenshot of Multivalued Section - -#### How to create a multivalued section - -In order to create a multivalued section we have to use `MultivaluedSection` type instead of the regular `Section` type. `MultivaluedSection` extends `Section` and has some additional properties to configure multivalued section behavior. - -let's dive into a code example... - -```swift -form +++ - MultivaluedSection(multivaluedOptions: [.Reorder, .Insert, .Delete], - header: "Multivalued TextField", - footer: ".Insert adds a 'Add Item' (Add New Tag) button row as last cell.") { - $0.addButtonProvider = { section in - return ButtonRow(){ - $0.title = "Add New Tag" - } - } - $0.multivaluedRowToInsertAt = { index in - return NameRow() { - $0.placeholder = "Tag Name" - } - } - $0 <<< NameRow() { - $0.placeholder = "Tag Name" - } - } -``` - -Previous code snippet shows how to create a multivalued section. In this case we want to insert, delete and reorder rows as multivaluedOptions argument indicates. - -`addButtonProvider` allows us to customize the button row which inserts a new row when tapped and `multivaluedOptions` contains `.Insert` value. - -`multivaluedRowToInsertAt` closure property is called by Eureka each time a new row needs to be inserted. In order to provide the row to add into multivalued section we should set this property. Eureka passes the index as closure parameter. Notice that we can return any kind of row, even custom rows, even though in most cases multivalued section rows are of the same type. - -Eureka automatically adds a button row when we create a insertable multivalued section. We can customize how the this button row looks like as we explained before. `showInsertIconInAddButton` property indicates if plus button (insert style) should appear in the left of the button, true by default. - -There are some considerations we need to have in mind when creating insertable sections. Any row added to the insertable multivalued section should be placed above the row that Eureka automatically adds to insert new rows. This can be easily achieved by adding these additional rows to the section from inside the section's initializer closure (last parameter of section initializer) so then Eureka adds the adds insert button at the end of the section. - -#### Editing mode - -By default Eureka will set the tableView's `isEditing` to true only if there is a MultivaluedSection in the form. This will be done in `viewWillAppear` the first time a form is presented. - -For more information on how to use multivalued sections please take a look at Eureka example project which contains several usage examples. - -#### Custom add button -If you want to use an add button which is not a `ButtonRow` then you can use `GenericMultivaluedSection`, where `AddButtonType` is the type of the row you want to use as add button. This is useful if you want to use a custom row to change the UI of the button. - -Example: - -```swift -GenericMultivaluedSection(multivaluedOptions: [.Reorder, .Insert, .Delete], { - $0.addButtonProvider = { section in - return LabelRow(){ - $0.title = "A Label row as add button" - } - } - // ... -} -``` - -### Validations - -Eureka 2.0.0 introduces the much requested built-in validations feature. - -A row has a collection of `Rules` and a specific configuration that determines when validation rules should be evaluated. - -There are some rules provided by default, but you can also create new ones on your own. - -The provided rules are: -* RuleRequired -* RuleEmail -* RuleURL -* RuleGreaterThan, RuleGreaterOrEqualThan, RuleSmallerThan, RuleSmallerOrEqualThan -* RuleMinLength, RuleMaxLength -* RuleClosure - -Let's see how to set up the validation rules. - -```swift - -override func viewDidLoad() { - super.viewDidLoad() - form - +++ Section(header: "Required Rule", footer: "Options: Validates on change") - - <<< TextRow() { - $0.title = "Required Rule" - $0.add(rule: RuleRequired()) - - // This could also have been achieved using a closure that returns nil if valid, or a ValidationError otherwise. - /* - let ruleRequiredViaClosure = RuleClosure { rowValue in - return (rowValue == nil || rowValue!.isEmpty) ? ValidationError(msg: "Field required!") : nil - } - $0.add(rule: ruleRequiredViaClosure) - */ - - $0.validationOptions = .validatesOnChange - } - .cellUpdate { cell, row in - if !row.isValid { - cell.titleLabel?.textColor = .systemRed - } - } - - +++ Section(header: "Email Rule, Required Rule", footer: "Options: Validates on change after blurred") - - <<< TextRow() { - $0.title = "Email Rule" - $0.add(rule: RuleRequired()) - $0.add(rule: RuleEmail()) - $0.validationOptions = .validatesOnChangeAfterBlurred - } - .cellUpdate { cell, row in - if !row.isValid { - cell.titleLabel?.textColor = .systemRed - } - } - -``` - -As you can see in the previous code snippet we can set up as many rules as we want in a row by invoking row's `add(rule:)` function. - -Row also provides `func remove(ruleWithIdentifier identifier: String)` to remove a rule. In order to use it we must assign an id to the rule after creating it. - -Sometimes the collection of rules we want to use on a row is the same we want to use on many other rows. In this case we can set up all validation rules using a `RuleSet` which is a collection of validation rules. - -```swift -var rules = RuleSet() -rules.add(rule: RuleRequired()) -rules.add(rule: RuleEmail()) - -let row = TextRow() { - $0.title = "Email Rule" - $0.add(ruleSet: rules) - $0.validationOptions = .validatesOnChangeAfterBlurred - } -``` - -Eureka allows us to specify when validation rules should be evaluated. We can do it by setting up `validationOptions` row's property, which can have the following values: - - -* `.validatesOnChange` - Validates whenever a row value changes. -* `.validatesOnBlur` - (Default value) validates right after the cell resigns first responder. Not applicable for all rows. -* `.validatesOnChangeAfterBlurred` - Validates whenever the row value changes after it resigns first responder for the first time. -* `.validatesOnDemand` - We should manually validate the row or form by invoking `validate()` method. - -If you want to validate the entire form (all the rows) you can manually invoke Form `validate()` method. - -#### How to get validation errors - -Each row has the `validationErrors` property that can be used to retrieve all validation errors. This property just holds the validation error list of the latest row validation execution, which means it doesn't evaluate the validation rules of the row. - -#### Note on types - -As expected, the Rules must use the same types as the Row object. Be extra careful to check the row type used. You might see a compiler error ("Incorrect arugment label in call (have 'rule:' expected 'ruleSet:')" that is not pointing to the problem when mixing types. - -### Swipe Actions - -By using swipe actions we can define multiple `leadingSwipe` and `trailingSwipe` actions per row. As swipe actions depend on iOS system features, `leadingSwipe` is available on iOS 11.0+ only. - -Let's see how to define swipe actions. - -```swift -let row = TextRow() { - let deleteAction = SwipeAction( - style: .destructive, - title: "Delete", - handler: { (action, row, completionHandler) in - //add your code here. - //make sure you call the completionHandler once done. - completionHandler?(true) - }) - deleteAction.image = UIImage(named: "icon-trash") - - $0.trailingSwipe.actions = [deleteAction] - $0.trailingSwipe.performsFirstActionWithFullSwipe = true - - //please be aware: `leadingSwipe` is only available on iOS 11+ only - let infoAction = SwipeAction( - style: .normal, - title: "Info", - handler: { (action, row, completionHandler) in - //add your code here. - //make sure you call the completionHandler once done. - completionHandler?(true) - }) - infoAction.actionBackgroundColor = .blue - infoAction.image = UIImage(named: "icon-info") - - $0.leadingSwipe.actions = [infoAction] - $0.leadingSwipe.performsFirstActionWithFullSwipe = true - } -``` - -Swipe Actions need `tableView.isEditing` be set to `false`. Eureka will set this to `true` if there is a MultivaluedSection in the form (in the `viewWillAppear`). -If you have both MultivaluedSections and swipe actions in the same form you should set `isEditing` according to your needs. - -## Custom rows - -It is very common that you need a row that is different from those included in Eureka. If this is the case you will have to create your own row but this should not be difficult. You can read [this tutorial on how to create custom rows](https://blog.xmartlabs.com/2016/09/06/Eureka-custom-row-tutorial/) to get started. You might also want to have a look at [EurekaCommunity] which includes some extra rows ready to be added to Eureka. - -### Basic custom rows - -To create a row with custom behaviour and appearance you'll probably want to create subclasses of `Row` and `Cell`. - -Remember that `Row` is the abstraction Eureka uses, while the `Cell` is the actual `UITableViewCell` in charge of the view. -As the `Row` contains the `Cell`, both `Row` and `Cell` must be defined for the same **value** type. - -```swift -// Custom Cell with value type: Bool -// The cell is defined using a .xib, so we can set outlets :) -public class CustomCell: Cell, CellType { - @IBOutlet weak var switchControl: UISwitch! - @IBOutlet weak var label: UILabel! - - public override func setup() { - super.setup() - switchControl.addTarget(self, action: #selector(CustomCell.switchValueChanged), for: .valueChanged) - } - - func switchValueChanged(){ - row.value = switchControl.on - row.updateCell() // Re-draws the cell which calls 'update' bellow - } - - public override func update() { - super.update() - backgroundColor = (row.value ?? false) ? .white : .black - } -} - -// The custom Row also has the cell: CustomCell and its correspond value -public final class CustomRow: Row, RowType { - required public init(tag: String?) { - super.init(tag: tag) - // We set the cellProvider to load the .xib corresponding to our cell - cellProvider = CellProvider(nibName: "CustomCell") - } -} -``` -The result:
-Screenshot of Disabled Row - -
-Custom rows need to subclass `Row` and conform to `RowType` protocol. -Custom cells need to subclass `Cell` and conform to `CellType` protocol. - -Just like the callbacks cellSetup and CellUpdate, the `Cell` has the setup and update methods where you can customize it. - - -### Custom inline rows - -An inline row is a specific type of row that shows dynamically a row below it, normally an inline row changes between an expanded and collapsed mode whenever the row is tapped. - -So to create an inline row we need 2 rows, the row that is "always" visible and the row that will expand/collapse. - -Another requirement is that the value type of these 2 rows must be the same. This means if one row holds a `String` value then the other must have a `String` value too. - -Once we have these 2 rows, we should make the top row type conform to `InlineRowType`. -This protocol requires you to define an `InlineRow` typealias and a `setupInlineRow` function. -The `InlineRow` type will be the type of the row that will expand/collapse. -Take this as an example: - -```swift -class PickerInlineRow : Row> where T: Equatable { - - public typealias InlineRow = PickerRow - open var options = [T]() - - required public init(tag: String?) { - super.init(tag: tag) - } - - public func setupInlineRow(_ inlineRow: InlineRow) { - inlineRow.options = self.options - inlineRow.displayValueFor = self.displayValueFor - inlineRow.cell.height = { UITableViewAutomaticDimension } - } -} -``` - -The `InlineRowType` will also add some methods to your inline row: - -```swift -func expandInlineRow() -func collapseInlineRow() -func toggleInlineRow() -``` - -These methods should work fine but should you want to override them keep in mind that it is `toggleInlineRow` that has to call `expandInlineRow` and `collapseInlineRow`. - -Finally you must invoke `toggleInlineRow()` when the row is selected, for example overriding `customDidSelect`: - -```swift -public override func customDidSelect() { - super.customDidSelect() - if !isDisabled { - toggleInlineRow() - } -} -``` - -### Custom Presenter rows - -**Note:** *A Presenter row is a row that presents a new UIViewController.* - -To create a custom Presenter row you must create a class that conforms the `PresenterRowType` protocol. It is highly recommended to subclass `SelectorRow` as it does conform to that protocol and adds other useful functionality. - -The PresenterRowType protocol is defined as follows: -```swift -public protocol PresenterRowType: TypedRowType { - - associatedtype PresentedControllerType : UIViewController, TypedRowControllerType - - /// Defines how the view controller will be presented, pushed, etc. - var presentationMode: PresentationMode? { get set } - - /// Will be called before the presentation occurs. - var onPresentCallback: ((FormViewController, PresentedControllerType) -> Void)? { get set } -} -``` - -The onPresentCallback will be called when the row is about to present another view controller. This is done in the `SelectorRow` so if you do not subclass it you will have to call it yourself. - -The `presentationMode` is what defines how the controller is presented and which controller is presented. This presentation can be using a Segue identifier, a segue class, presenting a controller modally or pushing to a specific view controller. For example a CustomPushRow can be defined like this: - - -Let's see an example.. - -```swift - -/// Generic row type where a user must select a value among several options. -open class SelectorRow: OptionsRow, PresenterRowType where Cell: BaseCell { - - - /// Defines how the view controller will be presented, pushed, etc. - open var presentationMode: PresentationMode>>? - - /// Will be called before the presentation occurs. - open var onPresentCallback: ((FormViewController, SelectorViewController>) -> Void)? - - required public init(tag: String?) { - super.init(tag: tag) - } - - /** - Extends `didSelect` method - */ - open override func customDidSelect() { - super.customDidSelect() - guard let presentationMode = presentationMode, !isDisabled else { return } - if let controller = presentationMode.makeController() { - controller.row = self - controller.title = selectorTitle ?? controller.title - onPresentCallback?(cell.formViewController()!, controller) - presentationMode.present(controller, row: self, presentingController: self.cell.formViewController()!) - } else { - presentationMode.present(nil, row: self, presentingController: self.cell.formViewController()!) - } - } - - /** - Prepares the pushed row setting its title and completion callback. - */ - open override func prepare(for segue: UIStoryboardSegue) { - super.prepare(for: segue) - guard let rowVC = segue.destination as Any as? SelectorViewController> else { return } - rowVC.title = selectorTitle ?? rowVC.title - rowVC.onDismissCallback = presentationMode?.onDismissCallback ?? rowVC.onDismissCallback - onPresentCallback?(cell.formViewController()!, rowVC) - rowVC.row = self - } -} - - -// SelectorRow conforms to PresenterRowType -public final class CustomPushRow: SelectorRow>, RowType { - - public required init(tag: String?) { - super.init(tag: tag) - presentationMode = .show(controllerProvider: ControllerProvider.callback { - return SelectorViewController(){ _ in } - }, onDismiss: { vc in - _ = vc.navigationController?.popViewController(animated: true) - }) - } -} -``` - - -### Subclassing cells using the same row - -Sometimes we want to change the UI look of one of our rows but without changing the row type and all the logic associated to one row. -There is currently one way to do this **if you are using cells that are instantiated from nib files**. Currently, none of Eureka's core rows are instantiated from nib files but some of the custom rows in [EurekaCommunity] are, in particular the [PostalAddressRow](https://github.com/EurekaCommunity/PostalAddressRow) which was moved there. - -What you have to do is: -* Create a nib file containing the cell you want to create. -* Then set the class of the cell to be the existing cell you want to modify (if you want to change something more apart from pure UI then you should subclass that cell). Make sure the module of that class is correctly set -* Connect the outlets to your class -* Tell your row to use the new nib file. This is done by setting the `cellProvider` variable to use this nib. You should do this in the initialiser, either in each concrete instantiation or using the `defaultRowInitializer`. For example: - -```swift -<<< PostalAddressRow() { - $0.cellProvider = CellProvider(nibName: "CustomNib", bundle: Bundle.main) -} -``` - -You could also create a new row for this. In that case try to inherit from the same superclass as the row you want to change to inherit its logic. - -There are some things to consider when you do this: -* If you want to see an example have a look at the [PostalAddressRow](https://github.com/EurekaCommunity/PostalAddressRow) or the [CreditCardRow](https://github.com/EurekaCommunity/CreditCardRow) which have use a custom nib file in their examples. -* If you get an error saying `Unknown class in Interface Builder file`, it might be that you have to instantiate that new type somewhere in your code to load it in the runtime. Calling `let t = YourClass.self` helped in my case. - - -## Row catalog - -### Controls Rows - - - - - - - - - - - - - - -
Label Row
- -


-
Button Row
- -


-
Check Row
- -


-
Switch Row
- -


-
Slider Row
- -


-
Stepper Row
- -


-
Text Area Row
- -


-
- -### Field Rows -These rows have a textfield on the right side of the cell. The difference between each one of them consists in a different capitalization, autocorrection and keyboard type configuration. - - - - - - -
- - - TextRow

- NameRow

- URLRow

- IntRow

- PhoneRow

- PasswordRow

- EmailRow

- DecimalRow

- TwitterRow

- AccountRow

- ZipCodeRow -
- -All of the `FieldRow` subtypes above have a `formatter` property of type `NSFormatter` which can be set to determine how that row's value should be displayed. A custom formatter for numbers with two digits after the decimal mark is included with Eureka (`DecimalFormatter`). The Example project also contains a `CurrencyFormatter` which displays a number as currency according to the user's locale. - -By default, setting a row's `formatter` only affects how a value is displayed when it is not being edited. To also format the value while the row is being edited, set `useFormatterDuringInput` to `true` when initializing the row. Formatting the value as it is being edited may require updating the cursor position and Eureka provides the following protocol that your formatter should conform to in order to handle cursor position: - -```swift -public protocol FormatterProtocol { - func getNewPosition(forPosition forPosition: UITextPosition, inTextInput textInput: UITextInput, oldValue: String?, newValue: String?) -> UITextPosition -} -``` - -Additionally, `FieldRow` subtypes have a `useFormatterOnDidBeginEditing` property. When using a `DecimalRow` with a formatter that allows decimal values and conforms to the user's locale (e.g. `DecimalFormatter`), if `useFormatterDuringInput` is `false`, `useFormatterOnDidBeginEditing` must be set to `true` so that the decimal mark in the value being edited matches the decimal mark on the keyboard. - -### Date Rows - -Date Rows hold a Date and allow us to set up a new value through UIDatePicker control. The mode of the UIDatePicker and the way how the date picker view is shown is what changes between them. - - - - - - -
-
Date Row - -
-Picker shown in the keyboard. -
-
-
Date Row (Inline) - -
-The row expands. -
-
-
Date Row (Picker) - -
-The picker is always visible. -
-
- -With those 3 styles (Normal, Inline & Picker), Eureka includes: - -+ **DateRow** -+ **TimeRow** -+ **DateTimeRow** -+ **CountDownRow** - -### Option Rows -These are rows with a list of options associated from which the user must choose. - -```swift -<<< ActionSheetRow() { - $0.title = "ActionSheetRow" - $0.selectorTitle = "Pick a number" - $0.options = ["One","Two","Three"] - $0.value = "Two" // initially selected - } -``` - - - - - - - - -
-
Alert Row
- -
-Will show an alert with the options to choose from. -
-
-
ActionSheet Row
- -
-Will show an action sheet with the options to choose from. -
-
-
Push Row
- -
-Will push to a new controller from where to choose options listed using Check rows. -
-
-
Multiple Selector Row
- -
-Like PushRow but allows the selection of multiple options. -
-
- - - - - - - -
Segmented Row
- -
-
Segmented Row (w/Title)
- -
-
Picker Row
- -
Presents options of a generic type through a picker view -
(There is also Picker Inline Row) -
-
- -### Built your own custom row? -Let us know about it, we would be glad to mention it here. :) - -* **LocationRow** (Included as custom row in the example project) - -Screenshot of Location Row - -## Installation - -#### CocoaPods - -[CocoaPods](https://cocoapods.org/) is a dependency manager for Cocoa projects. - -Specify Eureka into your project's `Podfile`: - -```ruby -source 'https://github.com/CocoaPods/Specs.git' -platform :ios, '9.0' -use_frameworks! - -pod 'Eureka' -``` - -Then run the following command: - -```bash -$ pod install -``` - -#### Swift Package Manager - -[Swift Package Manager](https://swift.org/package-manager/) is a tool for managing the distribution of Swift code. - -After you set up your `Package.swift` manifest file, you can add Eureka as a dependency by adding it to the dependencies value of your `Package.swift`. - -dependencies: [ - .package(url: "https://github.com/xmartlabs/Eureka.git", from: "5.3.6") -] - - -#### Carthage - -[Carthage](https://github.com/Carthage/Carthage) is a simple, decentralized dependency manager for Cocoa. - -Specify Eureka into your project's `Cartfile`: - -```ogdl -github "xmartlabs/Eureka" ~> 5.3 -``` - -#### Manually as Embedded Framework - -* Clone Eureka as a git [submodule](http://git-scm.com/docs/git-submodule) by running the following command from your project root git folder. - -```bash -$ git submodule add https://github.com/xmartlabs/Eureka.git -``` - -* Open Eureka folder that was created by the previous git submodule command and drag the Eureka.xcodeproj into the Project Navigator of your application's Xcode project. - -* Select the Eureka.xcodeproj in the Project Navigator and verify the deployment target matches with your application deployment target. - -* Select your project in the Xcode Navigation and then select your application target from the sidebar. Next select the "General" tab and click on the + button under the "Embedded Binaries" section. - -* Select `Eureka.framework` and we are done! - -## Getting involved - -* If you **want to contribute** please feel free to **submit pull requests**. -* If you **have a feature request** please **open an issue**. -* If you **found a bug** check older issues before submitting an issue. -* If you **need help** or would like to **ask general question**, use [StackOverflow]. (Tag `eureka-forms`). - -**Before contribute check the [CONTRIBUTING](CONTRIBUTING.md) file for more info.** - -If you use **Eureka** in your app We would love to hear about it! Drop us a line on [twitter]. - -## Authors - -* [Martin Barreto](https://github.com/mtnBarreto) ([@mtnBarreto](https://twitter.com/mtnBarreto)) -* [Mathias Claassen](https://github.com/mats-claassen) ([@mClaassen26](https://twitter.com/mClaassen26)) - -## FAQ - -#### How to change the text representation of the row value shown in the cell. - -Every row has the following property: - -```swift -/// Block variable used to get the String that should be displayed for the value of this row. -public var displayValueFor: ((T?) -> String?)? = { - return $0.map { String(describing: $0) } -} -``` - -You can set `displayValueFor` according the string value you want to display. - -#### How to get a Row using its tag value - -We can get a particular row by invoking any of the following functions exposed by the `Form` class: - -```swift -public func rowBy(tag: String) -> RowOf? -public func rowBy(tag: String) -> Row? -public func rowBy(tag: String) -> BaseRow? -``` - -For instance: - -```swift -let dateRow : DateRow? = form.rowBy(tag: "dateRowTag") -let labelRow: LabelRow? = form.rowBy(tag: "labelRowTag") - -let dateRow2: Row? = form.rowBy(tag: "dateRowTag") - -let labelRow2: BaseRow? = form.rowBy(tag: "labelRowTag") -``` - -#### How to get a Section using its tag value - -```swift -let section: Section? = form.sectionBy(tag: "sectionTag") -``` - -#### How to set the form values using a dictionary - -Invoking `setValues(values: [String: Any?])` which is exposed by `Form` class. - -For example: - -```swift -form.setValues(["IntRowTag": 8, "TextRowTag": "Hello world!", "PushRowTag": Company(name:"Xmartlabs")]) -``` - -Where `"IntRowTag"`, `"TextRowTag"`, `"PushRowTag"` are row tags (each one uniquely identifies a row) and `8`, `"Hello world!"`, `Company(name:"Xmartlabs")` are the corresponding row value to assign. - -The value type of a row must match with the value type of the corresponding dictionary value otherwise nil will be assigned. - -If the form was already displayed we have to reload the visible rows either by reloading the table view `tableView.reloadData()` or invoking `updateCell()` to each visible row. - -#### Row does not update after changing hidden or disabled condition - -After setting a condition, this condition is not automatically evaluated. If you want it to do so immediately you can call `.evaluateHidden()` or `.evaluateDisabled()`. - -This functions are just called when a row is added to the form and when a row it depends on changes. If the condition is changed when the row is being displayed then it must be reevaluated manually. - -#### onCellUnHighlight doesn't get called unless onCellHighlight is also defined - -Look at this [issue](https://github.com/xmartlabs/Eureka/issues/96). - -#### How to update a Section header/footer - -* Set up a new header/footer data .... - -```swift -section.header = HeaderFooterView(title: "Header title \(variable)") // use String interpolation -//or -var header = HeaderFooterView(.class) // most flexible way to set up a header using any view type -header.height = { 60 } // height can be calculated -header.onSetupView = { view, section in // each time the view is about to be displayed onSetupView is invoked. - view.backgroundColor = .orange -} -section.header = header -``` - -* Reload the Section to perform the changes - -```swift -section.reload() -``` - -#### How to customize Selector and MultipleSelector option cells - -`selectableRowSetup`, `selectableRowCellUpdate` and `selectableRowCellSetup` properties are provided to be able to customize SelectorViewController and MultipleSelectorViewController selectable cells. - -```swift -let row = PushRow() { - $0.title = "PushRow" - $0.options = [💁🏻, 🍐, 👦🏼, 🐗, 🐼, 🐻] - $0.value = 👦🏼 - $0.selectorTitle = "Choose an Emoji!" - }.onPresent { from, to in - to.dismissOnSelection = false - to.dismissOnChange = false - to.selectableRowSetup = { row in - row.cellProvider = CellProvider>(nibName: "EmojiCell", bundle: Bundle.main) - } - to.selectableRowCellUpdate = { cell, row in - cell.textLabel?.text = "Text " + row.selectableValue! // customization - cell.detailTextLabel?.text = "Detail " + row.selectableValue! - } - } - -``` - -#### Don't want to use Eureka custom operators? - -As we've said `Form` and `Section` types conform to `MutableCollection` and `RangeReplaceableCollection`. A Form is a collection of Sections and a Section is a collection of Rows. - -`RangeReplaceableCollection` protocol extension provides many useful methods to modify collection. - -```swift -extension RangeReplaceableCollection { - public mutating func append(_ newElement: Self.Element) - public mutating func append(contentsOf newElements: S) where S : Sequence, Self.Element == S.Element - public mutating func insert(_ newElement: Self.Element, at i: Self.Index) - public mutating func insert(contentsOf newElements: S, at i: Self.Index) where S : Collection, Self.Element == S.Element - public mutating func remove(at i: Self.Index) -> Self.Element - public mutating func removeSubrange(_ bounds: Range) - public mutating func removeFirst(_ n: Int) - public mutating func removeFirst() -> Self.Element - public mutating func removeAll(keepingCapacity keepCapacity: Bool) - public mutating func reserveCapacity(_ n: Self.IndexDistance) -} -``` - -These methods are used internally to implement the custom operators as shown bellow: - -```swift -public func +++(left: Form, right: Section) -> Form { - left.append(right) - return left -} - -public func +=(inout lhs: Form, rhs: C) where C.Element == Section { - lhs.append(contentsOf: rhs) -} - -public func <<<(left: Section, right: BaseRow) -> Section { - left.append(right) - return left -} - -public func +=(inout lhs: Section, rhs: C) where C.Element == BaseRow { - lhs.append(contentsOf: rhs) -} -``` - -You can see how the rest of custom operators are implemented [here](https://github.com/xmartlabs/Eureka/blob/master/Source/Core/Operators.swift). - -It's up to you to decide if you want to use Eureka custom operators or not. - -#### How to set up your form from a storyboard -The form is always displayed in a `UITableView`. You can set up your view controller in a storyboard and add a UITableView where you want it to be and then connect the outlet to FormViewController's `tableView` variable. This allows you to define a custom frame (possibly with constraints) for your form. - -All of this can also be done by programmatically changing frame, margins, etc. of the `tableView` of your FormViewController. - - - -[Introduction]: #introduction -[Requirements]: #requirements - -[How to create a Form]: #how-to-create-a-form -[Getting row values]: #getting-row-values -[How to get the form values]: #how-to-get-the-form-values -[Examples]: #examples -[Usage]: #usage -[Operators]: #operators -[Rows]: #rows -[Using the callbacks]: #using-the-callbacks -[Section Header and Footer]: #section-header-and-footer -[Custom rows]: #custom-rows -[Basic custom rows]: #basic-custom-rows -[Custom inline rows]: #custom-inline-rows -[Custom presenter rows]: #custom-presenter-rows -[How to create custom inline rows]: #how-to-create-custom-inline-rows -[Custom rows catalog]: #custom-rows-catalog -[Dynamically hide and show rows (or sections)]: #hide-show-rows -[Implementing a custom Presenter row]: #custom-presenter-row -[Extensibility]: #extensibility -[Row catalog]: #row-catalog -[Installation]: #installation -[FAQ]: #faq - -[List sections]: #list-sections -[Multivalued sections]: #multivalued-sections -[Validations]: #validations -[Swipe Actions]: #swipe-actions - - -[CustomCellsController]: Example/Example/ViewController.swift -[FormViewController]: Example/Source/Controllers.swift - - -[XLForm]: https://github.com/xmartlabs/XLForm -[DSL]: https://en.wikipedia.org/wiki/Domain-specific_language -[StackOverflow]: http://stackoverflow.com/questions/tagged/eureka-forms -[our blog post]: http://blog.xmartlabs.com/2015/09/29/Introducing-Eureka-iOS-form-library-written-in-pure-Swift/ -[twitter]: https://twitter.com/xmartlabs -[EurekaCommunity]: https://github.com/EurekaCommunity - -# Donate to Eureka - -So we can make Eureka even better!

-[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=HRMAH7WZ4QQ8E) - -# Change Log - -This can be found in the [CHANGELOG.md](CHANGELOG.md) file. diff --git a/Pods/Eureka/Source/Core/BaseRow.swift b/Pods/Eureka/Source/Core/BaseRow.swift deleted file mode 100644 index ee60820f7..000000000 --- a/Pods/Eureka/Source/Core/BaseRow.swift +++ /dev/null @@ -1,294 +0,0 @@ -// BaseRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -open class BaseRow: BaseRowType { - - var callbackOnChange: (() -> Void)? - var callbackCellUpdate: (() -> Void)? - var callbackCellSetup: Any? - var callbackCellOnSelection: (() -> Void)? - var callbackOnExpandInlineRow: Any? - var callbackOnCollapseInlineRow: Any? - var callbackOnCellHighlightChanged: (() -> Void)? - var callbackOnRowValidationChanged: (() -> Void)? - var _inlineRow: BaseRow? - - var _cachedOptionsData: Any? - - public var validationOptions: ValidationOptions = .validatesOnBlur - // validation state - public internal(set) var validationErrors = [ValidationError]() { - didSet { - guard validationErrors != oldValue else { return } - RowDefaults.onRowValidationChanged["\(type(of: self))"]?(baseCell, self) - callbackOnRowValidationChanged?() - updateCell() - } - } - - public internal(set) var wasBlurred = false - public internal(set) var wasChanged = false - - public var isValid: Bool { return validationErrors.isEmpty } - public var isHighlighted: Bool = false - - /// The title will be displayed in the textLabel of the row. - public var title: String? - - /// Parameter used when creating the cell for this row. - public var cellStyle = UITableViewCell.CellStyle.value1 - - /// String that uniquely identifies a row. Must be unique among rows and sections. - public var tag: String? - - /// The untyped cell associated to this row. - public var baseCell: BaseCell! { return nil } - - /// The untyped value of this row. - public var baseValue: Any? { - set {} - get { return nil } - } - - open func validate(quietly: Bool = false) -> [ValidationError] { - return [] - } - - // Reset validation - open func cleanValidationErrors() { - validationErrors = [] - } - - public static var estimatedRowHeight: CGFloat = 44.0 - - /// Condition that determines if the row should be disabled or not. - public var disabled: Condition? { - willSet { removeFromDisabledRowObservers() } - didSet { addToDisabledRowObservers() } - } - - /// Condition that determines if the row should be hidden or not. - public var hidden: Condition? { - willSet { removeFromHiddenRowObservers() } - didSet { addToHiddenRowObservers() } - } - - /// Returns if this row is currently disabled or not - public var isDisabled: Bool { return disabledCache } - - /// Returns if this row is currently hidden or not - public var isHidden: Bool { return hiddenCache } - - /// The section to which this row belongs. - open weak var section: Section? - - public lazy var trailingSwipe = {[unowned self] in SwipeConfiguration(self)}() - - //needs the accessor because if marked directly this throws "Stored properties cannot be marked potentially unavailable with '@available'" - private lazy var _leadingSwipe = {[unowned self] in SwipeConfiguration(self)}() - - @available(iOS 11,*) - public var leadingSwipe: SwipeConfiguration{ - get { return self._leadingSwipe } - set { self._leadingSwipe = newValue } - } - - public required init(tag: String? = nil) { - self.tag = tag - } - - /** - Method that reloads the cell - */ - open func updateCell() {} - - /** - Method called when the cell belonging to this row was selected. Must call the corresponding method in its cell. - */ - open func didSelect() {} - - open func prepare(for segue: UIStoryboardSegue) {} - - /** - Helps to pick destination part of the cell after scrolling - */ - open var destinationScrollPosition: UITableView.ScrollPosition? = UITableView.ScrollPosition.bottom - - /** - Returns the IndexPath where this row is in the current form. - */ - public final var indexPath: IndexPath? { - guard let sectionIndex = section?.index, let rowIndex = section?.firstIndex(of: self) else { return nil } - return IndexPath(row: rowIndex, section: sectionIndex) - } - - var hiddenCache = false - var disabledCache = false { - willSet { - if newValue && !disabledCache { - baseCell.cellResignFirstResponder() - } - } - } -} - -extension BaseRow { - - /** - Evaluates if the row should be hidden or not and updates the form accordingly - */ - public final func evaluateHidden() { - guard let h = hidden, let form = section?.form else { return } - switch h { - case .function(_, let callback): - hiddenCache = callback(form) - case .predicate(let predicate): - hiddenCache = predicate.evaluate(with: self, substitutionVariables: form.dictionaryValuesToEvaluatePredicate()) - } - if hiddenCache { - section?.hide(row: self) - } else { - section?.show(row: self) - } - } - - /** - Evaluates if the row should be disabled or not and updates it accordingly - */ - public final func evaluateDisabled() { - guard let d = disabled, let form = section?.form else { return } - switch d { - case .function(_, let callback): - disabledCache = callback(form) - case .predicate(let predicate): - disabledCache = predicate.evaluate(with: self, substitutionVariables: form.dictionaryValuesToEvaluatePredicate()) - } - updateCell() - } - - final func wasAddedTo(section: Section) { - self.section = section - if let t = tag { - assert(section.form?.rowsByTag[t] == nil, "Duplicate tag \(t)") - self.section?.form?.rowsByTag[t] = self - self.section?.form?.tagToValues[t] = baseValue != nil ? baseValue! : NSNull() - } - addToRowObservers() - evaluateHidden() - evaluateDisabled() - } - - final func addToHiddenRowObservers() { - guard let h = hidden else { return } - switch h { - case .function(let tags, _): - section?.form?.addRowObservers(to: self, rowTags: tags, type: .hidden) - case .predicate(let predicate): - section?.form?.addRowObservers(to: self, rowTags: predicate.predicateVars, type: .hidden) - } - } - - final func addToDisabledRowObservers() { - guard let d = disabled else { return } - switch d { - case .function(let tags, _): - section?.form?.addRowObservers(to: self, rowTags: tags, type: .disabled) - case .predicate(let predicate): - section?.form?.addRowObservers(to: self, rowTags: predicate.predicateVars, type: .disabled) - } - } - - final func addToRowObservers() { - addToHiddenRowObservers() - addToDisabledRowObservers() - } - - final func willBeRemovedFromForm() { - (self as? BaseInlineRowType)?.collapseInlineRow() - if let t = tag { - section?.form?.rowsByTag[t] = nil - section?.form?.tagToValues[t] = nil - } - removeFromRowObservers() - } - - final func willBeRemovedFromSection() { - willBeRemovedFromForm() - section = nil - } - - final func removeFromHiddenRowObservers() { - guard let h = hidden else { return } - switch h { - case .function(let tags, _): - section?.form?.removeRowObservers(from: self, rowTags: tags, type: .hidden) - case .predicate(let predicate): - section?.form?.removeRowObservers(from: self, rowTags: predicate.predicateVars, type: .hidden) - } - } - - final func removeFromDisabledRowObservers() { - guard let d = disabled else { return } - switch d { - case .function(let tags, _): - section?.form?.removeRowObservers(from: self, rowTags: tags, type: .disabled) - case .predicate(let predicate): - section?.form?.removeRowObservers(from: self, rowTags: predicate.predicateVars, type: .disabled) - } - } - - final func removeFromRowObservers() { - removeFromHiddenRowObservers() - removeFromDisabledRowObservers() - } -} - -extension BaseRow: Equatable, Hidable, Disableable {} - -extension BaseRow { - - public func reload(with rowAnimation: UITableView.RowAnimation = .none) { - guard let tableView = baseCell?.formViewController()?.tableView ?? (section?.form?.delegate as? FormViewController)?.tableView, let indexPath = indexPath else { return } - tableView.reloadRows(at: [indexPath], with: rowAnimation) - } - - public func deselect(animated: Bool = true) { - guard let indexPath = indexPath, - let tableView = baseCell?.formViewController()?.tableView ?? (section?.form?.delegate as? FormViewController)?.tableView else { return } - tableView.deselectRow(at: indexPath, animated: animated) - } - - public func select(animated: Bool = false, scrollPosition: UITableView.ScrollPosition = .none) { - guard let indexPath = indexPath, - let tableView = baseCell?.formViewController()?.tableView ?? (section?.form?.delegate as? FormViewController)?.tableView else { return } - tableView.selectRow(at: indexPath, animated: animated, scrollPosition: scrollPosition) - } -} - -public func == (lhs: BaseRow, rhs: BaseRow) -> Bool { - return lhs === rhs -} diff --git a/Pods/Eureka/Source/Core/Cell.swift b/Pods/Eureka/Source/Core/Cell.swift deleted file mode 100644 index 84d138f45..000000000 --- a/Pods/Eureka/Source/Core/Cell.swift +++ /dev/null @@ -1,174 +0,0 @@ -// Cell.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -/// Base class for the Eureka cells -@objc(EurekaBaseCell) -open class BaseCell: UITableViewCell, BaseCellType { - - /// Untyped row associated to this cell. - public var baseRow: BaseRow! { return nil } - - /// Block that returns the height for this cell. - public var height: (() -> CGFloat)? - - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - public required override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - /** - Function that returns the FormViewController this cell belongs to. - */ - public func formViewController() -> FormViewController? { - var responder: UIResponder? = self - while responder != nil { - if let formVC = responder as? FormViewController { - return formVC - } - responder = responder?.next - } - return nil - } - - open func setup() {} - open func update() {} - - open func didSelect() {} - - /** - If the cell can become first responder. By default returns false - */ - open func cellCanBecomeFirstResponder() -> Bool { - return false - } - - /** - Called when the cell becomes first responder - */ - @discardableResult - open func cellBecomeFirstResponder(withDirection: Direction = .down) -> Bool { - return becomeFirstResponder() - } - - /** - Called when the cell resigns first responder - */ - @discardableResult - open func cellResignFirstResponder() -> Bool { - return resignFirstResponder() - } -} - -/// Generic class that represents the Eureka cells. -open class Cell: BaseCell, TypedCellType where T: Equatable { - - public typealias Value = T - - /// The row associated to this cell - public weak var row: RowOf! - - private var updatingCellForTintColorDidChange = false - - /// Returns the navigationAccessoryView if it is defined or calls super if not. - override open var inputAccessoryView: UIView? { - if let v = formViewController()?.inputAccessoryView(for: row) { - return v - } - return super.inputAccessoryView - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - /** - Function responsible for setting up the cell at creation time. - */ - open override func setup() { - super.setup() - } - - /** - Function responsible for updating the cell each time it is reloaded. - */ - open override func update() { - super.update() - textLabel?.text = row.title - if #available(iOS 13.0, *) { - textLabel?.textColor = row.isDisabled ? .tertiaryLabel : .label - } else { - textLabel?.textColor = row.isDisabled ? .gray : .black - } - detailTextLabel?.text = row.displayValueFor?(row.value) ?? (row as? NoValueDisplayTextConformance)?.noValueDisplayText - } - - /** - Called when the cell was selected. - */ - open override func didSelect() {} - - override open var canBecomeFirstResponder: Bool { - return false - } - - open override func becomeFirstResponder() -> Bool { - let result = super.becomeFirstResponder() - if result { - formViewController()?.beginEditing(of: self) - } - return result - } - - open override func resignFirstResponder() -> Bool { - let result = super.resignFirstResponder() - if result { - formViewController()?.endEditing(of: self) - } - return result - } - - open override func tintColorDidChange() { - super.tintColorDidChange() - - /* Protection from infinite recursion in case an update method changes the tintColor */ - if !updatingCellForTintColorDidChange && row != nil { - updatingCellForTintColorDidChange = true - row.updateCell() - updatingCellForTintColorDidChange = false - } - } - - /// The untyped row associated to this cell. - public override var baseRow: BaseRow! { return row } -} diff --git a/Pods/Eureka/Source/Core/CellType.swift b/Pods/Eureka/Source/Core/CellType.swift deleted file mode 100644 index 95dc37815..000000000 --- a/Pods/Eureka/Source/Core/CellType.swift +++ /dev/null @@ -1,81 +0,0 @@ -// CellType.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -// MARK: Cell Protocols - -public protocol BaseCellType : AnyObject { - - /// Method that will return the height of the cell - var height : (() -> CGFloat)? { get } - - /** - Method called once when creating a cell. Responsible for setting up the cell. - */ - func setup() - - /** - Method called each time the cell is updated (e.g. 'cellForRowAtIndexPath' is called). Responsible for updating the cell. - */ - func update() - - /** - Method called each time the cell is selected (tapped on by the user). - */ - func didSelect() - - /** - Called when cell is about to become first responder - - - returns: If the cell should become first responder. - */ - func cellCanBecomeFirstResponder() -> Bool - - /** - Method called when the cell becomes first responder - */ - func cellBecomeFirstResponder(withDirection: Direction) -> Bool - - /** - Method called when the cell resigns first responder - */ - func cellResignFirstResponder() -> Bool - - /** - A reference to the controller in which the cell is displayed. - */ - func formViewController () -> FormViewController? -} - -public protocol TypedCellType: BaseCellType { - - associatedtype Value: Equatable - - /// The row associated to this cell. - var row: RowOf! { get set } -} - -public protocol CellType: TypedCellType {} diff --git a/Pods/Eureka/Source/Core/Core.swift b/Pods/Eureka/Source/Core/Core.swift deleted file mode 100644 index 4822a6cac..000000000 --- a/Pods/Eureka/Source/Core/Core.swift +++ /dev/null @@ -1,1151 +0,0 @@ -// Core.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -// MARK: Row - -internal class RowDefaults { - static var cellUpdate = [String: (BaseCell, BaseRow) -> Void]() - static var cellSetup = [String: (BaseCell, BaseRow) -> Void]() - static var onCellHighlightChanged = [String: (BaseCell, BaseRow) -> Void]() - static var rowInitialization = [String: (BaseRow) -> Void]() - static var onRowValidationChanged = [String: (BaseCell, BaseRow) -> Void]() - static var rawCellUpdate = [String: Any]() - static var rawCellSetup = [String: Any]() - static var rawOnCellHighlightChanged = [String: Any]() - static var rawRowInitialization = [String: Any]() - static var rawOnRowValidationChanged = [String: Any]() -} - -// MARK: FormCells - -public struct CellProvider where Cell: CellType { - - /// Nibname of the cell that will be created. - public private (set) var nibName: String? - - /// Bundle from which to get the nib file. - public private (set) var bundle: Bundle! - - public init() {} - - public init(nibName: String, bundle: Bundle? = nil) { - self.nibName = nibName - self.bundle = bundle ?? Bundle(for: Cell.self) - } - - /** - Creates the cell with the specified style. - - - parameter cellStyle: The style with which the cell will be created. - - - returns: the cell - */ - func makeCell(style: UITableViewCell.CellStyle) -> Cell { - if let nibName = self.nibName { - return bundle.loadNibNamed(nibName, owner: nil, options: nil)!.first as! Cell - } - return Cell.init(style: style, reuseIdentifier: nil) - } -} - -/** - Enumeration that defines how a controller should be created. - - - Callback->VCType: Creates the controller inside the specified block - - NibFile: Loads a controller from a nib file in some bundle - - StoryBoard: Loads the controller from a Storyboard by its storyboard id - */ -public enum ControllerProvider { - - /** - * Creates the controller inside the specified block - */ - case callback(builder: (() -> VCType)) - - /** - * Loads a controller from a nib file in some bundle - */ - case nibFile(name: String, bundle: Bundle?) - - /** - * Loads the controller from a Storyboard by its storyboard id - */ - case storyBoard(storyboardId: String, storyboardName: String, bundle: Bundle?) - - func makeController() -> VCType { - switch self { - case .callback(let builder): - return builder() - case .nibFile(let nibName, let bundle): - return VCType.init(nibName: nibName, bundle:bundle ?? Bundle(for: VCType.self)) - case .storyBoard(let storyboardId, let storyboardName, let bundle): - let sb = UIStoryboard(name: storyboardName, bundle: bundle ?? Bundle(for: VCType.self)) - return sb.instantiateViewController(withIdentifier: storyboardId) as! VCType - } - } -} - -/** - Defines how a controller should be presented. - - - Show?: Shows the controller with `showViewController(...)`. - - PresentModally?: Presents the controller modally. - - SegueName?: Performs the segue with the specified identifier (name). - - SegueClass?: Performs a segue from a segue class. - */ -public enum PresentationMode { - - /** - * Shows the controller, created by the specified provider, with `showViewController(...)`. - */ - case show(controllerProvider: ControllerProvider, onDismiss: ((UIViewController) -> Void)?) - - /** - * Presents the controller, created by the specified provider, modally. - */ - case presentModally(controllerProvider: ControllerProvider, onDismiss: ((UIViewController) -> Void)?) - - /** - * Performs the segue with the specified identifier (name). - */ - case segueName(segueName: String, onDismiss: ((UIViewController) -> Void)?) - - /** - * Performs a segue from a segue class. - */ - case segueClass(segueClass: UIStoryboardSegue.Type, onDismiss: ((UIViewController) -> Void)?) - - case popover(controllerProvider: ControllerProvider, onDismiss: ((UIViewController) -> Void)?) - - public var onDismissCallback: ((UIViewController) -> Void)? { - switch self { - case .show(_, let completion): - return completion - case .presentModally(_, let completion): - return completion - case .segueName(_, let completion): - return completion - case .segueClass(_, let completion): - return completion - case .popover(_, let completion): - return completion - } - } - - /** - Present the view controller provided by PresentationMode. Should only be used from custom row implementation. - - - parameter viewController: viewController to present if it makes sense (normally provided by makeController method) - - parameter row: associated row - - parameter presentingViewController: form view controller - */ - public func present(_ viewController: VCType!, row: BaseRow, presentingController: FormViewController) { - switch self { - case .show(_, _): - presentingController.show(viewController, sender: row) - case .presentModally(_, _): - presentingController.present(viewController, animated: true) - case .segueName(let segueName, _): - presentingController.performSegue(withIdentifier: segueName, sender: row) - case .segueClass(let segueClass, _): - let segue = segueClass.init(identifier: row.tag, source: presentingController, destination: viewController) - presentingController.prepare(for: segue, sender: row) - segue.perform() - case .popover(_, _): - guard let porpoverController = viewController.popoverPresentationController else { - fatalError() - } - porpoverController.sourceView = porpoverController.sourceView ?? presentingController.tableView - presentingController.present(viewController, animated: true) - } - - } - - /** - Creates the view controller specified by presentation mode. Should only be used from custom row implementation. - - - returns: the created view controller or nil depending on the PresentationMode type. - */ - public func makeController() -> VCType? { - switch self { - case .show(let controllerProvider, let completionCallback): - let controller = controllerProvider.makeController() - let completionController = controller as? RowControllerType - if let callback = completionCallback { - completionController?.onDismissCallback = callback - } - return controller - case .presentModally(let controllerProvider, let completionCallback): - let controller = controllerProvider.makeController() - let completionController = controller as? RowControllerType - if let callback = completionCallback { - completionController?.onDismissCallback = callback - } - return controller - case .popover(let controllerProvider, let completionCallback): - let controller = controllerProvider.makeController() - controller.modalPresentationStyle = .popover - let completionController = controller as? RowControllerType - if let callback = completionCallback { - completionController?.onDismissCallback = callback - } - return controller - default: - return nil - } - } -} - -/** - * Protocol to be implemented by custom formatters. - */ -public protocol FormatterProtocol { - func getNewPosition(forPosition: UITextPosition, inTextInput textInput: UITextInput, oldValue: String?, newValue: String?) -> UITextPosition -} - -// MARK: Predicate Machine - -enum ConditionType { - case hidden, disabled -} - -/** - Enumeration that are used to specify the disbaled and hidden conditions of rows - - - Function: A function that calculates the result - - Predicate: A predicate that returns the result - */ -public enum Condition { - /** - * Calculate the condition inside a block - * - * @param Array of tags of the rows this function depends on - * @param Form->Bool The block that calculates the result - * - * @return If the condition is true or false - */ - case function([String], (Form)->Bool) - - /** - * Calculate the condition using a NSPredicate - * - * @param NSPredicate The predicate that will be evaluated - * - * @return If the condition is true or false - */ - case predicate(NSPredicate) -} - -extension Condition : ExpressibleByBooleanLiteral { - - /** - Initialize a condition to return afixed boolean value always - */ - public init(booleanLiteral value: Bool) { - self = Condition.function([]) { _ in return value } - } -} - -extension Condition : ExpressibleByStringLiteral { - - /** - Initialize a Condition with a string that will be converted to a NSPredicate - */ - public init(stringLiteral value: String) { - self = .predicate(NSPredicate(format: value)) - } - - /** - Initialize a Condition with a string that will be converted to a NSPredicate - */ - public init(unicodeScalarLiteral value: String) { - self = .predicate(NSPredicate(format: value)) - } - - /** - Initialize a Condition with a string that will be converted to a NSPredicate - */ - public init(extendedGraphemeClusterLiteral value: String) { - self = .predicate(NSPredicate(format: value)) - } -} - -// MARK: Errors - -/** -Errors thrown by Eureka - - - duplicatedTag: When a section or row is inserted whose tag dows already exist - - rowNotInSection: When a row was expected to be in a Section, but is not. -*/ -public enum EurekaError: Error { - case duplicatedTag(tag: String) - case rowNotInSection(row: BaseRow) -} - -//Mark: FormViewController - -/** -* A protocol implemented by FormViewController -*/ -public protocol FormViewControllerProtocol { - var tableView: UITableView! { get } - - func beginEditing(of: Cell) - func endEditing(of: Cell) - - func insertAnimation(forRows rows: [BaseRow]) -> UITableView.RowAnimation - func deleteAnimation(forRows rows: [BaseRow]) -> UITableView.RowAnimation - func reloadAnimation(oldRows: [BaseRow], newRows: [BaseRow]) -> UITableView.RowAnimation - func insertAnimation(forSections sections: [Section]) -> UITableView.RowAnimation - func deleteAnimation(forSections sections: [Section]) -> UITableView.RowAnimation - func reloadAnimation(oldSections: [Section], newSections: [Section]) -> UITableView.RowAnimation -} - -/** - * Navigation options for a form view controller. - */ -public struct RowNavigationOptions: OptionSet { - - private enum NavigationOptions: Int { - case disabled = 0, enabled = 1, stopDisabledRow = 2, skipCanNotBecomeFirstResponderRow = 4 - } - public let rawValue: Int - public init(rawValue: Int) { self.rawValue = rawValue} - private init(_ options: NavigationOptions ) { self.rawValue = options.rawValue } - - /// No navigation. - public static let Disabled = RowNavigationOptions(.disabled) - - /// Full navigation. - public static let Enabled = RowNavigationOptions(.enabled) - - /// Break navigation when next row is disabled. - public static let StopDisabledRow = RowNavigationOptions(.stopDisabledRow) - - /// Break navigation when next row cannot become first responder. - public static let SkipCanNotBecomeFirstResponderRow = RowNavigationOptions(.skipCanNotBecomeFirstResponderRow) -} - -/** - * Defines the configuration for the keyboardType of FieldRows. - */ -public struct KeyboardReturnTypeConfiguration { - /// Used when the next row is available. - public var nextKeyboardType = UIReturnKeyType.next - - /// Used if next row is not available. - public var defaultKeyboardType = UIReturnKeyType.default - - public init() {} - - public init(nextKeyboardType: UIReturnKeyType, defaultKeyboardType: UIReturnKeyType) { - self.nextKeyboardType = nextKeyboardType - self.defaultKeyboardType = defaultKeyboardType - } -} - -/** - * Options that define when an inline row should collapse. - */ -public struct InlineRowHideOptions: OptionSet { - - private enum _InlineRowHideOptions: Int { - case never = 0, anotherInlineRowIsShown = 1, firstResponderChanges = 2 - } - public let rawValue: Int - public init(rawValue: Int) { self.rawValue = rawValue} - private init(_ options: _InlineRowHideOptions ) { self.rawValue = options.rawValue } - - /// Never collapse automatically. Only when user taps inline row. - public static let Never = InlineRowHideOptions(.never) - - /// Collapse qhen another inline row expands. Just one inline row will be expanded at a time. - public static let AnotherInlineRowIsShown = InlineRowHideOptions(.anotherInlineRowIsShown) - - /// Collapse when first responder changes. - public static let FirstResponderChanges = InlineRowHideOptions(.firstResponderChanges) -} - -/// View controller that shows a form. -@objc(EurekaFormViewController) -open class FormViewController: UIViewController, FormViewControllerProtocol, FormDelegate { - - @IBOutlet public var tableView: UITableView! - - private lazy var _form: Form = { [weak self] in - let form = Form() - form.delegate = self - return form - }() - - public var form: Form { - get { return _form } - set { - guard form !== newValue else { return } - _form.delegate = nil - tableView?.endEditing(false) - _form = newValue - _form.delegate = self - if isViewLoaded { - tableView?.reloadData() - } - } - } - - /// Extra space to leave between between the row in focus and the keyboard - open var rowKeyboardSpacing: CGFloat = 0 - - /// Enables animated scrolling on row navigation - open var animateScroll = false - - /// The default scroll position on the focussed cell when keyboard appears - open var defaultScrollPosition = UITableView.ScrollPosition.none - - /// Accessory view that is responsible for the navigation between rows - private var navigationAccessoryView: (UIView & NavigationAccessory)! - - /// Custom Accesory View to be used as a replacement - open var customNavigationAccessoryView: (UIView & NavigationAccessory)? { - return nil - } - - /// Defines the behaviour of the navigation between rows - public var navigationOptions: RowNavigationOptions? - public var tableViewStyle: UITableView.Style = .grouped - - public init(style: UITableView.Style) { - super.init(nibName: nil, bundle: nil) - tableViewStyle = style - } - - public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { - super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - } - - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func viewDidLoad() { - super.viewDidLoad() - navigationAccessoryView = customNavigationAccessoryView ?? NavigationAccessoryView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 44.0)) - navigationAccessoryView.autoresizingMask = .flexibleWidth - - if tableView == nil { - tableView = UITableView(frame: view.bounds, style: tableViewStyle) - tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight] - tableView.cellLayoutMarginsFollowReadableWidth = false - } - if tableView.superview == nil { - view.addSubview(tableView) - } - if tableView.delegate == nil { - tableView.delegate = self - } - if tableView.dataSource == nil { - tableView.dataSource = self - } - tableView.rowHeight = UITableView.automaticDimension - tableView.estimatedRowHeight = BaseRow.estimatedRowHeight - tableView.allowsSelectionDuringEditing = true - } - - open override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - animateTableView = true - let selectedIndexPaths = tableView.indexPathsForSelectedRows ?? [] - if !selectedIndexPaths.isEmpty { - if #available(iOS 13.0, *) { - if tableView.window != nil { - tableView.reloadRows(at: selectedIndexPaths, with: .none) - } - } else { - tableView.reloadRows(at: selectedIndexPaths, with: .none) - } - } - selectedIndexPaths.forEach { - tableView.selectRow(at: $0, animated: false, scrollPosition: .none) - } - - let deselectionAnimation = { [weak self] (context: UIViewControllerTransitionCoordinatorContext) in - selectedIndexPaths.forEach { - self?.tableView.deselectRow(at: $0, animated: context.isAnimated) - } - } - - let reselection = { [weak self] (context: UIViewControllerTransitionCoordinatorContext) in - if context.isCancelled { - selectedIndexPaths.forEach { - self?.tableView.selectRow(at: $0, animated: false, scrollPosition: .none) - } - } - } - - if let coordinator = transitionCoordinator { - coordinator.animate(alongsideTransition: deselectionAnimation, completion: reselection) - } else { - selectedIndexPaths.forEach { - tableView.deselectRow(at: $0, animated: false) - } - } - - NotificationCenter.default.addObserver(self, selector: #selector(FormViewController.keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(FormViewController.keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) - - if form.containsMultivaluedSection && (isBeingPresented || isMovingToParent) { - tableView.setEditing(true, animated: false) - } - } - - open override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil) - } - - open override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - super.prepare(for: segue, sender: sender) - let baseRow = sender as? BaseRow - baseRow?.prepare(for: segue) - } - - /** - Returns the navigation accessory view if it is enabled. Returns nil otherwise. - */ - open func inputAccessoryView(for row: BaseRow) -> UIView? { - let options = navigationOptions ?? Form.defaultNavigationOptions - guard options.contains(.Enabled) else { return nil } - guard row.baseCell.cellCanBecomeFirstResponder() else { return nil} - navigationAccessoryView.previousEnabled = nextRow(for: row, withDirection: .up) != nil - navigationAccessoryView.doneClosure = { [weak self] in - self?.navigationDone() - } - navigationAccessoryView.previousClosure = { [weak self] in - self?.navigationPrevious() - } - navigationAccessoryView.nextClosure = { [weak self] in - self?.navigationNext() - } - navigationAccessoryView.nextEnabled = nextRow(for: row, withDirection: .down) != nil - return navigationAccessoryView - } - - // MARK: FormViewControllerProtocol - - /** - Called when a cell becomes first responder - */ - public final func beginEditing(of cell: Cell) { - cell.row.isHighlighted = true - cell.row.updateCell() - RowDefaults.onCellHighlightChanged["\(type(of: cell.row!))"]?(cell, cell.row) - cell.row.callbackOnCellHighlightChanged?() - guard let _ = tableView, (form.inlineRowHideOptions ?? Form.defaultInlineRowHideOptions).contains(.FirstResponderChanges) else { return } - let row = cell.baseRow - let inlineRow = row?._inlineRow - for row in form.allRows.filter({ $0 !== row && $0 !== inlineRow && $0._inlineRow != nil }) { - if let inlineRow = row as? BaseInlineRowType { - inlineRow.collapseInlineRow() - } - } - } - - /** - Called when a cell resigns first responder - */ - public final func endEditing(of cell: Cell) { - cell.row.isHighlighted = false - cell.row.wasBlurred = true - RowDefaults.onCellHighlightChanged["\(type(of: cell.row!))"]?(cell, cell.row) - cell.row.callbackOnCellHighlightChanged?() - if cell.row.validationOptions.contains(.validatesOnBlur) || (cell.row.wasChanged && cell.row.validationOptions.contains(.validatesOnChangeAfterBlurred)) { - cell.row.validate() - } - cell.row.updateCell() - } - - /** - Returns the animation for the insertion of the given rows. - */ - open func insertAnimation(forRows rows: [BaseRow]) -> UITableView.RowAnimation { - return .fade - } - - /** - Returns the animation for the deletion of the given rows. - */ - open func deleteAnimation(forRows rows: [BaseRow]) -> UITableView.RowAnimation { - return .fade - } - - /** - Returns the animation for the reloading of the given rows. - */ - open func reloadAnimation(oldRows: [BaseRow], newRows: [BaseRow]) -> UITableView.RowAnimation { - return .automatic - } - - /** - Returns the animation for the insertion of the given sections. - */ - open func insertAnimation(forSections sections: [Section]) -> UITableView.RowAnimation { - return .automatic - } - - /** - Returns the animation for the deletion of the given sections. - */ - open func deleteAnimation(forSections sections: [Section]) -> UITableView.RowAnimation { - return .automatic - } - - /** - Returns the animation for the reloading of the given sections. - */ - open func reloadAnimation(oldSections: [Section], newSections: [Section]) -> UITableView.RowAnimation { - return .automatic - } - - // MARK: TextField and TextView Delegate - - open func textInputShouldBeginEditing(_ textInput: UITextInput, cell: Cell) -> Bool { - return true - } - - open func textInputDidBeginEditing(_ textInput: UITextInput, cell: Cell) { - if let row = cell.row as? KeyboardReturnHandler { - let next = nextRow(for: cell.row, withDirection: .down) - if let textField = textInput as? UITextField { - textField.returnKeyType = next != nil ? (row.keyboardReturnType?.nextKeyboardType ?? - (form.keyboardReturnType?.nextKeyboardType ?? Form.defaultKeyboardReturnType.nextKeyboardType )) : - (row.keyboardReturnType?.defaultKeyboardType ?? (form.keyboardReturnType?.defaultKeyboardType ?? - Form.defaultKeyboardReturnType.defaultKeyboardType)) - } else if let textView = textInput as? UITextView { - textView.returnKeyType = next != nil ? (row.keyboardReturnType?.nextKeyboardType ?? - (form.keyboardReturnType?.nextKeyboardType ?? Form.defaultKeyboardReturnType.nextKeyboardType )) : - (row.keyboardReturnType?.defaultKeyboardType ?? (form.keyboardReturnType?.defaultKeyboardType ?? - Form.defaultKeyboardReturnType.defaultKeyboardType)) - } - } - } - - open func textInputShouldEndEditing(_ textInput: UITextInput, cell: Cell) -> Bool { - return true - } - - open func textInputDidEndEditing(_ textInput: UITextInput, cell: Cell) { - - } - - open func textInput(_ textInput: UITextInput, shouldChangeCharactersInRange range: NSRange, replacementString string: String, cell: Cell) -> Bool { - return true - } - - open func textInputShouldClear(_ textInput: UITextInput, cell: Cell) -> Bool { - return true - } - - open func textInputShouldReturn(_ textInput: UITextInput, cell: Cell) -> Bool { - if let nextRow = nextRow(for: cell.row, withDirection: .down) { - if nextRow.baseCell.cellCanBecomeFirstResponder() { - nextRow.baseCell.cellBecomeFirstResponder() - return true - } - } - tableView?.endEditing(true) - return true - } - - // MARK: FormDelegate - - open func valueHasBeenChanged(for: BaseRow, oldValue: Any?, newValue: Any?) {} - - // MARK: UITableViewDelegate - - @objc open func tableView(_ tableView: UITableView, willBeginReorderingRowAtIndexPath indexPath: IndexPath) { - // end editing if inline cell is first responder - let row = form[indexPath] - if let inlineRow = row as? BaseInlineRowType, row._inlineRow != nil { - inlineRow.collapseInlineRow() - } - } - - // MARK: FormDelegate - - open func sectionsHaveBeenAdded(_ sections: [Section], at indexes: IndexSet) { - guard animateTableView else { return } - tableView?.beginUpdates() - tableView?.insertSections(indexes, with: insertAnimation(forSections: sections)) - tableView?.endUpdates() - } - - open func sectionsHaveBeenRemoved(_ sections: [Section], at indexes: IndexSet) { - guard animateTableView else { return } - tableView?.beginUpdates() - tableView?.deleteSections(indexes, with: deleteAnimation(forSections: sections)) - tableView?.endUpdates() - } - - open func sectionsHaveBeenReplaced(oldSections: [Section], newSections: [Section], at indexes: IndexSet) { - guard animateTableView else { return } - tableView?.beginUpdates() - tableView?.reloadSections(indexes, with: reloadAnimation(oldSections: oldSections, newSections: newSections)) - tableView?.endUpdates() - } - - open func rowsHaveBeenAdded(_ rows: [BaseRow], at indexes: [IndexPath]) { - guard animateTableView else { return } - tableView?.beginUpdates() - tableView?.insertRows(at: indexes, with: insertAnimation(forRows: rows)) - tableView?.endUpdates() - } - - open func rowsHaveBeenRemoved(_ rows: [BaseRow], at indexes: [IndexPath]) { - guard animateTableView else { return } - tableView?.beginUpdates() - tableView?.deleteRows(at: indexes, with: deleteAnimation(forRows: rows)) - tableView?.endUpdates() - } - - open func rowsHaveBeenReplaced(oldRows: [BaseRow], newRows: [BaseRow], at indexes: [IndexPath]) { - guard animateTableView else { return } - tableView?.beginUpdates() - tableView?.reloadRows(at: indexes, with: reloadAnimation(oldRows: oldRows, newRows: newRows)) - tableView?.endUpdates() - } - - // MARK: Private - - var oldBottomInset: CGFloat? - var animateTableView = false - - /** Calculates the height needed for a header or footer. */ - fileprivate func height(specifiedHeight: (() -> CGFloat)?, sectionView: UIView?, sectionTitle: String?) -> CGFloat { - if let height = specifiedHeight { - return height() - } - - if let sectionView = sectionView { - let height = sectionView.bounds.height - - if height == 0 { - return UITableView.automaticDimension - } - - return height - } - - if let sectionTitle = sectionTitle, - sectionTitle != "" { - return UITableView.automaticDimension - } - - // Fix for iOS 11+. By returning 0, we ensure that no section header or - // footer is shown when self-sizing is enabled (i.e. when - // tableView.estimatedSectionHeaderHeight or tableView.estimatedSectionFooterHeight - // == UITableView.automaticDimension). - if tableView.style == .plain { - return 0 - } - - return UITableView.automaticDimension - } -} - -extension FormViewController : UITableViewDelegate { - - // MARK: UITableViewDelegate - - open func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { - return indexPath - } - - open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard tableView == self.tableView else { return } - let row = form[indexPath] - // row.baseCell.cellBecomeFirstResponder() may be cause InlineRow collapsed then section count will be changed. Use orignal indexPath will out of section's bounds. - if !row.baseCell.cellCanBecomeFirstResponder() || !row.baseCell.cellBecomeFirstResponder() { - self.tableView?.endEditing(true) - } - row.didSelect() - } - - open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - guard tableView == self.tableView else { return tableView.rowHeight } - let row = form[indexPath.section][indexPath.row] - return row.baseCell.height?() ?? tableView.rowHeight - } - - open func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { - guard tableView == self.tableView else { return tableView.estimatedRowHeight } - let row = form[indexPath.section][indexPath.row] - return row.baseCell.height?() ?? tableView.estimatedRowHeight - } - - open func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - return form[section].header?.viewForSection(form[section], type: .header) - } - - open func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { - return form[section].footer?.viewForSection(form[section], type:.footer) - } - - open func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return height(specifiedHeight: form[section].header?.height, - sectionView: self.tableView(tableView, viewForHeaderInSection: section), - sectionTitle: self.tableView(tableView, titleForHeaderInSection: section)) - } - - open func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { - return height(specifiedHeight: form[section].footer?.height, - sectionView: self.tableView(tableView, viewForFooterInSection: section), - sectionTitle: self.tableView(tableView, titleForFooterInSection: section)) - } - - open func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { - let row = form[indexPath] - guard !row.isDisabled else { return false } - if row.trailingSwipe.actions.count > 0 { return true } - if #available(iOS 11,*), row.leadingSwipe.actions.count > 0 { return true } - guard let section = form[indexPath.section] as? BaseMultivaluedSection else { return false } - guard !(indexPath.row == section.count - 1 && section.multivaluedOptions.contains(.Insert) && section.showInsertIconInAddButton) else { - return true - } - if indexPath.row > 0 && section[indexPath.row - 1] is BaseInlineRowType && section[indexPath.row - 1]._inlineRow != nil { - return false - } - return true - } - - open func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { - if editingStyle == .delete { - let row = form[indexPath] - let section = row.section! - if let _ = row.baseCell.findFirstResponder() { - tableView.endEditing(true) - } - section.remove(at: indexPath.row) - } else if editingStyle == .insert { - guard var section = form[indexPath.section] as? BaseMultivaluedSection else { return } - guard let multivaluedRowToInsertAt = section.multivaluedRowToInsertAt else { - fatalError("Multivalued section multivaluedRowToInsertAt property must be set up") - } - let newRow = multivaluedRowToInsertAt(max(0, section.count - 1)) - section.insert(newRow, at: max(0, section.count - 1)) - DispatchQueue.main.async { - tableView.isEditing = !tableView.isEditing - tableView.isEditing = !tableView.isEditing - } - tableView.scrollToRow(at: IndexPath(row: section.count - 1, section: indexPath.section), at: .bottom, animated: true) - if newRow.baseCell.cellCanBecomeFirstResponder() { - newRow.baseCell.cellBecomeFirstResponder() - } else if let inlineRow = newRow as? BaseInlineRowType { - inlineRow.expandInlineRow() - } - } - } - - open func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { - guard let section = form[indexPath.section] as? BaseMultivaluedSection, section.multivaluedOptions.contains(.Reorder) && section.count > 1 else { - return false - } - if section.multivaluedOptions.contains(.Insert) && (section.count <= 2 || indexPath.row == (section.count - 1)) { - return false - } - if indexPath.row > 0 && section[indexPath.row - 1] is BaseInlineRowType && section[indexPath.row - 1]._inlineRow != nil { - return false - } - return true - } - - open func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath { - guard let section = form[sourceIndexPath.section] as? BaseMultivaluedSection else { return sourceIndexPath } - guard sourceIndexPath.section == proposedDestinationIndexPath.section else { return sourceIndexPath } - - let destRow = form[proposedDestinationIndexPath] - if destRow is BaseInlineRowType && destRow._inlineRow != nil { - return IndexPath(row: proposedDestinationIndexPath.row + (sourceIndexPath.row < proposedDestinationIndexPath.row ? 1 : -1), section:sourceIndexPath.section) - } - - if proposedDestinationIndexPath.row > 0 { - let previousRow = form[IndexPath(row: proposedDestinationIndexPath.row - 1, section: proposedDestinationIndexPath.section)] - if previousRow is BaseInlineRowType && previousRow._inlineRow != nil { - return IndexPath(row: proposedDestinationIndexPath.row + (sourceIndexPath.row < proposedDestinationIndexPath.row ? 1 : -1), section:sourceIndexPath.section) - } - } - if section.multivaluedOptions.contains(.Insert) && proposedDestinationIndexPath.row == section.count - 1 { - return IndexPath(row: section.count - 2, section: sourceIndexPath.section) - } - return proposedDestinationIndexPath - } - - open func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { - - guard var section = form[sourceIndexPath.section] as? BaseMultivaluedSection else { return } - if sourceIndexPath.row < section.count && destinationIndexPath.row < section.count && sourceIndexPath.row != destinationIndexPath.row { - - let sourceRow = form[sourceIndexPath] - animateTableView = false - section.remove(at: sourceIndexPath.row) - section.insert(sourceRow, at: destinationIndexPath.row) - animateTableView = true - // update the accessory view - let _ = inputAccessoryView(for: sourceRow) - } - } - - open func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { - guard let section = form[indexPath.section] as? BaseMultivaluedSection else { - if form[indexPath].trailingSwipe.actions.count > 0 { - return .delete - } - return .none - } - if section.multivaluedOptions.contains(.Insert) && indexPath.row == section.count - 1 { - return section.showInsertIconInAddButton ? .insert : .none - } - if section.multivaluedOptions.contains(.Delete) { - return .delete - } - return .none - } - - open func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool { - return self.tableView(tableView, editingStyleForRowAt: indexPath) != .none - } - - @available(iOS 11,*) - open func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - guard !form[indexPath].leadingSwipe.actions.isEmpty else { - return nil - } - return form[indexPath].leadingSwipe.contextualConfiguration - } - - @available(iOS 11,*) - open func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - guard !form[indexPath].trailingSwipe.actions.isEmpty else { - return nil - } - return form[indexPath].trailingSwipe.contextualConfiguration - } - - @available(macCatalyst, deprecated: 13.1, message: "UITableViewRowAction is deprecated, use leading/trailingSwipe actions instead") - @available(iOS, deprecated: 13, message: "UITableViewRowAction is deprecated, use leading/trailingSwipe actions instead") - open func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]?{ - guard let actions = form[indexPath].trailingSwipe.contextualActions as? [UITableViewRowAction], !actions.isEmpty else { - return nil - } - return actions - } -} - -extension FormViewController : UITableViewDataSource { - - // MARK: UITableViewDataSource - - open func numberOfSections(in tableView: UITableView) -> Int { - return form.count - } - - open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return form[section].count - } - - open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - form[indexPath].updateCell() - return form[indexPath].baseCell - } - - open func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { - return form[section].header?.title - } - - open func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { - return form[section].footer?.title - } - - - open func sectionIndexTitles(for tableView: UITableView) -> [String]? { - return nil - } - - open func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, at index: Int) -> Int { - return 0 - } -} - - -extension FormViewController : UIScrollViewDelegate { - - // MARK: UIScrollViewDelegate - - open func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { - guard let tableView = tableView, scrollView === tableView else { return } - tableView.endEditing(true) - } -} - -extension FormViewController { - - // MARK: KeyBoard Notifications - - /** - Called when the keyboard will appear. Adjusts insets of the tableView and scrolls it if necessary. - */ - @objc open func keyboardWillShow(_ notification: Notification) { - guard let table = tableView, let cell = table.findFirstResponder()?.formCell() else { return } - let keyBoardInfo = notification.userInfo! - let endFrame = keyBoardInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue - - let keyBoardFrame = table.window!.convert(endFrame.cgRectValue, to: table.superview) - var newBottomInset = table.frame.origin.y + table.frame.size.height - keyBoardFrame.origin.y + rowKeyboardSpacing - if #available(iOS 11.0, *) { - newBottomInset = newBottomInset - tableView.safeAreaInsets.bottom - } - var tableInsets = table.contentInset - var scrollIndicatorInsets = table.scrollIndicatorInsets - oldBottomInset = oldBottomInset ?? tableInsets.bottom - if newBottomInset > oldBottomInset! { - tableInsets.bottom = newBottomInset - scrollIndicatorInsets.bottom = tableInsets.bottom - UIView.beginAnimations(nil, context: nil) - UIView.setAnimationDuration((keyBoardInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as! Double)) - UIView.setAnimationCurve(UIView.AnimationCurve(rawValue: (keyBoardInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as! Int))!) - table.contentInset = tableInsets - table.scrollIndicatorInsets = scrollIndicatorInsets - if let selectedRow = table.indexPath(for: cell) { - if ProcessInfo.processInfo.operatingSystemVersion.majorVersion == 11 { - let rect = table.rectForRow(at: selectedRow) - table.scrollRectToVisible(rect, animated: animateScroll) - } else { - table.scrollToRow(at: selectedRow, at: defaultScrollPosition, animated: animateScroll) - } - } - UIView.commitAnimations() - } - } - - /** - Called when the keyboard will disappear. Adjusts insets of the tableView. - */ - @objc open func keyboardWillHide(_ notification: Notification) { - guard let table = tableView, let oldBottom = oldBottomInset else { return } - let keyBoardInfo = notification.userInfo! - var tableInsets = table.contentInset - var scrollIndicatorInsets = table.scrollIndicatorInsets - tableInsets.bottom = oldBottom - scrollIndicatorInsets.bottom = tableInsets.bottom - oldBottomInset = nil - UIView.beginAnimations(nil, context: nil) - UIView.setAnimationDuration((keyBoardInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as! Double)) - UIView.setAnimationCurve(UIView.AnimationCurve(rawValue: (keyBoardInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as! Int))!) - table.contentInset = tableInsets - table.scrollIndicatorInsets = scrollIndicatorInsets - UIView.commitAnimations() - } -} - -public enum Direction { case up, down } - -extension FormViewController { - - // MARK: Navigation Methods - - @objc func navigationDone() { - tableView?.endEditing(true) - } - - @objc func navigationPrevious() { - navigateTo(direction: .up) - } - - @objc func navigationNext() { - navigateTo(direction: .down) - } - - open override func pressesBegan(_ presses: Set, with event: UIPressesEvent?) { - var didHandleEvent = false - for press in presses { - guard let key = press.key, - key.keyCode == .keyboardTab, - !key.modifierFlags.contains(.command) else { continue } - if key.modifierFlags.contains(.shift) { - navigateTo(direction: .up) - } else { - navigateTo(direction: .down) - } - didHandleEvent = true - } - - if !didHandleEvent { - // Didn't handle this key press, so pass the event to the next responder. - super.pressesBegan(presses, with: event) - } - } - - public func navigateTo(direction: Direction) { - guard let currentCell = tableView?.findFirstResponder()?.formCell() else { return } - guard let currentIndexPath = tableView?.indexPath(for: currentCell) else { return } - guard let nextRow = nextRow(for: form[currentIndexPath], withDirection: direction) else { return } - if nextRow.baseCell.cellCanBecomeFirstResponder() { - tableView?.scrollToRow(at: nextRow.indexPath!, at: .none, animated: animateScroll) - nextRow.baseCell.cellBecomeFirstResponder(withDirection: direction) - } - } - - func nextRow(for currentRow: BaseRow, withDirection direction: Direction) -> BaseRow? { - - let options = navigationOptions ?? Form.defaultNavigationOptions - guard options.contains(.Enabled) else { return nil } - guard let next = direction == .down ? form.nextRow(for: currentRow) : form.previousRow(for: currentRow) else { return nil } - if next.isDisabled && options.contains(.StopDisabledRow) { - return nil - } - if !next.baseCell.cellCanBecomeFirstResponder() && !next.isDisabled && !options.contains(.SkipCanNotBecomeFirstResponderRow) { - return nil - } - if !next.isDisabled && next.baseCell.cellCanBecomeFirstResponder() { - return next - } - return nextRow(for: next, withDirection:direction) - } -} - -extension FormViewControllerProtocol { - - // MARK: Helpers - - func makeRowVisible(_ row: BaseRow, destinationScrollPosition: UITableView.ScrollPosition? = .bottom) { - guard let destinationScrollPosition = destinationScrollPosition else { return } - guard let cell = row.baseCell, let indexPath = row.indexPath, let tableView = tableView else { return } - if cell.window == nil || (tableView.contentOffset.y + tableView.frame.size.height <= cell.frame.origin.y + cell.frame.size.height) { - tableView.scrollToRow(at: indexPath, at: destinationScrollPosition, animated: true) - } - } -} diff --git a/Pods/Eureka/Source/Core/Form.swift b/Pods/Eureka/Source/Core/Form.swift deleted file mode 100644 index a03c1ee36..000000000 --- a/Pods/Eureka/Source/Core/Form.swift +++ /dev/null @@ -1,453 +0,0 @@ -// Form.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -/// The delegate of the Eureka form. -public protocol FormDelegate : AnyObject { - func sectionsHaveBeenAdded(_ sections: [Section], at: IndexSet) - func sectionsHaveBeenRemoved(_ sections: [Section], at: IndexSet) - func sectionsHaveBeenReplaced(oldSections: [Section], newSections: [Section], at: IndexSet) - func rowsHaveBeenAdded(_ rows: [BaseRow], at: [IndexPath]) - func rowsHaveBeenRemoved(_ rows: [BaseRow], at: [IndexPath]) - func rowsHaveBeenReplaced(oldRows: [BaseRow], newRows: [BaseRow], at: [IndexPath]) - func valueHasBeenChanged(for row: BaseRow, oldValue: Any?, newValue: Any?) -} - -// MARK: Form - -/// The class representing the Eureka form. -public final class Form { - - /// Defines the default options of the navigation accessory view. - public static var defaultNavigationOptions = RowNavigationOptions.Enabled.union(.SkipCanNotBecomeFirstResponderRow) - - /// The default options that define when an inline row will be hidden. Applies only when `inlineRowHideOptions` is nil. - public static var defaultInlineRowHideOptions = InlineRowHideOptions.FirstResponderChanges.union(.AnotherInlineRowIsShown) - - /// The options that define when an inline row will be hidden. If nil then `defaultInlineRowHideOptions` are used - public var inlineRowHideOptions: InlineRowHideOptions? - - /// Which `UIReturnKeyType` should be used by default. Applies only when `keyboardReturnType` is nil. - public static var defaultKeyboardReturnType = KeyboardReturnTypeConfiguration() - - /// Which `UIReturnKeyType` should be used in this form. If nil then `defaultKeyboardReturnType` is used - public var keyboardReturnType: KeyboardReturnTypeConfiguration? - - /// This form's delegate - public weak var delegate: FormDelegate? - - public init() {} - - /** - Returns the row at the given indexPath - */ - public subscript(indexPath: IndexPath) -> BaseRow { - return self[indexPath.section][indexPath.row] - } - - /** - Returns the row whose tag is passed as parameter. Uses a dictionary to get the row faster - */ - public func rowBy(tag: String) -> RowOf? where T: Equatable{ - let row: BaseRow? = rowBy(tag: tag) - return row as? RowOf - } - - /** - Returns the row whose tag is passed as parameter. Uses a dictionary to get the row faster - */ - public func rowBy(tag: String) -> Row? where Row: RowType{ - let row: BaseRow? = rowBy(tag: tag) - return row as? Row - } - - /** - Returns the row whose tag is passed as parameter. Uses a dictionary to get the row faster - */ - public func rowBy(tag: String) -> BaseRow? { - return rowsByTag[tag] - } - - /** - Returns the section whose tag is passed as parameter. - */ - public func sectionBy(tag: String) -> Section? { - return kvoWrapper._allSections.filter({ $0.tag == tag }).first - } - - /** - Method used to get all the values of all the rows of the form. Only rows with tag are included. - - - parameter includeHidden: If the values of hidden rows should be included. - - - returns: A dictionary mapping the rows tag to its value. [tag: value] - */ - public func values(includeHidden: Bool = false) -> [String: Any?] { - if includeHidden { - return getValues(for: allRows.filter({ $0.tag != nil })) - .merging(getValues(for: allSections.filter({ $0 is BaseMultivaluedSection && $0.tag != nil }) as? [BaseMultivaluedSection]), uniquingKeysWith: {(_, new) in new }) - } - return getValues(for: rows.filter({ $0.tag != nil })) - .merging(getValues(for: allSections.filter({ $0 is BaseMultivaluedSection && $0.tag != nil }) as? [BaseMultivaluedSection]), uniquingKeysWith: {(_, new) in new }) - } - - /** - Set values to the rows of this form - - - parameter values: A dictionary mapping tag to value of the rows to be set. [tag: value] - */ - public func setValues(_ values: [String: Any?]) { - for (key, value) in values { - let row: BaseRow? = rowBy(tag: key) - row?.baseValue = value - } - } - - /// The visible rows of this form - public var rows: [BaseRow] { return flatMap { $0 } } - - /// All the rows of this form. Includes the hidden rows. - public var allRows: [BaseRow] { return kvoWrapper._allSections.map({ $0.kvoWrapper._allRows }).flatMap { $0 } } - - /// All the sections of this form. Includes hidden sections. - public var allSections: [Section] { return kvoWrapper._allSections } - - /** - * Hides all the inline rows of this form. - */ - public func hideInlineRows() { - for row in self.allRows { - if let inlineRow = row as? BaseInlineRowType { - inlineRow.collapseInlineRow() - } - } - } - - // MARK: Private - - var rowObservers = [String: [ConditionType: [Taggable]]]() - var rowsByTag = [String: BaseRow]() - var tagToValues = [String: Any]() - lazy var kvoWrapper: KVOWrapper = { [unowned self] in return KVOWrapper(form: self) }() -} - -extension Form: Collection { - public var startIndex: Int { return 0 } - public var endIndex: Int { return kvoWrapper.sections.count } -} - -extension Form: MutableCollection { - - // MARK: MutableCollectionType - - public subscript (_ position: Int) -> Section { - get { return kvoWrapper.sections[position] as! Section } - set { - if position > kvoWrapper.sections.count { - assertionFailure("Form: Index out of bounds") - } - - if position < kvoWrapper.sections.count { - let oldSection = kvoWrapper.sections[position] - let oldSectionIndex = kvoWrapper._allSections.firstIndex(of: oldSection as! Section)! - // Remove the previous section from the form - kvoWrapper._allSections[oldSectionIndex].willBeRemovedFromForm() - kvoWrapper._allSections[oldSectionIndex] = newValue - } else { - kvoWrapper._allSections.append(newValue) - } - - kvoWrapper.sections[position] = newValue - newValue.wasAddedTo(form: self) - } - } - public func index(after i: Int) -> Int { - return i+1 <= endIndex ? i+1 : endIndex - } - public func index(before i: Int) -> Int { - return i > startIndex ? i-1 : startIndex - } - public var last: Section? { - return reversed().first - } - -} - -extension Form : RangeReplaceableCollection { - - // MARK: RangeReplaceableCollectionType - - public func append(_ formSection: Section) { - kvoWrapper.sections.insert(formSection, at: kvoWrapper.sections.count) - kvoWrapper._allSections.append(formSection) - formSection.wasAddedTo(form: self) - } - - public func append(contentsOf newElements: S) where S.Iterator.Element == Section { - kvoWrapper.sections.addObjects(from: newElements.map { $0 }) - kvoWrapper._allSections.append(contentsOf: newElements) - for section in newElements { - section.wasAddedTo(form: self) - } - } - - public func replaceSubrange( - _ subRange: Range, - with newElements: C - ) where C.Iterator.Element == Section { - for i in subRange.lowerBound..( - _ subRange: Range, - with newElements: C - ) where C.Iterator.Element == Section { - // Remove subrange in all sections - for i in subRange.reversed() where kvoWrapper._allSections.count > i { - let removed = kvoWrapper._allSections.remove(at: i) - removed.willBeRemovedFromForm() - } - - kvoWrapper._allSections.insert(contentsOf: newElements, at: indexForInsertion(at: subRange.lowerBound)) - - // Replace all visible sections by `kvoWrapper._allSections`, as hidden ones are being removed later anyway - kvoWrapper.sections.replaceObjects( - in: NSRange(location: 0, length: kvoWrapper.sections.count), - withObjectsFrom: kvoWrapper._allSections - ) - - for section in newElements { - section.wasAddedTo(form: self) - } - } - - public func removeAll(keepingCapacity keepCapacity: Bool = false) { - // not doing anything with capacity - - let sections = kvoWrapper._allSections - kvoWrapper.removeAllSections() - - for section in sections { - section.willBeRemovedFromForm() - } - } - - public func removeAll(where shouldBeRemoved: (Section) throws -> Bool) rethrows { - let indices = try kvoWrapper._allSections.enumerated() - .filter { try shouldBeRemoved($0.element)} - .map { $0.offset } - - var removedSections = [Section]() - for index in indices.reversed() { - removedSections.append(kvoWrapper._allSections.remove(at: index)) - } - kvoWrapper.sections.removeObjects(in: removedSections) - - removedSections.forEach { $0.willBeRemovedFromForm() } - } - - private func indexForInsertion(at index: Int) -> Int { - guard index != 0 else { return 0 } - - let section = kvoWrapper.sections[index-1] - if let i = kvoWrapper._allSections.firstIndex(of: section as! Section) { - return i + 1 - } - return kvoWrapper._allSections.count - } - -} - -extension Form { - - // MARK: Private Helpers - - class KVOWrapper: NSObject { - @objc dynamic private var _sections = NSMutableArray() - var sections: NSMutableArray { return mutableArrayValue(forKey: "_sections") } - var _allSections = [Section]() - private weak var form: Form? - - init(form: Form) { - self.form = form - super.init() - addObserver(self, forKeyPath: "_sections", options: [.new, .old], context:nil) - } - - deinit { - removeObserver(self, forKeyPath: "_sections") - _sections.removeAllObjects() - _allSections.removeAll() - } - - func removeAllSections() { - _sections = [] - _allSections.removeAll() - } - - public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { - let newSections = change?[NSKeyValueChangeKey.newKey] as? [Section] ?? [] - let oldSections = change?[NSKeyValueChangeKey.oldKey] as? [Section] ?? [] - guard let delegateValue = form?.delegate, let keyPathValue = keyPath, let changeType = change?[NSKeyValueChangeKey.kindKey] else { return } - guard keyPathValue == "_sections" else { return } - switch (changeType as! NSNumber).uintValue { - case NSKeyValueChange.setting.rawValue: - if newSections.count == 0 { - let indexSet = IndexSet(integersIn: 0.. [String: Any] { - return tagToValues - } - - func addRowObservers(to taggable: Taggable, rowTags: [String], type: ConditionType) { - for rowTag in rowTags { - if rowObservers[rowTag] == nil { - rowObservers[rowTag] = Dictionary() - } - if let _ = rowObservers[rowTag]?[type] { - if !rowObservers[rowTag]![type]!.contains(where: { $0 === taggable }) { - rowObservers[rowTag]?[type]!.append(taggable) - } - } else { - rowObservers[rowTag]?[type] = [taggable] - } - } - } - - func removeRowObservers(from taggable: Taggable, rowTags: [String], type: ConditionType) { - for rowTag in rowTags { - guard let arr = rowObservers[rowTag]?[type], let index = arr.firstIndex(where: { $0 === taggable }) else { continue } - rowObservers[rowTag]?[type]?.remove(at: index) - if rowObservers[rowTag]?[type]?.isEmpty == true { - rowObservers[rowTag] = nil - } - } - } - - func nextRow(for row: BaseRow) -> BaseRow? { - let allRows = rows - guard let index = allRows.firstIndex(of: row) else { return nil } - guard index < allRows.count - 1 else { return nil } - return allRows[index + 1] - } - - func previousRow(for row: BaseRow) -> BaseRow? { - let allRows = rows - guard let index = allRows.firstIndex(of: row) else { return nil } - guard index > 0 else { return nil } - return allRows[index - 1] - } - - func hideSection(_ section: Section) { - kvoWrapper.sections.remove(section) - } - - func showSection(_ section: Section) { - guard !kvoWrapper.sections.contains(section) else { return } - guard var index = kvoWrapper._allSections.firstIndex(of: section) else { return } - var formIndex = NSNotFound - while formIndex == NSNotFound && index > 0 { - index = index - 1 - let previous = kvoWrapper._allSections[index] - formIndex = kvoWrapper.sections.index(of: previous) - } - kvoWrapper.sections.insert(section, at: formIndex == NSNotFound ? 0 : formIndex + 1 ) - } - - var containsMultivaluedSection: Bool { - return kvoWrapper._allSections.contains { $0 is BaseMultivaluedSection } - } - - func getValues(for rows: [BaseRow]) -> [String: Any?] { - return rows.reduce([String: Any?]()) { - var result = $0 - result[$1.tag!] = $1.baseValue - return result - } - } - - func getValues(for multivaluedSections: [BaseMultivaluedSection]?) -> [String: [Any?]] { - return multivaluedSections?.reduce([String: [Any?]]()) { - var result = $0 - result[$1.tag!] = $1.values() - return result - } ?? [:] - } - -} - -extension Form { - - @discardableResult - public func validate(includeHidden: Bool = false, includeDisabled: Bool = true, quietly: Bool = false) -> [ValidationError] { - let rowsWithHiddenFilter = includeHidden ? allRows : rows - let rowsWithDisabledFilter = includeDisabled ? rowsWithHiddenFilter : rowsWithHiddenFilter.filter { $0.isDisabled != true } - - return rowsWithDisabledFilter.reduce([ValidationError]()) { res, row in - var res = res - res.append(contentsOf: row.validate(quietly: quietly)) - return res - } - } - - // Reset rows validation - public func cleanValidationErrors(){ - allRows.forEach { $0.cleanValidationErrors() } - } -} - diff --git a/Pods/Eureka/Source/Core/HeaderFooterView.swift b/Pods/Eureka/Source/Core/HeaderFooterView.swift deleted file mode 100644 index c04f33999..000000000 --- a/Pods/Eureka/Source/Core/HeaderFooterView.swift +++ /dev/null @@ -1,162 +0,0 @@ -// HeaderFooterView.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -/** - Enumeration used to generate views for the header and footer of a section. - - - Class: Will generate a view of the specified class. - - Callback->ViewType: Will generate the view as a result of the given closure. - - NibFile: Will load the view from a nib file. - */ -public enum HeaderFooterProvider { - - /** - * Will generate a view of the specified class. - */ - case `class` - - /** - * Will generate the view as a result of the given closure. - */ - case callback(()->ViewType) - - /** - * Will load the view from a nib file. - */ - case nibFile(name: String, bundle: Bundle?) - - internal func createView() -> ViewType { - switch self { - case .class: - return ViewType() - case .callback(let builder): - return builder() - case .nibFile(let nibName, let bundle): - return (bundle ?? Bundle(for: ViewType.self)).loadNibNamed(nibName, owner: nil, options: nil)![0] as! ViewType - } - } -} - -/** - * Represents headers and footers of sections - */ -public enum HeaderFooterType { - case header, footer -} - -/** - * Struct used to generate headers and footers either from a view or a String. - */ -public struct HeaderFooterView : ExpressibleByStringLiteral, HeaderFooterViewRepresentable { - - /// Holds the title of the view if it was set up with a String. - public var title: String? - - /// Generates the view. - public var viewProvider: HeaderFooterProvider? - - /// Closure called when the view is created. Useful to customize its appearance. - public var onSetupView: ((_ view: ViewType, _ section: Section) -> Void)? - - /// A closure that returns the height for the header or footer view. - public var height: (() -> CGFloat)? - - /** - This method can be called to get the view corresponding to the header or footer of a section in a specific controller. - - - parameter section: The section from which to get the view. - - parameter type: Either header or footer. - - parameter controller: The controller from which to get that view. - - - returns: The header or footer of the specified section. - */ - public func viewForSection(_ section: Section, type: HeaderFooterType) -> UIView? { - var view: ViewType? - if type == .header { - view = section.headerView as? ViewType ?? { - let result = viewProvider?.createView() - section.headerView = result - return result - }() - } else { - view = section.footerView as? ViewType ?? { - let result = viewProvider?.createView() - section.footerView = result - return result - }() - } - guard let v = view else { return nil } - onSetupView?(v, section) - return v - } - - /** - Initiates the view with a String as title - */ - public init?(title: String?) { - guard let t = title else { return nil } - self.init(stringLiteral: t) - } - - /** - Initiates the view with a view provider, ideal for customized headers or footers - */ - public init(_ provider: HeaderFooterProvider) { - viewProvider = provider - } - - /** - Initiates the view with a String as title - */ - public init(unicodeScalarLiteral value: String) { - self.title = value - } - - /** - Initiates the view with a String as title - */ - public init(extendedGraphemeClusterLiteral value: String) { - self.title = value - } - - /** - Initiates the view with a String as title - */ - public init(stringLiteral value: String) { - self.title = value - } -} - -extension UIView { - - func eurekaInvalidate() { - setNeedsUpdateConstraints() - updateConstraintsIfNeeded() - setNeedsLayout() - } - -} diff --git a/Pods/Eureka/Source/Core/Helpers.swift b/Pods/Eureka/Source/Core/Helpers.swift deleted file mode 100644 index 1fac73e53..000000000 --- a/Pods/Eureka/Source/Core/Helpers.swift +++ /dev/null @@ -1,79 +0,0 @@ -// Helpers.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -extension UIView { - - public func findFirstResponder() -> UIView? { - if isFirstResponder { return self } - for subView in subviews { - if let firstResponder = subView.findFirstResponder() { - return firstResponder - } - } - return nil - } - - public func formCell() -> BaseCell? { - if self is UITableViewCell { - return self as? BaseCell - } - return superview?.formCell() - } -} - -extension NSPredicate { - - var predicateVars: [String] { - var ret = [String]() - if let compoundPredicate = self as? NSCompoundPredicate { - for subPredicate in compoundPredicate.subpredicates where subPredicate is NSPredicate { - ret.append(contentsOf: (subPredicate as! NSPredicate).predicateVars) - } - } else if let comparisonPredicate = self as? NSComparisonPredicate { - ret.append(contentsOf: comparisonPredicate.leftExpression.expressionVars) - ret.append(contentsOf: comparisonPredicate.rightExpression.expressionVars) - } - return ret - } -} - -extension NSExpression { - - var expressionVars: [String] { - switch expressionType { - case .function, .variable: - let str = "\(self)" - if let range = str.range(of: ".") { - return [String(str[str.index(str.startIndex, offsetBy: 1).. Void) -> Self { - callbackOnExpandInlineRow = callback - return self - } - - /** - Sets a block to be executed when a row is collapsed. - */ - @discardableResult - public func onCollapseInlineRow(_ callback: @escaping (Cell, Self, InlineRow) -> Void) -> Self { - callbackOnCollapseInlineRow = callback - return self - } - - /// Returns the block that will be executed when this row expands - public var onCollapseInlineRowCallback: ((Cell, Self, InlineRow) -> Void)? { - return callbackOnCollapseInlineRow as! ((Cell, Self, InlineRow) -> Void)? - } - - /// Returns the block that will be executed when this row collapses - public var onExpandInlineRowCallback: ((Cell, Self, InlineRow) -> Void)? { - return callbackOnExpandInlineRow as! ((Cell, Self, InlineRow) -> Void)? - } - - public var isExpanded: Bool { return _inlineRow != nil } - public var isCollapsed: Bool { return !isExpanded } -} diff --git a/Pods/Eureka/Source/Core/NavigationAccessoryView.swift b/Pods/Eureka/Source/Core/NavigationAccessoryView.swift deleted file mode 100644 index 4605c7a22..000000000 --- a/Pods/Eureka/Source/Core/NavigationAccessoryView.swift +++ /dev/null @@ -1,155 +0,0 @@ -// NavigationAccessoryView.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -public protocol NavigationAccessory { - var doneClosure: (() -> ())? { get set } - var nextClosure: (() -> ())? { get set } - var previousClosure: (() -> ())? { get set } - - var previousEnabled: Bool { get set } - var nextEnabled: Bool { get set } -} - -/// Class for the navigation accessory view used in FormViewController -@objc(EurekaNavigationAccessoryView) -open class NavigationAccessoryView: UIToolbar, NavigationAccessory { - open var previousButton: UIBarButtonItem! - open var nextButton: UIBarButtonItem! - open var doneButton: UIBarButtonItem! - private var fixedSpace = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil) - private var flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) - - public override init(frame: CGRect) { - super.init(frame: CGRect(x: 0, y: 0, width: frame.size.width, height: 44.0)) - autoresizingMask = .flexibleWidth - fixedSpace.width = 22.0 - initializeChevrons() - initializeDoneButton() - setItems([previousButton, fixedSpace, nextButton, flexibleSpace, doneButton], animated: false) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - private func drawChevron(pointingRight: Bool) -> UIImage? { - // Hardcoded chevron size - let width = 12 - let height = 20 - - // Begin the image context, with which we are going to draw a chevron - UIGraphicsBeginImageContextWithOptions(CGSize(width: width, height: height), false, 0.0) - guard let context = UIGraphicsGetCurrentContext() else { return nil } - defer { - UIGraphicsEndImageContext() - } - - // The chevron looks like > or <. This can be drawn with 3 points; the Y coordinates of the points - // are independant of whether it is to be pointing left or right; the X coordinates will depend, as follows. - // The 1s are to ensure that the point of the chevron does not sit exactly on the edge of the frame, which - // would slightly truncate the point. - let chevronPointXCoordinate, chevronTailsXCoordinate: Int - if pointingRight { - chevronPointXCoordinate = width - 1 - chevronTailsXCoordinate = 1 - } else { - chevronPointXCoordinate = 1 - chevronTailsXCoordinate = width - 1 - } - - // Draw the lines and return the image - context.setLineWidth(1.5) - context.setLineCap(.square) - context.strokeLineSegments(between: [ - CGPoint(x: chevronTailsXCoordinate, y: 0), - CGPoint(x: chevronPointXCoordinate, y: height / 2), - CGPoint(x: chevronPointXCoordinate, y: height / 2), - CGPoint(x: chevronTailsXCoordinate, y: height) - ]) - - return UIGraphicsGetImageFromCurrentImageContext() - } - - private func initializeChevrons() { - var imageLeftChevron, imageRightChevron: UIImage? - if #available(iOS 13.0, *) { - // If we have access to SFSymbols, use the system chevron images, rather than faffing around with our own - imageLeftChevron = UIImage(systemName: "chevron.left") - imageRightChevron = UIImage(systemName: "chevron.right") - } else { - imageLeftChevron = drawChevron(pointingRight: false) - imageRightChevron = drawChevron(pointingRight: true) - } - - // RTL language support - imageLeftChevron = imageLeftChevron?.imageFlippedForRightToLeftLayoutDirection() - imageRightChevron = imageRightChevron?.imageFlippedForRightToLeftLayoutDirection() - - previousButton = UIBarButtonItem(image: imageLeftChevron, style: .plain, target: self, action: #selector(didTapPrevious)) - nextButton = UIBarButtonItem(image: imageRightChevron, style: .plain, target: self, action: #selector(didTapNext)) - } - - private func initializeDoneButton() { - doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(didTapDone)) - } - - open override func touchesBegan(_ touches: Set, with event: UIEvent?) {} - - public var doneClosure: (() -> ())? - public var nextClosure: (() -> ())? - public var previousClosure: (() -> ())? - - @objc private func didTapDone() { - doneClosure?() - } - - @objc private func didTapNext() { - nextClosure?() - } - - @objc private func didTapPrevious() { - previousClosure?() - } - - public var previousEnabled: Bool { - get { - return previousButton.isEnabled - } - set { - previousButton.isEnabled = newValue - } - } - - public var nextEnabled: Bool { - get { - return nextButton.isEnabled - } - set { - nextButton.isEnabled = newValue - } - } -} diff --git a/Pods/Eureka/Source/Core/Operators.swift b/Pods/Eureka/Source/Core/Operators.swift deleted file mode 100644 index a98a23d63..000000000 --- a/Pods/Eureka/Source/Core/Operators.swift +++ /dev/null @@ -1,161 +0,0 @@ -// Operators.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -// MARK: Operators - -precedencegroup FormPrecedence { - associativity: left - higherThan: LogicalConjunctionPrecedence -} - -precedencegroup SectionPrecedence { - associativity: left - higherThan: FormPrecedence -} - -infix operator +++ : FormPrecedence - -/** - Appends a section to a form - - - parameter left: the form - - parameter right: the section to be appended - - - returns: the updated form - */ -@discardableResult -public func +++ (left: Form, right: Section) -> Form { - left.append(right) - return left -} - -/** - Appends a row to the last section of a form - - - parameter left: the form - - parameter right: the row - */ -@discardableResult -public func +++ (left: Form, right: BaseRow) -> Form { - let section = Section() - let _ = left +++ section <<< right - return left -} - -/** - Creates a form with two sections - - - parameter left: the first section - - parameter right: the second section - - - returns: the created form - */ -@discardableResult -public func +++ (left: Section, right: Section) -> Form { - let form = Form() - let _ = form +++ left +++ right - return form -} - -/** - Appends the row wrapped in a new section - - - parameter left: a section of the form - - parameter right: a row to be appended - - - returns: the form - */ -@discardableResult -public func +++ (left: Section, right: BaseRow) -> Form { - let section = Section() - section <<< right - return left +++ section -} - -/** - Creates a form with two sections, each containing one row. - - - parameter left: The row for the first section - - parameter right: The row for the second section - - - returns: the created form - */ -@discardableResult -public func +++ (left: BaseRow, right: BaseRow) -> Form { - let form = Section() <<< left +++ Section() <<< right - return form -} - -infix operator <<< : SectionPrecedence - -/** - Appends a row to a section. - - - parameter left: the section - - parameter right: the row to be appended - - - returns: the section - */ -@discardableResult -public func <<< (left: Section, right: BaseRow) -> Section { - left.append(right) - return left -} - -/** - Creates a section with two rows - - - parameter left: The first row - - parameter right: The second row - - - returns: the created section - */ -@discardableResult -public func <<< (left: BaseRow, right: BaseRow) -> Section { - let section = Section() - section <<< left <<< right - return section -} - -/** - Appends a collection of rows to a section - - - parameter lhs: the section - - parameter rhs: the rows to be appended - */ -public func += (lhs: inout Section, rhs: C) where C.Iterator.Element == BaseRow { - lhs.append(contentsOf: rhs) -} - -/** - Appends a collection of section to a form - - - parameter lhs: the form - - parameter rhs: the sections to be appended - */ -public func += (lhs: inout Form, rhs: C) where C.Iterator.Element == Section { - lhs.append(contentsOf: rhs) -} diff --git a/Pods/Eureka/Source/Core/PresenterRowType.swift b/Pods/Eureka/Source/Core/PresenterRowType.swift deleted file mode 100644 index d0bc67319..000000000 --- a/Pods/Eureka/Source/Core/PresenterRowType.swift +++ /dev/null @@ -1,57 +0,0 @@ -// PresenterRowType.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -/** - * Protocol that every row that displays a new view controller must conform to. - * This includes presenting or pushing view controllers. - */ -public protocol PresenterRowType: TypedRowType { - - associatedtype PresentedControllerType : UIViewController, TypedRowControllerType - - /// Defines how the view controller will be presented, pushed, etc. - var presentationMode: PresentationMode? { get set } - - /// Will be called before the presentation occurs. - var onPresentCallback: ((FormViewController, PresentedControllerType) -> Void)? { get set } -} - -extension PresenterRowType { - - /** - Sets a block to be executed when the row presents a view controller - - - parameter callback: the block - - - returns: this row - */ - @discardableResult - public func onPresent(_ callback: ((FormViewController, PresentedControllerType) -> Void)?) -> Self { - onPresentCallback = callback - return self - } -} diff --git a/Pods/Eureka/Source/Core/Row.swift b/Pods/Eureka/Source/Core/Row.swift deleted file mode 100644 index a325e58c4..000000000 --- a/Pods/Eureka/Source/Core/Row.swift +++ /dev/null @@ -1,203 +0,0 @@ -// Row.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -open class RowOf: BaseRow where T: Equatable { - - private var _value: T? { - didSet { - guard _value != oldValue else { return } - guard let form = section?.form else { return } - if let delegate = form.delegate { - delegate.valueHasBeenChanged(for: self, oldValue: oldValue, newValue: value) - callbackOnChange?() - } - guard let t = tag else { return } - form.tagToValues[t] = (value != nil ? value! : NSNull()) - if let rowObservers = form.rowObservers[t]?[.hidden] { - for rowObserver in rowObservers { - (rowObserver as? Hidable)?.evaluateHidden() - } - } - if let rowObservers = form.rowObservers[t]?[.disabled] { - for rowObserver in rowObservers { - (rowObserver as? Disableable)?.evaluateDisabled() - } - } - } - } - - /// The typed value of this row. - open var value: T? { - set (newValue) { - _value = newValue - guard let _ = section?.form else { return } - wasChanged = true - if validationOptions.contains(.validatesOnChange) || (wasBlurred && validationOptions.contains(.validatesOnChangeAfterBlurred)) || (!isValid && validationOptions != .validatesOnDemand) { - validate() - } - } - get { - return _value - } - } - - /// The reset value of this row. Sets the value property to the value of this row on the resetValue method call. - open var resetValue: T? - - /// The untyped value of this row. - public override var baseValue: Any? { - get { return value } - set { value = newValue as? T } - } - - /// Block variable used to get the String that should be displayed for the value of this row. - public var displayValueFor: ((T?) -> String?)? = { - return $0.map { String(describing: $0) } - } - - public required init(tag: String?) { - super.init(tag: tag) - } - - public internal(set) var rules: [ValidationRuleHelper] = [] - - @discardableResult - open override func validate(quietly: Bool = false) -> [ValidationError] { - var vErrors = [ValidationError]() - #if swift(>=4.1) - vErrors = rules.compactMap { $0.validateFn(value) } - #else - vErrors = rules.flatMap { $0.validateFn(value) } - #endif - if (!quietly) { - validationErrors = vErrors - } - return vErrors - } - - /// Resets the value of the row. Setting it's value to it's reset value. - public func resetRowValue() { - value = resetValue - } - - /// Add a Validation rule for the Row - /// - Parameter rule: RuleType object to add - public func add(rule: Rule) where T == Rule.RowValueType { - let validFn: ((T?) -> ValidationError?) = { (val: T?) in - return rule.isValid(value: val) - } - rules.append(ValidationRuleHelper(validateFn: validFn, rule: rule)) - } - - /// Add a Validation rule set for the Row - /// - Parameter ruleSet: RuleSet set of rules to add - public func add(ruleSet: RuleSet) { - rules.append(contentsOf: ruleSet.rules) - } - - public func remove(ruleWithIdentifier identifier: String) { - if let index = rules.firstIndex(where: { (validationRuleHelper) -> Bool in - return validationRuleHelper.rule.id == identifier - }) { - rules.remove(at: index) - } - } - - public func removeAllRules() { - validationErrors.removeAll() - rules.removeAll() - } - -} - -/// Generic class that represents an Eureka row. -open class Row: RowOf, TypedRowType where Cell: BaseCell { - - /// Responsible for creating the cell for this row. - public var cellProvider = CellProvider() - - /// The type of the cell associated to this row. - public let cellType: Cell.Type! = Cell.self - - private var _cell: Cell! { - didSet { - RowDefaults.cellSetup["\(type(of: self))"]?(_cell, self) - (callbackCellSetup as? ((Cell) -> Void))?(_cell) - } - } - - /// The cell associated to this row. - public var cell: Cell! { - return _cell ?? { - let result = cellProvider.makeCell(style: self.cellStyle) - result.row = self - result.setup() - _cell = result - return _cell - }() - } - - /// The untyped cell associated to this row - public override var baseCell: BaseCell { return cell } - - public required init(tag: String?) { - super.init(tag: tag) - } - - /** - Method that reloads the cell - */ - override open func updateCell() { - super.updateCell() - cell.update() - customUpdateCell() - RowDefaults.cellUpdate["\(type(of: self))"]?(cell, self) - callbackCellUpdate?() - } - - /** - Method called when the cell belonging to this row was selected. Must call the corresponding method in its cell. - */ - open override func didSelect() { - super.didSelect() - if !isDisabled { - cell?.didSelect() - } - customDidSelect() - callbackCellOnSelection?() - } - - /** - Will be called inside `didSelect` method of the row. Can be used to customize row selection from the definition of the row. - */ - open func customDidSelect() {} - - /** - Will be called inside `updateCell` method of the row. Can be used to customize reloading a row from its definition. - */ - open func customUpdateCell() {} - -} diff --git a/Pods/Eureka/Source/Core/RowControllerType.swift b/Pods/Eureka/Source/Core/RowControllerType.swift deleted file mode 100644 index eb239f592..000000000 --- a/Pods/Eureka/Source/Core/RowControllerType.swift +++ /dev/null @@ -1,35 +0,0 @@ -// RowControllerType.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -/** - * Base protocol for view controllers presented by Eureka rows. - */ -public protocol RowControllerType: NSObjectProtocol { - - /// A closure to be called when the controller disappears. - var onDismissCallback: ((UIViewController) -> Void)? { get set } -} diff --git a/Pods/Eureka/Source/Core/RowProtocols.swift b/Pods/Eureka/Source/Core/RowProtocols.swift deleted file mode 100644 index 616d97a7a..000000000 --- a/Pods/Eureka/Source/Core/RowProtocols.swift +++ /dev/null @@ -1,62 +0,0 @@ -// RowProtocols.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -/** - * Protocol that view controllers pushed or presented by a row should conform to. - */ -public protocol TypedRowControllerType: RowControllerType { - associatedtype RowValue: Equatable - - /// The row that pushed or presented this controller - var row: RowOf! { get set } -} - -// MARK: Header Footer Protocols - -/** - * Protocol used to set headers and footers to sections. - * Can be set with a view or a String - */ -public protocol HeaderFooterViewRepresentable { - - /** - This method can be called to get the view corresponding to the header or footer of a section in a specific controller. - - - parameter section: The section from which to get the view. - - parameter type: Either header or footer. - - parameter controller: The controller from which to get that view. - - - returns: The header or footer of the specified section. - */ - func viewForSection(_ section: Section, type: HeaderFooterType) -> UIView? - - /// If the header or footer of a section was created with a String then it will be stored in the title. - var title: String? { get set } - - /// The height of the header or footer. - var height: (() -> CGFloat)? { get set } -} diff --git a/Pods/Eureka/Source/Core/RowType.swift b/Pods/Eureka/Source/Core/RowType.swift deleted file mode 100644 index 410cc90a3..000000000 --- a/Pods/Eureka/Source/Core/RowType.swift +++ /dev/null @@ -1,258 +0,0 @@ -// RowType.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -protocol Disableable: Taggable { - func evaluateDisabled() - var disabled: Condition? { get set } - var isDisabled: Bool { get } -} - -protocol Hidable: Taggable { - func evaluateHidden() - var hidden: Condition? { get set } - var isHidden: Bool { get } -} - -public protocol KeyboardReturnHandler: BaseRowType { - var keyboardReturnType: KeyboardReturnTypeConfiguration? { get set } -} - -public protocol Taggable: AnyObject { - var tag: String? { get set } -} - -public protocol BaseRowType: Taggable { - - /// The cell associated to this row. - var baseCell: BaseCell! { get } - - /// The section to which this row belongs. - var section: Section? { get } - - /// Parameter used when creating the cell for this row. - var cellStyle: UITableViewCell.CellStyle { get set } - - /// The title will be displayed in the textLabel of the row. - var title: String? { get set } - - /** - Method that should re-display the cell - */ - func updateCell() - - /** - Method called when the cell belonging to this row was selected. Must call the corresponding method in its cell. - */ - func didSelect() - - /** - Typically we don't need to explicitly call this method since it is called by Eureka framework. It will validates the row if you invoke it. - */ - func validate(quietly: Bool) -> [ValidationError] -} - -public protocol TypedRowType: BaseRowType { - - associatedtype Cell: BaseCell, TypedCellType - - /// The typed cell associated to this row. - var cell: Cell! { get } - - /// The typed value this row stores. - var value: Cell.Value? { get set } - - func add(rule: Rule) where Rule.RowValueType == Cell.Value - func remove(ruleWithIdentifier: String) -} - -/** - * Protocol that every row type has to conform to. - */ -public protocol RowType: TypedRowType { - init(_ tag: String?, _ initializer: (Self) -> Void) -} - -extension RowType where Self: BaseRow { - - /** - Default initializer for a row - */ - public init(_ tag: String? = nil, _ initializer: (Self) -> Void = { _ in }) { - self.init(tag: tag) - RowDefaults.rowInitialization["\(type(of: self))"]?(self) - initializer(self) - } -} - -extension RowType where Self: BaseRow { - - /// The default block executed when the cell is updated. Applies to every row of this type. - public static var defaultCellUpdate: ((Cell, Self) -> Void)? { - set { - if let newValue = newValue { - let wrapper: (BaseCell, BaseRow) -> Void = { (baseCell: BaseCell, baseRow: BaseRow) in - newValue(baseCell as! Cell, baseRow as! Self) - } - RowDefaults.cellUpdate["\(self)"] = wrapper - RowDefaults.rawCellUpdate["\(self)"] = newValue - } else { - RowDefaults.cellUpdate["\(self)"] = nil - RowDefaults.rawCellUpdate["\(self)"] = nil - } - } - get { return RowDefaults.rawCellUpdate["\(self)"] as? ((Cell, Self) -> Void) } - } - - /// The default block executed when the cell is created. Applies to every row of this type. - public static var defaultCellSetup: ((Cell, Self) -> Void)? { - set { - if let newValue = newValue { - let wrapper: (BaseCell, BaseRow) -> Void = { (baseCell: BaseCell, baseRow: BaseRow) in - newValue(baseCell as! Cell, baseRow as! Self) - } - RowDefaults.cellSetup["\(self)"] = wrapper - RowDefaults.rawCellSetup["\(self)"] = newValue - } else { - RowDefaults.cellSetup["\(self)"] = nil - RowDefaults.rawCellSetup["\(self)"] = nil - } - } - get { return RowDefaults.rawCellSetup["\(self)"] as? ((Cell, Self) -> Void) } - } - - /// The default block executed when the cell becomes first responder. Applies to every row of this type. - public static var defaultOnCellHighlightChanged: ((Cell, Self) -> Void)? { - set { - if let newValue = newValue { - let wrapper: (BaseCell, BaseRow) -> Void = { (baseCell: BaseCell, baseRow: BaseRow) in - newValue(baseCell as! Cell, baseRow as! Self) - } - RowDefaults.onCellHighlightChanged ["\(self)"] = wrapper - RowDefaults.rawOnCellHighlightChanged["\(self)"] = newValue - } else { - RowDefaults.onCellHighlightChanged["\(self)"] = nil - RowDefaults.rawOnCellHighlightChanged["\(self)"] = nil - } - } - get { return RowDefaults.rawOnCellHighlightChanged["\(self)"] as? ((Cell, Self) -> Void) } - } - - /// The default block executed to initialize a row. Applies to every row of this type. - public static var defaultRowInitializer: ((Self) -> Void)? { - set { - if let newValue = newValue { - let wrapper: (BaseRow) -> Void = { (baseRow: BaseRow) in - newValue(baseRow as! Self) - } - RowDefaults.rowInitialization["\(self)"] = wrapper - RowDefaults.rawRowInitialization["\(self)"] = newValue - } else { - RowDefaults.rowInitialization["\(self)"] = nil - RowDefaults.rawRowInitialization["\(self)"] = nil - } - } - get { return RowDefaults.rawRowInitialization["\(self)"] as? ((Self) -> Void) } - } - - /// The default block executed to initialize a row. Applies to every row of this type. - public static var defaultOnRowValidationChanged: ((Cell, Self) -> Void)? { - set { - if let newValue = newValue { - let wrapper: (BaseCell, BaseRow) -> Void = { (baseCell: BaseCell, baseRow: BaseRow) in - newValue(baseCell as! Cell, baseRow as! Self) - } - RowDefaults.onRowValidationChanged["\(self)"] = wrapper - RowDefaults.rawOnRowValidationChanged["\(self)"] = newValue - } else { - RowDefaults.onRowValidationChanged["\(self)"] = nil - RowDefaults.rawOnRowValidationChanged["\(self)"] = nil - } - } - get { return RowDefaults.rawOnRowValidationChanged["\(self)"] as? ((Cell, Self) -> Void) } - } - - /** - Sets a block to be called when the value of this row changes. - - - returns: this row - */ - @discardableResult - public func onChange(_ callback: @escaping (Self) -> Void) -> Self { - callbackOnChange = { [weak self] in callback(self!) } - return self - } - - /** - Sets a block to be called when the cell corresponding to this row is refreshed. - - - returns: this row - */ - @discardableResult - public func cellUpdate(_ callback: @escaping ((_ cell: Cell, _ row: Self) -> Void)) -> Self { - callbackCellUpdate = { [weak self] in callback(self!.cell, self!) } - return self - } - - /** - Sets a block to be called when the cell corresponding to this row is created. - - - returns: this row - */ - @discardableResult - public func cellSetup(_ callback: @escaping ((_ cell: Cell, _ row: Self) -> Void)) -> Self { - callbackCellSetup = { [weak self] (cell: Cell) in callback(cell, self!) } - return self - } - - /** - Sets a block to be called when the cell corresponding to this row is selected. - - - returns: this row - */ - @discardableResult - public func onCellSelection(_ callback: @escaping ((_ cell: Cell, _ row: Self) -> Void)) -> Self { - callbackCellOnSelection = { [weak self] in callback(self!.cell, self!) } - return self - } - - /** - Sets a block to be called when the cell corresponding to this row becomes or resigns the first responder. - - - returns: this row - */ - @discardableResult - public func onCellHighlightChanged(_ callback: @escaping (_ cell: Cell, _ row: Self) -> Void) -> Self { - callbackOnCellHighlightChanged = { [weak self] in callback(self!.cell, self!) } - return self - } - - @discardableResult - public func onRowValidationChanged(_ callback: @escaping (_ cell: Cell, _ row: Self) -> Void) -> Self { - callbackOnRowValidationChanged = { [weak self] in callback(self!.cell, self!) } - return self - } -} diff --git a/Pods/Eureka/Source/Core/Section.swift b/Pods/Eureka/Source/Core/Section.swift deleted file mode 100644 index 44561ddf3..000000000 --- a/Pods/Eureka/Source/Core/Section.swift +++ /dev/null @@ -1,598 +0,0 @@ -// Section.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -/// The delegate of the Eureka sections. -public protocol SectionDelegate: AnyObject { - func rowsHaveBeenAdded(_ rows: [BaseRow], at: IndexSet) - func rowsHaveBeenRemoved(_ rows: [BaseRow], at: IndexSet) - func rowsHaveBeenReplaced(oldRows: [BaseRow], newRows: [BaseRow], at: IndexSet) -} - -// MARK: Section - -extension Section : Equatable {} - -public func == (lhs: Section, rhs: Section) -> Bool { - return lhs === rhs -} - -extension Section : Hidable, SectionDelegate {} - -extension Section { - - public func reload(with rowAnimation: UITableView.RowAnimation = .none) { - guard let tableView = (form?.delegate as? FormViewController)?.tableView, let index = index, index < tableView.numberOfSections else { return } - tableView.reloadSections(IndexSet(integer: index), with: rowAnimation) - } -} - -extension Section { - - internal class KVOWrapper: NSObject { - - @objc dynamic private var _rows = NSMutableArray() - var rows: NSMutableArray { - return mutableArrayValue(forKey: "_rows") - } - var _allRows = [BaseRow]() - - private weak var section: Section? - - init(section: Section) { - self.section = section - super.init() - addObserver(self, forKeyPath: "_rows", options: [.new, .old], context:nil) - } - - deinit { - removeObserver(self, forKeyPath: "_rows") - _rows.removeAllObjects() - _allRows.removeAll() - } - - func removeAllRows() { - _rows = [] - _allRows.removeAll() - } - - public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { - let newRows = change![NSKeyValueChangeKey.newKey] as? [BaseRow] ?? [] - let oldRows = change![NSKeyValueChangeKey.oldKey] as? [BaseRow] ?? [] - guard let keyPathValue = keyPath, let changeType = change?[NSKeyValueChangeKey.kindKey] else { return } - let delegateValue = section?.form?.delegate - guard keyPathValue == "_rows" else { return } - switch (changeType as! NSNumber).uintValue { - case NSKeyValueChange.setting.rawValue: - if newRows.count == 0 { - let indexSet = IndexSet(integersIn: 0..(tag: String) -> Row? { - guard let index = kvoWrapper._allRows.firstIndex(where: { $0.tag == tag }) else { return nil } - return kvoWrapper._allRows[index] as? Row - } -} - -/// The class representing the sections in a Eureka form. -open class Section { - - /// The tag is used to uniquely identify a Section. Must be unique among sections and rows. - public var tag: String? - - /// The form that contains this section - public internal(set) weak var form: Form? - - /// The header of this section. - public var header: HeaderFooterViewRepresentable? { - willSet { - headerView = nil - } - } - - /// The footer of this section - public var footer: HeaderFooterViewRepresentable? { - willSet { - footerView = nil - } - } - - /// Index of this section in the form it belongs to. - public var index: Int? { return form?.firstIndex(of: self) } - - /// Condition that determines if the section should be hidden or not. - public var hidden: Condition? { - willSet { removeFromRowObservers() } - didSet { addToRowObservers() } - } - - /// Returns if the section is currently hidden or not. - public var isHidden: Bool { return hiddenCache } - - /// Returns all the rows in this section, including hidden rows. - public var allRows: [BaseRow] { - return kvoWrapper._allRows - } - - public required init() {} - - #if swift(>=4.1) - public required init(_ elements: S) where S: Sequence, S.Element == BaseRow { - self.append(contentsOf: elements) - } - #endif - - public init(_ initializer: @escaping (Section) -> Void) { - initializer(self) - } - - public init(_ header: String?, _ initializer: @escaping (Section) -> Void = { _ in }) { - if let header = header { - self.header = HeaderFooterView(stringLiteral: header) - } - initializer(self) - } - - public init(header: String?, footer: String?, _ initializer: (Section) -> Void = { _ in }) { - if let header = header { - self.header = HeaderFooterView(stringLiteral: header) - } - if let footer = footer { - self.footer = HeaderFooterView(stringLiteral: footer) - } - initializer(self) - } - - public init(footer: String?, _ initializer: (Section) -> Void = { _ in }) { - if let footer = footer { - self.footer = HeaderFooterView(stringLiteral: footer) - } - initializer(self) - } - - // MARK: SectionDelegate - - /** - * Delegate method called by the framework when one or more rows have been added to the section. - */ - open func rowsHaveBeenAdded(_ rows: [BaseRow], at: IndexSet) {} - - /** - * Delegate method called by the framework when one or more rows have been removed from the section. - */ - open func rowsHaveBeenRemoved(_ rows: [BaseRow], at: IndexSet) {} - - /** - * Delegate method called by the framework when one or more rows have been replaced in the section. - */ - open func rowsHaveBeenReplaced(oldRows: [BaseRow], newRows: [BaseRow], at: IndexSet) {} - - // MARK: Private - lazy var kvoWrapper: KVOWrapper = { [unowned self] in return KVOWrapper(section: self) }() - - var headerView: UIView? - var footerView: UIView? - var hiddenCache = false -} - -extension Section: MutableCollection, BidirectionalCollection { - - // MARK: MutableCollectionType - - public var startIndex: Int { return 0 } - public var endIndex: Int { return kvoWrapper.rows.count } - public subscript (position: Int) -> BaseRow { - get { - if position >= kvoWrapper.rows.count { - assertionFailure("Section: Index out of bounds") - } - return kvoWrapper.rows[position] as! BaseRow - } - set { - if position > kvoWrapper.rows.count { - assertionFailure("Section: Index out of bounds") - } - - if position < kvoWrapper.rows.count { - let oldRow = kvoWrapper.rows[position] - let oldRowIndex = kvoWrapper._allRows.firstIndex(of: oldRow as! BaseRow)! - // Remove the previous row from the form - kvoWrapper._allRows[oldRowIndex].willBeRemovedFromSection() - kvoWrapper._allRows[oldRowIndex] = newValue - } else { - kvoWrapper._allRows.append(newValue) - } - - kvoWrapper.rows[position] = newValue - newValue.wasAddedTo(section: self) - } - } - - public subscript (range: Range) -> ArraySlice { - get { return kvoWrapper.rows.map { $0 as! BaseRow }[range] } - set { replaceSubrange(range, with: newValue) } - } - - public func index(after i: Int) -> Int { return i + 1 } - public func index(before i: Int) -> Int { return i - 1 } - -} - -extension Section: RangeReplaceableCollection { - - // MARK: RangeReplaceableCollectionType - - public func append(_ formRow: BaseRow) { - kvoWrapper.rows.insert(formRow, at: kvoWrapper.rows.count) - kvoWrapper._allRows.append(formRow) - formRow.wasAddedTo(section: self) - } - - public func append(contentsOf newElements: S) where S.Iterator.Element == BaseRow { - kvoWrapper.rows.addObjects(from: newElements.map { $0 }) - kvoWrapper._allRows.append(contentsOf: newElements) - for row in newElements { - row.wasAddedTo(section: self) - } - } - - public func replaceSubrange(_ subrange: Range, with newElements: C) where C : Collection, C.Element == BaseRow { - for i in subrange.lowerBound.. Bool) rethrows { - let indices = try kvoWrapper._allRows.enumerated() - .filter { try shouldBeRemoved($0.element)} - .map { $0.offset } - - var removedRows = [BaseRow]() - for index in indices.reversed() { - removedRows.append(kvoWrapper._allRows.remove(at: index)) - } - kvoWrapper.rows.removeObjects(in: removedRows) - - removedRows.forEach { $0.willBeRemovedFromSection() } - } - - @discardableResult - public func remove(at position: Int) -> BaseRow { - let row = kvoWrapper.rows.object(at: position) as! BaseRow - row.willBeRemovedFromSection() - kvoWrapper.rows.removeObject(at: position) - if let index = kvoWrapper._allRows.firstIndex(of: row) { - kvoWrapper._allRows.remove(at: index) - } - - return row - } - - private func indexForInsertion(at index: Int) -> Int { - guard index != 0 else { return 0 } - - let row = kvoWrapper.rows[index-1] - if let i = kvoWrapper._allRows.firstIndex(of: row as! BaseRow) { - return i + 1 - } - return kvoWrapper._allRows.count - } - -} - -extension Section /* Condition */ { - - // MARK: Hidden/Disable Engine - - /** - Function that evaluates if the section should be hidden and updates it accordingly. - */ - public final func evaluateHidden() { - if let h = hidden, let f = form { - switch h { - case .function(_, let callback): - hiddenCache = callback(f) - case .predicate(let predicate): - hiddenCache = predicate.evaluate(with: self, substitutionVariables: f.dictionaryValuesToEvaluatePredicate()) - } - if hiddenCache { - form?.hideSection(self) - } else { - form?.showSection(self) - } - } - } - - /** - Internal function called when this section was added to a form. - */ - func wasAddedTo(form: Form) { - self.form = form - addToRowObservers() - evaluateHidden() - for row in kvoWrapper._allRows { - row.wasAddedTo(section: self) - } - } - - /** - Internal function called to add this section to the observers of certain rows. Called when the hidden variable is set and depends on other rows. - */ - func addToRowObservers() { - guard let h = hidden else { return } - switch h { - case .function(let tags, _): - form?.addRowObservers(to: self, rowTags: tags, type: .hidden) - case .predicate(let predicate): - form?.addRowObservers(to: self, rowTags: predicate.predicateVars, type: .hidden) - } - } - - /** - Internal function called when this section was removed from a form. - */ - func willBeRemovedFromForm() { - for row in kvoWrapper._allRows { - row.willBeRemovedFromForm() - } - removeFromRowObservers() - self.form = nil - } - - /** - Internal function called to remove this section from the observers of certain rows. Called when the hidden variable is changed. - */ - func removeFromRowObservers() { - guard let h = hidden else { return } - switch h { - case .function(let tags, _): - form?.removeRowObservers(from: self, rowTags: tags, type: .hidden) - case .predicate(let predicate): - form?.removeRowObservers(from: self, rowTags: predicate.predicateVars, type: .hidden) - } - } - - func hide(row: BaseRow) { - row.baseCell.cellResignFirstResponder() - (row as? BaseInlineRowType)?.collapseInlineRow() - kvoWrapper.rows.remove(row) - } - - func show(row: BaseRow) { - guard !kvoWrapper.rows.contains(row) else { return } - guard var index = kvoWrapper._allRows.firstIndex(of: row) else { return } - var formIndex = NSNotFound - while formIndex == NSNotFound && index > 0 { - index = index - 1 - let previous = kvoWrapper._allRows[index] - formIndex = kvoWrapper.rows.index(of: previous) - } - kvoWrapper.rows.insert(row, at: formIndex == NSNotFound ? 0 : formIndex + 1) - } -} - -extension Section /* Helpers */ { - - /** - * This method inserts a row after another row. - * It is useful if you want to insert a row after a row that is currently hidden. Otherwise use `insert(at: Int)`. - * It throws an error if the old row is not in this section. - */ - public func insert(row newRow: BaseRow, after previousRow: BaseRow) throws { - guard let rowIndex = (kvoWrapper._allRows as [BaseRow]).firstIndex(of: previousRow) else { - throw EurekaError.rowNotInSection(row: previousRow) - } - kvoWrapper._allRows.insert(newRow, at: index(after: rowIndex)) - show(row: newRow) - newRow.wasAddedTo(section: self) - } - -} - -/** - * Navigation options for a form view controller. - */ -public struct MultivaluedOptions: OptionSet { - - private enum Options: Int { - case none = 0, insert = 1, delete = 2, reorder = 4 - } - public let rawValue: Int - public init(rawValue: Int) { self.rawValue = rawValue} - private init(_ options: Options) { self.rawValue = options.rawValue } - - /// No multivalued. - public static let None = MultivaluedOptions(.none) - - /// Allows user to insert rows. - public static let Insert = MultivaluedOptions(.insert) - - /// Allows user to delete rows. - public static let Delete = MultivaluedOptions(.delete) - - /// Allows user to reorder rows - public static let Reorder = MultivaluedOptions(.reorder) -} - -/// Base class for multivalued sections. Use one of the subclasses. -open class BaseMultivaluedSection: Section { - public var multivaluedOptions: MultivaluedOptions - public var showInsertIconInAddButton = true - - public var multivaluedRowToInsertAt: ((Int) -> BaseRow)? - - public required init(multivaluedOptions: MultivaluedOptions = MultivaluedOptions.Insert.union(.Delete), - header: String? = nil, - footer: String? = nil, - _ initializer: (BaseMultivaluedSection) -> Void = { _ in }) { - self.multivaluedOptions = multivaluedOptions - super.init(header: header, footer: footer, {section in initializer(section as! BaseMultivaluedSection) }) - guard multivaluedOptions.contains(.Insert) else { return } - initialize() - } - - public required init() { - self.multivaluedOptions = MultivaluedOptions.Insert.union(.Delete) - super.init() - initialize() - } - - #if swift(>=4.1) - public required init(_ elements: S) where S : Sequence, S.Element == BaseRow { - self.multivaluedOptions = MultivaluedOptions.Insert.union(.Delete) - super.init(elements) - initialize() - } - #endif - - func initialize() { - // Overridden by subclasses - } - - /** - Method used to get all the values of the section. - - - returns: An Array mapping the row values. [value] - */ - public func values() -> [Any?] { - return kvoWrapper._allRows.filter({ $0.baseValue != nil }).map({ $0.baseValue }) - } - -} - -/// Generic multivalued section. Pass the type of the add button row as generic parameter. -open class GenericMultivaluedSection: BaseMultivaluedSection where AddButtonType: BaseRow { - - public var addButtonProvider: ((GenericMultivaluedSection) -> AddButtonType)! - - public required init(multivaluedOptions: MultivaluedOptions = MultivaluedOptions.Insert.union(.Delete), - header: String? = nil, - footer: String? = nil, - _ initializer: (GenericMultivaluedSection) -> Void = { _ in }) { - super.init(multivaluedOptions: multivaluedOptions, header: header, footer: footer, {section in initializer(section as! GenericMultivaluedSection) }) - } - - public required init() { - super.init() - } - - #if swift(>=4.1) - public required init(_ elements: S) where S : Sequence, S.Element == BaseRow { - super.init(elements) - } - #endif - - override func initialize() { - let addRow = addButtonProvider(self) - addRow.onCellSelection { cell, row in - guard !row.isDisabled else { return } - guard let tableView = cell.formViewController()?.tableView, let indexPath = row.indexPath else { return } - cell.formViewController()?.tableView(tableView, commit: .insert, forRowAt: indexPath) - } - self <<< addRow - } - -} - -/** - * Multivalued sections allows us to easily create insertable, deletable and reorderable sections. By using a multivalued section we can add multiple values for a certain field, such as telephone numbers in a contact. - */ -open class MultivaluedSection: GenericMultivaluedSection { - - override func initialize() { - if addButtonProvider == nil { - addButtonProvider = { _ in - return ButtonRow { - $0.title = "Add" - $0.cellStyle = .value1 - }.cellUpdate { cell, _ in - cell.textLabel?.textAlignment = .left - } - } - } - super.initialize() - } - -} diff --git a/Pods/Eureka/Source/Core/SelectableRowType.swift b/Pods/Eureka/Source/Core/SelectableRowType.swift deleted file mode 100644 index 266f28541..000000000 --- a/Pods/Eureka/Source/Core/SelectableRowType.swift +++ /dev/null @@ -1,32 +0,0 @@ -// SelectableRowType.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -/** - * Every row that shall be used in a SelectableSection must conform to this protocol. - */ -public protocol SelectableRowType: RowType { - var selectableValue: Cell.Value? { get set } -} diff --git a/Pods/Eureka/Source/Core/SelectableSection.swift b/Pods/Eureka/Source/Core/SelectableSection.swift deleted file mode 100644 index 5bc3fdcaf..000000000 --- a/Pods/Eureka/Source/Core/SelectableSection.swift +++ /dev/null @@ -1,155 +0,0 @@ -// SelectableSection.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -// MARK: SelectableSection - -/** - Defines how the selection works in a SelectableSection - - - MultipleSelection: Multiple options can be selected at once - - SingleSelection: Only one selection at a time. Can additionally specify if deselection is enabled or not. - */ -public enum SelectionType { - - /** - * Multiple options can be selected at once - */ - case multipleSelection - - /** - * Only one selection at a time. Can additionally specify if deselection is enabled or not. - */ - case singleSelection(enableDeselection: Bool) -} - -/** - * Protocol to be implemented by selectable sections types. Enables easier customization - */ -public protocol SelectableSectionType: Collection { - associatedtype SelectableRow: BaseRow, SelectableRowType - - /// Defines how the selection works (single / multiple selection) - var selectionType: SelectionType { get set } - - /// A closure called when a row of this section is selected. - var onSelectSelectableRow: ((SelectableRow.Cell, SelectableRow) -> Void)? { get set } - - func selectedRow() -> SelectableRow? - func selectedRows() -> [SelectableRow] -} - -extension SelectableSectionType where Element == BaseRow, Self: AnyObject { - /** - Returns the selected row of this section. Should be used if selectionType is SingleSelection - */ - public func selectedRow() -> SelectableRow? { - return selectedRows().first - } - - /** - Returns the selected rows of this section. Should be used if selectionType is MultipleSelection - */ - public func selectedRows() -> [SelectableRow] { - let selectedRows: [BaseRow] = self.filter { $0 is SelectableRow && $0.baseValue != nil } - return selectedRows.map { $0 as! SelectableRow } - } - - /** - Internal function used to set up a collection of rows before they are added to the section - */ - func prepare(selectableRows rows: [BaseRow]) { - for row in rows { - if let row = row as? SelectableRow { - row.onCellSelection { [weak self] cell, row in - guard let s = self, !row.isDisabled else { return } - switch s.selectionType { - case .multipleSelection: - row.value = row.value == nil ? row.selectableValue : nil - case let .singleSelection(enableDeselection): - s.forEach { - guard $0.baseValue != nil && $0 != row && $0 is SelectableRow else { return } - $0.baseValue = nil - $0.updateCell() - } - // Check if row is not already selected - if row.value == nil { - row.value = row.selectableValue - } else if enableDeselection { - row.value = nil - } - } - row.updateCell() - s.onSelectSelectableRow?(cell, row) - } - } - } - } - -} - -/// A subclass of Section that serves to create a section with a list of selectable options. -open class SelectableSection: Section, SelectableSectionType where Row: SelectableRowType, Row: BaseRow { - - public typealias SelectableRow = Row - - /// Defines how the selection works (single / multiple selection) - public var selectionType = SelectionType.singleSelection(enableDeselection: true) - - /// A closure called when a row of this section is selected. - public var onSelectSelectableRow: ((Row.Cell, Row) -> Void)? - - public override init(_ initializer: @escaping (SelectableSection) -> Void) { - super.init({ _ in }) - initializer(self) - } - - public init(_ header: String?, selectionType: SelectionType, _ initializer: @escaping (SelectableSection) -> Void = { _ in }) { - self.selectionType = selectionType - super.init(header, { _ in }) - initializer(self) - } - - public init(header: String?, footer: String?, selectionType: SelectionType, _ initializer: @escaping (SelectableSection) -> Void = { _ in }) { - self.selectionType = selectionType - super.init(header: header, footer: footer, { _ in }) - initializer(self) - } - - public required init() { - super.init() - } - - #if swift(>=4.1) - public required init(_ elements: S) where S : Sequence, S.Element == BaseRow { - super.init(elements) - } - #endif - - open override func rowsHaveBeenAdded(_ rows: [BaseRow], at: IndexSet) { - prepare(selectableRows: rows) - } -} diff --git a/Pods/Eureka/Source/Core/SwipeActions.swift b/Pods/Eureka/Source/Core/SwipeActions.swift deleted file mode 100644 index 6773ee9ad..000000000 --- a/Pods/Eureka/Source/Core/SwipeActions.swift +++ /dev/null @@ -1,152 +0,0 @@ -// -// Swipe.swift -// Eureka -// -// Created by Marco Betschart on 14.06.17. -// Copyright © 2017 Xmartlabs. All rights reserved. -// - -import Foundation -import UIKit - -public typealias SwipeActionHandler = (SwipeAction, BaseRow, ((Bool) -> Void)?) -> Void - -public class SwipeAction: ContextualAction { - let handler: SwipeActionHandler - let style: Style - - public var actionBackgroundColor: UIColor? - public var image: UIImage? - public var title: String? - - @available (*, deprecated, message: "Use actionBackgroundColor instead") - public var backgroundColor: UIColor? { - get { return actionBackgroundColor } - set { self.actionBackgroundColor = newValue } - } - - public init(style: Style, title: String?, handler: @escaping SwipeActionHandler){ - self.style = style - self.title = title - self.handler = handler - } - - func contextualAction(forRow: BaseRow) -> ContextualAction { - var action: ContextualAction - if #available(iOS 11, *){ - action = UIContextualAction(style: style.contextualStyle as! UIContextualAction.Style, title: title){ [weak self] action, view, completion -> Void in - guard let strongSelf = self else{ return } - strongSelf.handler(strongSelf, forRow) { shouldComplete in - if #available(iOS 13, *) { // starting in iOS 13, completion handler is not removing the row automatically, so we need to remove it ourselves - if shouldComplete && action.style == .destructive { - forRow.section?.remove(at: forRow.indexPath!.row) - } - } - completion(shouldComplete) - } - } - } else { - action = UITableViewRowAction(style: style.contextualStyle as! UITableViewRowAction.Style,title: title){ [weak self] (action, indexPath) -> Void in - guard let strongSelf = self else{ return } - strongSelf.handler(strongSelf, forRow) { _ in - DispatchQueue.main.async { - guard action.style == .destructive else { - forRow.baseCell?.formViewController()?.tableView?.setEditing(false, animated: true) - return - } - forRow.section?.remove(at: indexPath.row) - } - } - } - } - if let color = self.actionBackgroundColor { - action.actionBackgroundColor = color - } - if let image = self.image { - action.image = image - } - return action - } - - public enum Style { - case normal - case destructive - - var contextualStyle: ContextualStyle { - if #available(iOS 11, *){ - switch self{ - case .normal: - return UIContextualAction.Style.normal - case .destructive: - return UIContextualAction.Style.destructive - } - } else { - switch self{ - case .normal: - return UITableViewRowAction.Style.normal - case .destructive: - return UITableViewRowAction.Style.destructive - } - } - } - } -} - -public struct SwipeConfiguration { - - unowned var row: BaseRow - - init(_ row: BaseRow){ - self.row = row - } - - public var performsFirstActionWithFullSwipe = false - public var actions: [SwipeAction] = [] -} - -extension SwipeConfiguration { - @available(iOS 11.0, *) - var contextualConfiguration: UISwipeActionsConfiguration? { - let contextualConfiguration = UISwipeActionsConfiguration(actions: self.contextualActions as! [UIContextualAction]) - contextualConfiguration.performsFirstActionWithFullSwipe = self.performsFirstActionWithFullSwipe - return contextualConfiguration - } - - var contextualActions: [ContextualAction]{ - return self.actions.map { $0.contextualAction(forRow: self.row) } - } -} - -protocol ContextualAction { - var actionBackgroundColor: UIColor? { get set } - var image: UIImage? { get set } - var title: String? { get set } -} - -extension UITableViewRowAction: ContextualAction { - public var image: UIImage? { - get { return nil } - set { return } - } - - public var actionBackgroundColor: UIColor? { - get { return backgroundColor } - set { self.backgroundColor = newValue } - } -} - -@available(iOS 11.0, *) -extension UIContextualAction: ContextualAction { - - public var actionBackgroundColor: UIColor? { - get { return backgroundColor } - set { self.backgroundColor = newValue } - } - -} - -public protocol ContextualStyle{} -extension UITableViewRowAction.Style: ContextualStyle {} - -@available(iOS 11.0, *) -extension UIContextualAction.Style: ContextualStyle {} diff --git a/Pods/Eureka/Source/Core/Validation.swift b/Pods/Eureka/Source/Core/Validation.swift deleted file mode 100644 index fa999d328..000000000 --- a/Pods/Eureka/Source/Core/Validation.swift +++ /dev/null @@ -1,99 +0,0 @@ -// RowValidationType.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -public struct ValidationError: Equatable { - - public let msg: String - - public init(msg: String) { - self.msg = msg - } -} - -public func == (lhs: ValidationError, rhs: ValidationError) -> Bool { - return lhs.msg == rhs.msg -} - -public protocol BaseRuleType { - var id: String? { get set } - var validationError: ValidationError { get set } -} - -public protocol RuleType: BaseRuleType { - associatedtype RowValueType - - func isValid(value: RowValueType?) -> ValidationError? -} - -public struct ValidationOptions: OptionSet { - - public let rawValue: Int - - public init(rawValue: Int) { - self.rawValue = rawValue - } - - public static let validatesOnDemand = ValidationOptions(rawValue: 1 << 0) - public static let validatesOnChange = ValidationOptions(rawValue: 1 << 1) - public static let validatesOnBlur = ValidationOptions(rawValue: 1 << 2) - public static let validatesOnChangeAfterBlurred = ValidationOptions(rawValue: 1 << 3) - - public static let validatesAlways: ValidationOptions = [.validatesOnChange, .validatesOnBlur] -} - -public struct ValidationRuleHelper where T: Equatable { - let validateFn: ((T?) -> ValidationError?) - public let rule: BaseRuleType -} - -public struct RuleSet { - - internal var rules: [ValidationRuleHelper] = [] - - public init() {} - - /// Add a validation Rule to a Row - /// - Parameter rule: RuleType object typed to the same type of the Row.value - public mutating func add(rule: Rule) where T == Rule.RowValueType { - let validFn: ((T?) -> ValidationError?) = { (val: T?) in - return rule.isValid(value: val) - } - rules.append(ValidationRuleHelper(validateFn: validFn, rule: rule)) - } - - public mutating func remove(ruleWithIdentifier identifier: String) { - if let index = rules.firstIndex(where: { (validationRuleHelper) -> Bool in - return validationRuleHelper.rule.id == identifier - }) { - rules.remove(at: index) - } - } - - public mutating func removeAllRules() { - rules.removeAll() - } - -} diff --git a/Pods/Eureka/Source/Rows/ActionSheetRow.swift b/Pods/Eureka/Source/Rows/ActionSheetRow.swift deleted file mode 100644 index dc4801d75..000000000 --- a/Pods/Eureka/Source/Rows/ActionSheetRow.swift +++ /dev/null @@ -1,96 +0,0 @@ -// ActionSheetRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -open class AlertSelectorCell : Cell, CellType where T: Equatable { - - required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func update() { - super.update() - accessoryType = .none - editingAccessoryType = accessoryType - selectionStyle = row.isDisabled ? .none : .default - } - - open override func didSelect() { - super.didSelect() - row.deselect() - } -} - -open class _ActionSheetRow: AlertOptionsRow, PresenterRowType where Cell: BaseCell { - - public typealias ProviderType = SelectorAlertController<_ActionSheetRow> - - public var onPresentCallback: ((FormViewController, ProviderType) -> Void)? - lazy public var presentationMode: PresentationMode? = { - return .presentModally(controllerProvider: ControllerProvider.callback { [weak self] in - let vc = SelectorAlertController<_ActionSheetRow>(title: self?.selectorTitle, message: nil, preferredStyle: .actionSheet) - if let popView = vc.popoverPresentationController { - guard let cell = self?.cell, let tableView = cell.formViewController()?.tableView else { fatalError() } - popView.sourceView = tableView - popView.sourceRect = tableView.convert(cell.detailTextLabel?.frame ?? cell.textLabel?.frame ?? cell.contentView.frame, from: cell) - } - vc.row = self - return vc - }, - onDismiss: { [weak self] in - $0.dismiss(animated: true) - self?.cell?.formViewController()?.tableView?.reloadData() - }) - }() - - public required init(tag: String?) { - super.init(tag: tag) - } - - open override func customDidSelect() { - super.customDidSelect() - if let presentationMode = presentationMode, !isDisabled { - if let controller = presentationMode.makeController() { - controller.row = self - onPresentCallback?(cell.formViewController()!, controller) - presentationMode.present(controller, row: self, presentingController: cell.formViewController()!) - } else { - presentationMode.present(nil, row: self, presentingController: cell.formViewController()!) - } - } - } -} - -/// An options row where the user can select an option from an ActionSheet -public final class ActionSheetRow: _ActionSheetRow>, RowType where T: Equatable { - required public init(tag: String?) { - super.init(tag: tag) - } -} diff --git a/Pods/Eureka/Source/Rows/AlertRow.swift b/Pods/Eureka/Source/Rows/AlertRow.swift deleted file mode 100644 index b572e7e8b..000000000 --- a/Pods/Eureka/Source/Rows/AlertRow.swift +++ /dev/null @@ -1,66 +0,0 @@ -// AlertRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -import Foundation -import UIKit - -open class _AlertRow: AlertOptionsRow, PresenterRowType where Cell: BaseCell { - - public typealias PresentedController = SelectorAlertController<_AlertRow> - - open var onPresentCallback: ((FormViewController, PresentedController) -> Void)? - lazy open var presentationMode: PresentationMode? = { - return .presentModally(controllerProvider: ControllerProvider.callback { [weak self] in - let vc = PresentedController(title: self?.selectorTitle, message: nil, preferredStyle: .alert) - vc.row = self - return vc - }, onDismiss: { [weak self] in - $0.dismiss(animated: true) - self?.cell?.formViewController()?.tableView?.reloadData() - }) - }() - - public required init(tag: String?) { - super.init(tag: tag) - } - - open override func customDidSelect() { - super.customDidSelect() - if let presentationMode = presentationMode, !isDisabled { - if let controller = presentationMode.makeController() { - controller.row = self - onPresentCallback?(cell.formViewController()!, controller) - presentationMode.present(controller, row: self, presentingController: cell.formViewController()!) - } else { - presentationMode.present(nil, row: self, presentingController: cell.formViewController()!) - } - } - } -} - -/// An options row where the user can select an option from a modal Alert -public final class AlertRow: _AlertRow>, RowType { - required public init(tag: String?) { - super.init(tag: tag) - } -} diff --git a/Pods/Eureka/Source/Rows/ButtonRow.swift b/Pods/Eureka/Source/Rows/ButtonRow.swift deleted file mode 100644 index 41d5b5403..000000000 --- a/Pods/Eureka/Source/Rows/ButtonRow.swift +++ /dev/null @@ -1,104 +0,0 @@ -// ButtonRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -// MARK: ButtonCell - -open class ButtonCellOf: Cell, CellType { - - required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func update() { - super.update() - selectionStyle = row.isDisabled ? .none : .default - accessoryType = .none - editingAccessoryType = accessoryType - textLabel?.textAlignment = .center - textLabel?.textColor = tintColor.withAlphaComponent(row.isDisabled ? 0.3 : 1.0) - } - - open override func didSelect() { - super.didSelect() - row.deselect() - } -} - -public typealias ButtonCell = ButtonCellOf - -// MARK: ButtonRow - -open class _ButtonRowOf : Row> { - open var presentationMode: PresentationMode? - - required public init(tag: String?) { - super.init(tag: tag) - displayValueFor = nil - cellStyle = .default - } - - open override func customDidSelect() { - super.customDidSelect() - if !isDisabled { - if let presentationMode = presentationMode { - if let controller = presentationMode.makeController() { - presentationMode.present(controller, row: self, presentingController: self.cell.formViewController()!) - } else { - presentationMode.present(nil, row: self, presentingController: self.cell.formViewController()!) - } - } - } - } - - open override func customUpdateCell() { - super.customUpdateCell() - let leftAligmnment = presentationMode != nil - cell.textLabel?.textAlignment = leftAligmnment ? .left : .center - cell.accessoryType = !leftAligmnment || isDisabled ? .none : .disclosureIndicator - cell.editingAccessoryType = cell.accessoryType - cell.textLabel?.textColor = !leftAligmnment ? cell.tintColor.withAlphaComponent(isDisabled ? 0.3 : 1.0) : nil - } - - open override func prepare(for segue: UIStoryboardSegue) { - super.prepare(for: segue) - (segue.destination as? RowControllerType)?.onDismissCallback = presentationMode?.onDismissCallback - } -} - -/// A generic row with a button. The action of this button can be anything but normally will push a new view controller -public final class ButtonRowOf : _ButtonRowOf, RowType { - public required init(tag: String?) { - super.init(tag: tag) - } -} - -/// A row with a button and String value. The action of this button can be anything but normally will push a new view controller -public typealias ButtonRow = ButtonRowOf diff --git a/Pods/Eureka/Source/Rows/ButtonRowWithPresent.swift b/Pods/Eureka/Source/Rows/ButtonRowWithPresent.swift deleted file mode 100644 index 1a55fba9e..000000000 --- a/Pods/Eureka/Source/Rows/ButtonRowWithPresent.swift +++ /dev/null @@ -1,88 +0,0 @@ -// Rows.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -open class _ButtonRowWithPresent: Row>, PresenterRowType where VCType: UIViewController { - - open var presentationMode: PresentationMode? - open var onPresentCallback: ((FormViewController, VCType) -> Void)? - - required public init(tag: String?) { - super.init(tag: tag) - displayValueFor = nil - cellStyle = .default - } - - open override func customUpdateCell() { - super.customUpdateCell() - let leftAligmnment = presentationMode != nil - cell.textLabel?.textAlignment = leftAligmnment ? .left : .center - cell.accessoryType = !leftAligmnment || isDisabled ? .none : .disclosureIndicator - cell.editingAccessoryType = cell.accessoryType - if !leftAligmnment { - var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0 - cell.tintColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha) - cell.textLabel?.textColor = UIColor(red: red, green: green, blue: blue, alpha:isDisabled ? 0.3 : 1.0) - } else { - cell.textLabel?.textColor = nil - } - } - - open override func customDidSelect() { - super.customDidSelect() - if let presentationMode = presentationMode, !isDisabled { - if let controller = presentationMode.makeController() { - controller.row = self - onPresentCallback?(cell.formViewController()!, controller) - presentationMode.present(controller, row: self, presentingController: cell.formViewController()!) - } else { - presentationMode.present(nil, row: self, presentingController: cell.formViewController()!) - } - } - } - - open override func prepare(for segue: UIStoryboardSegue) { - super.prepare(for: segue) - guard let rowVC = segue.destination as? VCType else { - return - } - if let callback = presentationMode?.onDismissCallback { - rowVC.onDismissCallback = callback - } - rowVC.row = self - onPresentCallback?(cell.formViewController()!, rowVC) - } - -} - -// MARK: Rows - -/// A generic row with a button that presents a view controller when tapped -public final class ButtonRowWithPresent : _ButtonRowWithPresent, RowType where VCType: UIViewController { - public required init(tag: String?) { - super.init(tag: tag) - } -} diff --git a/Pods/Eureka/Source/Rows/CheckRow.swift b/Pods/Eureka/Source/Rows/CheckRow.swift deleted file mode 100644 index 49d54db48..000000000 --- a/Pods/Eureka/Source/Rows/CheckRow.swift +++ /dev/null @@ -1,83 +0,0 @@ -// CheckRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -// MARK: CheckCell - -open class CheckCell: Cell, CellType { - - required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func update() { - super.update() - accessoryType = row.value == true ? .checkmark : .none - editingAccessoryType = accessoryType - selectionStyle = .default - if row.isDisabled { - tintAdjustmentMode = .dimmed - selectionStyle = .none - } else { - tintAdjustmentMode = .automatic - } - } - - open override func setup() { - super.setup() - accessoryType = .checkmark - editingAccessoryType = accessoryType - } - - open override func didSelect() { - row.value = row.value ?? false ? false : true - row.deselect() - row.updateCell() - } - -} - -// MARK: CheckRow - -open class _CheckRow: Row { - - required public init(tag: String?) { - super.init(tag: tag) - displayValueFor = nil - } -} - -///// Boolean row that has a checkmark as accessoryType -public final class CheckRow: _CheckRow, RowType { - - required public init(tag: String?) { - super.init(tag: tag) - } -} diff --git a/Pods/Eureka/Source/Rows/Common/AlertOptionsRow.swift b/Pods/Eureka/Source/Rows/Common/AlertOptionsRow.swift deleted file mode 100644 index 46d12ed70..000000000 --- a/Pods/Eureka/Source/Rows/Common/AlertOptionsRow.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// AlertOptionsRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - - -import Foundation - -open class AlertOptionsRow : OptionsRow, AlertOptionsProviderRow where Cell: BaseCell { - - typealias OptionsProviderType = OptionsProvider - - open var cancelTitle: String? - - required public init(tag: String?) { - super.init(tag: tag) - } - -} diff --git a/Pods/Eureka/Source/Rows/Common/DateFieldRow.swift b/Pods/Eureka/Source/Rows/Common/DateFieldRow.swift deleted file mode 100644 index 5232a62e1..000000000 --- a/Pods/Eureka/Source/Rows/Common/DateFieldRow.swift +++ /dev/null @@ -1,144 +0,0 @@ -// DateFieldRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -public protocol DatePickerRowProtocol: AnyObject { - var minimumDate: Date? { get set } - var maximumDate: Date? { get set } - var minuteInterval: Int? { get set } -} - -open class DateCell: Cell, CellType { - - public var datePicker: UIDatePicker - - public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - datePicker = UIDatePicker() - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - datePicker = UIDatePicker() - super.init(coder: aDecoder) - } - - open override func setup() { - super.setup() - accessoryType = .none - editingAccessoryType = .none - datePicker.datePickerMode = datePickerMode() - datePicker.addTarget(self, action: #selector(DateCell.datePickerValueChanged(_:)), for: .valueChanged) - - #if swift(>=5.2) - if #available(iOS 13.4, *) { - datePicker.preferredDatePickerStyle = .wheels - } - #endif - } - - deinit { - datePicker.removeTarget(self, action: nil, for: .allEvents) - } - - open override func update() { - super.update() - selectionStyle = row.isDisabled ? .none : .default - datePicker.setDate(row.value ?? Date(), animated: row is CountDownPickerRow) - datePicker.minimumDate = (row as? DatePickerRowProtocol)?.minimumDate - datePicker.maximumDate = (row as? DatePickerRowProtocol)?.maximumDate - if let minuteIntervalValue = (row as? DatePickerRowProtocol)?.minuteInterval { - datePicker.minuteInterval = minuteIntervalValue - } - if row.isHighlighted { - textLabel?.textColor = tintColor - } - } - - open override func didSelect() { - super.didSelect() - row.deselect() - } - - override open var inputView: UIView? { - if let v = row.value { - datePicker.setDate(v, animated:row is CountDownRow) - } - return datePicker - } - - @objc(pickerDateChanged:) func datePickerValueChanged(_ sender: UIDatePicker) { - row.value = sender.date - detailTextLabel?.text = row.displayValueFor?(row.value) - } - - private func datePickerMode() -> UIDatePicker.Mode { - switch row { - case is DateRow: - return .date - case is TimeRow: - return .time - case is DateTimeRow: - return .dateAndTime - case is CountDownRow: - return .countDownTimer - default: - return .date - } - } - - open override func cellCanBecomeFirstResponder() -> Bool { - return canBecomeFirstResponder - } - - override open var canBecomeFirstResponder: Bool { - return !row.isDisabled - } -} - -open class _DateFieldRow: Row, DatePickerRowProtocol, NoValueDisplayTextConformance { - - /// The minimum value for this row's UIDatePicker - open var minimumDate: Date? - - /// The maximum value for this row's UIDatePicker - open var maximumDate: Date? - - /// The interval between options for this row's UIDatePicker - open var minuteInterval: Int? - - /// The formatter for the date picked by the user - open var dateFormatter: DateFormatter? - - open var noValueDisplayText: String? = nil - - required public init(tag: String?) { - super.init(tag: tag) - displayValueFor = { [unowned self] value in - guard let val = value, let formatter = self.dateFormatter else { return nil } - return formatter.string(from: val) - } - } -} diff --git a/Pods/Eureka/Source/Rows/Common/DateInlineFieldRow.swift b/Pods/Eureka/Source/Rows/Common/DateInlineFieldRow.swift deleted file mode 100644 index da9bfaac0..000000000 --- a/Pods/Eureka/Source/Rows/Common/DateInlineFieldRow.swift +++ /dev/null @@ -1,80 +0,0 @@ -// DateInlineFieldRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -open class DateInlineCell: Cell, CellType { - - public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func setup() { - super.setup() - accessoryType = .none - editingAccessoryType = .none - } - - open override func update() { - super.update() - selectionStyle = row.isDisabled ? .none : .default - } - - open override func didSelect() { - super.didSelect() - row.deselect() - } -} - -open class _DateInlineFieldRow: Row, DatePickerRowProtocol, NoValueDisplayTextConformance { - - /// The minimum value for this row's UIDatePicker - open var minimumDate: Date? - - /// The maximum value for this row's UIDatePicker - open var maximumDate: Date? - - /// The interval between options for this row's UIDatePicker - open var minuteInterval: Int? - - /// The formatter for the date picked by the user - open var dateFormatter: DateFormatter? - - open var noValueDisplayText: String? - - required public init(tag: String?) { - super.init(tag: tag) - dateFormatter = DateFormatter() - dateFormatter?.locale = Locale.current - displayValueFor = { [unowned self] value in - guard let val = value, let formatter = self.dateFormatter else { return nil } - return formatter.string(from: val) - } - } -} diff --git a/Pods/Eureka/Source/Rows/Common/DecimalFormatter.swift b/Pods/Eureka/Source/Rows/Common/DecimalFormatter.swift deleted file mode 100644 index 7eecf6229..000000000 --- a/Pods/Eureka/Source/Rows/Common/DecimalFormatter.swift +++ /dev/null @@ -1,59 +0,0 @@ -// DecimalFormatter.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -/// A custom formatter for numbers with two digits after the decimal mark -open class DecimalFormatter: NumberFormatter, FormatterProtocol { - - /// Creates the formatter with 2 Fraction Digits, Locale set to .current and .decimal NumberFormatter.Style - public override init() { - super.init() - locale = Locale.current - numberStyle = .decimal - minimumFractionDigits = 2 - maximumFractionDigits = 2 - } - - required public init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - /// Creates an NSNumber from the given String - /// - Parameter obj: Pointer to NSNumber object to assign - /// - Parameter string: String with number assumed to have the configured min. fraction digits. - /// - Parameter rangep: Unused range parameter - override open func getObjectValue(_ obj: AutoreleasingUnsafeMutablePointer?, for string: String, range rangep: UnsafeMutablePointer?) throws { - guard obj != nil else { return } - let str = string.components(separatedBy: CharacterSet.decimalDigits.inverted).joined(separator: "") - // Recover the number from the string in a way that forces the formatter's fraction digits - // numberWithoutDecimals / 10 ^ minimumFractionDigits - obj?.pointee = NSNumber(value: (Double(str) ?? 0.0)/Double(pow(10.0, Double(minimumFractionDigits)))) - } - - open func getNewPosition(forPosition position: UITextPosition, inTextInput textInput: UITextInput, oldValue: String?, newValue: String?) -> UITextPosition { - return textInput.position(from: position, offset:((newValue?.count ?? 0) - (oldValue?.count ?? 0))) ?? position - } -} diff --git a/Pods/Eureka/Source/Rows/Common/FieldRow.swift b/Pods/Eureka/Source/Rows/Common/FieldRow.swift deleted file mode 100644 index 0ad9e7d13..000000000 --- a/Pods/Eureka/Source/Rows/Common/FieldRow.swift +++ /dev/null @@ -1,494 +0,0 @@ -// FieldRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -public protocol InputTypeInitiable { - init?(string stringValue: String) -} - -public protocol FieldRowConformance : FormatterConformance { - var titlePercentage : CGFloat? { get set } - var placeholder : String? { get set } - var placeholderColor : UIColor? { get set } -} - -extension Int: InputTypeInitiable { - - public init?(string stringValue: String) { - self.init(stringValue, radix: 10) - } -} -extension Float: InputTypeInitiable { - public init?(string stringValue: String) { - self.init(stringValue) - } -} -extension String: InputTypeInitiable { - public init?(string stringValue: String) { - self.init(stringValue) - } -} -extension URL: InputTypeInitiable {} -extension Double: InputTypeInitiable { - public init?(string stringValue: String) { - self.init(stringValue) - } -} - -open class FormatteableRow: Row, FormatterConformance where Cell: BaseCell, Cell: TextInputCell { - - /// A formatter to be used to format the user's input - open var formatter: Formatter? - - /// If the formatter should be used while the user is editing the text. - open var useFormatterDuringInput = false - open var useFormatterOnDidBeginEditing: Bool? - - public required init(tag: String?) { - super.init(tag: tag) - displayValueFor = { [unowned self] value in - guard let v = value else { return nil } - guard let formatter = self.formatter else { return String(describing: v) } - if (self.cell.textInput as? UIView)?.isFirstResponder == true { - return self.useFormatterDuringInput ? formatter.editingString(for: v) : String(describing: v) - } - return formatter.string(for: v) - } - } - -} - -open class FieldRow: FormatteableRow, FieldRowConformance, KeyboardReturnHandler where Cell: BaseCell, Cell: TextFieldCell { - - /// Configuration for the keyboardReturnType of this row - open var keyboardReturnType: KeyboardReturnTypeConfiguration? - - /// The percentage of the cell that should be occupied by the textField - @available (*, deprecated, message: "Use titlePercentage instead") - open var textFieldPercentage : CGFloat? { - get { - return titlePercentage.map { 1 - $0 } - } - set { - titlePercentage = newValue.map { 1 - $0 } - } - } - - /// The percentage of the cell that should be occupied by the title (i.e. the titleLabel and optional imageView combined) - open var titlePercentage: CGFloat? - - /// The placeholder for the textField - open var placeholder: String? - - /// The textColor for the textField's placeholder - open var placeholderColor: UIColor? - - public required init(tag: String?) { - super.init(tag: tag) - } -} - -/** - * Protocol for cells that contain a UITextField - */ -public protocol TextInputCell { - var textInput: UITextInput { get } -} - -public protocol TextFieldCell: TextInputCell { - var textField: UITextField! { get } -} - -extension TextFieldCell { - - public var textInput: UITextInput { - return textField - } -} - -open class _FieldCell : Cell, UITextFieldDelegate, TextFieldCell where T: Equatable, T: InputTypeInitiable { - - @IBOutlet public weak var textField: UITextField! - @IBOutlet public weak var titleLabel: UILabel? - - fileprivate var observingTitleText = false - private var awakeFromNibCalled = false - - open var dynamicConstraints = [NSLayoutConstraint]() - - private var calculatedTitlePercentage: CGFloat = 0.7 - - public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - - let textField = UITextField() - self.textField = textField - textField.translatesAutoresizingMaskIntoConstraints = false - - super.init(style: style, reuseIdentifier: reuseIdentifier) - - setupTitleLabel() - - contentView.addSubview(titleLabel!) - contentView.addSubview(textField) - - NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: nil) { [weak self] _ in - guard let me = self else { return } - guard me.observingTitleText else { return } - me.titleLabel?.removeObserver(me, forKeyPath: "text") - me.observingTitleText = false - } - NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] _ in - guard let me = self else { return } - guard !me.observingTitleText else { return } - me.titleLabel?.addObserver(me, forKeyPath: "text", options: [.new, .old], context: nil) - me.observingTitleText = true - } - - NotificationCenter.default.addObserver(forName: UIContentSizeCategory.didChangeNotification, object: nil, queue: nil) { [weak self] _ in - self?.setupTitleLabel() - self?.setNeedsUpdateConstraints() - } - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func awakeFromNib() { - super.awakeFromNib() - awakeFromNibCalled = true - } - - deinit { - textField?.delegate = nil - textField?.removeTarget(self, action: nil, for: .allEvents) - guard !awakeFromNibCalled else { return } - if observingTitleText { - titleLabel?.removeObserver(self, forKeyPath: "text") - } - imageView?.removeObserver(self, forKeyPath: "image") - NotificationCenter.default.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: UIContentSizeCategory.didChangeNotification, object: nil) - } - - open override func setup() { - super.setup() - selectionStyle = .none - - if !awakeFromNibCalled { - titleLabel?.addObserver(self, forKeyPath: "text", options: [.new, .old], context: nil) - observingTitleText = true - imageView?.addObserver(self, forKeyPath: "image", options: [.new, .old], context: nil) - } - textField.addTarget(self, action: #selector(_FieldCell.textFieldDidChange(_:)), for: .editingChanged) - - if let titleLabel = titleLabel { - // Make sure the title takes over most of the empty space so that the text field starts editing at the back. - let priority = UILayoutPriority(rawValue: titleLabel.contentHuggingPriority(for: .horizontal).rawValue + 1) - textField.setContentHuggingPriority(priority, for: .horizontal) - } - } - - open override func update() { - super.update() - detailTextLabel?.text = nil - - if !awakeFromNibCalled { - if let title = row.title { - switch row.cellStyle { - case .subtitle: - textField.textAlignment = .left - textField.clearButtonMode = .whileEditing - default: - textField.textAlignment = title.isEmpty ? .left : .right - textField.clearButtonMode = title.isEmpty ? .whileEditing : .never - } - } else { - textField.textAlignment = .left - textField.clearButtonMode = .whileEditing - } - } else { - textLabel?.text = nil - titleLabel?.text = row.title - if #available(iOS 13.0, *) { - titleLabel?.textColor = row.isDisabled ? .tertiaryLabel : .label - } else { - titleLabel?.textColor = row.isDisabled ? .gray : .black - } - } - textField.delegate = self - textField.text = row.displayValueFor?(row.value) - textField.isEnabled = !row.isDisabled - if #available(iOS 13.0, *) { - textField.textColor = row.isDisabled ? .tertiaryLabel : .label - } else { - textField.textColor = row.isDisabled ? .gray : .black - } - textField.font = .preferredFont(forTextStyle: .body) - if let placeholder = (row as? FieldRowConformance)?.placeholder { - if let color = (row as? FieldRowConformance)?.placeholderColor { - textField.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: [.foregroundColor: color]) - } else { - textField.placeholder = (row as? FieldRowConformance)?.placeholder - } - } - if row.isHighlighted { - titleLabel?.textColor = tintColor - } - } - - open override func cellCanBecomeFirstResponder() -> Bool { - return !row.isDisabled && textField?.canBecomeFirstResponder == true - } - - open override func cellBecomeFirstResponder(withDirection: Direction) -> Bool { - return textField.becomeFirstResponder() - } - - open override func cellResignFirstResponder() -> Bool { - return textField.resignFirstResponder() - } - - open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { - let obj = object as AnyObject? - - if let keyPathValue = keyPath, let changeType = change?[NSKeyValueChangeKey.kindKey], - ((obj === titleLabel && keyPathValue == "text") || (obj === imageView && keyPathValue == "image")) && - (changeType as? NSNumber)?.uintValue == NSKeyValueChange.setting.rawValue { - setNeedsUpdateConstraints() - updateConstraintsIfNeeded() - } - } - - // MARK: Helpers - - open func customConstraints() { - - guard !awakeFromNibCalled else { return } - contentView.removeConstraints(dynamicConstraints) - dynamicConstraints = [] - - switch row.cellStyle { - case .subtitle: - var views: [String: AnyObject] = ["textField": textField] - - if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty { - views["titleLabel"] = titleLabel - dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|-[titleLabel]-3-[textField]-|", - options: .alignAllLeading, metrics: nil, views: views) - titleLabel.setContentHuggingPriority( - UILayoutPriority(textField.contentHuggingPriority(for: .vertical).rawValue + 1), for: .vertical) - dynamicConstraints.append(NSLayoutConstraint(item: titleLabel, attribute: .centerX, relatedBy: .equal, toItem: textField, attribute: .centerX, multiplier: 1, constant: 0)) - } else { - dynamicConstraints.append(NSLayoutConstraint(item: textField!, attribute: .centerY, relatedBy: .equal, toItem: contentView, attribute: .centerY, multiplier: 1, constant: 0)) - } - - if let imageView = imageView, let _ = imageView.image { - views["imageView"] = imageView - if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty { - dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[titleLabel]-|", options: [], metrics: nil, views: views) - dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[textField]-|", options: [], metrics: nil, views: views) - } else { - dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[textField]-|", options: [], metrics: nil, views: views) - } - } else { - if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty { - dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[titleLabel]-|", options: [], metrics: nil, views: views) - dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[textField]-|", options: [], metrics: nil, views: views) - } else { - dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[textField]-|", options: .alignAllLeft, metrics: nil, views: views) - } - } - - default: - var views: [String: AnyObject] = ["textField": textField] - dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|-[textField]-|", options: .alignAllLastBaseline, metrics: nil, views: views) - - if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty { - views["titleLabel"] = titleLabel - dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|-[titleLabel]-|", options: .alignAllLastBaseline, metrics: nil, views: views) - dynamicConstraints.append(NSLayoutConstraint(item: titleLabel, attribute: .centerY, relatedBy: .equal, toItem: textField, attribute: .centerY, multiplier: 1, constant: 0)) - } - - if let imageView = imageView, let _ = imageView.image { - views["imageView"] = imageView - if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty { - dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[titleLabel]-[textField]-|", options: [], metrics: nil, views: views) - dynamicConstraints.append(NSLayoutConstraint(item: titleLabel, - attribute: .width, - relatedBy: (row as? FieldRowConformance)?.titlePercentage != nil ? .equal : .lessThanOrEqual, - toItem: contentView, - attribute: .width, - multiplier: calculatedTitlePercentage, - constant: 0.0)) - } else { - dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[textField]-|", options: [], metrics: nil, views: views) - } - } else { - if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty { - dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[titleLabel]-[textField]-|", options: [], metrics: nil, views: views) - dynamicConstraints.append(NSLayoutConstraint(item: titleLabel, - attribute: .width, - relatedBy: (row as? FieldRowConformance)?.titlePercentage != nil ? .equal : .lessThanOrEqual, - toItem: contentView, - attribute: .width, - multiplier: calculatedTitlePercentage, - constant: 0.0)) - } else { - dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[textField]-|", options: .alignAllLeft, metrics: nil, views: views) - } - } - } - contentView.addConstraints(dynamicConstraints) - } - - open override func updateConstraints() { - customConstraints() - super.updateConstraints() - } - - @objc open func textFieldDidChange(_ textField: UITextField) { - - guard textField.markedTextRange == nil else { return } - - guard let textValue = textField.text else { - row.value = nil - return - } - guard let fieldRow = row as? FieldRowConformance, let formatter = fieldRow.formatter else { - row.value = textValue.isEmpty ? nil : (T.init(string: textValue) ?? row.value) - return - } - if fieldRow.useFormatterDuringInput { - let unsafePointer = UnsafeMutablePointer.allocate(capacity: 1) - defer { - unsafePointer.deallocate() - } - let value: AutoreleasingUnsafeMutablePointer = AutoreleasingUnsafeMutablePointer.init(unsafePointer) - let errorDesc: AutoreleasingUnsafeMutablePointer? = nil - if formatter.getObjectValue(value, for: textValue, errorDescription: errorDesc) { - row.value = value.pointee as? T - guard var selStartPos = textField.selectedTextRange?.start else { return } - let oldVal = textField.text - textField.text = row.displayValueFor?(row.value) - selStartPos = (formatter as? FormatterProtocol)?.getNewPosition(forPosition: selStartPos, inTextInput: textField, oldValue: oldVal, newValue: textField.text) ?? selStartPos - textField.selectedTextRange = textField.textRange(from: selStartPos, to: selStartPos) - return - } - } else { - let unsafePointer = UnsafeMutablePointer.allocate(capacity: 1) - defer { - unsafePointer.deallocate() - } - let value: AutoreleasingUnsafeMutablePointer = AutoreleasingUnsafeMutablePointer.init(unsafePointer) - let errorDesc: AutoreleasingUnsafeMutablePointer? = nil - if formatter.getObjectValue(value, for: textValue, errorDescription: errorDesc) { - row.value = value.pointee as? T - } else { - row.value = textValue.isEmpty ? nil : (T.init(string: textValue) ?? row.value) - } - } - } - - // MARK: Helpers - - private func setupTitleLabel() { - titleLabel = self.textLabel - titleLabel?.translatesAutoresizingMaskIntoConstraints = false - titleLabel?.setContentHuggingPriority(UILayoutPriority(rawValue: 500), for: .horizontal) - titleLabel?.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 1000), for: .horizontal) - } - - private func displayValue(useFormatter: Bool) -> String? { - guard let v = row.value else { return nil } - if let formatter = (row as? FormatterConformance)?.formatter, useFormatter { - return textField?.isFirstResponder == true ? formatter.editingString(for: v) : formatter.string(for: v) - } - return String(describing: v) - } - - // MARK: TextFieldDelegate - - open func textFieldDidBeginEditing(_ textField: UITextField) { - formViewController()?.beginEditing(of: self) - formViewController()?.textInputDidBeginEditing(textField, cell: self) - if let fieldRowConformance = row as? FormatterConformance, let _ = fieldRowConformance.formatter, fieldRowConformance.useFormatterOnDidBeginEditing ?? fieldRowConformance.useFormatterDuringInput { - textField.text = displayValue(useFormatter: true) - } else { - textField.text = displayValue(useFormatter: false) - } - } - - open func textFieldDidEndEditing(_ textField: UITextField) { - formViewController()?.endEditing(of: self) - formViewController()?.textInputDidEndEditing(textField, cell: self) - textFieldDidChange(textField) - textField.text = displayValue(useFormatter: (row as? FormatterConformance)?.formatter != nil) - } - - open func textFieldShouldReturn(_ textField: UITextField) -> Bool { - return formViewController()?.textInputShouldReturn(textField, cell: self) ?? true - } - - open func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - return formViewController()?.textInput(textField, shouldChangeCharactersInRange:range, replacementString:string, cell: self) ?? true - } - - open func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { - return formViewController()?.textInputShouldBeginEditing(textField, cell: self) ?? true - } - - open func textFieldShouldClear(_ textField: UITextField) -> Bool { - return formViewController()?.textInputShouldClear(textField, cell: self) ?? true - } - - open func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { - return formViewController()?.textInputShouldEndEditing(textField, cell: self) ?? true - } - - open override func layoutSubviews() { - super.layoutSubviews() - guard let row = (row as? FieldRowConformance) else { return } - defer { - // As titleLabel is the textLabel, iOS may re-layout without updating constraints, for example: - // swiping, showing alert or actionsheet from the same section. - // thus we need forcing update to use customConstraints() - setNeedsUpdateConstraints() - updateConstraintsIfNeeded() - } - guard let titlePercentage = row.titlePercentage else { return } - var targetTitleWidth = bounds.size.width * titlePercentage - if let imageView = imageView, let _ = imageView.image, let titleLabel = titleLabel { - var extraWidthToSubtract = titleLabel.frame.minX - imageView.frame.minX // Left-to-right interface layout - if UIView.userInterfaceLayoutDirection(for: self.semanticContentAttribute) == .rightToLeft { - extraWidthToSubtract = imageView.frame.maxX - titleLabel.frame.maxX - } - targetTitleWidth -= extraWidthToSubtract - } - calculatedTitlePercentage = targetTitleWidth / contentView.bounds.size.width - } -} diff --git a/Pods/Eureka/Source/Rows/Common/GenericMultipleSelectorRow.swift b/Pods/Eureka/Source/Rows/Common/GenericMultipleSelectorRow.swift deleted file mode 100644 index 232ca6e85..000000000 --- a/Pods/Eureka/Source/Rows/Common/GenericMultipleSelectorRow.swift +++ /dev/null @@ -1,86 +0,0 @@ -// GenericMultipleSelectorRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -/// Generic options selector row that allows multiple selection. -open class GenericMultipleSelectorRow: Row, PresenterRowType, NoValueDisplayTextConformance, OptionsProviderRow - where Cell: BaseCell, Cell.Value == Set { - - public typealias PresentedController = MultipleSelectorViewController> - - /// Defines how the view controller will be presented, pushed, etc. - open var presentationMode: PresentationMode? - - /// Will be called before the presentation occurs. - open var onPresentCallback: ((FormViewController, PresentedController) -> Void)? - - /// Title to be displayed for the options - open var selectorTitle: String? - open var noValueDisplayText: String? - - /// Options from which the user will choose - open var optionsProvider: OptionsProvider? - - required public init(tag: String?) { - super.init(tag: tag) - displayValueFor = { (rowValue: Set?) in - return rowValue?.map({ String(describing: $0) }).sorted().joined(separator: ", ") - } - presentationMode = .show(controllerProvider: ControllerProvider.callback { - return MultipleSelectorViewController>() - }, onDismiss: { vc in - let _ = vc.navigationController?.popViewController(animated: true) - }) - } - - /** - Extends `didSelect` method - */ - open override func customDidSelect() { - super.customDidSelect() - guard let presentationMode = presentationMode, !isDisabled else { return } - if let controller = presentationMode.makeController() { - controller.row = self - controller.title = selectorTitle ?? controller.title - onPresentCallback?(cell.formViewController()!, controller) - presentationMode.present(controller, row: self, presentingController: self.cell.formViewController()!) - } else { - presentationMode.present(nil, row: self, presentingController: self.cell.formViewController()!) - } - } - - /** - Prepares the pushed row setting its title and completion callback. - */ - open override func prepare(for segue: UIStoryboardSegue) { - super.prepare(for: segue) - guard let rowVC = segue.destination as Any as? PresentedController else { return } - rowVC.title = selectorTitle ?? rowVC.title - rowVC.onDismissCallback = presentationMode?.onDismissCallback ?? rowVC.onDismissCallback - onPresentCallback?(cell.formViewController()!, rowVC) - rowVC.row = self - } -} diff --git a/Pods/Eureka/Source/Rows/Common/OptionsRow.swift b/Pods/Eureka/Source/Rows/Common/OptionsRow.swift deleted file mode 100644 index b57b04046..000000000 --- a/Pods/Eureka/Source/Rows/Common/OptionsRow.swift +++ /dev/null @@ -1,37 +0,0 @@ -// OptionsRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -open class OptionsRow : Row, NoValueDisplayTextConformance, OptionsProviderRow where Cell: BaseCell { - - open var optionsProvider: OptionsProvider? - - open var selectorTitle: String? - open var noValueDisplayText: String? - - required public init(tag: String?) { - super.init(tag: tag) - } -} diff --git a/Pods/Eureka/Source/Rows/Common/Protocols.swift b/Pods/Eureka/Source/Rows/Common/Protocols.swift deleted file mode 100644 index 143e6c8f6..000000000 --- a/Pods/Eureka/Source/Rows/Common/Protocols.swift +++ /dev/null @@ -1,35 +0,0 @@ -// Protocols.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -public protocol FormatterConformance: AnyObject { - var formatter: Formatter? { get set } - var useFormatterDuringInput: Bool { get set } - var useFormatterOnDidBeginEditing: Bool? { get set } -} - -public protocol NoValueDisplayTextConformance: AnyObject { - var noValueDisplayText: String? { get set } -} diff --git a/Pods/Eureka/Source/Rows/Common/SelectorRow.swift b/Pods/Eureka/Source/Rows/Common/SelectorRow.swift deleted file mode 100644 index 361e33357..000000000 --- a/Pods/Eureka/Source/Rows/Common/SelectorRow.swift +++ /dev/null @@ -1,87 +0,0 @@ -// SelectorRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -open class PushSelectorCell : Cell, CellType { - - required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func update() { - super.update() - accessoryType = .disclosureIndicator - editingAccessoryType = accessoryType - selectionStyle = row.isDisabled ? .none : .default - } -} - -/// Generic row type where a user must select a value among several options. -open class SelectorRow: OptionsRow, PresenterRowType where Cell: BaseCell { - - - /// Defines how the view controller will be presented, pushed, etc. - open var presentationMode: PresentationMode>>? - - /// Will be called before the presentation occurs. - open var onPresentCallback: ((FormViewController, SelectorViewController>) -> Void)? - - required public init(tag: String?) { - super.init(tag: tag) - } - - /** - Extends `didSelect` method - */ - open override func customDidSelect() { - super.customDidSelect() - guard let presentationMode = presentationMode, !isDisabled else { return } - if let controller = presentationMode.makeController() { - controller.row = self - controller.title = selectorTitle ?? controller.title - onPresentCallback?(cell.formViewController()!, controller) - presentationMode.present(controller, row: self, presentingController: self.cell.formViewController()!) - } else { - presentationMode.present(nil, row: self, presentingController: self.cell.formViewController()!) - } - } - - /** - Prepares the pushed row setting its title and completion callback. - */ - open override func prepare(for segue: UIStoryboardSegue) { - super.prepare(for: segue) - guard let rowVC = segue.destination as Any as? SelectorViewController> else { return } - rowVC.title = selectorTitle ?? rowVC.title - rowVC.onDismissCallback = presentationMode?.onDismissCallback ?? rowVC.onDismissCallback - onPresentCallback?(cell.formViewController()!, rowVC) - rowVC.row = self - } -} diff --git a/Pods/Eureka/Source/Rows/Controllers/MultipleSelectorViewController.swift b/Pods/Eureka/Source/Rows/Controllers/MultipleSelectorViewController.swift deleted file mode 100644 index c5cf99a22..000000000 --- a/Pods/Eureka/Source/Rows/Controllers/MultipleSelectorViewController.swift +++ /dev/null @@ -1,149 +0,0 @@ -// MultipleSelectorViewController.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -/// Selector Controller that enables multiple selection -open class _MultipleSelectorViewController : FormViewController, TypedRowControllerType where Row: BaseRow, Row.Cell.Value == OptionsRow.OptionsProviderType.Option, OptionsRow.OptionsProviderType.Option: Hashable { - - /// The row that pushed or presented this controller - public var row: RowOf>! - - public var selectableRowSetup: ((_ row: Row) -> Void)? - public var selectableRowCellSetup: ((_ cell: Row.Cell, _ row: Row) -> Void)? - public var selectableRowCellUpdate: ((_ cell: Row.Cell, _ row: Row) -> Void)? - - /// A closure to be called when the controller disappears. - public var onDismissCallback: ((UIViewController) -> Void)? - - /// A closure that should return key for particular row value. - /// This key is later used to break options by sections. - public var sectionKeyForValue: ((Row.Cell.Value) -> (String))? - - /// A closure that returns header title for a section for particular key. - /// By default returns the key itself. - public var sectionHeaderTitleForKey: ((String) -> String?)? = { $0 } - - /// A closure that returns footer title for a section for particular key. - public var sectionFooterTitleForKey: ((String) -> String?)? - - - public var optionsProviderRow: OptionsRow { - return row as! OptionsRow - } - - - override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { - super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - } - - convenience public init(_ callback: ((UIViewController) -> Void)?) { - self.init(nibName: nil, bundle: nil) - onDismissCallback = callback - } - - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func viewDidLoad() { - super.viewDidLoad() - setupForm() - } - - open func setupForm() { - optionsProviderRow.optionsProvider?.options(for: self) { [weak self] (options: [OptionsRow.OptionsProviderType.Option]?) in - guard let strongSelf = self, let options = options else { return } - strongSelf.optionsProviderRow.cachedOptionsData = options - strongSelf.setupForm(with: options) - } - } - - open func setupForm(with options: [OptionsRow.OptionsProviderType.Option]) { - if let optionsBySections = optionsBySections(with: options) { - for (sectionKey, options) in optionsBySections { - form +++ section(with: options, - header: sectionHeaderTitleForKey?(sectionKey), - footer: sectionFooterTitleForKey?(sectionKey)) - } - } else { - form +++ section(with: options, header: row.title, footer: nil) - } - } - - open func optionsBySections(with options: [OptionsRow.OptionsProviderType.Option]) -> [(String, [Row.Cell.Value])]? { - guard let sectionKeyForValue = sectionKeyForValue else { return nil } - - let sections = options.reduce([:]) { (reduced, option) -> [String: [Row.Cell.Value]] in - var reduced = reduced - let key = sectionKeyForValue(option) - var items = reduced[key] ?? [] - items.append(option) - reduced[key] = items - return reduced - } - - return sections.sorted(by: { (lhs, rhs) in lhs.0 < rhs.0 }) - } - - func section(with options: [OptionsRow.OptionsProviderType.Option], header: String?, footer: String?) -> SelectableSection { - let section = SelectableSection(header: header ?? "", footer: footer ?? "", selectionType: .multipleSelection) { section in - section.onSelectSelectableRow = { [weak self] _, selectableRow in - var newValue: Set = self?.row.value ?? [] - if let selectableValue = selectableRow.value { - newValue.insert(selectableValue) - } else { - newValue.remove(selectableRow.selectableValue!) - } - self?.row.value = newValue - } - } - for option in options { - section <<< Row.init { lrow in - lrow.title = String(describing: option) - lrow.selectableValue = option - lrow.value = self.row.value?.contains(option) ?? false ? option : nil - self.selectableRowSetup?(lrow) - }.cellSetup { [weak self] cell, row in - self?.selectableRowCellSetup?(cell, row) - }.cellUpdate { [weak self] cell, row in - self?.selectableRowCellUpdate?(cell, row) - } - } - return section - } -} - -open class MultipleSelectorViewController: _MultipleSelectorViewController, OptionsRow> where OptionsRow.OptionsProviderType.Option: Hashable{ - - override public init(nibName nibNameOrNil: String? = nil, bundle nibBundleOrNil: Bundle? = nil) { - super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - } - - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - -} diff --git a/Pods/Eureka/Source/Rows/Controllers/SelectorAlertController.swift b/Pods/Eureka/Source/Rows/Controllers/SelectorAlertController.swift deleted file mode 100644 index e5ee8302e..000000000 --- a/Pods/Eureka/Source/Rows/Controllers/SelectorAlertController.swift +++ /dev/null @@ -1,80 +0,0 @@ -// SelectorAlertController.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -/// Specific type, Responsible for the options passed to a selector alert view controller -public protocol AlertOptionsProviderRow: OptionsProviderRow { - - var cancelTitle: String? { get set } - -} - -/// Selector UIAlertController -open class SelectorAlertController: UIAlertController, TypedRowControllerType where AlertOptionsRow.OptionsProviderType.Option == AlertOptionsRow.Cell.Value, AlertOptionsRow: BaseRow { - - /// The row that pushed or presented this controller - public var row: RowOf! - - @available(*, deprecated, message: "Use AlertOptionsRow.cancelTitle instead.") - public var cancelTitle = NSLocalizedString("Cancel", comment: "") - - /// A closure to be called when the controller disappears. - public var onDismissCallback: ((UIViewController) -> Void)? - - /// Options provider to use to get available options. - /// If not set will use synchronous data provider built with `row.dataProvider.arrayData`. - // public var optionsProvider: OptionsProvider? - public var optionsProviderRow: AlertOptionsRow { - return row as Any as! AlertOptionsRow - } - - override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { - super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - convenience public init(_ callback: ((UIViewController) -> Void)?) { - self.init() - onDismissCallback = callback - } - - open override func viewDidLoad() { - super.viewDidLoad() - guard let options = optionsProviderRow.options else { return } - let cancelTitle = optionsProviderRow.cancelTitle ?? NSLocalizedString("Cancel", comment: "") - addAction(UIAlertAction(title: cancelTitle, style: .cancel, handler: nil)) - for option in options { - addAction(UIAlertAction(title: row.displayValueFor?(option), style: .default, handler: { [weak self] _ in - self?.row.value = option - self?.onDismissCallback?(self!) - })) - } - } - -} diff --git a/Pods/Eureka/Source/Rows/Controllers/SelectorViewController.swift b/Pods/Eureka/Source/Rows/Controllers/SelectorViewController.swift deleted file mode 100644 index 2e2a88ad4..000000000 --- a/Pods/Eureka/Source/Rows/Controllers/SelectorViewController.swift +++ /dev/null @@ -1,232 +0,0 @@ -// SelectorViewController.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -/** - * Responsible for the options passed to a selector view controller - */ - -public protocol OptionsProviderRow: TypedRowType { - associatedtype OptionsProviderType: OptionsProviderConformance - - var optionsProvider: OptionsProviderType? { get set } - - var cachedOptionsData: [OptionsProviderType.Option]? { get set } -} - -extension OptionsProviderRow where Self: BaseRow { - - public var options: [OptionsProviderType.Option]? { - set (newValue){ - let optProvider = OptionsProviderType.init(array: newValue) - optionsProvider = optProvider - } - get { - return self.cachedOptionsData ?? optionsProvider?.optionsArray - } - } - - public var cachedOptionsData: [OptionsProviderType.Option]? { - get { - return self._cachedOptionsData as? [OptionsProviderType.Option] - } - set { - self._cachedOptionsData = newValue - } - } -} - -public protocol OptionsProviderConformance: ExpressibleByArrayLiteral { - associatedtype Option: Equatable - - init(array: [Option]?) - func options(for selectorViewController: FormViewController, completion: @escaping ([Option]?) -> Void) - var optionsArray: [Option]? { get } - -} - -/// Provider of selectable options. -public enum OptionsProvider: OptionsProviderConformance { - - /// Synchronous provider that provides array of options it was initialized with - case array([T]?) - /// Provider that uses closure it was initialized with to provide options. Can be synchronous or asynchronous. - case lazy((FormViewController, @escaping ([T]?) -> Void) -> Void) - - public init(array: [T]?) { - self = .array(array) - } - - public init(arrayLiteral elements: T...) { - self = .array(elements) - } - - public func options(for selectorViewController: FormViewController, completion: @escaping ([T]?) -> Void) { - switch self { - case let .array(array): - completion(array) - case let .lazy(fetch): - fetch(selectorViewController, completion) - } - } - - public var optionsArray: [T]?{ - switch self { - case let .array(arrayData): - return arrayData - default: - return nil - } - } -} - -open class _SelectorViewController: FormViewController, TypedRowControllerType where Row: BaseRow, Row.Cell.Value == OptionsRow.OptionsProviderType.Option { - - /// The row that pushed or presented this controller - public var row: RowOf! - public var enableDeselection = true - public var dismissOnSelection = true - public var dismissOnChange = true - - public var selectableRowSetup: ((_ row: Row) -> Void)? - public var selectableRowCellUpdate: ((_ cell: Row.Cell, _ row: Row) -> Void)? - public var selectableRowCellSetup: ((_ cell: Row.Cell, _ row: Row) -> Void)? - - /// A closure to be called when the controller disappears. - public var onDismissCallback: ((UIViewController) -> Void)? - - /// A closure that should return key for particular row value. - /// This key is later used to break options by sections. - public var sectionKeyForValue: ((Row.Cell.Value) -> (String))? - - /// A closure that returns header title for a section for particular key. - /// By default returns the key itself. - public var sectionHeaderTitleForKey: ((String) -> String?)? = { $0 } - - /// A closure that returns footer title for a section for particular key. - public var sectionFooterTitleForKey: ((String) -> String?)? - - public var optionsProviderRow: OptionsRow { - return row as! OptionsRow - } - - override public init(style: UITableView.Style) { - super.init(style: style) - } - - override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { - super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - } - - convenience public init(_ callback: ((UIViewController) -> Void)?) { - self.init(nibName: nil, bundle: nil) - onDismissCallback = callback - } - - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func viewDidLoad() { - super.viewDidLoad() - setupForm() - } - - open func setupForm() { - let optProvider = optionsProviderRow.optionsProvider - optProvider?.options(for: self) { [weak self] (options: [Row.Cell.Value]?) in - guard let strongSelf = self, let options = options else { return } - strongSelf.optionsProviderRow.cachedOptionsData = options - strongSelf.setupForm(with: options) - } - } - - open func setupForm(with options: [Row.Cell.Value]) { - if let optionsBySections = optionsBySections(with: options) { - for (sectionKey, options) in optionsBySections { - form +++ section(with: options, - header: sectionHeaderTitleForKey?(sectionKey), - footer: sectionFooterTitleForKey?(sectionKey)) - } - } else { - form +++ section(with: options, header: nil, footer: nil) - } - } - - func optionsBySections(with options: [Row.Cell.Value]) -> [(String, [Row.Cell.Value])]? { - guard let sectionKeyForValue = sectionKeyForValue else { return nil } - - let sections = options.reduce([:]) { (reduced, option) -> [String: [Row.Cell.Value]] in - var reduced = reduced - let key = sectionKeyForValue(option) - reduced[key] = (reduced[key] ?? []) + [option] - return reduced - } - - return sections.sorted(by: { (lhs, rhs) in lhs.0 < rhs.0 }) - } - - func section(with options: [Row.Cell.Value], header: String?, footer: String?) -> SelectableSection { - let section = SelectableSection(header: header, footer: footer, selectionType: .singleSelection(enableDeselection: enableDeselection)) { section in - section.onSelectSelectableRow = { [weak self] _, row in - let changed = self?.row.value != row.value - self?.row.value = row.value - - if let form = row.section?.form { - for section in form where section !== row.section && section is SelectableSection { - let section = section as Any as! SelectableSection - if let selectedRow = section.selectedRow(), selectedRow !== row { - selectedRow.value = nil - selectedRow.updateCell() - } - } - } - - if self?.dismissOnSelection == true || (changed && self?.dismissOnChange == true) { - self?.onDismissCallback?(self!) - } - } - } - for option in options { - section <<< Row.init(String(describing: option)) { lrow in - lrow.title = self.row.displayValueFor?(option) - lrow.selectableValue = option - lrow.value = self.row.value == option ? option : nil - self.selectableRowSetup?(lrow) - }.cellSetup { [weak self] cell, row in - self?.selectableRowCellSetup?(cell, row) - }.cellUpdate { [weak self] cell, row in - self?.selectableRowCellUpdate?(cell, row) - } - } - return section - } - -} - -/// Selector Controller (used to select one option among a list) -open class SelectorViewController: _SelectorViewController, OptionsRow> { -} diff --git a/Pods/Eureka/Source/Rows/DateInlineRow.swift b/Pods/Eureka/Source/Rows/DateInlineRow.swift deleted file mode 100644 index f799cee02..000000000 --- a/Pods/Eureka/Source/Rows/DateInlineRow.swift +++ /dev/null @@ -1,214 +0,0 @@ -// DateInliuneRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -extension DatePickerRowProtocol { - - func configureInlineRow(_ inlineRow: DatePickerRowProtocol) { - inlineRow.minimumDate = minimumDate - inlineRow.maximumDate = maximumDate - inlineRow.minuteInterval = minuteInterval - } - - func configurePickerStyle(_ cell: DatePickerCell, _ mode: UIDatePicker.Mode = .dateAndTime) { - cell.datePicker.datePickerMode = mode - // For Xcode 11.4 and above - #if swift(>=5.2) - if #available(iOS 14.0, *) { - #if swift(>=5.3) && !(os(OSX) || (os(iOS) && targetEnvironment(macCatalyst))) - cell.datePicker.preferredDatePickerStyle = .inline - #else - cell.datePicker.preferredDatePickerStyle = .wheels - #endif - } - else if #available(iOS 13.4, *) { - cell.datePicker.preferredDatePickerStyle = .wheels - } - #endif - } - -} - -open class _DateInlineRow: _DateInlineFieldRow { - - public typealias InlineRow = DatePickerRow - - public required init(tag: String?) { - super.init(tag: tag) - dateFormatter?.timeStyle = .none - dateFormatter?.dateStyle = .medium - } - - open func setupInlineRow(_ inlineRow: DatePickerRow) { - configureInlineRow(inlineRow) - configurePickerStyle(inlineRow.cell, .date) - } -} - -open class _TimeInlineRow: _DateInlineFieldRow { - - public typealias InlineRow = TimePickerRow - - public required init(tag: String?) { - super.init(tag: tag) - dateFormatter?.timeStyle = .short - dateFormatter?.dateStyle = .none - } - - open func setupInlineRow(_ inlineRow: TimePickerRow) { - configureInlineRow(inlineRow) - configurePickerStyle(inlineRow.cell, .time) - } -} - -open class _DateTimeInlineRow: _DateInlineFieldRow { - - public typealias InlineRow = DateTimePickerRow - - public required init(tag: String?) { - super.init(tag: tag) - dateFormatter?.timeStyle = .short - dateFormatter?.dateStyle = .short - } - - open func setupInlineRow(_ inlineRow: DateTimePickerRow) { - configureInlineRow(inlineRow) - configurePickerStyle(inlineRow.cell) - } -} - -open class _CountDownInlineRow: _DateInlineFieldRow { - - public typealias InlineRow = CountDownPickerRow - - public required init(tag: String?) { - super.init(tag: tag) - displayValueFor = { - guard let date = $0 else { - return nil - } - - let dateComponents = Calendar.current.dateComponents([.hour, .minute], from: date) - return DateComponentsFormatter.localizedString(from: dateComponents, unitsStyle: .full)?.replacingOccurrences(of: ",", with: "") - } - } - - public func setupInlineRow(_ inlineRow: CountDownPickerRow) { - configureInlineRow(inlineRow) - } -} - -/// A row with an Date as value where the user can select a date from an inline picker view. -public final class DateInlineRow_: _DateInlineRow, RowType, InlineRowType { - required public init(tag: String?) { - super.init(tag: tag) - onExpandInlineRow { cell, row, _ in - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - } - - public override func customDidSelect() { - super.customDidSelect() - if !isDisabled { - toggleInlineRow() - } - } -} - -public typealias DateInlineRow = DateInlineRow_ - -/// A row with an Date as value where the user can select date and time from an inline picker view. -public final class DateTimeInlineRow_: _DateTimeInlineRow, RowType, InlineRowType { - required public init(tag: String?) { - super.init(tag: tag) - onExpandInlineRow { cell, row, _ in - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - } - - public override func customDidSelect() { - super.customDidSelect() - if !isDisabled { - toggleInlineRow() - } - } -} - -public typealias DateTimeInlineRow = DateTimeInlineRow_ - -/// A row with an Date as value where the user can select a time from an inline picker view. -public final class TimeInlineRow_: _TimeInlineRow, RowType, InlineRowType { - required public init(tag: String?) { - super.init(tag: tag) - onExpandInlineRow { cell, row, _ in - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - } - - public override func customDidSelect() { - super.customDidSelect() - if !isDisabled { - toggleInlineRow() - } - } -} - -public typealias TimeInlineRow = TimeInlineRow_ - -///// A row with an Date as value where the user can select hour and minute as a countdown timer in an inline picker view. -public final class CountDownInlineRow_: _CountDownInlineRow, RowType, InlineRowType { - required public init(tag: String?) { - super.init(tag: tag) - onExpandInlineRow { cell, row, _ in - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - } - - public override func customDidSelect() { - super.customDidSelect() - if !isDisabled { - toggleInlineRow() - } - } -} - -public typealias CountDownInlineRow = CountDownInlineRow_ diff --git a/Pods/Eureka/Source/Rows/DatePickerRow.swift b/Pods/Eureka/Source/Rows/DatePickerRow.swift deleted file mode 100644 index 8b3f58579..000000000 --- a/Pods/Eureka/Source/Rows/DatePickerRow.swift +++ /dev/null @@ -1,156 +0,0 @@ -// DateRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -open class DatePickerCell: Cell, CellType { - - @IBOutlet weak public var datePicker: UIDatePicker! - - public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - let datePicker = UIDatePicker() - self.datePicker = datePicker - self.datePicker.translatesAutoresizingMaskIntoConstraints = false - - super.init(style: style, reuseIdentifier: reuseIdentifier) - - self.contentView.addSubview(self.datePicker) - self.contentView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[picker]-0-|", options: [], metrics: nil, views: ["picker": self.datePicker!])) - self.contentView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[picker]-0-|", options: [], metrics: nil, views: ["picker": self.datePicker!])) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func setup() { - super.setup() - selectionStyle = .none - accessoryType = .none - editingAccessoryType = .none - height = { UITableView.automaticDimension } - datePicker.datePickerMode = datePickerMode() - datePicker.addTarget(self, action: #selector(DatePickerCell.datePickerValueChanged(_:)), for: .valueChanged) - - if datePicker.datePickerMode != .countDownTimer { - #if swift(>=5.2) - if #available(iOS 14.0, *) { - #if swift(>=5.3) && !(os(OSX) || (os(iOS) && targetEnvironment(macCatalyst))) - datePicker.preferredDatePickerStyle = .inline - #else - datePicker.preferredDatePickerStyle = .wheels - #endif - } else if #available(iOS 13.4, *) { - datePicker.preferredDatePickerStyle = .wheels - } - #endif - } - } - - deinit { - datePicker?.removeTarget(self, action: nil, for: .allEvents) - } - - open override func update() { - super.update() - selectionStyle = row.isDisabled ? .none : .default - datePicker.isUserInteractionEnabled = !row.isDisabled - detailTextLabel?.text = nil - textLabel?.text = nil - datePicker.setDate(row.value ?? Date(), animated: row is CountDownPickerRow) - datePicker.minimumDate = (row as? DatePickerRowProtocol)?.minimumDate - datePicker.maximumDate = (row as? DatePickerRowProtocol)?.maximumDate - if let minuteIntervalValue = (row as? DatePickerRowProtocol)?.minuteInterval { - datePicker.minuteInterval = minuteIntervalValue - } - } - - @objc(pickerDateChanged:) func datePickerValueChanged(_ sender: UIDatePicker) { - row?.value = sender.date - - // workaround for UIDatePicker bug when it doesn't trigger "value changed" event after trying to pick 00:00 value - // for details see this comment: https://stackoverflow.com/questions/20181980/uidatepicker-bug-uicontroleventvaluechanged-after-hitting-minimum-internal#comment56681891_20204225 - if sender.datePickerMode == .countDownTimer && sender.countDownDuration == TimeInterval(sender.minuteInterval * 60) { - datePicker.countDownDuration = sender.countDownDuration - } - - } - - private func datePickerMode() -> UIDatePicker.Mode { - switch row { - case is DatePickerRow: - return .date - case is TimePickerRow: - return .time - case is DateTimePickerRow: - return .dateAndTime - case is CountDownPickerRow: - return .countDownTimer - default: - return .date - } - } - -} - -open class _DatePickerRow: Row, DatePickerRowProtocol { - - open var minimumDate: Date? - open var maximumDate: Date? - open var minuteInterval: Int? - - required public init(tag: String?) { - super.init(tag: tag) - displayValueFor = nil - } -} - -/// A row with an Date as value where the user can select a date directly. -public final class DatePickerRow: _DatePickerRow, RowType { - public required init(tag: String?) { - super.init(tag: tag) - } -} - -/// A row with an Date as value where the user can select a time directly. -public final class TimePickerRow: _DatePickerRow, RowType { - public required init(tag: String?) { - super.init(tag: tag) - } -} - -/// A row with an Date as value where the user can select date and time directly. -public final class DateTimePickerRow: _DatePickerRow, RowType { - public required init(tag: String?) { - super.init(tag: tag) - } -} - -/// A row with an Date as value where the user can select hour and minute as a countdown timer. -public final class CountDownPickerRow: _DatePickerRow, RowType { - public required init(tag: String?) { - super.init(tag: tag) - } -} diff --git a/Pods/Eureka/Source/Rows/DateRow.swift b/Pods/Eureka/Source/Rows/DateRow.swift deleted file mode 100644 index efb9e6b1b..000000000 --- a/Pods/Eureka/Source/Rows/DateRow.swift +++ /dev/null @@ -1,99 +0,0 @@ -// DateRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -import Foundation - -open class _DateRow: _DateFieldRow { - required public init(tag: String?) { - super.init(tag: tag) - dateFormatter = DateFormatter() - dateFormatter?.timeStyle = .none - dateFormatter?.dateStyle = .medium - dateFormatter?.locale = Locale.current - } -} - -open class _TimeRow: _DateFieldRow { - required public init(tag: String?) { - super.init(tag: tag) - dateFormatter = DateFormatter() - dateFormatter?.timeStyle = .short - dateFormatter?.dateStyle = .none - dateFormatter?.locale = Locale.current - } -} - -open class _DateTimeRow: _DateFieldRow { - required public init(tag: String?) { - super.init(tag: tag) - dateFormatter = DateFormatter() - dateFormatter?.timeStyle = .short - dateFormatter?.dateStyle = .short - dateFormatter?.locale = Locale.current - } -} - -open class _CountDownRow: _DateFieldRow { - required public init(tag: String?) { - super.init(tag: tag) - displayValueFor = { [unowned self] value in - guard let val = value else { - return nil - } - if let formatter = self.dateFormatter { - return formatter.string(from: val) - } - - let dateComponents = Calendar.current.dateComponents([.hour, .minute], from: val) - return DateComponentsFormatter.localizedString(from: dateComponents, unitsStyle: .full)?.replacingOccurrences(of: ",", with: "") - } - } -} - -/// A row with an Date as value where the user can select a date from a picker view. -public final class DateRow: _DateRow, RowType { - required public init(tag: String?) { - super.init(tag: tag) - } -} - -/// A row with an Date as value where the user can select a time from a picker view. -public final class TimeRow: _TimeRow, RowType { - required public init(tag: String?) { - super.init(tag: tag) - } -} - -/// A row with an Date as value where the user can select date and time from a picker view. -public final class DateTimeRow: _DateTimeRow, RowType { - required public init(tag: String?) { - super.init(tag: tag) - } -} - -/// A row with an Date as value where the user can select hour and minute as a countdown timer in a picker view. -public final class CountDownRow: _CountDownRow, RowType { - required public init(tag: String?) { - super.init(tag: tag) - } -} diff --git a/Pods/Eureka/Source/Rows/DoublePickerInputRow.swift b/Pods/Eureka/Source/Rows/DoublePickerInputRow.swift deleted file mode 100644 index 3a519d8aa..000000000 --- a/Pods/Eureka/Source/Rows/DoublePickerInputRow.swift +++ /dev/null @@ -1,112 +0,0 @@ -// -// DoublePickerInputRow.swift -// Eureka -// -// Created by Mathias Claassen on 5/10/18. -// Copyright © 2018 Xmartlabs. All rights reserved. -// - -import Foundation -import UIKit - -open class DoublePickerInputCell : _PickerInputCell> where A: Equatable, B: Equatable { - - private var pickerRow: _DoublePickerInputRow! { return row as? _DoublePickerInputRow } - - public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func update() { - super.update() - if let selectedValue = pickerRow.value, let indexA = pickerRow.firstOptions().firstIndex(of: selectedValue.a), - let indexB = pickerRow.secondOptions(selectedValue.a).firstIndex(of: selectedValue.b) { - picker.selectRow(indexA, inComponent: 0, animated: true) - picker.selectRow(indexB, inComponent: 1, animated: true) - } - } - - open override func numberOfComponents(in pickerView: UIPickerView) -> Int { - return 2 - } - - open override func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { - return component == 0 ? pickerRow.firstOptions().count : pickerRow.secondOptions(pickerRow.selectedFirst()).count - } - - open override func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { - if component == 0 { - return pickerRow.displayValueForFirstRow(pickerRow.firstOptions()[row]) - } else { - return pickerRow.displayValueForSecondRow(pickerRow.secondOptions(pickerRow.selectedFirst())[row]) - } - } - - open override func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { - if component == 0 { - let a = pickerRow.firstOptions()[row] - if let value = pickerRow.value { - guard value.a != a else { - return - } - if pickerRow.secondOptions(a).contains(value.b) { - pickerRow.value = Tuple(a: a, b: value.b) - pickerView.reloadComponent(1) - update() - return - } else { - pickerRow.value = Tuple(a: a, b: pickerRow.secondOptions(a)[0]) - } - } else { - pickerRow.value = Tuple(a: a, b: pickerRow.secondOptions(a)[0]) - } - pickerView.reloadComponent(1) - pickerView.selectRow(0, inComponent: 1, animated: true) - } else { - let a = pickerRow.selectedFirst() - pickerRow.value = Tuple(a: a, b: pickerRow.secondOptions(a)[row]) - } - update() - } -} - -open class _DoublePickerInputRow : Row>, NoValueDisplayTextConformance { - - open var noValueDisplayText: String? = nil - /// Options for first component. Will be called often so should be O(1) - public var firstOptions: (() -> [A]) = {[]} - /// Options for second component given the selected value from the first component. Will be called often so should be O(1) - public var secondOptions: ((A) -> [B]) = {_ in []} - - /// Modify the displayed values for the first picker row. - public var displayValueForFirstRow: ((A) -> (String)) = { a in return String(describing: a) } - /// Modify the displayed values for the second picker row. - public var displayValueForSecondRow: ((B) -> (String)) = { b in return String(describing: b) } - - required public init(tag: String?) { - super.init(tag: tag) - } - - func selectedFirst() -> A { - return value?.a ?? firstOptions()[0] - } - -} - -/// A generic row where the user can pick an option from a picker view displayed in the keyboard area -public final class DoublePickerInputRow: _DoublePickerInputRow, RowType where A: Equatable, B: Equatable { - - required public init(tag: String?) { - super.init(tag: tag) - self.displayValueFor = { [weak self] tuple in - guard let tuple = tuple else { - return self?.noValueDisplayText - } - return String(describing: tuple.a) + ", " + String(describing: tuple.b) - } - } -} diff --git a/Pods/Eureka/Source/Rows/DoublePickerRow.swift b/Pods/Eureka/Source/Rows/DoublePickerRow.swift deleted file mode 100644 index b5e2cbd83..000000000 --- a/Pods/Eureka/Source/Rows/DoublePickerRow.swift +++ /dev/null @@ -1,129 +0,0 @@ -// -// MultiplePickerRow.swift -// Eureka -// -// Created by Mathias Claassen on 5/8/18. -// Copyright © 2018 Xmartlabs. All rights reserved. -// - -import Foundation -import UIKit - -public struct Tuple { - - public let a: A - public let b: B - - public init(a: A, b: B) { - self.a = a - self.b = b - } - -} - -extension Tuple: Equatable {} - -public func == (lhs: Tuple, rhs: Tuple) -> Bool { - return lhs.a == rhs.a && lhs.b == rhs.b -} - -// MARK: MultiplePickerCell - -open class DoublePickerCell : _PickerCell> where A: Equatable, B: Equatable { - - private var pickerRow: _DoublePickerRow? { return row as? _DoublePickerRow } - - public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func update() { - super.update() - if let selectedValue = pickerRow?.value, let indexA = pickerRow?.firstOptions().firstIndex(of: selectedValue.a), - let indexB = pickerRow?.secondOptions(selectedValue.a).firstIndex(of: selectedValue.b) { - picker.selectRow(indexA, inComponent: 0, animated: true) - picker.selectRow(indexB, inComponent: 1, animated: true) - } - } - - open override func numberOfComponents(in pickerView: UIPickerView) -> Int { - return 2 - } - - open override func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { - guard let pickerRow = pickerRow else { return 0 } - return component == 0 ? pickerRow.firstOptions().count : pickerRow.secondOptions(pickerRow.selectedFirst()).count - } - - open override func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { - guard let pickerRow = pickerRow else { return "" } - if component == 0 { - return pickerRow.displayValueForFirstRow(pickerRow.firstOptions()[row]) - } else { - return pickerRow.displayValueForSecondRow(pickerRow.secondOptions(pickerRow.selectedFirst())[row]) - } - } - - open override func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { - guard let pickerRow = pickerRow else { return } - if component == 0 { - let a = pickerRow.firstOptions()[row] - if let value = pickerRow.value { - guard value.a != a else { - return - } - if pickerRow.secondOptions(a).contains(value.b) { - pickerRow.value = Tuple(a: a, b: value.b) - pickerView.reloadComponent(1) - return - } else { - pickerRow.value = Tuple(a: a, b: pickerRow.secondOptions(a)[0]) - } - } else { - pickerRow.value = Tuple(a: a, b: pickerRow.secondOptions(a)[0]) - } - pickerView.reloadComponent(1) - pickerView.selectRow(0, inComponent: 1, animated: true) - } else { - let a = pickerRow.selectedFirst() - pickerRow.value = Tuple(a: a, b: pickerRow.secondOptions(a)[row]) - } - } - -} - -// MARK: PickerRow -open class _DoublePickerRow : Row> where A: Equatable, B: Equatable { - - /// Options for first component. Will be called often so should be O(1) - public var firstOptions: (() -> [A]) = {[]} - /// Options for second component given the selected value from the first component. Will be called often so should be O(1) - public var secondOptions: ((A) -> [B]) = {_ in []} - - /// Modify the displayed values for the first picker row. - public var displayValueForFirstRow: ((A) -> (String)) = { a in return String(describing: a) } - /// Modify the displayed values for the second picker row. - public var displayValueForSecondRow: ((B) -> (String)) = { b in return String(describing: b) } - - required public init(tag: String?) { - super.init(tag: tag) - } - - func selectedFirst() -> A { - return value?.a ?? firstOptions()[0] - } - -} - -/// A generic row where the user can pick an option from a picker view -public final class DoublePickerRow: _DoublePickerRow, RowType where A: Equatable, B: Equatable { - - required public init(tag: String?) { - super.init(tag: tag) - } - -} diff --git a/Pods/Eureka/Source/Rows/FieldsRow.swift b/Pods/Eureka/Source/Rows/FieldsRow.swift deleted file mode 100644 index 171b34759..000000000 --- a/Pods/Eureka/Source/Rows/FieldsRow.swift +++ /dev/null @@ -1,399 +0,0 @@ -// FieldsRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -open class TextCell: _FieldCell, CellType { - - required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func setup() { - super.setup() - textField.autocorrectionType = .default - textField.autocapitalizationType = .sentences - textField.keyboardType = .default - } -} - -open class IntCell: _FieldCell, CellType { - - required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func setup() { - super.setup() - textField.autocorrectionType = .default - textField.autocapitalizationType = .none - textField.keyboardType = .numberPad - } -} - -open class PhoneCell: _FieldCell, CellType { - - required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func setup() { - super.setup() - textField.keyboardType = .phonePad - if #available(iOS 10,*) { - textField.textContentType = .telephoneNumber - } - } -} - -open class NameCell: _FieldCell, CellType { - - required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func setup() { - super.setup() - textField.autocorrectionType = .no - textField.autocapitalizationType = .words - textField.keyboardType = .asciiCapable - if #available(iOS 10,*) { - textField.textContentType = .name - } - } -} - -open class EmailCell: _FieldCell, CellType { - - required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func setup() { - super.setup() - textField.autocorrectionType = .no - textField.autocapitalizationType = .none - textField.keyboardType = .emailAddress - if #available(iOS 10,*) { - textField.textContentType = .emailAddress - } - } -} - -open class PasswordCell: _FieldCell, CellType { - - required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func setup() { - super.setup() - textField.autocorrectionType = .no - textField.autocapitalizationType = .none - textField.keyboardType = .asciiCapable - textField.isSecureTextEntry = true - if let textLabel = textLabel { - textField.setContentHuggingPriority(textLabel.contentHuggingPriority(for: .horizontal) - 1, for: .horizontal) - } - if #available(iOS 11,*) { - textField.textContentType = .password - } - } -} - -open class DecimalCell: _FieldCell, CellType { - - required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func setup() { - super.setup() - textField.autocorrectionType = .no - textField.keyboardType = .decimalPad - } -} - -open class URLCell: _FieldCell, CellType { - - required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func setup() { - super.setup() - textField.autocorrectionType = .no - textField.autocapitalizationType = .none - textField.keyboardType = .URL - if #available(iOS 10,*) { - textField.textContentType = .URL - } - } -} - -open class TwitterCell: _FieldCell, CellType { - - required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func setup() { - super.setup() - textField.autocorrectionType = .no - textField.autocapitalizationType = .none - textField.keyboardType = .twitter - } -} - -open class AccountCell: _FieldCell, CellType { - - required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func setup() { - super.setup() - textField.autocorrectionType = .no - textField.autocapitalizationType = .none - textField.keyboardType = .asciiCapable - if #available(iOS 11,*) { - textField.textContentType = .username - } - } -} - -open class ZipCodeCell: _FieldCell, CellType { - - required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func update() { - super.update() - textField.autocorrectionType = .no - textField.autocapitalizationType = .allCharacters - textField.keyboardType = .numbersAndPunctuation - if #available(iOS 10,*) { - textField.textContentType = .postalCode - } - } -} - -open class _TextRow: FieldRow { - public required init(tag: String?) { - super.init(tag: tag) - } -} - -open class _IntRow: FieldRow { - public required init(tag: String?) { - super.init(tag: tag) - let numberFormatter = NumberFormatter() - numberFormatter.locale = Locale.current - numberFormatter.numberStyle = .decimal - numberFormatter.minimumFractionDigits = 0 - formatter = numberFormatter - } -} - -open class _PhoneRow: FieldRow { - public required init(tag: String?) { - super.init(tag: tag) - } -} - -open class _NameRow: FieldRow { - public required init(tag: String?) { - super.init(tag: tag) - } -} - -open class _EmailRow: FieldRow { - public required init(tag: String?) { - super.init(tag: tag) - } -} - -open class _PasswordRow: FieldRow { - public required init(tag: String?) { - super.init(tag: tag) - } -} - -open class _DecimalRow: FieldRow { - public required init(tag: String?) { - super.init(tag: tag) - let numberFormatter = NumberFormatter() - numberFormatter.locale = Locale.current - numberFormatter.numberStyle = .decimal - numberFormatter.minimumFractionDigits = 2 - formatter = numberFormatter - } -} - -open class _URLRow: FieldRow { - public required init(tag: String?) { - super.init(tag: tag) - } -} - -open class _TwitterRow: FieldRow { - public required init(tag: String?) { - super.init(tag: tag) - } -} - -open class _AccountRow: FieldRow { - public required init(tag: String?) { - super.init(tag: tag) - } -} - -open class _ZipCodeRow: FieldRow { - public required init(tag: String?) { - super.init(tag: tag) - } -} - -/// A String valued row where the user can enter arbitrary text. -public final class TextRow: _TextRow, RowType { - required public init(tag: String?) { - super.init(tag: tag) - } -} - -/// A String valued row where the user can enter names. Biggest difference to TextRow is that it autocapitalization is set to Words. -public final class NameRow: _NameRow, RowType { - required public init(tag: String?) { - super.init(tag: tag) - } -} - -/// A String valued row where the user can enter secure text. -public final class PasswordRow: _PasswordRow, RowType { - required public init(tag: String?) { - super.init(tag: tag) - } -} - -/// A String valued row where the user can enter an email address. -public final class EmailRow: _EmailRow, RowType { - required public init(tag: String?) { - super.init(tag: tag) - } -} - -/// A String valued row where the user can enter a twitter username. -public final class TwitterRow: _TwitterRow, RowType { - required public init(tag: String?) { - super.init(tag: tag) - } -} - -/// A String valued row where the user can enter a simple account username. -public final class AccountRow: _AccountRow, RowType { - required public init(tag: String?) { - super.init(tag: tag) - } -} - -/// A String valued row where the user can enter a zip code. -public final class ZipCodeRow: _ZipCodeRow, RowType { - required public init(tag: String?) { - super.init(tag: tag) - } -} - -/// A row where the user can enter an integer number. -public final class IntRow: _IntRow, RowType { - required public init(tag: String?) { - super.init(tag: tag) - } -} - -/// A row where the user can enter a decimal number. -public final class DecimalRow: _DecimalRow, RowType { - required public init(tag: String?) { - super.init(tag: tag) - } -} - -/// A row where the user can enter an URL. The value of this row will be a URL. -public final class URLRow: _URLRow, RowType { - required public init(tag: String?) { - super.init(tag: tag) - } -} - -/// A String valued row where the user can enter a phone number. -public final class PhoneRow: _PhoneRow, RowType { - required public init(tag: String?) { - super.init(tag: tag) - } -} diff --git a/Pods/Eureka/Source/Rows/LabelRow.swift b/Pods/Eureka/Source/Rows/LabelRow.swift deleted file mode 100644 index 45b95cc26..000000000 --- a/Pods/Eureka/Source/Rows/LabelRow.swift +++ /dev/null @@ -1,61 +0,0 @@ -// LabelRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -// MARK: LabelCell - -open class LabelCellOf: Cell, CellType { - - required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func setup() { - super.setup() - selectionStyle = .none - } -} - -public typealias LabelCell = LabelCellOf - -// MARK: LabelRow - -open class _LabelRow: Row { - required public init(tag: String?) { - super.init(tag: tag) - } -} - -/// Simple row that can show title and value but is not editable by user. -public final class LabelRow: _LabelRow, RowType { - required public init(tag: String?) { - super.init(tag: tag) - } -} diff --git a/Pods/Eureka/Source/Rows/MultipleSelectorRow.swift b/Pods/Eureka/Source/Rows/MultipleSelectorRow.swift deleted file mode 100644 index 466a285f2..000000000 --- a/Pods/Eureka/Source/Rows/MultipleSelectorRow.swift +++ /dev/null @@ -1,38 +0,0 @@ -// MultipleSelectorRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -open class _MultipleSelectorRow: GenericMultipleSelectorRow where Cell: BaseCell, Cell.Value == Set { - public required init(tag: String?) { - super.init(tag: tag) - } -} - -/// A selector row where the user can pick several options from a pushed view controller -public final class MultipleSelectorRow : _MultipleSelectorRow>>, RowType { - public required init(tag: String?) { - super.init(tag: tag) - } -} diff --git a/Pods/Eureka/Source/Rows/PickerInlineRow.swift b/Pods/Eureka/Source/Rows/PickerInlineRow.swift deleted file mode 100644 index 818c47950..000000000 --- a/Pods/Eureka/Source/Rows/PickerInlineRow.swift +++ /dev/null @@ -1,202 +0,0 @@ -// PickerInlineRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -open class PickerInlineCell : Cell, CellType { - - required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func setup() { - super.setup() - accessoryType = .none - editingAccessoryType = .none - } - - open override func update() { - super.update() - selectionStyle = row.isDisabled ? .none : .default - } - - open override func didSelect() { - super.didSelect() - row.deselect() - } -} - -// MARK: PickerInlineRow - -open class _PickerInlineRow : Row>, NoValueDisplayTextConformance where T: Equatable { - - public typealias InlineRow = PickerRow - open var options = [T]() - open var noValueDisplayText: String? - - required public init(tag: String?) { - super.init(tag: tag) - } -} - -/// A generic inline row where the user can pick an option from a picker view which shows and hides itself automatically -public final class PickerInlineRow : _PickerInlineRow, RowType, InlineRowType where T: Equatable { - - required public init(tag: String?) { - super.init(tag: tag) - onExpandInlineRow { cell, row, _ in - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - } - - public override func customDidSelect() { - super.customDidSelect() - if !isDisabled { - toggleInlineRow() - } - } - - public func setupInlineRow(_ inlineRow: InlineRow) { - inlineRow.options = self.options - inlineRow.displayValueFor = self.displayValueFor - inlineRow.cell.height = { UITableView.automaticDimension } - } -} - -open class _DoublePickerInlineRow : Row>>, NoValueDisplayTextConformance where A: Equatable, B: Equatable { - - public typealias InlineRow = DoublePickerRow - - /// Options for first component. Will be called often so should be O(1) - public var firstOptions: (() -> [A]) = {[]} - - /// Options for second component given the selected value from the first component. Will be called often so should be O(1) - public var secondOptions: ((A) -> [B]) = { _ in [] } - - public var noValueDisplayText: String? - - required public init(tag: String?) { - super.init(tag: tag) - self.displayValueFor = { [weak self] tuple in - if let tuple = tuple { - return String(describing: tuple.a) + ", " + String(describing: tuple.b) - } - return self?.noValueDisplayText - } - } -} - -/// A generic inline row where the user can pick an option from a picker view which shows and hides itself automatically -public final class DoublePickerInlineRow : _DoublePickerInlineRow, RowType, InlineRowType where A: Equatable, B: Equatable { - - required public init(tag: String?) { - super.init(tag: tag) - onExpandInlineRow { cell, row, _ in - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - } - - public override func customDidSelect() { - super.customDidSelect() - if !isDisabled { - toggleInlineRow() - } - } - - public func setupInlineRow(_ inlineRow: InlineRow) { - inlineRow.firstOptions = firstOptions - inlineRow.secondOptions = secondOptions - inlineRow.displayValueFor = self.displayValueFor - inlineRow.cell.height = { UITableView.automaticDimension } - } -} - -open class _TriplePickerInlineRow : Row>>, NoValueDisplayTextConformance - where A: Equatable, B: Equatable, C: Equatable { - - public typealias InlineRow = TriplePickerRow - - /// Options for first component. Will be called often so should be O(1) - public var firstOptions: (() -> [A]) = {[]} - /// Options for second component given the selected value from the first component. Will be called often so should be O(1) - public var secondOptions: ((A) -> [B]) = { _ in [] } - /// Options for third component given the selected value from the first and second components. Will be called often so should be O(1) - public var thirdOptions: ((A, B) -> [C]) = {_, _ in []} - - open var noValueDisplayText: String? - - required public init(tag: String?) { - super.init(tag: tag) - self.displayValueFor = { [weak self] tuple in - if let tuple = tuple { - return String(describing: tuple.a) + ", " + String(describing: tuple.b) + ", " + String(describing: tuple.c) - } - return self?.noValueDisplayText - } - } -} - -/// A generic inline row where the user can pick an option from a picker view which shows and hides itself automatically -public final class TriplePickerInlineRow : _TriplePickerInlineRow, RowType, InlineRowType - where A: Equatable, B: Equatable, C: Equatable { - - required public init(tag: String?) { - super.init(tag: tag) - onExpandInlineRow { cell, row, _ in - let color = cell.detailTextLabel?.textColor - row.onCollapseInlineRow { cell, _, _ in - cell.detailTextLabel?.textColor = color - } - cell.detailTextLabel?.textColor = cell.tintColor - } - } - - public override func customDidSelect() { - super.customDidSelect() - if !isDisabled { - toggleInlineRow() - } - } - - public func setupInlineRow(_ inlineRow: InlineRow) { - inlineRow.firstOptions = firstOptions - inlineRow.secondOptions = secondOptions - inlineRow.thirdOptions = thirdOptions - inlineRow.displayValueFor = self.displayValueFor - inlineRow.cell.height = { UITableView.automaticDimension } - } -} diff --git a/Pods/Eureka/Source/Rows/PickerInputRow.swift b/Pods/Eureka/Source/Rows/PickerInputRow.swift deleted file mode 100644 index 5e7a25797..000000000 --- a/Pods/Eureka/Source/Rows/PickerInputRow.swift +++ /dev/null @@ -1,159 +0,0 @@ -// PickerInputRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -// MARK: PickerInputCell - -open class _PickerInputCell : Cell, CellType, UIPickerViewDataSource, UIPickerViewDelegate where T: Equatable { - - lazy public var picker: UIPickerView = { - let picker = UIPickerView() - picker.translatesAutoresizingMaskIntoConstraints = false - return picker - }() - - fileprivate var pickerInputRow: _PickerInputRow? { return row as? _PickerInputRow } - - public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func setup() { - super.setup() - accessoryType = .none - editingAccessoryType = .none - picker.delegate = self - picker.dataSource = self - } - - deinit { - picker.delegate = nil - picker.dataSource = nil - } - - open override func update() { - super.update() - selectionStyle = row.isDisabled ? .none : .default - - if row.title?.isEmpty == false { - detailTextLabel?.text = row.displayValueFor?(row.value) ?? (row as? NoValueDisplayTextConformance)?.noValueDisplayText - } else { - textLabel?.text = row.displayValueFor?(row.value) ?? (row as? NoValueDisplayTextConformance)?.noValueDisplayText - detailTextLabel?.text = nil - } - - if #available(iOS 13.0, *) { - textLabel?.textColor = row.isDisabled ? .tertiaryLabel : .label - } else { - textLabel?.textColor = row.isDisabled ? .gray : .black - } - if row.isHighlighted { - textLabel?.textColor = tintColor - } - - picker.reloadAllComponents() - } - - open override func didSelect() { - super.didSelect() - row.deselect() - } - - open override var inputView: UIView? { - return picker - } - - open override func cellCanBecomeFirstResponder() -> Bool { - return canBecomeFirstResponder - } - - override open var canBecomeFirstResponder: Bool { - return !row.isDisabled - } - - open func numberOfComponents(in pickerView: UIPickerView) -> Int { - return 1 - } - - open func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { - return pickerInputRow?.options.count ?? 0 - } - - open func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { - return pickerInputRow?.displayValueFor?(pickerInputRow?.options[row]) - } - - open func pickerView(_ pickerView: UIPickerView, didSelectRow rowNumber: Int, inComponent component: Int) { - if let picker = pickerInputRow, picker.options.count > rowNumber { - picker.value = picker.options[rowNumber] - update() - } - } -} - -open class PickerInputCell: _PickerInputCell where T: Equatable { - - public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func update() { - super.update() - if let selectedValue = pickerInputRow?.value, let index = pickerInputRow?.options.firstIndex(of: selectedValue) { - picker.selectRow(index, inComponent: 0, animated: true) - } - } - -} - -// MARK: PickerInputRow - -open class _PickerInputRow : Row>, NoValueDisplayTextConformance where T: Equatable { - open var noValueDisplayText: String? = nil - - open var options = [T]() - - required public init(tag: String?) { - super.init(tag: tag) - - } -} - -/// A generic row where the user can pick an option from a picker view displayed in the keyboard area -public final class PickerInputRow: _PickerInputRow, RowType where T: Equatable { - - required public init(tag: String?) { - super.init(tag: tag) - } -} diff --git a/Pods/Eureka/Source/Rows/PickerRow.swift b/Pods/Eureka/Source/Rows/PickerRow.swift deleted file mode 100644 index 29761ca3d..000000000 --- a/Pods/Eureka/Source/Rows/PickerRow.swift +++ /dev/null @@ -1,137 +0,0 @@ -// PickerRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -// MARK: PickerCell - -open class _PickerCell : Cell, CellType, UIPickerViewDataSource, UIPickerViewDelegate where T: Equatable { - - @IBOutlet public weak var picker: UIPickerView! - - fileprivate var pickerRow: _PickerRow? { return row as? _PickerRow } - - public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - let pickerView = UIPickerView() - self.picker = pickerView - self.picker?.translatesAutoresizingMaskIntoConstraints = false - - super.init(style: style, reuseIdentifier: reuseIdentifier) - - self.contentView.addSubview(pickerView) - self.contentView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[picker]-0-|", options: [], metrics: nil, views: ["picker": pickerView])) - self.contentView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[picker]-0-|", options: [], metrics: nil, views: ["picker": pickerView])) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func setup() { - super.setup() - accessoryType = .none - editingAccessoryType = .none - height = { UITableView.automaticDimension } - picker.delegate = self - picker.dataSource = self - } - - open override func update() { - super.update() - textLabel?.text = nil - detailTextLabel?.text = nil - picker.reloadAllComponents() - } - - deinit { - picker?.delegate = nil - picker?.dataSource = nil - } - - open var pickerTextAttributes: [NSAttributedString.Key: Any]? - - open func numberOfComponents(in pickerView: UIPickerView) -> Int { - return 1 - } - - open func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { - return pickerRow?.options.count ?? 0 - } - - open func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { - return pickerRow?.displayValueFor?(pickerRow?.options[row]) - } - - open func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { - if let picker = pickerRow, !picker.options.isEmpty { - picker.value = picker.options[row] - } - } - - open func pickerView(_ pickerView: UIPickerView, attributedTitleForRow row: Int, forComponent component: Int) -> NSAttributedString? { - guard let pickerTextAttributes = pickerTextAttributes, let text = self.pickerView(pickerView, titleForRow: row, forComponent: component) else { - return nil - } - return NSAttributedString(string: text, attributes: pickerTextAttributes) - } -} - -open class PickerCell : _PickerCell where T: Equatable { - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - open override func update() { - super.update() - if let selectedValue = pickerRow?.value, let index = pickerRow?.options.firstIndex(of: selectedValue) { - picker.selectRow(index, inComponent: 0, animated: true) - } - } - -} - -// MARK: PickerRow - -open class _PickerRow : Row> where T: Equatable { - - open var options = [T]() - - required public init(tag: String?) { - super.init(tag: tag) - } -} - -/// A generic row where the user can pick an option from a picker view -public final class PickerRow: _PickerRow, RowType where T: Equatable { - - required public init(tag: String?) { - super.init(tag: tag) - } -} diff --git a/Pods/Eureka/Source/Rows/PopoverSelectorRow.swift b/Pods/Eureka/Source/Rows/PopoverSelectorRow.swift deleted file mode 100644 index 4a4513c71..000000000 --- a/Pods/Eureka/Source/Rows/PopoverSelectorRow.swift +++ /dev/null @@ -1,55 +0,0 @@ -// PopoverSelectorRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -open class _PopoverSelectorRow : SelectorRow where Cell: BaseCell { - - public required init(tag: String?) { - super.init(tag: tag) - onPresentCallback = { [weak self] (_, viewController) -> Void in - guard let porpoverController = viewController.popoverPresentationController, let tableView = self?.baseCell.formViewController()?.tableView, let cell = self?.cell else { - fatalError() - } - porpoverController.sourceView = tableView - porpoverController.sourceRect = tableView.convert(cell.detailTextLabel?.frame ?? cell.textLabel?.frame ?? cell.contentView.frame, from: cell) - } - presentationMode = .popover(controllerProvider: ControllerProvider.callback { return SelectorViewController> { _ in } }, onDismiss: { [weak self] in - $0.dismiss(animated: true) - self?.reload() - }) - } - - open override func didSelect() { - deselect() - super.didSelect() - } -} - -public final class PopoverSelectorRow : _PopoverSelectorRow>, RowType { - public required init(tag: String?) { - super.init(tag: tag) - } -} diff --git a/Pods/Eureka/Source/Rows/PushRow.swift b/Pods/Eureka/Source/Rows/PushRow.swift deleted file mode 100644 index faf27400f..000000000 --- a/Pods/Eureka/Source/Rows/PushRow.swift +++ /dev/null @@ -1,42 +0,0 @@ -// PushRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -open class _PushRow: SelectorRow where Cell: BaseCell { - - public required init(tag: String?) { - super.init(tag: tag) - presentationMode = .show(controllerProvider: ControllerProvider.callback { return SelectorViewController> { _ in } }, onDismiss: { vc in - let _ = vc.navigationController?.popViewController(animated: true) }) - } -} - -/// A selector row where the user can pick an option from a pushed view controller -public final class PushRow : _PushRow>, RowType { - public required init(tag: String?) { - super.init(tag: tag) - } -} diff --git a/Pods/Eureka/Source/Rows/SegmentedRow.swift b/Pods/Eureka/Source/Rows/SegmentedRow.swift deleted file mode 100644 index 236bf64ed..000000000 --- a/Pods/Eureka/Source/Rows/SegmentedRow.swift +++ /dev/null @@ -1,194 +0,0 @@ -// SegmentedRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -// MARK: SegmentedCell - -open class SegmentedCell : Cell, CellType { - - @IBOutlet public weak var segmentedControl: UISegmentedControl! - @IBOutlet public weak var titleLabel: UILabel? - - private var dynamicConstraints = [NSLayoutConstraint]() - fileprivate var observingTitleText = false - private var awakeFromNibCalled = false - - required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - - let segmentedControl = UISegmentedControl() - segmentedControl.translatesAutoresizingMaskIntoConstraints = false - segmentedControl.setContentHuggingPriority(UILayoutPriority(rawValue: 250), for: .horizontal) - self.segmentedControl = segmentedControl - - self.titleLabel = self.textLabel - self.titleLabel?.translatesAutoresizingMaskIntoConstraints = false - self.titleLabel?.setContentHuggingPriority(UILayoutPriority(rawValue: 500), for: .horizontal) - self.titleLabel?.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - - NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: nil) { [weak self] _ in - guard let me = self else { return } - guard me.observingTitleText else { return } - me.titleLabel?.removeObserver(me, forKeyPath: "text") - me.observingTitleText = false - } - NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] _ in - guard let me = self else { return } - guard !me.observingTitleText else { return } - me.titleLabel?.addObserver(me, forKeyPath: "text", options: [.new, .old], context: nil) - me.observingTitleText = true - } - - NotificationCenter.default.addObserver(forName: UIContentSizeCategory.didChangeNotification, object: nil, queue: nil) { [weak self] _ in - self?.titleLabel = self?.textLabel - self?.setNeedsUpdateConstraints() - } - contentView.addSubview(titleLabel!) - contentView.addSubview(segmentedControl) - titleLabel?.addObserver(self, forKeyPath: "text", options: [.old, .new], context: nil) - observingTitleText = true - imageView?.addObserver(self, forKeyPath: "image", options: [.old, .new], context: nil) - - contentView.addConstraint(NSLayoutConstraint(item: segmentedControl, attribute: .centerY, relatedBy: .equal, toItem: contentView, attribute: .centerY, multiplier: 1, constant: 0)) - - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func awakeFromNib() { - super.awakeFromNib() - awakeFromNibCalled = true - } - - deinit { - segmentedControl.removeTarget(self, action: nil, for: .allEvents) - if !awakeFromNibCalled { - if observingTitleText { - titleLabel?.removeObserver(self, forKeyPath: "text") - } - imageView?.removeObserver(self, forKeyPath: "image") - NotificationCenter.default.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil) - NotificationCenter.default.removeObserver(self, name: UIContentSizeCategory.didChangeNotification, object: nil) - } - - } - - open override func setup() { - super.setup() - selectionStyle = .none - segmentedControl.addTarget(self, action: #selector(SegmentedCell.valueChanged), for: .valueChanged) - } - - open override func update() { - super.update() - detailTextLabel?.text = nil - - updateSegmentedControl() - segmentedControl.selectedSegmentIndex = selectedIndex() ?? UISegmentedControl.noSegment - segmentedControl.isEnabled = !row.isDisabled - } - - @objc (valueDidChange) func valueChanged() { - row.value = (row as! OptionsRow).options?[segmentedControl.selectedSegmentIndex] - } - - open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { - let obj = object as AnyObject? - - if let changeType = change, let _ = keyPath, ((obj === titleLabel && keyPath == "text") || (obj === imageView && keyPath == "image")) && - (changeType[NSKeyValueChangeKey.kindKey] as? NSNumber)?.uintValue == NSKeyValueChange.setting.rawValue, !awakeFromNibCalled { - setNeedsUpdateConstraints() - updateConstraintsIfNeeded() - } - } - - func updateSegmentedControl() { - segmentedControl.removeAllSegments() - - (row as! OptionsRow).options?.reversed().forEach { - if let image = $0 as? UIImage { - segmentedControl.insertSegment(with: image, at: 0, animated: false) - } else { - segmentedControl.insertSegment(withTitle: row.displayValueFor?($0) ?? "", at: 0, animated: false) - } - } - } - - open override func updateConstraints() { - guard !awakeFromNibCalled else { - super.updateConstraints() - return - } - contentView.removeConstraints(dynamicConstraints) - dynamicConstraints = [] - var views: [String: AnyObject] = ["segmentedControl": segmentedControl] - - var hasImageView = false - var hasTitleLabel = false - - if let imageView = imageView, let _ = imageView.image { - views["imageView"] = imageView - hasImageView = true - } - - if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty { - views["titleLabel"] = titleLabel - hasTitleLabel = true - dynamicConstraints.append(NSLayoutConstraint(item: titleLabel, attribute: .centerY, relatedBy: .equal, toItem: contentView, attribute: .centerY, multiplier: 1, constant: 0)) - } - - dynamicConstraints.append(NSLayoutConstraint(item: segmentedControl!, attribute: .width, relatedBy: .greaterThanOrEqual, toItem: contentView, attribute: .width, multiplier: 0.3, constant: 0.0)) - - if hasImageView && hasTitleLabel { - dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[titleLabel]-[segmentedControl]-|", options: [], metrics: nil, views: views) - } else if hasImageView && !hasTitleLabel { - dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-[segmentedControl]-|", options: [], metrics: nil, views: views) - } else if !hasImageView && hasTitleLabel { - dynamicConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-[titleLabel]-[segmentedControl]-|", options: .alignAllCenterY, metrics: nil, views: views) - } else { - dynamicConstraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-[segmentedControl]-|", options: .alignAllCenterY, metrics: nil, views: views) - } - contentView.addConstraints(dynamicConstraints) - super.updateConstraints() - } - - func selectedIndex() -> Int? { - guard let value = row.value else { return nil } - return (row as! OptionsRow).options?.firstIndex(of: value) - } -} - -// MARK: SegmentedRow - -/// An options row where the user can select an option from an UISegmentedControl -public final class SegmentedRow: OptionsRow>, RowType { - required public init(tag: String?) { - super.init(tag: tag) - } -} diff --git a/Pods/Eureka/Source/Rows/SelectableRows/ListCheckRow.swift b/Pods/Eureka/Source/Rows/SelectableRows/ListCheckRow.swift deleted file mode 100644 index 68e2c5e5b..000000000 --- a/Pods/Eureka/Source/Rows/SelectableRows/ListCheckRow.swift +++ /dev/null @@ -1,70 +0,0 @@ -// ListCheckRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -open class ListCheckCell : Cell, CellType { - - required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func update() { - super.update() - accessoryType = row.value != nil ? .checkmark : .none - editingAccessoryType = accessoryType - selectionStyle = .default - if row.isDisabled { - tintAdjustmentMode = .dimmed - selectionStyle = .none - } else { - tintAdjustmentMode = .automatic - } - } - - open override func setup() { - super.setup() - accessoryType = .checkmark - editingAccessoryType = accessoryType - } - - open override func didSelect() { - row.deselect() - row.updateCell() - } - -} - -public final class ListCheckRow: Row>, SelectableRowType, RowType where T: Equatable { - public var selectableValue: T? - required public init(tag: String?) { - super.init(tag: tag) - displayValueFor = nil - } -} diff --git a/Pods/Eureka/Source/Rows/SliderRow.swift b/Pods/Eureka/Source/Rows/SliderRow.swift deleted file mode 100644 index a2fcac82a..000000000 --- a/Pods/Eureka/Source/Rows/SliderRow.swift +++ /dev/null @@ -1,175 +0,0 @@ -// SliderRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import UIKit - -/// The cell of the SliderRow -open class SliderCell: Cell, CellType { - - private var awakeFromNibCalled = false - - @IBOutlet open weak var titleLabel: UILabel! - @IBOutlet open weak var valueLabel: UILabel! - @IBOutlet open weak var slider: UISlider! - - open var formatter: NumberFormatter? - - public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: .value1, reuseIdentifier: reuseIdentifier) - - NotificationCenter.default.addObserver(forName: UIContentSizeCategory.didChangeNotification, object: nil, queue: nil) { [weak self] _ in - guard let me = self else { return } - if me.shouldShowTitle { - me.titleLabel = me.textLabel - me.valueLabel = me.detailTextLabel - me.setNeedsUpdateConstraints() - } - } - } - - deinit { - guard !awakeFromNibCalled else { return } - NotificationCenter.default.removeObserver(self, name: UIContentSizeCategory.didChangeNotification, object: nil) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - awakeFromNibCalled = true - } - - open override func setup() { - super.setup() - if !awakeFromNibCalled { - let title = textLabel - textLabel?.translatesAutoresizingMaskIntoConstraints = false - textLabel?.setContentHuggingPriority(UILayoutPriority(rawValue: 500), for: .horizontal) - self.titleLabel = title - - let value = detailTextLabel - value?.translatesAutoresizingMaskIntoConstraints = false - value?.setContentHuggingPriority(UILayoutPriority(500), for: .horizontal) - value?.adjustsFontSizeToFitWidth = true - value?.minimumScaleFactor = 0.5 - self.valueLabel = value - - let slider = UISlider() - slider.translatesAutoresizingMaskIntoConstraints = false - slider.setContentHuggingPriority(UILayoutPriority(rawValue: 500), for: .horizontal) - self.slider = slider - - if shouldShowTitle { - contentView.addSubview(titleLabel) - } - - if !sliderRow.shouldHideValue { - contentView.addSubview(valueLabel) - } - contentView.addSubview(slider) - setNeedsUpdateConstraints() - } - selectionStyle = .none - slider.minimumValue = 0 - slider.maximumValue = 10 - slider.addTarget(self, action: #selector(SliderCell.valueChanged), for: .valueChanged) - } - - open override func update() { - super.update() - titleLabel.text = row.title - titleLabel.isHidden = !shouldShowTitle - valueLabel.text = row.displayValueFor?(row.value ?? slider.minimumValue) - valueLabel.isHidden = sliderRow.shouldHideValue - slider.value = row.value ?? slider.minimumValue - slider.isEnabled = !row.isDisabled - } - - @objc (valueDidChange) func valueChanged() { - let roundedValue: Float - let steps = Float(sliderRow.steps) - if steps > 0 { - let stepValue = round((slider.value - slider.minimumValue) / (slider.maximumValue - slider.minimumValue) * steps) - let stepAmount = (slider.maximumValue - slider.minimumValue) / steps - roundedValue = stepValue * stepAmount + self.slider.minimumValue - } else { - roundedValue = slider.value - } - row.value = roundedValue - row.updateCell() - } - - var shouldShowTitle: Bool { - return row?.title?.isEmpty == false - } - - private var sliderRow: SliderRow { - return row as! SliderRow - } - - open override func updateConstraints() { - customConstraints() - super.updateConstraints() - } - - open var dynamicConstraints = [NSLayoutConstraint]() - - open func customConstraints() { - guard !awakeFromNibCalled else { return } - contentView.removeConstraints(dynamicConstraints) - dynamicConstraints = [] - - var views: [String : Any] = ["titleLabel": titleLabel!, "slider": slider!, "valueLabel": valueLabel!] - let metrics = ["spacing": 15.0] - valueLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - - let title = shouldShowTitle ? "[titleLabel]-spacing-" : "" - let value = !sliderRow.shouldHideValue ? "-[valueLabel]" : "" - - if let imageView = imageView, let _ = imageView.image { - views["imageView"] = imageView - let hContraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-[imageView]-(15)-\(title)[slider]\(value)-|", options: .alignAllCenterY, metrics: metrics, views: views) - imageView.translatesAutoresizingMaskIntoConstraints = false - dynamicConstraints.append(contentsOf: hContraints) - } else { - let hContraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-\(title)[slider]\(value)-|", options: .alignAllCenterY, metrics: metrics, views: views) - dynamicConstraints.append(contentsOf: hContraints) - } - let vContraint = NSLayoutConstraint(item: slider!, attribute: .centerY, relatedBy: .equal, toItem: contentView, attribute: .centerY, multiplier: 1, constant: 0) - dynamicConstraints.append(vContraint) - contentView.addConstraints(dynamicConstraints) - } - -} - - -/// A row that displays a UISlider. If there is a title set then the title and value will appear above the UISlider. -public final class SliderRow: Row, RowType { - - public var steps: UInt = 20 - public var shouldHideValue = false - - required public init(tag: String?) { - super.init(tag: tag) - } -} diff --git a/Pods/Eureka/Source/Rows/StepperRow.swift b/Pods/Eureka/Source/Rows/StepperRow.swift deleted file mode 100644 index e7ae39e50..000000000 --- a/Pods/Eureka/Source/Rows/StepperRow.swift +++ /dev/null @@ -1,157 +0,0 @@ -// -// StepperRow.swift -// Eureka -// -// Created by Andrew Holt on 3/4/16. -// Copyright © 2016 Xmartlabs. All rights reserved. -// - -import UIKit - -// MARK: StepperCell - -open class StepperCell: Cell, CellType { - - - @IBOutlet open weak var stepper: UIStepper! - @IBOutlet open weak var valueLabel: UILabel! - @IBOutlet open weak var titleLabel: UILabel! - - private var awakeFromNibCalled = false - - required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: .value1, reuseIdentifier: reuseIdentifier) - - NotificationCenter.default.addObserver(forName: UIContentSizeCategory.didChangeNotification, object: nil, queue: nil) { [weak self] _ in - guard let me = self else { return } - if me.shouldShowTitle { - me.titleLabel = me.textLabel - me.setNeedsUpdateConstraints() - } - } - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - awakeFromNibCalled = true - } - - open override func setup() { - super.setup() - if !awakeFromNibCalled { - let title = textLabel - textLabel?.translatesAutoresizingMaskIntoConstraints = false - textLabel?.setContentHuggingPriority(UILayoutPriority(rawValue: 500), for: .horizontal) - self.titleLabel = title - - let stepper = UIStepper() - stepper.translatesAutoresizingMaskIntoConstraints = false - stepper.setContentHuggingPriority(UILayoutPriority(rawValue: 500), for: .horizontal) - self.stepper = stepper - - if shouldShowTitle { - contentView.addSubview(titleLabel) - } - - setupValueLabel() - contentView.addSubview(stepper) - setNeedsUpdateConstraints() - } - selectionStyle = .none - stepper.addTarget(self, action: #selector(StepperCell.valueChanged), for: .valueChanged) - } - - open func setupValueLabel() { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.setContentHuggingPriority(UILayoutPriority(500), for: .horizontal) - label.adjustsFontSizeToFitWidth = true - label.minimumScaleFactor = 0.5 - self.valueLabel = label - contentView.addSubview(valueLabel) - } - - open override func update() { - super.update() - detailTextLabel?.text = nil - stepper.isEnabled = !row.isDisabled - - titleLabel.isHidden = !shouldShowTitle - stepper.value = row.value ?? 0 - stepper.alpha = row.isDisabled ? 0.3 : 1.0 - valueLabel?.textColor = tintColor - valueLabel?.alpha = row.isDisabled ? 0.3 : 1.0 - valueLabel?.text = row.displayValueFor?(row.value) - } - - @objc(valueDidChange) func valueChanged() { - row.value = stepper.value - row.updateCell() - } - - var shouldShowTitle: Bool { - return row?.title?.isEmpty == false - } - - private var stepperRow: StepperRow { - return row as! StepperRow - } - - deinit { - stepper.removeTarget(self, action: nil, for: .allEvents) - guard !awakeFromNibCalled else { return } - NotificationCenter.default.removeObserver(self, name: UIContentSizeCategory.didChangeNotification, object: nil) - } - - open override func updateConstraints() { - customConstraints() - super.updateConstraints() - } - - open var dynamicConstraints = [NSLayoutConstraint]() - - open func customConstraints() { - guard !awakeFromNibCalled else { return } - contentView.removeConstraints(dynamicConstraints) - dynamicConstraints = [] - - var views: [String : Any] = ["titleLabel": titleLabel!, "stepper": stepper!, "valueLabel": valueLabel!] - let metrics = ["spacing": 15.0] - valueLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - - let title = shouldShowTitle ? "[titleLabel]-(>=15@250)-" : "" - - if let imageView = imageView, let _ = imageView.image { - views["imageView"] = imageView - let hContraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-[imageView]-(15)-\(title)[valueLabel]-[stepper]-|", options: .alignAllCenterY, metrics: metrics, views: views) - imageView.translatesAutoresizingMaskIntoConstraints = false - dynamicConstraints.append(contentsOf: hContraints) - } else { - let hContraints = NSLayoutConstraint.constraints(withVisualFormat: "H:|-\(title)[valueLabel]-[stepper]-|", options: .alignAllCenterY, metrics: metrics, views: views) - dynamicConstraints.append(contentsOf: hContraints) - } - let vContraint = NSLayoutConstraint(item: stepper!, attribute: .centerY, relatedBy: .equal, toItem: contentView, attribute: .centerY, multiplier: 1, constant: 0) - dynamicConstraints.append(vContraint) - contentView.addConstraints(dynamicConstraints) - } - -} - -// MARK: StepperRow - -open class _StepperRow: Row { - required public init(tag: String?) { - super.init(tag: tag) - displayValueFor = { value in - guard let value = value else { return nil } - return DecimalFormatter().string(from: NSNumber(value: value)) } - } -} - -/// Double row that has a UIStepper as accessoryType -public final class StepperRow: _StepperRow, RowType { - required public init(tag: String?) { - super.init(tag: tag) - } -} diff --git a/Pods/Eureka/Source/Rows/SwitchRow.swift b/Pods/Eureka/Source/Rows/SwitchRow.swift deleted file mode 100644 index bbd0ee501..000000000 --- a/Pods/Eureka/Source/Rows/SwitchRow.swift +++ /dev/null @@ -1,81 +0,0 @@ -// SwitchRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -// MARK: SwitchCell - -open class SwitchCell: Cell, CellType { - - @IBOutlet public weak var switchControl: UISwitch! - - required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - let switchC = UISwitch() - switchControl = switchC - accessoryView = switchControl - editingAccessoryView = accessoryView - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func setup() { - super.setup() - selectionStyle = .none - switchControl.addTarget(self, action: #selector(SwitchCell.valueChanged), for: .valueChanged) - } - - deinit { - switchControl?.removeTarget(self, action: nil, for: .allEvents) - } - - open override func update() { - super.update() - switchControl.isOn = row.value ?? false - switchControl.isEnabled = !row.isDisabled - } - - @objc (valueDidChange) func valueChanged() { - row.value = switchControl?.isOn ?? false - } -} - -// MARK: SwitchRow - -open class _SwitchRow: Row { - required public init(tag: String?) { - super.init(tag: tag) - displayValueFor = nil - } -} - -/// Boolean row that has a UISwitch as accessoryType -public final class SwitchRow: _SwitchRow, RowType { - required public init(tag: String?) { - super.init(tag: tag) - } -} diff --git a/Pods/Eureka/Source/Rows/TextAreaRow.swift b/Pods/Eureka/Source/Rows/TextAreaRow.swift deleted file mode 100644 index c2050674d..000000000 --- a/Pods/Eureka/Source/Rows/TextAreaRow.swift +++ /dev/null @@ -1,336 +0,0 @@ -// TextAreaRow.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -// TODO: Temporary workaround for Xcode 10 beta -#if swift(>=4.2) -import UIKit.UIGeometry -extension UIEdgeInsets { - static let zero = UIEdgeInsets() -} -#endif - -public enum TextAreaHeight { - case fixed(cellHeight: CGFloat) - case dynamic(initialTextViewHeight: CGFloat) -} - -public enum TextAreaMode { - case normal - case readOnly -} - -protocol TextAreaConformance: FormatterConformance { - var placeholder: String? { get set } - var textAreaHeight: TextAreaHeight { get set } - var titlePercentage: CGFloat? { get set} -} - -/** - * Protocol for cells that contain a UITextView - */ -public protocol AreaCell: TextInputCell { - var textView: UITextView! { get } -} - -extension AreaCell { - public var textInput: UITextInput { - return textView - } -} - -open class _TextAreaCell : Cell, UITextViewDelegate, AreaCell where T: Equatable, T: InputTypeInitiable { - - @IBOutlet public weak var textView: UITextView! - @IBOutlet public weak var placeholderLabel: UILabel? - - private var awakeFromNibCalled = false - - required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - - super.init(style: style, reuseIdentifier: reuseIdentifier) - - let textView = UITextView() - self.textView = textView - textView.translatesAutoresizingMaskIntoConstraints = false - textView.keyboardType = .default - textView.font = .preferredFont(forTextStyle: .body) - textView.textContainer.lineFragmentPadding = 0 - textView.textContainerInset = UIEdgeInsets.zero - textView.backgroundColor = .clear - contentView.addSubview(textView) - - let placeholderLabel = UILabel() - self.placeholderLabel = placeholderLabel - placeholderLabel.translatesAutoresizingMaskIntoConstraints = false - placeholderLabel.numberOfLines = 0 - if #available(iOS 13.0, *) { - placeholderLabel.textColor = UIColor.tertiaryLabel - } else { - placeholderLabel.textColor = UIColor(white: 0, alpha: 0.22) - } - placeholderLabel.font = textView.font - contentView.addSubview(placeholderLabel) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func awakeFromNib() { - super.awakeFromNib() - awakeFromNibCalled = true - } - - open var dynamicConstraints = [NSLayoutConstraint]() - - open override func setup() { - super.setup() - let textAreaRow = row as! TextAreaConformance - switch textAreaRow.textAreaHeight { - case .dynamic(_): - height = { UITableView.automaticDimension } - textView.isScrollEnabled = false - case .fixed(let cellHeight): - height = { cellHeight } - } - - textView.delegate = self - selectionStyle = .none - if !awakeFromNibCalled { - imageView?.addObserver(self, forKeyPath: "image", options: [.new, .old], context: nil) - } - setNeedsUpdateConstraints() - } - - deinit { - textView?.delegate = nil - if !awakeFromNibCalled { - imageView?.removeObserver(self, forKeyPath: "image") - } - } - - open override func update() { - super.update() - textLabel?.text = nil - detailTextLabel?.text = nil - textView.isEditable = !row.isDisabled - if #available(iOS 13.0, *) { - textView.textColor = row.isDisabled ? .tertiaryLabel : .label - } else { - textView.textColor = row.isDisabled ? .gray : .black - } - textView.text = row.displayValueFor?(row.value) - placeholderLabel?.text = (row as? TextAreaConformance)?.placeholder - placeholderLabel?.isHidden = textView.text.count != 0 - } - - open override func cellCanBecomeFirstResponder() -> Bool { - return !row.isDisabled && textView?.canBecomeFirstResponder == true - } - - open override func cellBecomeFirstResponder(withDirection: Direction) -> Bool { - // workaround to solve https://github.com/xmartlabs/Eureka/issues/887 UIKit issue - textView?.perform(#selector(UITextView.becomeFirstResponder), with: nil, afterDelay: 0.0) - return true - - } - - open override func cellResignFirstResponder() -> Bool { - return textView?.resignFirstResponder() ?? true - } - - open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { - let obj = object as AnyObject? - - if let keyPathValue = keyPath, let changeType = change?[NSKeyValueChangeKey.kindKey], obj === imageView && keyPathValue == "image" && - (changeType as? NSNumber)?.uintValue == NSKeyValueChange.setting.rawValue, !awakeFromNibCalled { - setNeedsUpdateConstraints() - updateConstraintsIfNeeded() - } - } - - //Mark: Helpers - - private func displayValue(useFormatter: Bool) -> String? { - guard let v = row.value else { return nil } - if let formatter = (row as? FormatterConformance)?.formatter, useFormatter { - return textView?.isFirstResponder == true ? formatter.editingString(for: v) : formatter.string(for: v) - } - return String(describing: v) - } - - // MARK: TextFieldDelegate - - open func textViewDidBeginEditing(_ textView: UITextView) { - formViewController()?.beginEditing(of: self) - formViewController()?.textInputDidBeginEditing(textView, cell: self) - if let textAreaConformance = (row as? TextAreaConformance), let _ = textAreaConformance.formatter, textAreaConformance.useFormatterOnDidBeginEditing ?? textAreaConformance.useFormatterDuringInput { - textView.text = self.displayValue(useFormatter: true) - } else { - textView.text = self.displayValue(useFormatter: false) - } - } - - open func textViewDidEndEditing(_ textView: UITextView) { - formViewController()?.endEditing(of: self) - formViewController()?.textInputDidEndEditing(textView, cell: self) - textViewDidChange(textView) - textView.text = displayValue(useFormatter: (row as? FormatterConformance)?.formatter != nil) - } - - open func textViewDidChange(_ textView: UITextView) { - - if let textAreaConformance = row as? TextAreaConformance, case .dynamic = textAreaConformance.textAreaHeight, let tableView = formViewController()?.tableView { - let currentOffset = tableView.contentOffset - UIView.performWithoutAnimation { - tableView.beginUpdates() - tableView.endUpdates() - } - tableView.setContentOffset(currentOffset, animated: false) - } - placeholderLabel?.isHidden = textView.text.count != 0 - guard let textValue = textView.text else { - row.value = nil - return - } - guard let formatterRow = row as? FormatterConformance, let formatter = formatterRow.formatter else { - row.value = textValue.isEmpty ? nil : (T.init(string: textValue) ?? row.value) - return - } - if formatterRow.useFormatterDuringInput { - let value: AutoreleasingUnsafeMutablePointer = AutoreleasingUnsafeMutablePointer.init(UnsafeMutablePointer.allocate(capacity: 1)) - let errorDesc: AutoreleasingUnsafeMutablePointer? = nil - if formatter.getObjectValue(value, for: textValue, errorDescription: errorDesc) { - row.value = value.pointee as? T - guard var selStartPos = textView.selectedTextRange?.start else { return } - let oldVal = textView.text - textView.text = row.displayValueFor?(row.value) - selStartPos = (formatter as? FormatterProtocol)?.getNewPosition(forPosition: selStartPos, inTextInput: textView, oldValue: oldVal, newValue: textView.text) ?? selStartPos - textView.selectedTextRange = textView.textRange(from: selStartPos, to: selStartPos) - return - } - } else { - let value: AutoreleasingUnsafeMutablePointer = AutoreleasingUnsafeMutablePointer.init(UnsafeMutablePointer.allocate(capacity: 1)) - let errorDesc: AutoreleasingUnsafeMutablePointer? = nil - if formatter.getObjectValue(value, for: textValue, errorDescription: errorDesc) { - row.value = value.pointee as? T - } - } - } - - open func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - return formViewController()?.textInput(textView, shouldChangeCharactersInRange: range, replacementString: text, cell: self) ?? true - } - - open func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { - if let textAreaRow = self.row as? _TextAreaRow, textAreaRow.textAreaMode == .readOnly { - return false - } - return formViewController()?.textInputShouldBeginEditing(textView, cell: self) ?? true - } - - open func textViewShouldEndEditing(_ textView: UITextView) -> Bool { - return formViewController()?.textInputShouldEndEditing(textView, cell: self) ?? true - } - - open override func updateConstraints() { - customConstraints() - super.updateConstraints() - } - - open func customConstraints() { - guard !awakeFromNibCalled else { return } - - contentView.removeConstraints(dynamicConstraints) - dynamicConstraints = [] - var views: [String: AnyObject] = ["textView": textView, "label": placeholderLabel!] - dynamicConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "V:|-[label]", options: [], metrics: nil, views: views)) - if let textAreaConformance = row as? TextAreaConformance, case .dynamic(let initialTextViewHeight) = textAreaConformance.textAreaHeight { - dynamicConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "V:|-[textView(>=initialHeight@800)]-|", options: [], metrics: ["initialHeight": initialTextViewHeight], views: views)) - } else { - dynamicConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "V:|-[textView]-|", options: [], metrics: nil, views: views)) - } - if let imageView = imageView, let _ = imageView.image { - views["imageView"] = imageView - dynamicConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[textView]-|", options: [], metrics: nil, views: views)) - dynamicConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "H:[imageView]-(15)-[label]-|", options: [], metrics: nil, views: views)) - } else if let titlePercentage = (row as? TextAreaConformance)?.titlePercentage, titlePercentage > 0.0 { - textView.textAlignment = .right - dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:[textView]-|", options: [], metrics: nil, views: views) - let sideSpaces = (layoutMargins.right + layoutMargins.left) - dynamicConstraints.append(NSLayoutConstraint(item: textView!, - attribute: .width, - relatedBy: .equal, - toItem: contentView, - attribute: .width, - multiplier: 1 - titlePercentage, - constant: -sideSpaces)) - } else { - dynamicConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "H:|-[textView]-|", options: [], metrics: nil, views: views)) - dynamicConstraints.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "H:|-[label]-|", options: [], metrics: nil, views: views)) - } - contentView.addConstraints(dynamicConstraints) - } - -} - -open class TextAreaCell: _TextAreaCell, CellType { - - required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } -} - -open class AreaRow: FormatteableRow, TextAreaConformance where Cell: BaseCell, Cell: AreaCell { - - open var placeholder: String? - open var textAreaHeight = TextAreaHeight.fixed(cellHeight: 110) - open var textAreaMode = TextAreaMode.normal - /// The percentage of the cell that should be occupied by the remaining space to the left of the textArea. This is equivalent to the space occupied by a title in FieldRow, making the textArea aligned to fieldRows using the same titlePercentage. This behavior works only if the cell does not contain an image, due to its automatically set constraints in the cell. - open var titlePercentage: CGFloat? - - public required init(tag: String?) { - super.init(tag: tag) - } -} - -open class _TextAreaRow: AreaRow { - required public init(tag: String?) { - super.init(tag: tag) - } -} - -/// A row with a UITextView where the user can enter large text. -public final class TextAreaRow: _TextAreaRow, RowType { - required public init(tag: String?) { - super.init(tag: tag) - } -} diff --git a/Pods/Eureka/Source/Rows/TriplePickerInputRow.swift b/Pods/Eureka/Source/Rows/TriplePickerInputRow.swift deleted file mode 100644 index bbd5d93a7..000000000 --- a/Pods/Eureka/Source/Rows/TriplePickerInputRow.swift +++ /dev/null @@ -1,158 +0,0 @@ -// -// TriplePickerInputRow.swift -// Eureka -// -// Created by Mathias Claassen on 5/10/18. -// Copyright © 2018 Xmartlabs. All rights reserved. -// - -import Foundation -import UIKit - -open class TriplePickerInputCell : _PickerInputCell> where A: Equatable, B: Equatable, C: Equatable { - - private var pickerRow: _TriplePickerInputRow! { return row as? _TriplePickerInputRow } - - public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func update() { - super.update() - if let selectedValue = pickerRow.value, let indexA = pickerRow.firstOptions().firstIndex(of: selectedValue.a), - let indexB = pickerRow.secondOptions(selectedValue.a).firstIndex(of: selectedValue.b), - let indexC = pickerRow.thirdOptions(selectedValue.a, selectedValue.b).firstIndex(of: selectedValue.c){ - picker.selectRow(indexA, inComponent: 0, animated: true) - picker.selectRow(indexB, inComponent: 1, animated: true) - picker.selectRow(indexC, inComponent: 2, animated: true) - } - } - - open override func numberOfComponents(in pickerView: UIPickerView) -> Int { - return 3 - } - - open override func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { - if component == 0 { - return pickerRow.firstOptions().count - } else if component == 1 { - return pickerRow.secondOptions(pickerRow.selectedFirst()).count - } else { - return pickerRow.thirdOptions(pickerRow.selectedFirst(), pickerRow.selectedSecond()).count - } - } - - open override func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { - if component == 0 { - return pickerRow.displayValueForFirstRow(pickerRow.firstOptions()[row]) - } else if component == 1 { - return pickerRow.displayValueForSecondRow(pickerRow.secondOptions(pickerRow.selectedFirst())[row]) - } else { - return pickerRow.displayValueForThirdRow(pickerRow.thirdOptions(pickerRow.selectedFirst(), pickerRow.selectedSecond())[row]) - } - } - - open override func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { - if component == 0 { - let a = pickerRow.firstOptions()[row] - if let value = pickerRow.value { - guard value.a != a else { - return - } - - let b: B = pickerRow.secondOptions(a).contains(value.b) ? value.b : pickerRow.secondOptions(a)[0] - let c: C = pickerRow.thirdOptions(a, b).contains(value.c) ? value.c : pickerRow.thirdOptions(a, b)[0] - pickerRow.value = Tuple3(a: a, b: b, c: c) - pickerView.reloadComponent(1) - pickerView.reloadComponent(2) - if b != value.b { - pickerView.selectRow(0, inComponent: 1, animated: true) - } - if c != value.c { - pickerView.selectRow(0, inComponent: 2, animated: true) - } - } else { - let b = pickerRow.secondOptions(a)[0] - pickerRow.value = Tuple3(a: a, b: b, c: pickerRow.thirdOptions(a, b)[0]) - pickerView.reloadComponent(1) - pickerView.reloadComponent(2) - pickerView.selectRow(0, inComponent: 1, animated: true) - pickerView.selectRow(0, inComponent: 2, animated: true) - } - } else if component == 1 { - let a = pickerRow.selectedFirst() - let b = pickerRow.secondOptions(a)[row] - if let value = pickerRow.value { - guard value.b != b else { - return - } - if pickerRow.thirdOptions(a, b).contains(value.c) { - pickerRow.value = Tuple3(a: a, b: b, c: value.c) - pickerView.reloadComponent(2) - update() - return - } else { - pickerRow.value = Tuple3(a: a, b: b, c: pickerRow.thirdOptions(a, b)[0]) - } - } else { - pickerRow.value = Tuple3(a: a, b: b, c: pickerRow.thirdOptions(a, b)[0]) - } - pickerView.reloadComponent(2) - pickerView.selectRow(0, inComponent: 2, animated: true) - } else { - let a = pickerRow.selectedFirst() - let b = pickerRow.selectedSecond() - pickerRow.value = Tuple3(a: a, b: b, c: pickerRow.thirdOptions(a, b)[row]) - } - update() - } -} - -open class _TriplePickerInputRow : Row>, NoValueDisplayTextConformance { - - open var noValueDisplayText: String? = nil - /// Options for first component. Will be called often so should be O(1) - public var firstOptions: (() -> [A]) = {[]} - /// Options for second component given the selected value from the first component. Will be called often so should be O(1) - public var secondOptions: ((A) -> [B]) = {_ in []} - /// Options for third component given the selected value from the first and second components. Will be called often so should be O(1) - public var thirdOptions: ((A, B) -> [C]) = {_, _ in []} - - /// Modify the displayed values for the first picker row. - public var displayValueForFirstRow: ((A) -> (String)) = { a in return String(describing: a) } - /// Modify the displayed values for the second picker row. - public var displayValueForSecondRow: ((B) -> (String)) = { b in return String(describing: b) } - /// Modify the displayed values for the third picker row. - public var displayValueForThirdRow: ((C) -> (String)) = { c in return String(describing: c) } - - required public init(tag: String?) { - super.init(tag: tag) - } - - func selectedFirst() -> A { - return value?.a ?? firstOptions()[0] - } - - func selectedSecond() -> B { - return value?.b ?? secondOptions(selectedFirst())[0] - } - -} - -/// A generic row where the user can pick an option from a picker view displayed in the keyboard area -public final class TriplePickerInputRow: _TriplePickerInputRow, RowType where A: Equatable, B: Equatable, C: Equatable { - - required public init(tag: String?) { - super.init(tag: tag) - self.displayValueFor = { [weak self] tuple in - guard let tuple = tuple else { - return self?.noValueDisplayText - } - return String(describing: tuple.a) + ", " + String(describing: tuple.b) + ", " + String(describing: tuple.c) - } - } -} diff --git a/Pods/Eureka/Source/Rows/TriplePickerRow.swift b/Pods/Eureka/Source/Rows/TriplePickerRow.swift deleted file mode 100644 index 06df8279d..000000000 --- a/Pods/Eureka/Source/Rows/TriplePickerRow.swift +++ /dev/null @@ -1,176 +0,0 @@ -// -// TriplePickerRow.swift -// Eureka -// -// Created by Mathias Claassen on 5/9/18. -// Copyright © 2018 Xmartlabs. All rights reserved. -// - -import Foundation -import UIKit - -public struct Tuple3 { - public let a: A - public let b: B - public let c: C - - public init(a: A, b: B, c: C) { - self.a = a - self.b = b - self.c = c - } - -} - -extension Tuple3: Equatable {} - -public func == (lhs: Tuple3, rhs: Tuple3) -> Bool { - return lhs.a == rhs.a && lhs.b == rhs.b && lhs.c == rhs.c -} - -// MARK: MultiplePickerCell - -open class TriplePickerCell : _PickerCell> where A: Equatable, B: Equatable, C: Equatable { - - private var pickerRow: _TriplePickerRow? { return row as? _TriplePickerRow } - - public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - open override func update() { - super.update() - if let selectedValue = pickerRow?.value, let indexA = pickerRow?.firstOptions().firstIndex(of: selectedValue.a), - let indexB = pickerRow?.secondOptions(selectedValue.a).firstIndex(of: selectedValue.b), - let indexC = pickerRow?.thirdOptions(selectedValue.a, selectedValue.b).firstIndex(of: selectedValue.c) { - picker.selectRow(indexA, inComponent: 0, animated: true) - picker.selectRow(indexB, inComponent: 1, animated: true) - picker.selectRow(indexC, inComponent: 2, animated: true) - } - } - - open override func numberOfComponents(in pickerView: UIPickerView) -> Int { - return 3 - } - - open override func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { - guard let pickerRow = pickerRow else { return 0 } - if component == 0 { - return pickerRow.firstOptions().count - } else if component == 1 { - return pickerRow.secondOptions(pickerRow.selectedFirst()).count - } else { - return pickerRow.thirdOptions(pickerRow.selectedFirst(), pickerRow.selectedSecond()).count - } - } - - open override func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { - guard let pickerRow = pickerRow else { return "" } - if component == 0 { - return pickerRow.displayValueForFirstRow(pickerRow.firstOptions()[row]) - } else if component == 1 { - return pickerRow.displayValueForSecondRow(pickerRow.secondOptions(pickerRow.selectedFirst())[row]) - } else { - return pickerRow.displayValueForThirdRow(pickerRow.thirdOptions(pickerRow.selectedFirst(), pickerRow.selectedSecond())[row]) - } - } - - open override func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { - guard let pickerRow = pickerRow else { return } - if component == 0 { - let a = pickerRow.firstOptions()[row] - if let value = pickerRow.value { - guard value.a != a else { - return - } - - let b: B = pickerRow.secondOptions(a).contains(value.b) ? value.b : pickerRow.secondOptions(a)[0] - let c: C = pickerRow.thirdOptions(a, b).contains(value.c) ? value.c : pickerRow.thirdOptions(a, b)[0] - pickerRow.value = Tuple3(a: a, b: b, c: c) - pickerView.reloadComponent(1) - pickerView.reloadComponent(2) - if b != value.b { - pickerView.selectRow(0, inComponent: 1, animated: true) - } - if c != value.c { - pickerView.selectRow(0, inComponent: 2, animated: true) - } - } else { - let b = pickerRow.secondOptions(a)[0] - pickerRow.value = Tuple3(a: a, b: b, c: pickerRow.thirdOptions(a, b)[0]) - pickerView.reloadComponent(1) - pickerView.reloadComponent(2) - pickerView.selectRow(0, inComponent: 1, animated: true) - pickerView.selectRow(0, inComponent: 2, animated: true) - } - } else if component == 1 { - let a = pickerRow.selectedFirst() - let b = pickerRow.secondOptions(a)[row] - if let value = pickerRow.value { - guard value.b != b else { - return - } - if pickerRow.thirdOptions(a, b).contains(value.c) { - pickerRow.value = Tuple3(a: a, b: b, c: value.c) - pickerView.reloadComponent(2) - return - } else { - pickerRow.value = Tuple3(a: a, b: b, c: pickerRow.thirdOptions(a, b)[0]) - } - } else { - pickerRow.value = Tuple3(a: a, b: b, c: pickerRow.thirdOptions(a, b)[0]) - } - pickerView.reloadComponent(2) - pickerView.selectRow(0, inComponent: 2, animated: true) - } else { - let a = pickerRow.selectedFirst() - let b = pickerRow.selectedSecond() - pickerRow.value = Tuple3(a: a, b: b, c: pickerRow.thirdOptions(a, b)[row]) - } - } - -} - -// MARK: PickerRow -open class _TriplePickerRow : Row> where A: Equatable, B: Equatable, C: Equatable { - - /// Options for first component. Will be called often so should be O(1) - public var firstOptions: (() -> [A]) = {[]} - /// Options for second component given the selected value from the first component. Will be called often so should be O(1) - public var secondOptions: ((A) -> [B]) = {_ in []} - /// Options for third component given the selected value from the first and second components. Will be called often so should be O(1) - public var thirdOptions: ((A, B) -> [C]) = {_, _ in []} - - /// Modify the displayed values for the first picker row. - public var displayValueForFirstRow: ((A) -> (String)) = { a in return String(describing: a) } - /// Modify the displayed values for the second picker row. - public var displayValueForSecondRow: ((B) -> (String)) = { b in return String(describing: b) } - /// Modify the displayed values for the third picker row. - public var displayValueForThirdRow: ((C) -> (String)) = { c in return String(describing: c) } - - required public init(tag: String?) { - super.init(tag: tag) - } - - func selectedFirst() -> A { - return value?.a ?? firstOptions()[0] - } - - func selectedSecond() -> B { - return value?.b ?? secondOptions(selectedFirst())[0] - } - -} - -/// A generic row where the user can pick an option from a picker view -public final class TriplePickerRow: _TriplePickerRow, RowType where A: Equatable, B: Equatable, C: Equatable { - - required public init(tag: String?) { - super.init(tag: tag) - } - -} diff --git a/Pods/Eureka/Source/Validations/RuleClosure.swift b/Pods/Eureka/Source/Validations/RuleClosure.swift deleted file mode 100644 index e5f5b3649..000000000 --- a/Pods/Eureka/Source/Validations/RuleClosure.swift +++ /dev/null @@ -1,43 +0,0 @@ -// RuleClosure.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -public struct RuleClosure: RuleType { - - public var id: String? - public var validationError: ValidationError - - public var closure: (T?) -> ValidationError? - - public func isValid(value: T?) -> ValidationError? { - return closure(value) - } - - public init(validationError: ValidationError = ValidationError(msg: "Field validation fails.."), id: String? = nil, closure: @escaping ((T?) -> ValidationError?)) { - self.validationError = validationError - self.closure = closure - self.id = id - } -} diff --git a/Pods/Eureka/Source/Validations/RuleEmail.swift b/Pods/Eureka/Source/Validations/RuleEmail.swift deleted file mode 100644 index 87d77376c..000000000 --- a/Pods/Eureka/Source/Validations/RuleEmail.swift +++ /dev/null @@ -1,33 +0,0 @@ -// RuleEmail.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -public class RuleEmail: RuleRegExp { - - public init(msg: String = "Field value should be a valid email!", id: String? = nil) { - super.init(regExpr: RegExprPattern.EmailAddress.rawValue, allowsEmpty: true, msg: msg, id: id) - } - -} diff --git a/Pods/Eureka/Source/Validations/RuleEqualsToRow.swift b/Pods/Eureka/Source/Validations/RuleEqualsToRow.swift deleted file mode 100644 index bcabd9b35..000000000 --- a/Pods/Eureka/Source/Validations/RuleEqualsToRow.swift +++ /dev/null @@ -1,55 +0,0 @@ -// RuleRequire.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -public struct RuleEqualsToRow: RuleType { - - public init(form: Form, tag: String, msg: String = "Fields don't match!", id: String? = nil) { - self.validationError = ValidationError(msg: msg) - self.form = form - self.tag = tag - self.row = nil - self.id = id - } - - public init(row: RowOf, msg: String = "Fields don't match!", id: String? = nil) { - self.validationError = ValidationError(msg: msg) - self.form = nil - self.tag = nil - self.row = row - self.id = id - } - - public var id: String? - public var validationError: ValidationError - public weak var form: Form? - public var tag: String? - public weak var row: RowOf? - - public func isValid(value: T?) -> ValidationError? { - let rowAux: RowOf = row ?? form!.rowBy(tag: tag!)! - return rowAux.value == value ? nil : validationError - } -} diff --git a/Pods/Eureka/Source/Validations/RuleLength.swift b/Pods/Eureka/Source/Validations/RuleLength.swift deleted file mode 100644 index 41a1d0dce..000000000 --- a/Pods/Eureka/Source/Validations/RuleLength.swift +++ /dev/null @@ -1,84 +0,0 @@ -// RuleLength.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -public struct RuleMinLength: RuleType { - - let min: UInt - - public var id: String? - public var validationError: ValidationError - - public init(minLength: UInt, msg: String? = nil, id: String? = nil) { - let ruleMsg = msg ?? "Field value must have at least \(minLength) characters" - min = minLength - validationError = ValidationError(msg: ruleMsg) - self.id = id - } - - public func isValid(value: String?) -> ValidationError? { - guard let value = value, !value.isEmpty else { return nil } - return value.count < Int(min) ? validationError : nil - } -} - -public struct RuleMaxLength: RuleType { - - let max: UInt - - public var id: String? - public var validationError: ValidationError - - public init(maxLength: UInt, msg: String? = nil, id: String? = nil) { - let ruleMsg = msg ?? "Field value must have less than \(maxLength) characters" - max = maxLength - validationError = ValidationError(msg: ruleMsg) - self.id = id - } - - public func isValid(value: String?) -> ValidationError? { - guard let value = value, !value.isEmpty else { return nil } - return value.count > Int(max) ? validationError : nil - } -} - -public struct RuleExactLength: RuleType { - let length: UInt - - public var id: String? - public var validationError: ValidationError - - public init(exactLength: UInt, msg: String? = nil, id: String? = nil) { - let ruleMsg = msg ?? "Field value must have exactly \(exactLength) characters" - length = exactLength - validationError = ValidationError(msg: ruleMsg) - self.id = id - } - - public func isValid(value: String?) -> ValidationError? { - guard let value = value, !value.isEmpty else { return nil } - return value.count != Int(length) ? validationError : nil - } -} diff --git a/Pods/Eureka/Source/Validations/RuleRange.swift b/Pods/Eureka/Source/Validations/RuleRange.swift deleted file mode 100644 index 92da58983..000000000 --- a/Pods/Eureka/Source/Validations/RuleRange.swift +++ /dev/null @@ -1,109 +0,0 @@ -// RuleRange.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -public struct RuleGreaterThan: RuleType { - - let min: T - - public var id: String? - public var validationError: ValidationError - - public init(min: T, msg: String? = nil, id: String? = nil) { - let ruleMsg = msg ?? "Field value must be greater than \(min)" - self.min = min - self.validationError = ValidationError(msg: ruleMsg) - self.id = id - } - - public func isValid(value: T?) -> ValidationError? { - guard let val = value else { return nil } - guard val > min else { return validationError } - return nil - } -} - -public struct RuleGreaterOrEqualThan: RuleType { - - let min: T - - public var id: String? - public var validationError: ValidationError - - public init(min: T, msg: String? = nil, id: String? = nil) { - let ruleMsg = msg ?? "Field value must be greater or equals than \(min)" - self.min = min - self.validationError = ValidationError(msg: ruleMsg) - self.id = id - } - - public func isValid(value: T?) -> ValidationError? { - guard let val = value else { return nil } - guard val >= min else { return validationError } - return nil - } -} - -public struct RuleSmallerThan: RuleType { - - let max: T - - public var id: String? - public var validationError: ValidationError - - public init(max: T, msg: String? = nil, id: String? = nil) { - let ruleMsg = msg ?? "Field value must be smaller than \(max)" - self.max = max - self.validationError = ValidationError(msg: ruleMsg) - self.id = id - } - - public func isValid(value: T?) -> ValidationError? { - guard let val = value else { return nil } - guard val < max else { return validationError } - return nil - } -} - -public struct RuleSmallerOrEqualThan: RuleType { - - let max: T - - public var id: String? - public var validationError: ValidationError - - public init(max: T, msg: String? = nil, id: String? = nil) { - let ruleMsg = msg ?? "Field value must be smaller or equals than \(max)" - self.max = max - self.validationError = ValidationError(msg: ruleMsg) - self.id = id - } - - public func isValid(value: T?) -> ValidationError? { - guard let val = value else { return nil } - guard val <= max else { return validationError } - return nil - } -} diff --git a/Pods/Eureka/Source/Validations/RuleRegExp.swift b/Pods/Eureka/Source/Validations/RuleRegExp.swift deleted file mode 100644 index 601ecd056..000000000 --- a/Pods/Eureka/Source/Validations/RuleRegExp.swift +++ /dev/null @@ -1,61 +0,0 @@ -// RegexRule.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -public enum RegExprPattern: String { - case EmailAddress = "^[_A-Za-z0-9-+!?#$%'`*/=~^{}|]+(\\.[_A-Za-z0-9-+!?#$%'`*/=~^{}|]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*(\\.[A-Za-z‌​]{2,})$" - case URL = "((https|http)://)((\\w|-)+)(([.]|[/])((\\w|-)+))+([/?#]\\S*)?" - case ContainsNumber = ".*\\d.*" - case ContainsCapital = "^.*?[A-Z].*?$" - case ContainsLowercase = "^.*?[a-z].*?$" -} - -open class RuleRegExp: RuleType { - - public var regExpr: String = "" - public var id: String? - public var validationError: ValidationError - public var allowsEmpty = true - - public init(regExpr: String, allowsEmpty: Bool = true, msg: String = "Invalid field value!", id: String? = nil) { - self.validationError = ValidationError(msg: msg) - self.regExpr = regExpr - self.allowsEmpty = allowsEmpty - self.id = id - } - - public func isValid(value: String?) -> ValidationError? { - if let value = value, !value.isEmpty { - let predicate = NSPredicate(format: "SELF MATCHES %@", regExpr) - guard predicate.evaluate(with: value) else { - return validationError - } - return nil - } else if !allowsEmpty { - return validationError - } - return nil - } -} diff --git a/Pods/Eureka/Source/Validations/RuleRequired.swift b/Pods/Eureka/Source/Validations/RuleRequired.swift deleted file mode 100644 index 390fc1a65..000000000 --- a/Pods/Eureka/Source/Validations/RuleRequired.swift +++ /dev/null @@ -1,43 +0,0 @@ -// RuleRequire.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation - -public struct RuleRequired: RuleType { - - public init(msg: String = "Field required!", id: String? = nil) { - self.validationError = ValidationError(msg: msg) - self.id = id - } - - public var id: String? - public var validationError: ValidationError - - public func isValid(value: T?) -> ValidationError? { - if let str = value as? String { - return str.isEmpty ? validationError : nil - } - return value != nil ? nil : validationError - } -} diff --git a/Pods/Eureka/Source/Validations/RuleURL.swift b/Pods/Eureka/Source/Validations/RuleURL.swift deleted file mode 100644 index 8c5f31d4f..000000000 --- a/Pods/Eureka/Source/Validations/RuleURL.swift +++ /dev/null @@ -1,54 +0,0 @@ -// RuleURL.swift -// Eureka ( https://github.com/xmartlabs/Eureka ) -// -// Copyright (c) 2016 Xmartlabs SRL ( http://xmartlabs.com ) -// -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import UIKit - -public struct RuleURL: RuleType { - - public init(allowsEmpty: Bool = true, requiresProtocol: Bool = false, msg: String = "Field value must be an URL!", id: String? = nil) { - validationError = ValidationError(msg: msg) - self.allowsEmpty = allowsEmpty - self.requiresProtocol = requiresProtocol - self.id = id - } - - public var id: String? - public var allowsEmpty = true - public var requiresProtocol = false - public var validationError: ValidationError - - public func isValid(value: URL?) -> ValidationError? { - if let value = value, value.absoluteString.isEmpty == false { - let predicate = NSPredicate(format:"SELF MATCHES %@", RegExprPattern.URL.rawValue) - guard predicate.evaluate(with: value.absoluteString) else { - return validationError - } - return nil - } else if !allowsEmpty { - return validationError - } - return nil - } -} diff --git a/Pods/Local Podspecs/Eureka.podspec.json b/Pods/Local Podspecs/Eureka.podspec.json deleted file mode 100644 index 5b5ace588..000000000 --- a/Pods/Local Podspecs/Eureka.podspec.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "Eureka", - "version": "5.3.6", - "license": "MIT", - "summary": "Elegant iOS Forms in pure Swift", - "homepage": "https://github.com/xmartlabs/Eureka", - "social_media_url": "http://twitter.com/xmartlabs", - "authors": { - "Martin Barreto": "martin@xmartlabs.com", - "Mathias Claassen": "mathias@xmartlabs.com" - }, - "source": { - "git": "https://github.com/xmartlabs/Eureka.git", - "tag": "5.3.6" - }, - "platforms": { - "ios": "9.0" - }, - "ios": { - "frameworks": [ - "UIKit", - "Foundation" - ] - }, - "source_files": "Source/**/*.swift", - "requires_arc": true, - "swift_versions": "5.0", - "swift_version": "5.0" -} diff --git a/Pods/Manifest.lock b/Pods/Manifest.lock index 82e0343dc..9a7875837 100644 --- a/Pods/Manifest.lock +++ b/Pods/Manifest.lock @@ -3,13 +3,11 @@ PODS: - Charts/Core (= 4.1.0) - Charts/Core (4.1.0): - SwiftAlgorithms (~> 1.0) - - Eureka (5.3.6) - ShareClient (1.2) - SwiftAlgorithms (1.0.0) DEPENDENCIES: - Charts - - Eureka (from `https://github.com/xmartlabs/Eureka.git`) - ShareClient (from `https://github.com/loopandlearn/dexcom-share-client-swift.git`, branch `loopfollow`) SPEC REPOS: @@ -18,23 +16,17 @@ SPEC REPOS: - SwiftAlgorithms EXTERNAL SOURCES: - Eureka: - :git: https://github.com/xmartlabs/Eureka.git ShareClient: :branch: loopfollow :git: https://github.com/loopandlearn/dexcom-share-client-swift.git CHECKOUT OPTIONS: - Eureka: - :commit: 044e31674d319c8edb19d993a5f8ea4e24641542 - :git: https://github.com/xmartlabs/Eureka.git ShareClient: :commit: d7a3323a014d41827ee177a92d528786f9f09e75 :git: https://github.com/loopandlearn/dexcom-share-client-swift.git SPEC CHECKSUMS: Charts: ce0768268078eee0336f122c3c4ca248e4e204c5 - Eureka: 28ad9dec6286cd7cd601fdf8e8df39bb7356a8f4 ShareClient: 60b911c95e73b0ea9c5aad6d194a9c6b5f34b741 SwiftAlgorithms: 38dda4731d19027fdeee1125f973111bf3386b53 diff --git a/Pods/Pods.xcodeproj/project.pbxproj b/Pods/Pods.xcodeproj/project.pbxproj index 9b39dd4da..6a2b71889 100644 --- a/Pods/Pods.xcodeproj/project.pbxproj +++ b/Pods/Pods.xcodeproj/project.pbxproj @@ -9,18 +9,14 @@ /* Begin PBXBuildFile section */ 0031CEAD857A1BA158C0D2BE1870A438 /* Animator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB10D8BF2F0C2E9B18C2130751D75984 /* Animator.swift */; }; 00351B69906B07C1950B97C40F65FABE /* RadarChartData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C51F17D0D2858BA593E783811A2CF7E6 /* RadarChartData.swift */; }; - 008E02A88C10E3688B4431BFD393F0BE /* DateRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77F3AB1C7064C79A86A62BE1BBB6068C /* DateRow.swift */; }; 00E171A24B8B214FDC447BED6BB62C31 /* BarLineScatterCandleBubbleChartDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90B940BBEE17272D0FA9602CB3F1E1 /* BarLineScatterCandleBubbleChartDataSet.swift */; }; 0382A6D93C8BB1218EB1DBA3CF1B3BA7 /* BubbleChartData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F1D0991883487B252954C6AD93262DD /* BubbleChartData.swift */; }; 04360A4DF646DD99E7AF0AC83FAA56AB /* CandleChartDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF58BE2BB556CCFFD3F153CD246BB3B2 /* CandleChartDataSet.swift */; }; - 0568E4D0A08C598F5AE7DDDA0DD59923 /* ActionSheetRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 463B1FFE93072EEE11F06544067346A2 /* ActionSheetRow.swift */; }; - 05E063B80D84B40AEEDC7DC5797FA3FD /* InlineRowType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1363CD1C407802F39155303528BAB434 /* InlineRowType.swift */; }; 078FF70DB7D0BDBE768AB0FA72E19D14 /* RadarChartDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 631260726B8649201DF3262C04AE423B /* RadarChartDataSet.swift */; }; 094130854F03255D5BD94EAE3C6797B4 /* LineRadarChartDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35C07C88A101976F7D43BE7BE4673A37 /* LineRadarChartDataSet.swift */; }; 0BB393E009EB565C1342EEA8D02C9098 /* BarLineScatterCandleBubbleChartData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C00C3496A26A3B8ED45E73F3E3AFDAC /* BarLineScatterCandleBubbleChartData.swift */; }; 0CA67293F83A1EB1004D16D1878B4007 /* LineRadarChartDataSetProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D4A498BBC8E4513D82F78F75036A171 /* LineRadarChartDataSetProtocol.swift */; }; 0D73D4FBCA95E51CDA036332FE5031B4 /* Pods-LoopFollow-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 7E41188828C3ADE7A96C4DA6ED1CCDA2 /* Pods-LoopFollow-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 0FC606DB282EDA95D6DA95289AA85C61 /* DoublePickerInputRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74854C0CAA3FF3AAE613D06C13CA0511 /* DoublePickerInputRow.swift */; }; 1251E7AD01C341EFCB0A7A4B70942433 /* BarChartDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CAB9310175D2D05534B5FAA40BDABB8 /* BarChartDataSet.swift */; }; 1267184A54805C22C30908BFA58279C9 /* BubbleChartDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9B0CD14B51E9BB24006EA87AD5758F /* BubbleChartDataProvider.swift */; }; 13CC355E8703534C3651E1676F54989C /* Compacted.swift in Sources */ = {isa = PBXBuildFile; fileRef = 417ACD10C75E540CA5E23120521A8BA9 /* Compacted.swift */; }; @@ -28,7 +24,6 @@ 1611A02A50E8ECD5F31ECF1CDDCCFF7F /* LegendEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 719A12B42A88A75148E0A9C8D6C618F0 /* LegendEntry.swift */; }; 16362413D2756A8A8BD2E5D20726520D /* LineScatterCandleRadarChartDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 524C2C5AB08DAC0B62469BC9BAD2BBFC /* LineScatterCandleRadarChartDataSet.swift */; }; 16C5A86B21B4BF6C530A96DC25C9ECDB /* PieChartDataEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9D1DB1BA530D126D35D218E4439458 /* PieChartDataEntry.swift */; }; - 184CBCE994131F7140410E0B17E94ADE /* SelectorAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2DF6A6FC9E680841A26786E9267590E /* SelectorAlertController.swift */; }; 18F6D2659567747929F4F67444C6DB5A /* Indexed.swift in Sources */ = {isa = PBXBuildFile; fileRef = F574273E89B9C97C369C430B67186E14 /* Indexed.swift */; }; 195360D36D20DEA54AC170FB9C9ECB44 /* Suffix.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72D375E25E043A018C9D9E87EF64256 /* Suffix.swift */; }; 1956BBD2D0434340107A6BE2B68FE4FE /* Intersperse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3897C4A6B7F162E479564409C5CEFB13 /* Intersperse.swift */; }; @@ -36,66 +31,44 @@ 1C7423F964AEF53EA37E1B4F2F4C8A3D /* MoveViewJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0651C9C3E1F7212ACFC0DF4E3E00323D /* MoveViewJob.swift */; }; 1C903C670AE552000F47F9FF69946132 /* Chunked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 593C5B76ABCE8F5324D1A34783FACC5B /* Chunked.swift */; }; 1CFCB7624A2D0A7B088428BB898E452E /* Chain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A34EC75AF41BCC80FDC54C5AB3DC008 /* Chain.swift */; }; - 1E048CCA6D1D1B00892BB5A0D87A4D2E /* RowControllerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5833C2A8E049B2690A7651CB63B183C /* RowControllerType.swift */; }; - 205C36FBEAF46B5278F998984AE7AF18 /* LabelRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEB89F713B887858D9493B533D256B7B /* LabelRow.swift */; }; 22725F307C8A12F85C2E583CE9D67A68 /* CrossShapeRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E09B56CDA6CE4B961DE77CE43BF6517D /* CrossShapeRenderer.swift */; }; - 25740149FB6D4EE7E113A1FA47B73554 /* SelectableRowType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF612F2F2ED80B5AA7DA7388FF9D3A9 /* SelectableRowType.swift */; }; 25D7B9CB6D83A2899E364531C8B465E0 /* XAxisRendererRadarChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52FC9F1F7847D290D9CD7AB6024341FF /* XAxisRendererRadarChart.swift */; }; 26414DCD243931AD2DF5A7FAEFA9A2F1 /* AxisBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC79A108E546A80FD2E858996EB42D7 /* AxisBase.swift */; }; 280B9154E146F8A47E47F484ACC815F0 /* ValueFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7ACE4DAB3C5C84A671AFE0A24BC32E8 /* ValueFormatter.swift */; }; 28699DA45BB94636CDD55D302CC36D4C /* FillFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421EDDDD69FD4DEF3F201DC7F4EDD885 /* FillFormatter.swift */; }; 28C0C336867276BB3F88C94BDCF5924F /* AnimatedZoomViewJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F619C3ACAC8BB97F4CBF04C055CCB24 /* AnimatedZoomViewJob.swift */; }; - 28D8448B496B314D91BAE0EE321646BE /* SelectableSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C040C5E0351A26CDFA6243686064CC2 /* SelectableSection.swift */; }; 290F4D27B047ECE9E11FE65D45659AA9 /* Platform+Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0ACEEB9003A6780541B487AD7AD20F /* Platform+Color.swift */; }; 295E7D31962EAA43CE02A22D291AC827 /* YAxisRendererHorizontalBarChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = F448E25F6F3095E674026E503F71BED7 /* YAxisRendererHorizontalBarChart.swift */; }; - 298553E2ECB829B696DF592E6990249A /* HeaderFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54DF8106B99B7AAD32343B0FCA61D868 /* HeaderFooterView.swift */; }; 2D7A2EBC2FC440678E3F1D63D375EA31 /* CombinedChartData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7210F7399765A3931B62D5370EF1B4FC /* CombinedChartData.swift */; }; - 2E35BA439FF3906792D64F465A358A13 /* TriplePickerInputRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91757002F52622DC87D0A1967FE2900E /* TriplePickerInputRow.swift */; }; - 2ECA780FC4691E846B5EB4919997A5F4 /* PickerInlineRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A15229B55953AF8036FBE93E658A18C9 /* PickerInlineRow.swift */; }; 30F6164C5F15445D6BC5D8B22A4131BE /* Permutations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D4695EDCBBD2F14A96C466589AC26B4 /* Permutations.swift */; }; - 31B0A34AA01CC0E71ECA6B7BA7D7EC88 /* AlertOptionsRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DA7213CB1ED6CD5B946A3A1EA36CFE3 /* AlertOptionsRow.swift */; }; 341C4C56C45E00A6B58E63A7EDF7F7C1 /* BubbleChartDataSetProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97C19D311E35002501E760D893F35E2F /* BubbleChartDataSetProtocol.swift */; }; 35BFC4C3A46CF7EEC15BEA91AC6CE39B /* HorizontalBarHighlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 896D7BB34CDE275E92DC01AC1F533F26 /* HorizontalBarHighlighter.swift */; }; 3641D2A955EB369FD725C07BAF82E96F /* Renderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6124C7D917D4C7DC28F7503EBCD596D7 /* Renderer.swift */; }; - 3719CDB1B700C19AB04163DA75B09815 /* Core.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649C6079A86B7348DD9A91E50537DC34 /* Core.swift */; }; 37C035EBB523276E74097ACE8B88D7DE /* ChartLimitLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE2943C8EAB9AD013BB0D24A3E135AF /* ChartLimitLine.swift */; }; - 3927F2677F09C815B6BC70E94023E335 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D245E0514AAC1A2B9A6D5EA2F383E90F /* UIKit.framework */; }; - 3A84D68B004228D5F71FC0FC889FFC07 /* RuleLength.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DB609C6137ED4B1E648CCC3F5325177 /* RuleLength.swift */; }; - 3AE49F9C5930DCD727D11A42EA89E640 /* FieldsRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B501803A2305DE95A984916552E769A /* FieldsRow.swift */; }; - 3AEA68431EF66EB90B4E3F1074CF8125 /* RuleEqualsToRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376D2C21D79D7A27AA844F0E178BC43B /* RuleEqualsToRow.swift */; }; 3BCB9E6E546029CDD29933889ECF313D /* LineChartDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F86CBC5DB9A328A34A07A52C1CD5605 /* LineChartDataProvider.swift */; }; 3BFFA0E6A588A61617609032FF7D48DF /* SwiftAlgorithms-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 44A177D8AC2CE9A04AEC62303E1C012C /* SwiftAlgorithms-dummy.m */; }; 3C1FAA2B78B8750635AC8D2B088703BA /* PieHighlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C97D67CE26870DFAA8AFE096DDF8F6CF /* PieHighlighter.swift */; }; 3C863D4E5E34C7C2173E0B557157A469 /* ChevronDownShapeRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10E2D718053926F7C362A0242C707398 /* ChevronDownShapeRenderer.swift */; }; 3CCE8A616A323BDE56FA02CAB953BA18 /* BubbleChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F21D683A3D7551D0D6ED48F9BCBDDCB /* BubbleChartView.swift */; }; - 3CEB7EACBD4EB21B47517CC48A994655 /* RuleURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2561CCE278142444D8BDF2C3820193E /* RuleURL.swift */; }; 3F7DE7DD33C07E28ABBF80B3AFBCDF44 /* HorizontalBarChartRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC882B9BE58FE2D2FCB58677AABEF941 /* HorizontalBarChartRenderer.swift */; }; - 40E090A253097EFA55694770F19FEBAE /* CellType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F15455C959FB8C71196A046437C0EED5 /* CellType.swift */; }; 42361A518637B458CBB971D21E43346E /* CombinedChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B4B217AE01ABE5E9A1B3FEE24ACD3E /* CombinedChartView.swift */; }; 4304139B7049F735A4879E7A14DCA236 /* ScatterChartRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D975F08944D56553EE8F518D883AABD /* ScatterChartRenderer.swift */; }; 4412B151ED4921518E19FA644D282D99 /* LineRadarRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B0F987BD66CF8FC4B3368F8EB644D6A /* LineRadarRenderer.swift */; }; - 46A11B142CC018088902C05BA2707513 /* Validation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9D47A544442AC0D1EACE1A847B3DAE4 /* Validation.swift */; }; 46D629F718073D7735C888E42E1EAB17 /* PieChartDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E1E5F4713322837D7506143802ED222 /* PieChartDataSet.swift */; }; - 4889C566B538BE9C98F084E685CB3C8E /* StepperRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E05949DEB432C9594F64A3B0BEB4BDD /* StepperRow.swift */; }; 494DAE02C6BD413F95D9B35E4C9B8473 /* Platform+Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7724018046776D153BF2F519E62ED100 /* Platform+Accessibility.swift */; }; 49F28A30CAC0F0F5E874185195C05EAA /* Joined.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B5D397F5631F7981765A1517F8E752B /* Joined.swift */; }; - 4BADBE8D79E82E6BCA97708DB5E0623C /* RuleEmail.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8C6D9AA85CFF72CA6E5519187388A6F /* RuleEmail.swift */; }; 4C3E4C1A74F42DC91C726DC9BD262891 /* CombinedChartDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF14DB72944084EE52D83024A8168D3A /* CombinedChartDataProvider.swift */; }; 4C7DA896256A295E7DEADFCA8B0136EE /* LineChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0047677618FCD329E548FDDC0454A8AC /* LineChartView.swift */; }; 4CDE5AD6B91F8E1859C196089A01F927 /* BarLineScatterCandleBubbleChartDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CDBEAB7607F1CDCCB6229DF611C0D3B /* BarLineScatterCandleBubbleChartDataProvider.swift */; }; 4E0CE7C1CD75F9926C1453AC1AFCFBF7 /* BarChartDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42EF3C460FF3BA09B964B4B078B2FFDC /* BarChartDataProvider.swift */; }; 50711078539C633D63AEE4D4F4B0642D /* TransformerHorizontalBarChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 127D620D9103C646821EE1A9FFDC6AB4 /* TransformerHorizontalBarChart.swift */; }; 56C71B82E08666FBEC2E57178FF7EB18 /* Platform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22D8A4241B43E6A486B3ABFE7CE176D2 /* Platform.swift */; }; - 56E322CBEF2E9EA5777EC3846F888BDB /* PushRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F117C19DCF36AF0FB2648E2D25EA325 /* PushRow.swift */; }; 5751FF8F73931CF52C588FC93DF87069 /* HorizontalBarChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F34D137537BF079F35059435323D7452 /* HorizontalBarChartView.swift */; }; - 57FB8912F3DEFA71CF0932D6338A2BF9 /* TextAreaRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ACC6C7F798E27DF68E09694E5645ECF /* TextAreaRow.swift */; }; 5829E60F3B1D9D7166301BC4AA17E83D /* ViewPortJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3651675689D3E922771781C1C7EBF4 /* ViewPortJob.swift */; }; 5851F6B6C46E7426DA8693AD1781EDB5 /* ShareClient-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 185455CC1A16D809E6CD52CA7ED7C218 /* ShareClient-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; 585FD9DA0B21341F777A5B39ED1CA562 /* RadarChartDataEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 323A7094F3358B4E346E033FE9AAF79E /* RadarChartDataEntry.swift */; }; 58889ECB9711CC8D285F5689C9DF4584 /* ChartColorTemplates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04CA046D8066D2043078135381049F0A /* ChartColorTemplates.swift */; }; - 5CF70EB49A6DE1B80886F0FD3F780522 /* MultipleSelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C7DEB37A5D25E3675AB9B8D8C02F7F /* MultipleSelectorViewController.swift */; }; 5D998F2454D188B00E592ECF3DFB6371 /* PieChartData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85FBB9A8A3775D305F8D23740840158D /* PieChartData.swift */; }; - 5DDCB215B020A3C11AAF9EA8766AD7D5 /* PresenterRowType.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA24EB386C74479E70533D8F03191FED /* PresenterRowType.swift */; }; 5E6D0EA6A7250B44F8D1416E97248FC2 /* Split.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35CE8AE4FB4FAA915248FFA91AE62338 /* Split.swift */; }; 5FFB0442423AFC0BFC70DA91859C1FA7 /* SquareShapeRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8151C448D7FF1AE4C18A1D9BAAB84F5 /* SquareShapeRenderer.swift */; }; 60F208C320EA137DF1E35F58D848DE9A /* CandleStickChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16B9EB6993E134E20A22EFB3F975FAAB /* CandleStickChartView.swift */; }; @@ -103,7 +76,6 @@ 6138784D2228EEEEC197C541EDB9282C /* BarChartData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640DB089E7BC7FA9C4C5D67D1E58393A /* BarChartData.swift */; }; 62425D090FD054F868558C00A83BBCF6 /* MarkerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C90F7AE53155C1804331C13F95834B2 /* MarkerView.swift */; }; 6291997C9089F0F4FF4B0172331B31AC /* RadarChartRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D024D84271BC3B8711840FD1825F8191 /* RadarChartRenderer.swift */; }; - 6328C306E03099885C3EFCA798C9F5D1 /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D826433F23D509AB5013DA7F1D59F6 /* Operators.swift */; }; 6351CC48D4CD08A64B93027110D0BFD7 /* Marker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347B2727DAB3007AE54E938A6EDAC926 /* Marker.swift */; }; 646FE6A25A724BC685702BFB101E021E /* ChartBaseDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14145B7AD1FC1EC76D1DB43D3100AC2 /* ChartBaseDataSet.swift */; }; 64B59451EB2C31914C278348F911C715 /* CandleChartDataSetProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E41D7AE4F0AB44736E2E49B07F83AC6 /* CandleChartDataSetProtocol.swift */; }; @@ -113,51 +85,35 @@ 68047C5BD5B0C77274FAB5CD38F2EF0E /* TriangleShapeRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9222FB7877363AAC2E66F4E9BD6BE22F /* TriangleShapeRenderer.swift */; }; 68A3CE924047F6FF8D7BDDE68BCBD4C1 /* BarHighlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB44185A42C56CCC7B7F24D8A4259044 /* BarHighlighter.swift */; }; 6916BF3166BE703933C43515B50ADBD7 /* Windows.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D152598E6F7AD2209244F3255EBE2B3 /* Windows.swift */; }; - 697D5116696270B7361C180573C495D3 /* AlertRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39347FDF57F777C67EA286FDF94A50A0 /* AlertRow.swift */; }; - 69B91E7FF91730260D4137B09C015636 /* Protocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B8E73E6E0A1CA1E6E26B307989E3F9 /* Protocols.swift */; }; 69CED83D06805776035CF76DD1DE2C2A /* ChartViewBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7FC29B40706B2526531F041CD6E5F37 /* ChartViewBase.swift */; }; 6B89D2717D3A324DF55655D69A21AAAC /* ChartHighlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9A488FE97E31BCF19AE78B98C18EE81 /* ChartHighlighter.swift */; }; - 6B905F2AC740B767D002FBB0CA795A7C /* RuleClosure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 445973D461C2CC4957A3BB54B5427B6F /* RuleClosure.swift */; }; 6B9186D7227163690B8397FAADFF77D3 /* DefaultAxisValueFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 194A8936BCA6DD2C7CC0C0003C0F27BB /* DefaultAxisValueFormatter.swift */; }; 6D878FE6964089A7F47EB406F566ECC0 /* ScatterChartDataSetProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60B7899F2AE639742DDEE13C16276275 /* ScatterChartDataSetProtocol.swift */; }; 6E65B4450EE038F9F1DF3F7443BF53DB /* Unique.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99770C7A6099A970C34D7824A7CE280D /* Unique.swift */; }; - 6EC583C2F9CEC762B806A22700E9D800 /* CheckRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D1BC6FE5DDEE26D858B939E9B4E82C /* CheckRow.swift */; }; 701A1DF09A74742FFCF8BCE3F5A9370B /* SwiftAlgorithms-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 9B7EBBA8C350C40C7F423CDD638EDB2E /* SwiftAlgorithms-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; 70CACB146AA2603CDD775285FA9A4CE9 /* Range.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35D58369D6A0FF961EC41F36B0B7D29C /* Range.swift */; }; - 73780F56C4A6F73B569E3009F9AEF09F /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2646204CB36CF4EF6057C2B31604DCA0 /* Helpers.swift */; }; 75E493B239F29100777CBB38AAAE08D7 /* BarLineScatterCandleBubbleRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5AFD1B5788F5477102C8DD0F7EAF50 /* BarLineScatterCandleBubbleRenderer.swift */; }; 782E096BE9488B722DA0F64794A45D40 /* ShareClient.h in Headers */ = {isa = PBXBuildFile; fileRef = A5822CA2A05671BAA9BC5BD4A3BA5D6B /* ShareClient.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7899A9D1751880EC7A2649DF35617F67 /* Sequence+KeyPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2629C2C6B479EEB26E2F6DA58C30B81 /* Sequence+KeyPath.swift */; }; 7A52005D206FA5BEC462EC1ED5E34423 /* XAxisRendererHorizontalBarChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48FC9D5D68CA4D70155A8959C1179E28 /* XAxisRendererHorizontalBarChart.swift */; }; - 7B59CC19DE0A8FC37B7E78A578968DD4 /* PickerInputRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6207FB4695D962F2B6DF8DD11635A620 /* PickerInputRow.swift */; }; 7B8211FE68633C9F9EF8C2729FACE32A /* MinMax.swift in Sources */ = {isa = PBXBuildFile; fileRef = 939E4F680A1A71C7BFC00F9917F3E5EB /* MinMax.swift */; }; - 7BD16251FF2F7EA5F18AEACF5DE44C23 /* DateInlineFieldRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FA087B627BA46F392CACF1BD142B7AA /* DateInlineFieldRow.swift */; }; 7C09D13338B7A0BF4A0EB9763EE60339 /* ChevronUpShapeRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 910799424A3C8A57366C4CE76BE79683 /* ChevronUpShapeRenderer.swift */; }; - 7F7B5556719352AD2C7A756587D348BB /* DatePickerRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 914C75C3E73448BCC86DF693AD52CAF8 /* DatePickerRow.swift */; }; 80E88D848625ADD942D49552233940C3 /* XAxisRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837358B5E2BC5C139A82049EB2895D05 /* XAxisRenderer.swift */; }; 839F873A001FE9169E9978BE89195B5F /* ScatterChartDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02BD1A15A0561B886A937317473FCF4D /* ScatterChartDataSet.swift */; }; - 869DB6D9A7692FDEC1FF5537C0413274 /* NavigationAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8F21E10186E4376CA7F0655ECE328A5 /* NavigationAccessoryView.swift */; }; - 873FEA8F7A7D862C00A2335E311DEDC6 /* SegmentedRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03BC64D825C8295FFA3E396F2B8D254C /* SegmentedRow.swift */; }; 8809E0E77C38BC2164874FB080404B2C /* Transformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 162C2F9A6894149851210E55E6188A43 /* Transformer.swift */; }; 88DD483D2937E149E31768EAB7567563 /* ZoomViewJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFD6267883E63FA87B3CF88C8773CCD6 /* ZoomViewJob.swift */; }; 8BF58ABE3315B02170CA950504A6AF7D /* Product.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80DDB7F7B5FBDC211213044A06C72601 /* Product.swift */; }; - 8F2CC08A2EA2A12CF906555985307D8A /* RuleRequired.swift in Sources */ = {isa = PBXBuildFile; fileRef = D359B9F836FA5FFF4380D1F522C364D8 /* RuleRequired.swift */; }; 8FD826D1119F87EB638FAC888F74BE51 /* LineChartData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E12C56B79F0D3D4E449C70724B3B0D2 /* LineChartData.swift */; }; 8FDCA72F349897774711AA8AB1F23522 /* CandleChartDataEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1714C5DC04BE72E509DC4E7686AA857F /* CandleChartDataEntry.swift */; }; - 8FF92C53FAAAC2ED824C1E535C187424 /* SwipeActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45C6E3F0D68C882F76B85AE0C2AE35EE /* SwipeActions.swift */; }; 905D5792F0F95A1D50E0FA79AFF58278 /* FlattenCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C320E80DD7BFAAA5494A8BB4D7A226F /* FlattenCollection.swift */; }; - 91B0EE8C7679DFFD7D0D27DC32BD07D3 /* RowProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2748ACE938277243245CED76CF901E32 /* RowProtocols.swift */; }; 92C7BAE4CC1EA95C35F3CE1D2DFB463B /* YAxis.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8FE53DE5C6214CB9D501CF937A4A88A /* YAxis.swift */; }; 92F85BA954CAD476F1964EFD067DF138 /* LineChartRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 791893088C1075827778DDAFED99EF63 /* LineChartRenderer.swift */; }; 93E19EA5B507AAA18B0961FD52FDD452 /* CircleShapeRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFA8A221F0F4BA00670608D2F68AFBC2 /* CircleShapeRenderer.swift */; }; 941AA0A965F188AD2989B5B10CAEBB5D /* BubbleChartDataSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = F84585BA74D5FD5927560642A3D8484D /* BubbleChartDataSet.swift */; }; 95826C07F14DFF13BAA349F49C84917F /* Platform+Graphics.swift in Sources */ = {isa = PBXBuildFile; fileRef = EECF04D337A793DE5FFF6D492CE60D96 /* Platform+Graphics.swift */; }; - 95DEEE38D632C816737E8AB538428A50 /* SelectorRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DAD47DCABD4ECD1C9DFFEAA022A9E17 /* SelectorRow.swift */; }; 968CA639D9CA2E9D8A41F1038F03C2AE /* Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E525EC535F76BFC47EF4F3E62BA3654 /* Description.swift */; }; 97528183B1A64DF1AFB370063327C922 /* PieRadarChartViewBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A46A27661B10DDD04786AD58706C55BC /* PieRadarChartViewBase.swift */; }; - 9760DCC9A11340DC2E74E00007F9D3DF /* Eureka-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 47135570F8D5A9E934D001C53B4C0A79 /* Eureka-dummy.m */; }; 99288EBF5E5716EF2F27C0978E602446 /* AxisValueFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA175D42C8D8C40B7D7B4EBA4DFCB613 /* AxisValueFormatter.swift */; }; - 995A8A4C2445BD86C2DC375B14394682 /* ListCheckRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50AFF7F977813F95AC11C077C50CDEF6 /* ListCheckRow.swift */; }; 99F7945EE1671BCBB0ED04D6AC9361C4 /* Reductions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAAB281721B2A421266EF830D312BC5A /* Reductions.swift */; }; 9BCAF45D55CDD35A801F95CFE539AE56 /* Platform+Touch Handling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E2A8B52E2E9746619F1216001D0241C /* Platform+Touch Handling.swift */; }; 9C6B381899CCB8A5784CAEA7FCF140F8 /* RadarChartDataSetProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FDEC8B77727E37AE06D95B16E7B4E24 /* RadarChartDataSetProtocol.swift */; }; @@ -166,8 +122,6 @@ 9FDE058C5DF72BAE2D498F2EEDD060CA /* CandleStickChartRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF9EB617791E412932FA1E750D934E36 /* CandleStickChartRenderer.swift */; }; A09370996D959442F01BF83FAD70F978 /* ScatterChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B26B283F8811F230A0E0B4FDD225F42 /* ScatterChartView.swift */; }; A18DBD8983244A378ED1D37297C8B2D1 /* IndexAxisValueFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF0AC8618DB750D79CB08822A30BEEB0 /* IndexAxisValueFormatter.swift */; }; - A2254B25181B13F76C598F0CF5A40FE7 /* RowType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AB5F5FEADDDFDB8B4D1F3BFE449F81E /* RowType.swift */; }; - A3A234EA035289FD713D51AEAF1072C8 /* Form.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0351341FCAFDAA74F7778AF04C9EBE3 /* Form.swift */; }; A3BC53817CA91A9716FD1CE460F088A5 /* ChartDataSetProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D18390EFF0AD4A6E0E073DD78EC6A8A /* ChartDataSetProtocol.swift */; }; A5A2B62222D6B52255157E45BBFC2D78 /* DefaultFillFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74FF6EA327297107B81FF9FC08218F94 /* DefaultFillFormatter.swift */; }; A90A272F4B1735E0644FD2464064E428 /* LineScatterCandleRadarChartDataSetProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AE70E6A38EFD6CC5B06E07C9B8C51DE /* LineScatterCandleRadarChartDataSetProtocol.swift */; }; @@ -177,47 +131,30 @@ AE29D92CF287E95326A50122F8279EE8 /* Charts-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 64CDF688CF8AB9F834E7C349894B685F /* Charts-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; AE3DA54A9DB44D03DF6609CCE1E028F6 /* Pods-LoopFollow-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 28609CC9D3828C150821F3AA18752A77 /* Pods-LoopFollow-dummy.m */; }; AED0A95039ECF3204009B8DE9DAE62CE /* FirstNonNil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24CB775967538B3FB4C47212A2F8B948 /* FirstNonNil.swift */; }; - AF4BD9FB7CC229682ED2E26F7E6106A7 /* PopoverSelectorRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F364E36A952C20EBF4684241A269EC /* PopoverSelectorRow.swift */; }; B0A7C2F3CAB5C031F93D81777FD5A421 /* CandleChartDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78F904F74C2B50A8192A7167CAC3C05E /* CandleChartDataProvider.swift */; }; B2162A26B36709F6F1B6976DF1A65DAB /* DataRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E40805D1503067A2E679DBFB30BBB8C5 /* DataRenderer.swift */; }; - B24FAE4A9C6BD92AD64EE7859C2A9D9A /* MultipleSelectorRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5A708FABC6817F1F5703662646AD8A5 /* MultipleSelectorRow.swift */; }; B2DD7CF96035374C7AC80D88B8904691 /* ShapeRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7614E1E025FCFFE4FAEA31D79ABD0661 /* ShapeRenderer.swift */; }; B4015BE1313094DACA3DBB86191C5030 /* CombinedHighlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABCA6CDFA3950D0A999A692D99867D2 /* CombinedHighlighter.swift */; }; B4D7DBBE6B8329E0947FFC9D1721B3CD /* YAxisRendererRadarChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = D35FFE6EE2BE8E73855DC66951E7AA12 /* YAxisRendererRadarChart.swift */; }; - B5E04676C82AC7C36204AB782DD780B9 /* SliderRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0953166A991EECDD39429ABF7F7A9212 /* SliderRow.swift */; }; B6580C63B353D52672121417CD3E4DF9 /* LegendRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC42ADFADC10F4104EF63EB5AAA16E1 /* LegendRenderer.swift */; }; B709B3F0FD7D89C78DE139C07924760B /* Highlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 910C2C8431806F8015BAADD54CFF96DC /* Highlighter.swift */; }; - B88B7CAA00CEB3B5032091BFA45BD219 /* ButtonRowWithPresent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60C304388A3314E30D8620C11790DC5B /* ButtonRowWithPresent.swift */; }; BDFE5C038BFAEDE5843A3BE7B78DAB9B /* AnimatedViewPortJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7554208627CE880C0922FFD835465C /* AnimatedViewPortJob.swift */; }; BE85BF81DBD61E106AE6168F19DBBF0A /* CandleChartData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3F77E6983047F48C68D513994F9217 /* CandleChartData.swift */; }; BEE4705FCD33AD7CCB2B1C8C593CE13D /* DataApproximator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C12EA28BEC17D2657CB75BFAEAC0C7A /* DataApproximator.swift */; }; BF4F92B93CCB21881A32201D5283A39E /* BarChartRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7017CD37B02CFCAD0F28034D7D28355A /* BarChartRenderer.swift */; }; - C11DBC286F46067EFC0E4C15FF014CC6 /* RuleRegExp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ABEC5837F666677B0816396BF6EDB41 /* RuleRegExp.swift */; }; C13FB2874A90207FF36E5D61F352A95B /* Stride.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A750E80653FD14B27F8579B9E00B94F /* Stride.swift */; }; - C241A011154907D4A4B377DB529E9C9D /* DoublePickerRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC8B3414FF880FE6C38F41AB0DC76F5 /* DoublePickerRow.swift */; }; C2CBE67B4FDE638A3DEB5A444B075D28 /* ScatterChartData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0744A6F4A9EC646DAE57C8935BB5D5B /* ScatterChartData.swift */; }; C3CF1A177501EA2D297FDABC1E531C77 /* ChartDataEntryBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F3AE2D43E5A05FCCE568A3DEC1D68F /* ChartDataEntryBase.swift */; }; C472373DFA73FC416CCDB97CFA586249 /* ChartAnimationEasing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E81B8B1A7437FAF56106A8F686AF40F /* ChartAnimationEasing.swift */; }; C66E82B3E5B66A711DD8F256171BE4B8 /* Rotate.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD62CC8A6BE60A76674236FFF80DBAF /* Rotate.swift */; }; - C6C91F0B055C43A0B6A6865AFDB7614E /* GenericMultipleSelectorRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F082AA646AB49C314004DBB19E3F3085 /* GenericMultipleSelectorRow.swift */; }; C7E4A381095F15F90606D7C157BD9ABA /* ChartUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73DBDE6ADB4EFCB0BEC6D4BEBE993C55 /* ChartUtils.swift */; }; - C87A5CE74D4AF4B1180FA67B63CF7A5A /* ButtonRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAD950E015B2FC6DE72638E5978614FC /* ButtonRow.swift */; }; - C959E50620A9AF66A128EE6249CCD68C /* Row.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7391F5AD340E2F5C60BC1905E0DDC412 /* Row.swift */; }; CA589FE7EC72C5BD7743D8EADBDB914E /* BarChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0055C3AB0AE0067E0AF50DA569843398 /* BarChartView.swift */; }; - CC70380478EE7E1E404437B6AA51BA33 /* SwitchRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 064C06660526CDB34A90BC0F8952E907 /* SwitchRow.swift */; }; - CE0E20A3203F862A375294551E8B8862 /* FieldRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC5DB7370FF5E560126694672372703 /* FieldRow.swift */; }; CE4052508D5568AC510602198FDDF212 /* Combinations.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC11923FC3EEE39D1E9BB7D730ABD68B /* Combinations.swift */; }; - CF4332D8352284FA91A83CD4EF21DA8E /* PickerRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75774B6B12A8E05908EA4FAC95CB47BB /* PickerRow.swift */; }; CF491A4A998823F62438EFF79A37028E /* ViewPortHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D54B32DD68706AD7AB243DE8165B8F /* ViewPortHandler.swift */; }; CF53CF4998B07E3B054A32F34598D911 /* CombinedChartRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55CFF79657B6FDE2B1BC8C2C101E4819 /* CombinedChartRenderer.swift */; }; - D22FB0AF38A4FB41C149F5C606088E1E /* Cell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8B206369E7794EE03CDB81B6454C835 /* Cell.swift */; }; D41B06FABC2611E44E4C8E39FC415E19 /* ShareClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 269AF5F8DD17D78A0DC4ADFF2FC6E161 /* ShareClient.swift */; }; D44078A1F1CDB70D6EE87689EE274CAD /* AnimatedMoveViewJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 550FC939A985EF1F598C47237FCEADD0 /* AnimatedMoveViewJob.swift */; }; - D7F2FB7E382C1C653590BF2870F77C8C /* TriplePickerRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57F8AAAC0731CEF69400E06C9A733966 /* TriplePickerRow.swift */; }; D806EE791E0C1722A40410F0084F63E6 /* BarChartDataEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 026F25159638B0615F69DBE2054DD74D /* BarChartDataEntry.swift */; }; - D8DE47A06D3E4B45A6C42AF1DAA87CBE /* RuleRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49A72FD9D0222D44350927E3FDE48A3B /* RuleRange.swift */; }; - D98E8FA7AB3231918519A8C6C5B998D7 /* Eureka-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 30D27A7845021043EFAEAE8DE5DEB9E0 /* Eureka-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; - DA4D5827D6BB320D51945616E0E2B2D6 /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47B7052BEAEC159AC4DB9C28AE87E88F /* Section.swift */; }; DB13171585A147FFDE19FED165897AD2 /* PieChartDataSetProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 053D919370829DCF76BE4128B30656A0 /* PieChartDataSetProtocol.swift */; }; DC9F92CD3F1388C597428F67A8D931B7 /* Partition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BD660179AA5D052339B088934ECB658 /* Partition.swift */; }; DD062811D95B55D3C25F0FDE3008F558 /* RadarHighlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 948A283C104BDD9D9688A0E2D795698E /* RadarHighlighter.swift */; }; @@ -228,25 +165,19 @@ E0505857120DFFE9D41FE45D55406372 /* ComponentBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B8C70494C7C7B66D6F1FB0A3EE70381 /* ComponentBase.swift */; }; E0B8C1B3C25E624638B2C2FD105BAD35 /* YAxisRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8B684C281EB3348C147390CC832689 /* YAxisRenderer.swift */; }; E0C8CFE39BA0D706F759E929033C0091 /* DefaultValueFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3473E42A5000F936DAB93D6009E6F9 /* DefaultValueFormatter.swift */; }; - E1770D334AAD9C153792021A44FD6CA2 /* DecimalFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81E86DC9A69302F26DBF96E59EF42A0 /* DecimalFormatter.swift */; }; E184578DB7A0DC4E3FE5C1AE680CB77C /* ShareClient-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = D41E1543E1C48B54FFD88D04046B84EB /* ShareClient-dummy.m */; }; E2618C372D7762C1796CE24D6AAAFC37 /* Charts-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 1B141C9B60C3C9033C8984CDD8092AF4 /* Charts-dummy.m */; }; E36428757F9C876042F5E93B2FF83F10 /* PieRadarHighlighter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E0778A4002F3ADC7C1B16EC33774A0A /* PieRadarHighlighter.swift */; }; - E3D3815401288A32B309F2454204ABFA /* BaseRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9835CD66397846D129C53CB02CEA2199 /* BaseRow.swift */; }; E4465C942848B98723C8F3D642B92BC0 /* ScatterChartDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3AED254D55D17BB8EA5337BC6262FC1 /* ScatterChartDataProvider.swift */; }; E769F96699384D9B8E2E7D35D86438BC /* BarLineScatterCandleBubbleChartDataSetProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38871A0A7CA272870A24C2EEBC9F2B8 /* BarLineScatterCandleBubbleChartDataSetProtocol.swift */; }; E887E039D78DBED7F592D9F4782FD30E /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAB6F611E86A4758835A715E4B4184F6 /* Foundation.framework */; }; E91ABF4FF1105B2C7E0C812E8AF4B7EE /* PieChartRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F278BBD4B126E05895AFDAB808E64C68 /* PieChartRenderer.swift */; }; E95EA2841D37DFBF3466B135E5230EE8 /* Highlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EF7405D232DFDC4008C8401DE983BCD /* Highlight.swift */; }; E97DAF36AF1DDA6607CCE6FFEF2A070F /* EitherSequence.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF672D34AEB39D95AD9315C751AA09E1 /* EitherSequence.swift */; }; - EA1F322A2BC1D842773115E8B60374CD /* DateInlineRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47493593728F91332014990AA8497CF /* DateInlineRow.swift */; }; EAED453972AC1583C649AB1A2B1E02E4 /* Fill.swift in Sources */ = {isa = PBXBuildFile; fileRef = B15B2590757F14DE05C07A427600E68A /* Fill.swift */; }; EBCA949CA3D0FC990102FA358E0E26C2 /* AdjacentPairs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F370D1C8ED64C66C14E6C1099B5E972 /* AdjacentPairs.swift */; }; - EEA5878C3C5A22697B2429EAE6DFFB2B /* SelectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6116D5666C5AA3A405FA69AFC3E7817 /* SelectorViewController.swift */; }; EF96959C3049DAF932F792D19608F437 /* BubbleChartDataEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = A21D1B75DCEF0D4F9B98A11E33C41EDB /* BubbleChartDataEntry.swift */; }; EFE503C8BD962191136CFAB1255DFFE1 /* MarkerImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4AA57736F60FDBCF6AA4FA44E71682 /* MarkerImage.swift */; }; - F2FF6C2BA26E1FBC0F603CCAA690E948 /* OptionsRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 066A470BC83019FF736A9F4E1F671E31 /* OptionsRow.swift */; }; - F364336C552955B98128F87389D7F7F6 /* DateFieldRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B99954AAFDEFB38844275070432B078 /* DateFieldRow.swift */; }; F4FAC7F902EC4E73E672C6840BAD1ACE /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAB6F611E86A4758835A715E4B4184F6 /* Foundation.framework */; }; F8DDF36D6B4FF40D16F8D1F19DD11173 /* XAxis.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA19E07D8FCAFF8056DDCD4180BD7604 /* XAxis.swift */; }; F8EF071EA2E3480E0895B783AA777028 /* RadarChartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0187F74F0F2E1ADE4F205C203F88DE85 /* RadarChartView.swift */; }; @@ -254,7 +185,6 @@ FC1FAF373A9EA9E5B476E3A30C226C56 /* AxisRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB7EBFD830ED3E0392581253C75C715F /* AxisRenderer.swift */; }; FD8BFF36768B8F569E269ED06BD10557 /* Platform+Gestures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09C552536D704E376A3AB7010EEA316A /* Platform+Gestures.swift */; }; FEAE72583E6CFCD21D3AF7406D7C3A22 /* DataApproximator+N.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC49C8B5D37DFB82AA08BC6BC19AD0C5 /* DataApproximator+N.swift */; }; - FEBB964F0D174EF5001425DB6FA36B79 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EAB6F611E86A4758835A715E4B4184F6 /* Foundation.framework */; }; FF363E21E3A10ADC15B24525D6E6F3E9 /* XShapeRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58E677C70EAC3CA7313DCFEC5A60D81 /* XShapeRenderer.swift */; }; /* End PBXBuildFile section */ @@ -280,13 +210,6 @@ remoteGlobalIDString = 32507FDB9BAD6EF17DCB14A888ECA5D9; remoteInfo = ShareClient; }; - C415689D09364110E409F97C1313D4E9 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 88BE8BE7A2FE6CED4193CCD8FF3CBEF7; - remoteInfo = Eureka; - }; D1CEF5E80FC13999B22AC6EAF15E629A /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; @@ -302,27 +225,19 @@ 0187F74F0F2E1ADE4F205C203F88DE85 /* RadarChartView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RadarChartView.swift; path = Source/Charts/Charts/RadarChartView.swift; sourceTree = ""; }; 026F25159638B0615F69DBE2054DD74D /* BarChartDataEntry.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarChartDataEntry.swift; path = Source/Charts/Data/Implementations/Standard/BarChartDataEntry.swift; sourceTree = ""; }; 02BD1A15A0561B886A937317473FCF4D /* ScatterChartDataSet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ScatterChartDataSet.swift; path = Source/Charts/Data/Implementations/Standard/ScatterChartDataSet.swift; sourceTree = ""; }; - 03BC64D825C8295FFA3E396F2B8D254C /* SegmentedRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SegmentedRow.swift; path = Source/Rows/SegmentedRow.swift; sourceTree = ""; }; 04CA046D8066D2043078135381049F0A /* ChartColorTemplates.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChartColorTemplates.swift; path = Source/Charts/Utils/ChartColorTemplates.swift; sourceTree = ""; }; 053D919370829DCF76BE4128B30656A0 /* PieChartDataSetProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PieChartDataSetProtocol.swift; path = Source/Charts/Data/Interfaces/PieChartDataSetProtocol.swift; sourceTree = ""; }; - 064C06660526CDB34A90BC0F8952E907 /* SwitchRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SwitchRow.swift; path = Source/Rows/SwitchRow.swift; sourceTree = ""; }; 0651C9C3E1F7212ACFC0DF4E3E00323D /* MoveViewJob.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MoveViewJob.swift; path = Source/Charts/Jobs/MoveViewJob.swift; sourceTree = ""; }; - 066A470BC83019FF736A9F4E1F671E31 /* OptionsRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OptionsRow.swift; path = Source/Rows/Common/OptionsRow.swift; sourceTree = ""; }; - 082977F3777A7D434AB35C357B487F83 /* Eureka */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Eureka; path = Eureka.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 08518D8C01A8EA542E198BA722EA03C9 /* ChartDataEntry.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChartDataEntry.swift; path = Source/Charts/Data/Implementations/Standard/ChartDataEntry.swift; sourceTree = ""; }; - 0953166A991EECDD39429ABF7F7A9212 /* SliderRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SliderRow.swift; path = Source/Rows/SliderRow.swift; sourceTree = ""; }; 09C552536D704E376A3AB7010EEA316A /* Platform+Gestures.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Platform+Gestures.swift"; path = "Source/Charts/Utils/Platform+Gestures.swift"; sourceTree = ""; }; - 0ACC6C7F798E27DF68E09694E5645ECF /* TextAreaRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TextAreaRow.swift; path = Source/Rows/TextAreaRow.swift; sourceTree = ""; }; 0E12C56B79F0D3D4E449C70724B3B0D2 /* LineChartData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LineChartData.swift; path = Source/Charts/Data/Implementations/Standard/LineChartData.swift; sourceTree = ""; }; 10E2D718053926F7C362A0242C707398 /* ChevronDownShapeRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChevronDownShapeRenderer.swift; path = Source/Charts/Renderers/Scatter/ChevronDownShapeRenderer.swift; sourceTree = ""; }; 127D620D9103C646821EE1A9FFDC6AB4 /* TransformerHorizontalBarChart.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TransformerHorizontalBarChart.swift; path = Source/Charts/Utils/TransformerHorizontalBarChart.swift; sourceTree = ""; }; - 1363CD1C407802F39155303528BAB434 /* InlineRowType.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = InlineRowType.swift; path = Source/Core/InlineRowType.swift; sourceTree = ""; }; 1572E89B2070E8BDB7D3E2CBF51DE672 /* Pods-LoopFollow-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-LoopFollow-acknowledgements.plist"; sourceTree = ""; }; 162C2F9A6894149851210E55E6188A43 /* Transformer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Transformer.swift; path = Source/Charts/Utils/Transformer.swift; sourceTree = ""; }; 16B9EB6993E134E20A22EFB3F975FAAB /* CandleStickChartView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CandleStickChartView.swift; path = Source/Charts/Charts/CandleStickChartView.swift; sourceTree = ""; }; 16D6C652182636AC8246D4751AEC2B19 /* Pods-LoopFollow-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-LoopFollow-Info.plist"; sourceTree = ""; }; 1714C5DC04BE72E509DC4E7686AA857F /* CandleChartDataEntry.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CandleChartDataEntry.swift; path = Source/Charts/Data/Implementations/Standard/CandleChartDataEntry.swift; sourceTree = ""; }; - 17D1BC6FE5DDEE26D858B939E9B4E82C /* CheckRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CheckRow.swift; path = Source/Rows/CheckRow.swift; sourceTree = ""; }; 185455CC1A16D809E6CD52CA7ED7C218 /* ShareClient-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "ShareClient-umbrella.h"; sourceTree = ""; }; 18F3AE2D43E5A05FCCE568A3DEC1D68F /* ChartDataEntryBase.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChartDataEntryBase.swift; path = Source/Charts/Data/Implementations/Standard/ChartDataEntryBase.swift; sourceTree = ""; }; 194A8936BCA6DD2C7CC0C0003C0F27BB /* DefaultAxisValueFormatter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DefaultAxisValueFormatter.swift; path = Source/Charts/Formatters/DefaultAxisValueFormatter.swift; sourceTree = ""; }; @@ -333,13 +248,10 @@ 1D9B0CD14B51E9BB24006EA87AD5758F /* BubbleChartDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BubbleChartDataProvider.swift; path = Source/Charts/Interfaces/BubbleChartDataProvider.swift; sourceTree = ""; }; 1EE2943C8EAB9AD013BB0D24A3E135AF /* ChartLimitLine.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChartLimitLine.swift; path = Source/Charts/Components/ChartLimitLine.swift; sourceTree = ""; }; 1F1D0991883487B252954C6AD93262DD /* BubbleChartData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BubbleChartData.swift; path = Source/Charts/Data/Implementations/Standard/BubbleChartData.swift; sourceTree = ""; }; - 1FA087B627BA46F392CACF1BD142B7AA /* DateInlineFieldRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DateInlineFieldRow.swift; path = Source/Rows/Common/DateInlineFieldRow.swift; sourceTree = ""; }; 22D8A4241B43E6A486B3ABFE7CE176D2 /* Platform.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Platform.swift; path = Source/Charts/Utils/Platform.swift; sourceTree = ""; }; 24CB775967538B3FB4C47212A2F8B948 /* FirstNonNil.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FirstNonNil.swift; path = Sources/Algorithms/FirstNonNil.swift; sourceTree = ""; }; 25F46CE8645206F6372CC648F2859C38 /* SwiftAlgorithms.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = SwiftAlgorithms.modulemap; sourceTree = ""; }; - 2646204CB36CF4EF6057C2B31604DCA0 /* Helpers.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Helpers.swift; path = Source/Core/Helpers.swift; sourceTree = ""; }; 269AF5F8DD17D78A0DC4ADFF2FC6E161 /* ShareClient.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ShareClient.swift; path = ShareClient/ShareClient.swift; sourceTree = ""; }; - 2748ACE938277243245CED76CF901E32 /* RowProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RowProtocols.swift; path = Source/Core/RowProtocols.swift; sourceTree = ""; }; 28609CC9D3828C150821F3AA18752A77 /* Pods-LoopFollow-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-LoopFollow-dummy.m"; sourceTree = ""; }; 29D95C88FFDCD29BA7600CB351EEFE0D /* Charts.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Charts.release.xcconfig; sourceTree = ""; }; 2A750E80653FD14B27F8579B9E00B94F /* Stride.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Stride.swift; path = Sources/Algorithms/Stride.swift; sourceTree = ""; }; @@ -350,24 +262,16 @@ 2D4695EDCBBD2F14A96C466589AC26B4 /* Permutations.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Permutations.swift; path = Sources/Algorithms/Permutations.swift; sourceTree = ""; }; 2E2A8B52E2E9746619F1216001D0241C /* Platform+Touch Handling.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Platform+Touch Handling.swift"; path = "Source/Charts/Utils/Platform+Touch Handling.swift"; sourceTree = ""; }; 2E525EC535F76BFC47EF4F3E62BA3654 /* Description.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Description.swift; path = Source/Charts/Components/Description.swift; sourceTree = ""; }; - 2E5D627D1ADFD545DE4D7CE2E37A83B5 /* Eureka.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Eureka.release.xcconfig; sourceTree = ""; }; 2EF7405D232DFDC4008C8401DE983BCD /* Highlight.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Highlight.swift; path = Source/Charts/Highlight/Highlight.swift; sourceTree = ""; }; - 30D27A7845021043EFAEAE8DE5DEB9E0 /* Eureka-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Eureka-umbrella.h"; sourceTree = ""; }; - 31D826433F23D509AB5013DA7F1D59F6 /* Operators.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Operators.swift; path = Source/Core/Operators.swift; sourceTree = ""; }; 323A7094F3358B4E346E033FE9AAF79E /* RadarChartDataEntry.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RadarChartDataEntry.swift; path = Source/Charts/Data/Implementations/Standard/RadarChartDataEntry.swift; sourceTree = ""; }; 347B2727DAB3007AE54E938A6EDAC926 /* Marker.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Marker.swift; path = Source/Charts/Components/Marker.swift; sourceTree = ""; }; 3533AA36E9AC21252154C8D96477D1DE /* ChartDataSet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChartDataSet.swift; path = Source/Charts/Data/Implementations/Standard/ChartDataSet.swift; sourceTree = ""; }; 35C07C88A101976F7D43BE7BE4673A37 /* LineRadarChartDataSet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LineRadarChartDataSet.swift; path = Source/Charts/Data/Implementations/Standard/LineRadarChartDataSet.swift; sourceTree = ""; }; 35CE8AE4FB4FAA915248FFA91AE62338 /* Split.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Split.swift; path = Sources/Algorithms/Split.swift; sourceTree = ""; }; 35D58369D6A0FF961EC41F36B0B7D29C /* Range.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Range.swift; path = Source/Charts/Highlight/Range.swift; sourceTree = ""; }; - 35F5C5661A73C8D457BFCAA43A272924 /* Eureka.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Eureka.debug.xcconfig; sourceTree = ""; }; 372DF5E1B5F170A496FC14377B49E5BA /* ShareClient-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "ShareClient-Info.plist"; sourceTree = ""; }; - 376D2C21D79D7A27AA844F0E178BC43B /* RuleEqualsToRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RuleEqualsToRow.swift; path = Source/Validations/RuleEqualsToRow.swift; sourceTree = ""; }; 3897C4A6B7F162E479564409C5CEFB13 /* Intersperse.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Intersperse.swift; path = Sources/Algorithms/Intersperse.swift; sourceTree = ""; }; - 39347FDF57F777C67EA286FDF94A50A0 /* AlertRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AlertRow.swift; path = Source/Rows/AlertRow.swift; sourceTree = ""; }; - 3AF612F2F2ED80B5AA7DA7388FF9D3A9 /* SelectableRowType.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SelectableRowType.swift; path = Source/Core/SelectableRowType.swift; sourceTree = ""; }; 3B26B283F8811F230A0E0B4FDD225F42 /* ScatterChartView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ScatterChartView.swift; path = Source/Charts/Charts/ScatterChartView.swift; sourceTree = ""; }; - 3B501803A2305DE95A984916552E769A /* FieldsRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FieldsRow.swift; path = Source/Rows/FieldsRow.swift; sourceTree = ""; }; 3BD660179AA5D052339B088934ECB658 /* Partition.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Partition.swift; path = Sources/Algorithms/Partition.swift; sourceTree = ""; }; 3C3473E42A5000F936DAB93D6009E6F9 /* DefaultValueFormatter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DefaultValueFormatter.swift; path = Source/Charts/Formatters/DefaultValueFormatter.swift; sourceTree = ""; }; 3C5AFD1B5788F5477102C8DD0F7EAF50 /* BarLineScatterCandleBubbleRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarLineScatterCandleBubbleRenderer.swift; path = Source/Charts/Renderers/BarLineScatterCandleBubbleRenderer.swift; sourceTree = ""; }; @@ -378,54 +282,35 @@ 421EDDDD69FD4DEF3F201DC7F4EDD885 /* FillFormatter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FillFormatter.swift; path = Source/Charts/Formatters/FillFormatter.swift; sourceTree = ""; }; 42CC27FD093CC4B3D0F27B928F7744AF /* ShareClient.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = ShareClient.debug.xcconfig; sourceTree = ""; }; 42EF3C460FF3BA09B964B4B078B2FFDC /* BarChartDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarChartDataProvider.swift; path = Source/Charts/Interfaces/BarChartDataProvider.swift; sourceTree = ""; }; - 43F364E36A952C20EBF4684241A269EC /* PopoverSelectorRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PopoverSelectorRow.swift; path = Source/Rows/PopoverSelectorRow.swift; sourceTree = ""; }; - 445973D461C2CC4957A3BB54B5427B6F /* RuleClosure.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RuleClosure.swift; path = Source/Validations/RuleClosure.swift; sourceTree = ""; }; 44A177D8AC2CE9A04AEC62303E1C012C /* SwiftAlgorithms-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "SwiftAlgorithms-dummy.m"; sourceTree = ""; }; 44CEB1F80C1D008B41FFA0F59DCBBD9E /* SwiftAlgorithms.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = SwiftAlgorithms.release.xcconfig; sourceTree = ""; }; - 45C6E3F0D68C882F76B85AE0C2AE35EE /* SwipeActions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SwipeActions.swift; path = Source/Core/SwipeActions.swift; sourceTree = ""; }; - 463B1FFE93072EEE11F06544067346A2 /* ActionSheetRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ActionSheetRow.swift; path = Source/Rows/ActionSheetRow.swift; sourceTree = ""; }; - 47135570F8D5A9E934D001C53B4C0A79 /* Eureka-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Eureka-dummy.m"; sourceTree = ""; }; - 47B7052BEAEC159AC4DB9C28AE87E88F /* Section.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Section.swift; path = Source/Core/Section.swift; sourceTree = ""; }; 48FC9D5D68CA4D70155A8959C1179E28 /* XAxisRendererHorizontalBarChart.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = XAxisRendererHorizontalBarChart.swift; path = Source/Charts/Renderers/XAxisRendererHorizontalBarChart.swift; sourceTree = ""; }; - 49A72FD9D0222D44350927E3FDE48A3B /* RuleRange.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RuleRange.swift; path = Source/Validations/RuleRange.swift; sourceTree = ""; }; 49B4B217AE01ABE5E9A1B3FEE24ACD3E /* CombinedChartView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CombinedChartView.swift; path = Source/Charts/Charts/CombinedChartView.swift; sourceTree = ""; }; - 4ABEC5837F666677B0816396BF6EDB41 /* RuleRegExp.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RuleRegExp.swift; path = Source/Validations/RuleRegExp.swift; sourceTree = ""; }; 4AE70E6A38EFD6CC5B06E07C9B8C51DE /* LineScatterCandleRadarChartDataSetProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LineScatterCandleRadarChartDataSetProtocol.swift; path = Source/Charts/Data/Interfaces/LineScatterCandleRadarChartDataSetProtocol.swift; sourceTree = ""; }; - 4BACF8B4E5759037E1CC8A6423FF7B67 /* Pods-LoopFollow */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = "Pods-LoopFollow"; path = Pods_LoopFollow.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 4DA7213CB1ED6CD5B946A3A1EA36CFE3 /* AlertOptionsRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AlertOptionsRow.swift; path = Source/Rows/Common/AlertOptionsRow.swift; sourceTree = ""; }; + 4BACF8B4E5759037E1CC8A6423FF7B67 /* Pods_LoopFollow.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LoopFollow.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4F370D1C8ED64C66C14E6C1099B5E972 /* AdjacentPairs.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AdjacentPairs.swift; path = Sources/Algorithms/AdjacentPairs.swift; sourceTree = ""; }; 4F619C3ACAC8BB97F4CBF04C055CCB24 /* AnimatedZoomViewJob.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AnimatedZoomViewJob.swift; path = Source/Charts/Jobs/AnimatedZoomViewJob.swift; sourceTree = ""; }; 4FDEC8B77727E37AE06D95B16E7B4E24 /* RadarChartDataSetProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RadarChartDataSetProtocol.swift; path = Source/Charts/Data/Interfaces/RadarChartDataSetProtocol.swift; sourceTree = ""; }; - 50AFF7F977813F95AC11C077C50CDEF6 /* ListCheckRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ListCheckRow.swift; path = Source/Rows/SelectableRows/ListCheckRow.swift; sourceTree = ""; }; 51D54B32DD68706AD7AB243DE8165B8F /* ViewPortHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ViewPortHandler.swift; path = Source/Charts/Utils/ViewPortHandler.swift; sourceTree = ""; }; 524C2C5AB08DAC0B62469BC9BAD2BBFC /* LineScatterCandleRadarChartDataSet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LineScatterCandleRadarChartDataSet.swift; path = Source/Charts/Data/Implementations/Standard/LineScatterCandleRadarChartDataSet.swift; sourceTree = ""; }; 52FC9F1F7847D290D9CD7AB6024341FF /* XAxisRendererRadarChart.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = XAxisRendererRadarChart.swift; path = Source/Charts/Renderers/XAxisRendererRadarChart.swift; sourceTree = ""; }; 536232CFD052147F39AE8D2636847BB8 /* SwiftAlgorithms-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "SwiftAlgorithms-prefix.pch"; sourceTree = ""; }; - 54DF8106B99B7AAD32343B0FCA61D868 /* HeaderFooterView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HeaderFooterView.swift; path = Source/Core/HeaderFooterView.swift; sourceTree = ""; }; 550FC939A985EF1F598C47237FCEADD0 /* AnimatedMoveViewJob.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AnimatedMoveViewJob.swift; path = Source/Charts/Jobs/AnimatedMoveViewJob.swift; sourceTree = ""; }; 55CFF79657B6FDE2B1BC8C2C101E4819 /* CombinedChartRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CombinedChartRenderer.swift; path = Source/Charts/Renderers/CombinedChartRenderer.swift; sourceTree = ""; }; - 57F8AAAC0731CEF69400E06C9A733966 /* TriplePickerRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TriplePickerRow.swift; path = Source/Rows/TriplePickerRow.swift; sourceTree = ""; }; 593C5B76ABCE8F5324D1A34783FACC5B /* Chunked.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Chunked.swift; path = Sources/Algorithms/Chunked.swift; sourceTree = ""; }; 5B90B940BBEE17272D0FA9602CB3F1E1 /* BarLineScatterCandleBubbleChartDataSet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarLineScatterCandleBubbleChartDataSet.swift; path = Source/Charts/Data/Implementations/Standard/BarLineScatterCandleBubbleChartDataSet.swift; sourceTree = ""; }; 5C4AA57736F60FDBCF6AA4FA44E71682 /* MarkerImage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MarkerImage.swift; path = Source/Charts/Components/MarkerImage.swift; sourceTree = ""; }; 5CAB9310175D2D05534B5FAA40BDABB8 /* BarChartDataSet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarChartDataSet.swift; path = Source/Charts/Data/Implementations/Standard/BarChartDataSet.swift; sourceTree = ""; }; 5D152598E6F7AD2209244F3255EBE2B3 /* Windows.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Windows.swift; path = Sources/Algorithms/Windows.swift; sourceTree = ""; }; - 5E05949DEB432C9594F64A3B0BEB4BDD /* StepperRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = StepperRow.swift; path = Source/Rows/StepperRow.swift; sourceTree = ""; }; 5E0778A4002F3ADC7C1B16EC33774A0A /* PieRadarHighlighter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PieRadarHighlighter.swift; path = Source/Charts/Highlight/PieRadarHighlighter.swift; sourceTree = ""; }; - 5F117C19DCF36AF0FB2648E2D25EA325 /* PushRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PushRow.swift; path = Source/Rows/PushRow.swift; sourceTree = ""; }; 60B7899F2AE639742DDEE13C16276275 /* ScatterChartDataSetProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ScatterChartDataSetProtocol.swift; path = Source/Charts/Data/Interfaces/ScatterChartDataSetProtocol.swift; sourceTree = ""; }; - 60C304388A3314E30D8620C11790DC5B /* ButtonRowWithPresent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ButtonRowWithPresent.swift; path = Source/Rows/ButtonRowWithPresent.swift; sourceTree = ""; }; 6124C7D917D4C7DC28F7503EBCD596D7 /* Renderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Renderer.swift; path = Source/Charts/Renderers/Renderer.swift; sourceTree = ""; }; - 6207FB4695D962F2B6DF8DD11635A620 /* PickerInputRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PickerInputRow.swift; path = Source/Rows/PickerInputRow.swift; sourceTree = ""; }; 631260726B8649201DF3262C04AE423B /* RadarChartDataSet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RadarChartDataSet.swift; path = Source/Charts/Data/Implementations/Standard/RadarChartDataSet.swift; sourceTree = ""; }; 640DB089E7BC7FA9C4C5D67D1E58393A /* BarChartData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarChartData.swift; path = Source/Charts/Data/Implementations/Standard/BarChartData.swift; sourceTree = ""; }; - 649C6079A86B7348DD9A91E50537DC34 /* Core.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Core.swift; path = Source/Core/Core.swift; sourceTree = ""; }; 64CDF688CF8AB9F834E7C349894B685F /* Charts-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Charts-umbrella.h"; sourceTree = ""; }; - 6B99954AAFDEFB38844275070432B078 /* DateFieldRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DateFieldRow.swift; path = Source/Rows/Common/DateFieldRow.swift; sourceTree = ""; }; 6C90F7AE53155C1804331C13F95834B2 /* MarkerView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MarkerView.swift; path = Source/Charts/Components/MarkerView.swift; sourceTree = ""; }; 6CC42ADFADC10F4104EF63EB5AAA16E1 /* LegendRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LegendRenderer.swift; path = Source/Charts/Renderers/LegendRenderer.swift; sourceTree = ""; }; 6D4A498BBC8E4513D82F78F75036A171 /* LineRadarChartDataSetProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LineRadarChartDataSetProtocol.swift; path = Source/Charts/Data/Interfaces/LineRadarChartDataSetProtocol.swift; sourceTree = ""; }; - 6DB609C6137ED4B1E648CCC3F5325177 /* RuleLength.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RuleLength.swift; path = Source/Validations/RuleLength.swift; sourceTree = ""; }; 6E1E5F4713322837D7506143802ED222 /* PieChartDataSet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PieChartDataSet.swift; path = Source/Charts/Data/Implementations/Standard/PieChartDataSet.swift; sourceTree = ""; }; 6E3651675689D3E922771781C1C7EBF4 /* ViewPortJob.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ViewPortJob.swift; path = Source/Charts/Jobs/ViewPortJob.swift; sourceTree = ""; }; 6F86CBC5DB9A328A34A07A52C1CD5605 /* LineChartDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LineChartDataProvider.swift; path = Source/Charts/Interfaces/LineChartDataProvider.swift; sourceTree = ""; }; @@ -433,20 +318,15 @@ 7017CD37B02CFCAD0F28034D7D28355A /* BarChartRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarChartRenderer.swift; path = Source/Charts/Renderers/BarChartRenderer.swift; sourceTree = ""; }; 719A12B42A88A75148E0A9C8D6C618F0 /* LegendEntry.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LegendEntry.swift; path = Source/Charts/Components/LegendEntry.swift; sourceTree = ""; }; 7210F7399765A3931B62D5370EF1B4FC /* CombinedChartData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CombinedChartData.swift; path = Source/Charts/Data/Implementations/Standard/CombinedChartData.swift; sourceTree = ""; }; - 7391F5AD340E2F5C60BC1905E0DDC412 /* Row.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Row.swift; path = Source/Core/Row.swift; sourceTree = ""; }; 73DBDE6ADB4EFCB0BEC6D4BEBE993C55 /* ChartUtils.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChartUtils.swift; path = Source/Charts/Utils/ChartUtils.swift; sourceTree = ""; }; - 74854C0CAA3FF3AAE613D06C13CA0511 /* DoublePickerInputRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DoublePickerInputRow.swift; path = Source/Rows/DoublePickerInputRow.swift; sourceTree = ""; }; 74FF6EA327297107B81FF9FC08218F94 /* DefaultFillFormatter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DefaultFillFormatter.swift; path = Source/Charts/Formatters/DefaultFillFormatter.swift; sourceTree = ""; }; - 75774B6B12A8E05908EA4FAC95CB47BB /* PickerRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PickerRow.swift; path = Source/Rows/PickerRow.swift; sourceTree = ""; }; 7614E1E025FCFFE4FAEA31D79ABD0661 /* ShapeRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ShapeRenderer.swift; path = Source/Charts/Renderers/Scatter/ShapeRenderer.swift; sourceTree = ""; }; 7634384D32BB7CAD35606CF6E2A6DC01 /* Cycle.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Cycle.swift; path = Sources/Algorithms/Cycle.swift; sourceTree = ""; }; 7724018046776D153BF2F519E62ED100 /* Platform+Accessibility.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Platform+Accessibility.swift"; path = "Source/Charts/Utils/Platform+Accessibility.swift"; sourceTree = ""; }; 77C5C283029332E30F3E763EA8969893 /* BarChartDataSetProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarChartDataSetProtocol.swift; path = Source/Charts/Data/Interfaces/BarChartDataSetProtocol.swift; sourceTree = ""; }; - 77F3AB1C7064C79A86A62BE1BBB6068C /* DateRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DateRow.swift; path = Source/Rows/DateRow.swift; sourceTree = ""; }; 78F904F74C2B50A8192A7167CAC3C05E /* CandleChartDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CandleChartDataProvider.swift; path = Source/Charts/Interfaces/CandleChartDataProvider.swift; sourceTree = ""; }; 791893088C1075827778DDAFED99EF63 /* LineChartRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LineChartRenderer.swift; path = Source/Charts/Renderers/LineChartRenderer.swift; sourceTree = ""; }; 7ABCA6CDFA3950D0A999A692D99867D2 /* CombinedHighlighter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CombinedHighlighter.swift; path = Source/Charts/Highlight/CombinedHighlighter.swift; sourceTree = ""; }; - 7C040C5E0351A26CDFA6243686064CC2 /* SelectableSection.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SelectableSection.swift; path = Source/Core/SelectableSection.swift; sourceTree = ""; }; 7C7647DE512322E9172332E77B1A4DEA /* LineScatterCandleRadarRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LineScatterCandleRadarRenderer.swift; path = Source/Charts/Renderers/LineScatterCandleRadarRenderer.swift; sourceTree = ""; }; 7E41188828C3ADE7A96C4DA6ED1CCDA2 /* Pods-LoopFollow-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-LoopFollow-umbrella.h"; sourceTree = ""; }; 7EC4B9836E8A2EDD714B8642902033B4 /* BubbleChartRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BubbleChartRenderer.swift; path = Source/Charts/Renderers/BubbleChartRenderer.swift; sourceTree = ""; }; @@ -457,41 +337,31 @@ 85FBB9A8A3775D305F8D23740840158D /* PieChartData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PieChartData.swift; path = Source/Charts/Data/Implementations/Standard/PieChartData.swift; sourceTree = ""; }; 896D7BB34CDE275E92DC01AC1F533F26 /* HorizontalBarHighlighter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HorizontalBarHighlighter.swift; path = Source/Charts/Highlight/HorizontalBarHighlighter.swift; sourceTree = ""; }; 8A34EC75AF41BCC80FDC54C5AB3DC008 /* Chain.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Chain.swift; path = Sources/Algorithms/Chain.swift; sourceTree = ""; }; - 8AB5F5FEADDDFDB8B4D1F3BFE449F81E /* RowType.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RowType.swift; path = Source/Core/RowType.swift; sourceTree = ""; }; 8B8C70494C7C7B66D6F1FB0A3EE70381 /* ComponentBase.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ComponentBase.swift; path = Source/Charts/Components/ComponentBase.swift; sourceTree = ""; }; 8D2868AF0526000F8348B80A354EEF4B /* BarLineChartViewBase.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarLineChartViewBase.swift; path = Source/Charts/Charts/BarLineChartViewBase.swift; sourceTree = ""; }; 8D7BD095AE41ABB08964C328CA70F842 /* ChartData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChartData.swift; path = Source/Charts/Data/Implementations/Standard/ChartData.swift; sourceTree = ""; }; 910799424A3C8A57366C4CE76BE79683 /* ChevronUpShapeRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChevronUpShapeRenderer.swift; path = Source/Charts/Renderers/Scatter/ChevronUpShapeRenderer.swift; sourceTree = ""; }; 910C2C8431806F8015BAADD54CFF96DC /* Highlighter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Highlighter.swift; path = Source/Charts/Highlight/Highlighter.swift; sourceTree = ""; }; - 914C75C3E73448BCC86DF693AD52CAF8 /* DatePickerRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DatePickerRow.swift; path = Source/Rows/DatePickerRow.swift; sourceTree = ""; }; - 91757002F52622DC87D0A1967FE2900E /* TriplePickerInputRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TriplePickerInputRow.swift; path = Source/Rows/TriplePickerInputRow.swift; sourceTree = ""; }; 9222FB7877363AAC2E66F4E9BD6BE22F /* TriangleShapeRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TriangleShapeRenderer.swift; path = Source/Charts/Renderers/Scatter/TriangleShapeRenderer.swift; sourceTree = ""; }; 939E4F680A1A71C7BFC00F9917F3E5EB /* MinMax.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MinMax.swift; path = Sources/Algorithms/MinMax.swift; sourceTree = ""; }; 948A283C104BDD9D9688A0E2D795698E /* RadarHighlighter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RadarHighlighter.swift; path = Source/Charts/Highlight/RadarHighlighter.swift; sourceTree = ""; }; 97C19D311E35002501E760D893F35E2F /* BubbleChartDataSetProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BubbleChartDataSetProtocol.swift; path = Source/Charts/Data/Interfaces/BubbleChartDataSetProtocol.swift; sourceTree = ""; }; - 9835CD66397846D129C53CB02CEA2199 /* BaseRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BaseRow.swift; path = Source/Core/BaseRow.swift; sourceTree = ""; }; 98BFFBB31523D438EF5F22D84F734782 /* Charts.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = Charts.modulemap; sourceTree = ""; }; 99770C7A6099A970C34D7824A7CE280D /* Unique.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Unique.swift; path = Sources/Algorithms/Unique.swift; sourceTree = ""; }; 9B7EBBA8C350C40C7F423CDD638EDB2E /* SwiftAlgorithms-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "SwiftAlgorithms-umbrella.h"; sourceTree = ""; }; 9C00C3496A26A3B8ED45E73F3E3AFDAC /* BarLineScatterCandleBubbleChartData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarLineScatterCandleBubbleChartData.swift; path = Source/Charts/Data/Implementations/Standard/BarLineScatterCandleBubbleChartData.swift; sourceTree = ""; }; 9D18390EFF0AD4A6E0E073DD78EC6A8A /* ChartDataSetProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChartDataSetProtocol.swift; path = Source/Charts/Data/Interfaces/ChartDataSetProtocol.swift; sourceTree = ""; }; - 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; - 9DAD47DCABD4ECD1C9DFFEAA022A9E17 /* SelectorRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SelectorRow.swift; path = Source/Rows/Common/SelectorRow.swift; sourceTree = ""; }; + 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; 9E41D7AE4F0AB44736E2E49B07F83AC6 /* CandleChartDataSetProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CandleChartDataSetProtocol.swift; path = Source/Charts/Data/Interfaces/CandleChartDataSetProtocol.swift; sourceTree = ""; }; 9F21D683A3D7551D0D6ED48F9BCBDDCB /* BubbleChartView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BubbleChartView.swift; path = Source/Charts/Charts/BubbleChartView.swift; sourceTree = ""; }; 9F3F77E6983047F48C68D513994F9217 /* CandleChartData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CandleChartData.swift; path = Source/Charts/Data/Implementations/Standard/CandleChartData.swift; sourceTree = ""; }; A0878FA8411033349498923D4630B956 /* Pods-LoopFollow-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-LoopFollow-frameworks.sh"; sourceTree = ""; }; - A15229B55953AF8036FBE93E658A18C9 /* PickerInlineRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PickerInlineRow.swift; path = Source/Rows/PickerInlineRow.swift; sourceTree = ""; }; - A1C1B977ED8804E8AEEC884E7359EE58 /* Charts */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Charts; path = Charts.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A1C1B977ED8804E8AEEC884E7359EE58 /* Charts.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Charts.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A21D1B75DCEF0D4F9B98A11E33C41EDB /* BubbleChartDataEntry.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BubbleChartDataEntry.swift; path = Source/Charts/Data/Implementations/Standard/BubbleChartDataEntry.swift; sourceTree = ""; }; - A2DF6A6FC9E680841A26786E9267590E /* SelectorAlertController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SelectorAlertController.swift; path = Source/Rows/Controllers/SelectorAlertController.swift; sourceTree = ""; }; A46A27661B10DDD04786AD58706C55BC /* PieRadarChartViewBase.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PieRadarChartViewBase.swift; path = Source/Charts/Charts/PieRadarChartViewBase.swift; sourceTree = ""; }; A5822CA2A05671BAA9BC5BD4A3BA5D6B /* ShareClient.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ShareClient.h; path = ShareClient/ShareClient.h; sourceTree = ""; }; - A5833C2A8E049B2690A7651CB63B183C /* RowControllerType.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RowControllerType.swift; path = Source/Core/RowControllerType.swift; sourceTree = ""; }; A7ACE4DAB3C5C84A671AFE0A24BC32E8 /* ValueFormatter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ValueFormatter.swift; path = Source/Charts/Formatters/ValueFormatter.swift; sourceTree = ""; }; - A7B8E73E6E0A1CA1E6E26B307989E3F9 /* Protocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Protocols.swift; path = Source/Rows/Common/Protocols.swift; sourceTree = ""; }; A7FC29B40706B2526531F041CD6E5F37 /* ChartViewBase.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChartViewBase.swift; path = Source/Charts/Charts/ChartViewBase.swift; sourceTree = ""; }; - AA24EB386C74479E70533D8F03191FED /* PresenterRowType.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PresenterRowType.swift; path = Source/Core/PresenterRowType.swift; sourceTree = ""; }; AAAB281721B2A421266EF830D312BC5A /* Reductions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Reductions.swift; path = Sources/Algorithms/Reductions.swift; sourceTree = ""; }; AB44185A42C56CCC7B7F24D8A4259044 /* BarHighlighter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarHighlighter.swift; path = Source/Charts/Highlight/BarHighlighter.swift; sourceTree = ""; }; AB5301EE4DDA3C7B5DF0C20E0BC9B49E /* SwiftAlgorithms-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "SwiftAlgorithms-Info.plist"; sourceTree = ""; }; @@ -501,44 +371,30 @@ B9A488FE97E31BCF19AE78B98C18EE81 /* ChartHighlighter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChartHighlighter.swift; path = Source/Charts/Highlight/ChartHighlighter.swift; sourceTree = ""; }; BC11923FC3EEE39D1E9BB7D730ABD68B /* Combinations.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Combinations.swift; path = Sources/Algorithms/Combinations.swift; sourceTree = ""; }; BE6D57171DBF58C479650D72C1FE7CA4 /* Legend.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Legend.swift; path = Source/Charts/Components/Legend.swift; sourceTree = ""; }; - BFC8B3414FF880FE6C38F41AB0DC76F5 /* DoublePickerRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DoublePickerRow.swift; path = Source/Rows/DoublePickerRow.swift; sourceTree = ""; }; - C0351341FCAFDAA74F7778AF04C9EBE3 /* Form.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Form.swift; path = Source/Core/Form.swift; sourceTree = ""; }; - C2465E4C7B40A2D725278AEE6113E535 /* ShareClient */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = ShareClient; path = ShareClient.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C2465E4C7B40A2D725278AEE6113E535 /* ShareClient.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ShareClient.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C51F17D0D2858BA593E783811A2CF7E6 /* RadarChartData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RadarChartData.swift; path = Source/Charts/Data/Implementations/Standard/RadarChartData.swift; sourceTree = ""; }; - C6116D5666C5AA3A405FA69AFC3E7817 /* SelectorViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SelectorViewController.swift; path = Source/Rows/Controllers/SelectorViewController.swift; sourceTree = ""; }; C72D375E25E043A018C9D9E87EF64256 /* Suffix.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Suffix.swift; path = Sources/Algorithms/Suffix.swift; sourceTree = ""; }; - C8B206369E7794EE03CDB81B6454C835 /* Cell.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Cell.swift; path = Source/Core/Cell.swift; sourceTree = ""; }; C8FE53DE5C6214CB9D501CF937A4A88A /* YAxis.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = YAxis.swift; path = Source/Charts/Components/YAxis.swift; sourceTree = ""; }; - C90FF4B5002FD90F9794D8A196F3BA1A /* Eureka-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Eureka-prefix.pch"; sourceTree = ""; }; C97D67CE26870DFAA8AFE096DDF8F6CF /* PieHighlighter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PieHighlighter.swift; path = Source/Charts/Highlight/PieHighlighter.swift; sourceTree = ""; }; C9A7AF5397F6039307E067E746BEA9DC /* ShareClient.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = ShareClient.modulemap; sourceTree = ""; }; CC49C8B5D37DFB82AA08BC6BC19AD0C5 /* DataApproximator+N.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "DataApproximator+N.swift"; path = "Source/Charts/Filters/DataApproximator+N.swift"; sourceTree = ""; }; - CDC5DB7370FF5E560126694672372703 /* FieldRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FieldRow.swift; path = Source/Rows/Common/FieldRow.swift; sourceTree = ""; }; CE0ACEEB9003A6780541B487AD7AD20F /* Platform+Color.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Platform+Color.swift"; path = "Source/Charts/Utils/Platform+Color.swift"; sourceTree = ""; }; CF0AC8618DB750D79CB08822A30BEEB0 /* IndexAxisValueFormatter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = IndexAxisValueFormatter.swift; path = Source/Charts/Formatters/IndexAxisValueFormatter.swift; sourceTree = ""; }; CF58BE2BB556CCFFD3F153CD246BB3B2 /* CandleChartDataSet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CandleChartDataSet.swift; path = Source/Charts/Data/Implementations/Standard/CandleChartDataSet.swift; sourceTree = ""; }; CFA8A221F0F4BA00670608D2F68AFBC2 /* CircleShapeRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CircleShapeRenderer.swift; path = Source/Charts/Renderers/Scatter/CircleShapeRenderer.swift; sourceTree = ""; }; D024D84271BC3B8711840FD1825F8191 /* RadarChartRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RadarChartRenderer.swift; path = Source/Charts/Renderers/RadarChartRenderer.swift; sourceTree = ""; }; - D092B0076BB7443BDAA65A26F2641A30 /* Eureka-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Eureka-Info.plist"; sourceTree = ""; }; D14145B7AD1FC1EC76D1DB43D3100AC2 /* ChartBaseDataSet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChartBaseDataSet.swift; path = Source/Charts/Data/Implementations/ChartBaseDataSet.swift; sourceTree = ""; }; D245E0514AAC1A2B9A6D5EA2F383E90F /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; }; D2629C2C6B479EEB26E2F6DA58C30B81 /* Sequence+KeyPath.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Sequence+KeyPath.swift"; path = "Source/Charts/Utils/Sequence+KeyPath.swift"; sourceTree = ""; }; - D359B9F836FA5FFF4380D1F522C364D8 /* RuleRequired.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RuleRequired.swift; path = Source/Validations/RuleRequired.swift; sourceTree = ""; }; D35FFE6EE2BE8E73855DC66951E7AA12 /* YAxisRendererRadarChart.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = YAxisRendererRadarChart.swift; path = Source/Charts/Renderers/YAxisRendererRadarChart.swift; sourceTree = ""; }; D41E1543E1C48B54FFD88D04046B84EB /* ShareClient-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "ShareClient-dummy.m"; sourceTree = ""; }; - D47493593728F91332014990AA8497CF /* DateInlineRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DateInlineRow.swift; path = Source/Rows/DateInlineRow.swift; sourceTree = ""; }; D58E677C70EAC3CA7313DCFEC5A60D81 /* XShapeRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = XShapeRenderer.swift; path = Source/Charts/Renderers/Scatter/XShapeRenderer.swift; sourceTree = ""; }; - D5C7DEB37A5D25E3675AB9B8D8C02F7F /* MultipleSelectorViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MultipleSelectorViewController.swift; path = Source/Rows/Controllers/MultipleSelectorViewController.swift; sourceTree = ""; }; - D81E86DC9A69302F26DBF96E59EF42A0 /* DecimalFormatter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DecimalFormatter.swift; path = Source/Rows/Common/DecimalFormatter.swift; sourceTree = ""; }; - D8C6D9AA85CFF72CA6E5519187388A6F /* RuleEmail.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RuleEmail.swift; path = Source/Validations/RuleEmail.swift; sourceTree = ""; }; D8DF094DF5E5637883401F7A64517AB0 /* ShareClient.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = ShareClient.release.xcconfig; sourceTree = ""; }; D93D093D2D751BD6FA681010A1ECCCA5 /* SwiftAlgorithms.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = SwiftAlgorithms.debug.xcconfig; sourceTree = ""; }; DA50DFBF9517D3E54497B473A622FFD5 /* Charts-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Charts-Info.plist"; sourceTree = ""; }; - DAD950E015B2FC6DE72638E5978614FC /* ButtonRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ButtonRow.swift; path = Source/Rows/ButtonRow.swift; sourceTree = ""; }; DB10D8BF2F0C2E9B18C2130751D75984 /* Animator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Animator.swift; path = Source/Charts/Animation/Animator.swift; sourceTree = ""; }; DB7554208627CE880C0922FFD835465C /* AnimatedViewPortJob.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AnimatedViewPortJob.swift; path = Source/Charts/Jobs/AnimatedViewPortJob.swift; sourceTree = ""; }; DC882B9BE58FE2D2FCB58677AABEF941 /* HorizontalBarChartRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HorizontalBarChartRenderer.swift; path = Source/Charts/Renderers/HorizontalBarChartRenderer.swift; sourceTree = ""; }; - DEB89F713B887858D9493B533D256B7B /* LabelRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LabelRow.swift; path = Source/Rows/LabelRow.swift; sourceTree = ""; }; DF14DB72944084EE52D83024A8168D3A /* CombinedChartDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CombinedChartDataProvider.swift; path = Source/Charts/Interfaces/CombinedChartDataProvider.swift; sourceTree = ""; }; DF672D34AEB39D95AD9315C751AA09E1 /* EitherSequence.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = EitherSequence.swift; path = Sources/Algorithms/EitherSequence.swift; sourceTree = ""; }; DFD6267883E63FA87B3CF88C8773CCD6 /* ZoomViewJob.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ZoomViewJob.swift; path = Source/Charts/Jobs/ZoomViewJob.swift; sourceTree = ""; }; @@ -546,7 +402,6 @@ E18735DC81F8F3BB4967404CF8ED7E84 /* ChartDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ChartDataProvider.swift; path = Source/Charts/Interfaces/ChartDataProvider.swift; sourceTree = ""; }; E38871A0A7CA272870A24C2EEBC9F2B8 /* BarLineScatterCandleBubbleChartDataSetProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BarLineScatterCandleBubbleChartDataSetProtocol.swift; path = Source/Charts/Data/Interfaces/BarLineScatterCandleBubbleChartDataSetProtocol.swift; sourceTree = ""; }; E40805D1503067A2E679DBFB30BBB8C5 /* DataRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DataRenderer.swift; path = Source/Charts/Renderers/DataRenderer.swift; sourceTree = ""; }; - E5836BA12B4D08431CFA71E4A245325C /* Eureka.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = Eureka.modulemap; sourceTree = ""; }; E8151C448D7FF1AE4C18A1D9BAAB84F5 /* SquareShapeRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SquareShapeRenderer.swift; path = Source/Charts/Renderers/Scatter/SquareShapeRenderer.swift; sourceTree = ""; }; EAB6F611E86A4758835A715E4B4184F6 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; EB7EBFD830ED3E0392581253C75C715F /* AxisRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AxisRenderer.swift; path = Source/Charts/Renderers/AxisRenderer.swift; sourceTree = ""; }; @@ -557,20 +412,14 @@ EF9EB617791E412932FA1E750D934E36 /* CandleStickChartRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CandleStickChartRenderer.swift; path = Source/Charts/Renderers/CandleStickChartRenderer.swift; sourceTree = ""; }; EFAEA5F16C2629852715CED66DB5173B /* Trim.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Trim.swift; path = Sources/Algorithms/Trim.swift; sourceTree = ""; }; F0744A6F4A9EC646DAE57C8935BB5D5B /* ScatterChartData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ScatterChartData.swift; path = Source/Charts/Data/Implementations/Standard/ScatterChartData.swift; sourceTree = ""; }; - F082AA646AB49C314004DBB19E3F3085 /* GenericMultipleSelectorRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = GenericMultipleSelectorRow.swift; path = Source/Rows/Common/GenericMultipleSelectorRow.swift; sourceTree = ""; }; F0EFA056547237258760018B2FE0CAD6 /* Pods-LoopFollow-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-LoopFollow-acknowledgements.markdown"; sourceTree = ""; }; - F15455C959FB8C71196A046437C0EED5 /* CellType.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CellType.swift; path = Source/Core/CellType.swift; sourceTree = ""; }; - F2561CCE278142444D8BDF2C3820193E /* RuleURL.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RuleURL.swift; path = Source/Validations/RuleURL.swift; sourceTree = ""; }; F278BBD4B126E05895AFDAB808E64C68 /* PieChartRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PieChartRenderer.swift; path = Source/Charts/Renderers/PieChartRenderer.swift; sourceTree = ""; }; F34D137537BF079F35059435323D7452 /* HorizontalBarChartView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HorizontalBarChartView.swift; path = Source/Charts/Charts/HorizontalBarChartView.swift; sourceTree = ""; }; F3AED254D55D17BB8EA5337BC6262FC1 /* ScatterChartDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ScatterChartDataProvider.swift; path = Source/Charts/Interfaces/ScatterChartDataProvider.swift; sourceTree = ""; }; - F3E7458F7D38C89A9DE4BBACD3450C88 /* SwiftAlgorithms */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = SwiftAlgorithms; path = Algorithms.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F3E7458F7D38C89A9DE4BBACD3450C88 /* Algorithms.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Algorithms.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F448E25F6F3095E674026E503F71BED7 /* YAxisRendererHorizontalBarChart.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = YAxisRendererHorizontalBarChart.swift; path = Source/Charts/Renderers/YAxisRendererHorizontalBarChart.swift; sourceTree = ""; }; F574273E89B9C97C369C430B67186E14 /* Indexed.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Indexed.swift; path = Sources/Algorithms/Indexed.swift; sourceTree = ""; }; - F5A708FABC6817F1F5703662646AD8A5 /* MultipleSelectorRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MultipleSelectorRow.swift; path = Source/Rows/MultipleSelectorRow.swift; sourceTree = ""; }; F84585BA74D5FD5927560642A3D8484D /* BubbleChartDataSet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BubbleChartDataSet.swift; path = Source/Charts/Data/Implementations/Standard/BubbleChartDataSet.swift; sourceTree = ""; }; - F8F21E10186E4376CA7F0655ECE328A5 /* NavigationAccessoryView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NavigationAccessoryView.swift; path = Source/Core/NavigationAccessoryView.swift; sourceTree = ""; }; - F9D47A544442AC0D1EACE1A847B3DAE4 /* Validation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Validation.swift; path = Source/Core/Validation.swift; sourceTree = ""; }; FA175D42C8D8C40B7D7B4EBA4DFCB613 /* AxisValueFormatter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AxisValueFormatter.swift; path = Source/Charts/Formatters/AxisValueFormatter.swift; sourceTree = ""; }; FA19E07D8FCAFF8056DDCD4180BD7604 /* XAxis.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = XAxis.swift; path = Source/Charts/Components/XAxis.swift; sourceTree = ""; }; FAC79A108E546A80FD2E858996EB42D7 /* AxisBase.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AxisBase.swift; path = Source/Charts/Components/AxisBase.swift; sourceTree = ""; }; @@ -578,15 +427,6 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - 04298F5DEE9CFE9AD337DFE74F07F490 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - FEBB964F0D174EF5001425DB6FA36B79 /* Foundation.framework in Frameworks */, - 3927F2677F09C815B6BC70E94023E335 /* UIKit.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 5DA9E0A9AAAF4649D65F510D01A75121 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -626,7 +466,6 @@ isa = PBXGroup; children = ( E5EBEB4E3DD769CF77FBA0FB4DD5A204 /* Charts */, - ADC2C025B505CB26B7100F9ED9F7A3B7 /* Eureka */, 93B5F75356B6491998DC2064F7F3591F /* ShareClient */, A03D44A5466EE402B13CCEE8C95D8E91 /* SwiftAlgorithms */, ); @@ -644,11 +483,10 @@ 2C61111B3BA91EE50CDF6C9086AE0897 /* Products */ = { isa = PBXGroup; children = ( - A1C1B977ED8804E8AEEC884E7359EE58 /* Charts */, - 082977F3777A7D434AB35C357B487F83 /* Eureka */, - 4BACF8B4E5759037E1CC8A6423FF7B67 /* Pods-LoopFollow */, - C2465E4C7B40A2D725278AEE6113E535 /* ShareClient */, - F3E7458F7D38C89A9DE4BBACD3450C88 /* SwiftAlgorithms */, + A1C1B977ED8804E8AEEC884E7359EE58 /* Charts.framework */, + 4BACF8B4E5759037E1CC8A6423FF7B67 /* Pods_LoopFollow.framework */, + C2465E4C7B40A2D725278AEE6113E535 /* ShareClient.framework */, + F3E7458F7D38C89A9DE4BBACD3450C88 /* Algorithms.framework */, ); name = Products; sourceTree = ""; @@ -840,7 +678,6 @@ 269AF5F8DD17D78A0DC4ADFF2FC6E161 /* ShareClient.swift */, 8D3A1BD14866C606F6AE6DA32BA8C1F4 /* Support Files */, ); - name = ShareClient; path = ShareClient; sourceTree = ""; }; @@ -873,85 +710,9 @@ 5D152598E6F7AD2209244F3255EBE2B3 /* Windows.swift */, ED8149DD2EC8C7BA0180268D4B9A8034 /* Support Files */, ); - name = SwiftAlgorithms; path = SwiftAlgorithms; sourceTree = ""; }; - ADC2C025B505CB26B7100F9ED9F7A3B7 /* Eureka */ = { - isa = PBXGroup; - children = ( - 463B1FFE93072EEE11F06544067346A2 /* ActionSheetRow.swift */, - 4DA7213CB1ED6CD5B946A3A1EA36CFE3 /* AlertOptionsRow.swift */, - 39347FDF57F777C67EA286FDF94A50A0 /* AlertRow.swift */, - 9835CD66397846D129C53CB02CEA2199 /* BaseRow.swift */, - DAD950E015B2FC6DE72638E5978614FC /* ButtonRow.swift */, - 60C304388A3314E30D8620C11790DC5B /* ButtonRowWithPresent.swift */, - C8B206369E7794EE03CDB81B6454C835 /* Cell.swift */, - F15455C959FB8C71196A046437C0EED5 /* CellType.swift */, - 17D1BC6FE5DDEE26D858B939E9B4E82C /* CheckRow.swift */, - 649C6079A86B7348DD9A91E50537DC34 /* Core.swift */, - 6B99954AAFDEFB38844275070432B078 /* DateFieldRow.swift */, - 1FA087B627BA46F392CACF1BD142B7AA /* DateInlineFieldRow.swift */, - D47493593728F91332014990AA8497CF /* DateInlineRow.swift */, - 914C75C3E73448BCC86DF693AD52CAF8 /* DatePickerRow.swift */, - 77F3AB1C7064C79A86A62BE1BBB6068C /* DateRow.swift */, - D81E86DC9A69302F26DBF96E59EF42A0 /* DecimalFormatter.swift */, - 74854C0CAA3FF3AAE613D06C13CA0511 /* DoublePickerInputRow.swift */, - BFC8B3414FF880FE6C38F41AB0DC76F5 /* DoublePickerRow.swift */, - CDC5DB7370FF5E560126694672372703 /* FieldRow.swift */, - 3B501803A2305DE95A984916552E769A /* FieldsRow.swift */, - C0351341FCAFDAA74F7778AF04C9EBE3 /* Form.swift */, - F082AA646AB49C314004DBB19E3F3085 /* GenericMultipleSelectorRow.swift */, - 54DF8106B99B7AAD32343B0FCA61D868 /* HeaderFooterView.swift */, - 2646204CB36CF4EF6057C2B31604DCA0 /* Helpers.swift */, - 1363CD1C407802F39155303528BAB434 /* InlineRowType.swift */, - DEB89F713B887858D9493B533D256B7B /* LabelRow.swift */, - 50AFF7F977813F95AC11C077C50CDEF6 /* ListCheckRow.swift */, - F5A708FABC6817F1F5703662646AD8A5 /* MultipleSelectorRow.swift */, - D5C7DEB37A5D25E3675AB9B8D8C02F7F /* MultipleSelectorViewController.swift */, - F8F21E10186E4376CA7F0655ECE328A5 /* NavigationAccessoryView.swift */, - 31D826433F23D509AB5013DA7F1D59F6 /* Operators.swift */, - 066A470BC83019FF736A9F4E1F671E31 /* OptionsRow.swift */, - A15229B55953AF8036FBE93E658A18C9 /* PickerInlineRow.swift */, - 6207FB4695D962F2B6DF8DD11635A620 /* PickerInputRow.swift */, - 75774B6B12A8E05908EA4FAC95CB47BB /* PickerRow.swift */, - 43F364E36A952C20EBF4684241A269EC /* PopoverSelectorRow.swift */, - AA24EB386C74479E70533D8F03191FED /* PresenterRowType.swift */, - A7B8E73E6E0A1CA1E6E26B307989E3F9 /* Protocols.swift */, - 5F117C19DCF36AF0FB2648E2D25EA325 /* PushRow.swift */, - 7391F5AD340E2F5C60BC1905E0DDC412 /* Row.swift */, - A5833C2A8E049B2690A7651CB63B183C /* RowControllerType.swift */, - 2748ACE938277243245CED76CF901E32 /* RowProtocols.swift */, - 8AB5F5FEADDDFDB8B4D1F3BFE449F81E /* RowType.swift */, - 445973D461C2CC4957A3BB54B5427B6F /* RuleClosure.swift */, - D8C6D9AA85CFF72CA6E5519187388A6F /* RuleEmail.swift */, - 376D2C21D79D7A27AA844F0E178BC43B /* RuleEqualsToRow.swift */, - 6DB609C6137ED4B1E648CCC3F5325177 /* RuleLength.swift */, - 49A72FD9D0222D44350927E3FDE48A3B /* RuleRange.swift */, - 4ABEC5837F666677B0816396BF6EDB41 /* RuleRegExp.swift */, - D359B9F836FA5FFF4380D1F522C364D8 /* RuleRequired.swift */, - F2561CCE278142444D8BDF2C3820193E /* RuleURL.swift */, - 47B7052BEAEC159AC4DB9C28AE87E88F /* Section.swift */, - 03BC64D825C8295FFA3E396F2B8D254C /* SegmentedRow.swift */, - 3AF612F2F2ED80B5AA7DA7388FF9D3A9 /* SelectableRowType.swift */, - 7C040C5E0351A26CDFA6243686064CC2 /* SelectableSection.swift */, - A2DF6A6FC9E680841A26786E9267590E /* SelectorAlertController.swift */, - 9DAD47DCABD4ECD1C9DFFEAA022A9E17 /* SelectorRow.swift */, - C6116D5666C5AA3A405FA69AFC3E7817 /* SelectorViewController.swift */, - 0953166A991EECDD39429ABF7F7A9212 /* SliderRow.swift */, - 5E05949DEB432C9594F64A3B0BEB4BDD /* StepperRow.swift */, - 45C6E3F0D68C882F76B85AE0C2AE35EE /* SwipeActions.swift */, - 064C06660526CDB34A90BC0F8952E907 /* SwitchRow.swift */, - 0ACC6C7F798E27DF68E09694E5645ECF /* TextAreaRow.swift */, - 91757002F52622DC87D0A1967FE2900E /* TriplePickerInputRow.swift */, - 57F8AAAC0731CEF69400E06C9A733966 /* TriplePickerRow.swift */, - F9D47A544442AC0D1EACE1A847B3DAE4 /* Validation.swift */, - DBA520F0C92D0D031D4508B8226CAA9E /* Support Files */, - ); - name = Eureka; - path = Eureka; - sourceTree = ""; - }; AEDEF3B82F3D135001FB2D1778DAD53F /* Support Files */ = { isa = PBXGroup; children = ( @@ -995,28 +756,12 @@ path = "Target Support Files/Pods-LoopFollow"; sourceTree = ""; }; - DBA520F0C92D0D031D4508B8226CAA9E /* Support Files */ = { - isa = PBXGroup; - children = ( - E5836BA12B4D08431CFA71E4A245325C /* Eureka.modulemap */, - 47135570F8D5A9E934D001C53B4C0A79 /* Eureka-dummy.m */, - D092B0076BB7443BDAA65A26F2641A30 /* Eureka-Info.plist */, - C90FF4B5002FD90F9794D8A196F3BA1A /* Eureka-prefix.pch */, - 30D27A7845021043EFAEAE8DE5DEB9E0 /* Eureka-umbrella.h */, - 35F5C5661A73C8D457BFCAA43A272924 /* Eureka.debug.xcconfig */, - 2E5D627D1ADFD545DE4D7CE2E37A83B5 /* Eureka.release.xcconfig */, - ); - name = "Support Files"; - path = "../Target Support Files/Eureka"; - sourceTree = ""; - }; E5EBEB4E3DD769CF77FBA0FB4DD5A204 /* Charts */ = { isa = PBXGroup; children = ( 64FE9452040001E25B5A3EB8D58108DB /* Core */, AEDEF3B82F3D135001FB2D1778DAD53F /* Support Files */, ); - name = Charts; path = Charts; sourceTree = ""; }; @@ -1038,14 +783,6 @@ /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ - 27FF107BE894F2270BA6E56FBEED3F40 /* Headers */ = { - isa = PBXHeadersBuildPhase; - buildActionMask = 2147483647; - files = ( - D98E8FA7AB3231918519A8C6C5B998D7 /* Eureka-umbrella.h in Headers */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 2D480C09AACD90D4C214D8CB1D26CDC8 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; @@ -1097,7 +834,7 @@ ); name = SwiftAlgorithms; productName = Algorithms; - productReference = F3E7458F7D38C89A9DE4BBACD3450C88 /* SwiftAlgorithms */; + productReference = F3E7458F7D38C89A9DE4BBACD3450C88 /* Algorithms.framework */; productType = "com.apple.product-type.framework"; }; 32507FDB9BAD6EF17DCB14A888ECA5D9 /* ShareClient */ = { @@ -1115,25 +852,7 @@ ); name = ShareClient; productName = ShareClient; - productReference = C2465E4C7B40A2D725278AEE6113E535 /* ShareClient */; - productType = "com.apple.product-type.framework"; - }; - 88BE8BE7A2FE6CED4193CCD8FF3CBEF7 /* Eureka */ = { - isa = PBXNativeTarget; - buildConfigurationList = 2BD6078BBF4ADE85FF441A7911BD9F20 /* Build configuration list for PBXNativeTarget "Eureka" */; - buildPhases = ( - 27FF107BE894F2270BA6E56FBEED3F40 /* Headers */, - 0CE9B2A0F503C382EE4DD96A78D4C89A /* Sources */, - 04298F5DEE9CFE9AD337DFE74F07F490 /* Frameworks */, - C47833098716CBD801DB087167BADD1B /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Eureka; - productName = Eureka; - productReference = 082977F3777A7D434AB35C357B487F83 /* Eureka */; + productReference = C2465E4C7B40A2D725278AEE6113E535 /* ShareClient.framework */; productType = "com.apple.product-type.framework"; }; BF03B0B2B8B052092CB5F90CC5FB3757 /* Charts */ = { @@ -1152,7 +871,7 @@ ); name = Charts; productName = Charts; - productReference = A1C1B977ED8804E8AEEC884E7359EE58 /* Charts */; + productReference = A1C1B977ED8804E8AEEC884E7359EE58 /* Charts.framework */; productType = "com.apple.product-type.framework"; }; E187D4B9C2B76F773546AF1E39D2ECCD /* Pods-LoopFollow */ = { @@ -1168,13 +887,12 @@ ); dependencies = ( BD234F929A36F384DAB2F1A302F4F777 /* PBXTargetDependency */, - 417EF9FB78516B959611C40A27824B03 /* PBXTargetDependency */, 72D00FFECA0A202EFBCFBD5DC6BDFB6C /* PBXTargetDependency */, FBBCCF26094223773400B746C8E1F6DB /* PBXTargetDependency */, ); name = "Pods-LoopFollow"; productName = Pods_LoopFollow; - productReference = 4BACF8B4E5759037E1CC8A6423FF7B67 /* Pods-LoopFollow */; + productReference = 4BACF8B4E5759037E1CC8A6423FF7B67 /* Pods_LoopFollow.framework */; productType = "com.apple.product-type.framework"; }; /* End PBXNativeTarget section */ @@ -1200,7 +918,6 @@ projectRoot = ""; targets = ( BF03B0B2B8B052092CB5F90CC5FB3757 /* Charts */, - 88BE8BE7A2FE6CED4193CCD8FF3CBEF7 /* Eureka */, E187D4B9C2B76F773546AF1E39D2ECCD /* Pods-LoopFollow */, 32507FDB9BAD6EF17DCB14A888ECA5D9 /* ShareClient */, 2C511EE63C6E7D5843B971D1E441119E /* SwiftAlgorithms */, @@ -1230,13 +947,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - C47833098716CBD801DB087167BADD1B /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; F0985DF3FCB396C10277C61C2746A59B /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1396,80 +1106,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 0CE9B2A0F503C382EE4DD96A78D4C89A /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 0568E4D0A08C598F5AE7DDDA0DD59923 /* ActionSheetRow.swift in Sources */, - 31B0A34AA01CC0E71ECA6B7BA7D7EC88 /* AlertOptionsRow.swift in Sources */, - 697D5116696270B7361C180573C495D3 /* AlertRow.swift in Sources */, - E3D3815401288A32B309F2454204ABFA /* BaseRow.swift in Sources */, - C87A5CE74D4AF4B1180FA67B63CF7A5A /* ButtonRow.swift in Sources */, - B88B7CAA00CEB3B5032091BFA45BD219 /* ButtonRowWithPresent.swift in Sources */, - D22FB0AF38A4FB41C149F5C606088E1E /* Cell.swift in Sources */, - 40E090A253097EFA55694770F19FEBAE /* CellType.swift in Sources */, - 6EC583C2F9CEC762B806A22700E9D800 /* CheckRow.swift in Sources */, - 3719CDB1B700C19AB04163DA75B09815 /* Core.swift in Sources */, - F364336C552955B98128F87389D7F7F6 /* DateFieldRow.swift in Sources */, - 7BD16251FF2F7EA5F18AEACF5DE44C23 /* DateInlineFieldRow.swift in Sources */, - EA1F322A2BC1D842773115E8B60374CD /* DateInlineRow.swift in Sources */, - 7F7B5556719352AD2C7A756587D348BB /* DatePickerRow.swift in Sources */, - 008E02A88C10E3688B4431BFD393F0BE /* DateRow.swift in Sources */, - E1770D334AAD9C153792021A44FD6CA2 /* DecimalFormatter.swift in Sources */, - 0FC606DB282EDA95D6DA95289AA85C61 /* DoublePickerInputRow.swift in Sources */, - C241A011154907D4A4B377DB529E9C9D /* DoublePickerRow.swift in Sources */, - 9760DCC9A11340DC2E74E00007F9D3DF /* Eureka-dummy.m in Sources */, - CE0E20A3203F862A375294551E8B8862 /* FieldRow.swift in Sources */, - 3AE49F9C5930DCD727D11A42EA89E640 /* FieldsRow.swift in Sources */, - A3A234EA035289FD713D51AEAF1072C8 /* Form.swift in Sources */, - C6C91F0B055C43A0B6A6865AFDB7614E /* GenericMultipleSelectorRow.swift in Sources */, - 298553E2ECB829B696DF592E6990249A /* HeaderFooterView.swift in Sources */, - 73780F56C4A6F73B569E3009F9AEF09F /* Helpers.swift in Sources */, - 05E063B80D84B40AEEDC7DC5797FA3FD /* InlineRowType.swift in Sources */, - 205C36FBEAF46B5278F998984AE7AF18 /* LabelRow.swift in Sources */, - 995A8A4C2445BD86C2DC375B14394682 /* ListCheckRow.swift in Sources */, - B24FAE4A9C6BD92AD64EE7859C2A9D9A /* MultipleSelectorRow.swift in Sources */, - 5CF70EB49A6DE1B80886F0FD3F780522 /* MultipleSelectorViewController.swift in Sources */, - 869DB6D9A7692FDEC1FF5537C0413274 /* NavigationAccessoryView.swift in Sources */, - 6328C306E03099885C3EFCA798C9F5D1 /* Operators.swift in Sources */, - F2FF6C2BA26E1FBC0F603CCAA690E948 /* OptionsRow.swift in Sources */, - 2ECA780FC4691E846B5EB4919997A5F4 /* PickerInlineRow.swift in Sources */, - 7B59CC19DE0A8FC37B7E78A578968DD4 /* PickerInputRow.swift in Sources */, - CF4332D8352284FA91A83CD4EF21DA8E /* PickerRow.swift in Sources */, - AF4BD9FB7CC229682ED2E26F7E6106A7 /* PopoverSelectorRow.swift in Sources */, - 5DDCB215B020A3C11AAF9EA8766AD7D5 /* PresenterRowType.swift in Sources */, - 69B91E7FF91730260D4137B09C015636 /* Protocols.swift in Sources */, - 56E322CBEF2E9EA5777EC3846F888BDB /* PushRow.swift in Sources */, - C959E50620A9AF66A128EE6249CCD68C /* Row.swift in Sources */, - 1E048CCA6D1D1B00892BB5A0D87A4D2E /* RowControllerType.swift in Sources */, - 91B0EE8C7679DFFD7D0D27DC32BD07D3 /* RowProtocols.swift in Sources */, - A2254B25181B13F76C598F0CF5A40FE7 /* RowType.swift in Sources */, - 6B905F2AC740B767D002FBB0CA795A7C /* RuleClosure.swift in Sources */, - 4BADBE8D79E82E6BCA97708DB5E0623C /* RuleEmail.swift in Sources */, - 3AEA68431EF66EB90B4E3F1074CF8125 /* RuleEqualsToRow.swift in Sources */, - 3A84D68B004228D5F71FC0FC889FFC07 /* RuleLength.swift in Sources */, - D8DE47A06D3E4B45A6C42AF1DAA87CBE /* RuleRange.swift in Sources */, - C11DBC286F46067EFC0E4C15FF014CC6 /* RuleRegExp.swift in Sources */, - 8F2CC08A2EA2A12CF906555985307D8A /* RuleRequired.swift in Sources */, - 3CEB7EACBD4EB21B47517CC48A994655 /* RuleURL.swift in Sources */, - DA4D5827D6BB320D51945616E0E2B2D6 /* Section.swift in Sources */, - 873FEA8F7A7D862C00A2335E311DEDC6 /* SegmentedRow.swift in Sources */, - 25740149FB6D4EE7E113A1FA47B73554 /* SelectableRowType.swift in Sources */, - 28D8448B496B314D91BAE0EE321646BE /* SelectableSection.swift in Sources */, - 184CBCE994131F7140410E0B17E94ADE /* SelectorAlertController.swift in Sources */, - 95DEEE38D632C816737E8AB538428A50 /* SelectorRow.swift in Sources */, - EEA5878C3C5A22697B2429EAE6DFFB2B /* SelectorViewController.swift in Sources */, - B5E04676C82AC7C36204AB782DD780B9 /* SliderRow.swift in Sources */, - 4889C566B538BE9C98F084E685CB3C8E /* StepperRow.swift in Sources */, - 8FF92C53FAAAC2ED824C1E535C187424 /* SwipeActions.swift in Sources */, - CC70380478EE7E1E404437B6AA51BA33 /* SwitchRow.swift in Sources */, - 57FB8912F3DEFA71CF0932D6338A2BF9 /* TextAreaRow.swift in Sources */, - 2E35BA439FF3906792D64F465A358A13 /* TriplePickerInputRow.swift in Sources */, - D7F2FB7E382C1C653590BF2870F77C8C /* TriplePickerRow.swift in Sources */, - 46A11B142CC018088902C05BA2707513 /* Validation.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; D6D8EC531B387AE324F4FBC6AFF93EAC /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1522,12 +1158,6 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 417EF9FB78516B959611C40A27824B03 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - name = Eureka; - target = 88BE8BE7A2FE6CED4193CCD8FF3CBEF7 /* Eureka */; - targetProxy = C415689D09364110E409F97C1313D4E9 /* PBXContainerItemProxy */; - }; 72D00FFECA0A202EFBCFBD5DC6BDFB6C /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = ShareClient; @@ -1559,7 +1189,6 @@ isa = XCBuildConfiguration; baseConfigurationReference = B1DF43F4B1524F385F6090756FA3016A /* Pods-LoopFollow.debug.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; CLANG_ENABLE_OBJC_WEAK = NO; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; @@ -1729,7 +1358,6 @@ isa = XCBuildConfiguration; baseConfigurationReference = EBE2A8C176CC9C76279BE195BDA90B7F /* Pods-LoopFollow.release.xcconfig */; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; CLANG_ENABLE_OBJC_WEAK = NO; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; @@ -1763,75 +1391,6 @@ }; name = Release; }; - AE59D8F42374C7745ABF1A1848E33758 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 35F5C5661A73C8D457BFCAA43A272924 /* Eureka.debug.xcconfig */; - buildSettings = { - "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_PREFIX_HEADER = "Target Support Files/Eureka/Eureka-prefix.pch"; - INFOPLIST_FILE = "Target Support Files/Eureka/Eureka-Info.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MODULEMAP_FILE = "Target Support Files/Eureka/Eureka.modulemap"; - PRODUCT_MODULE_NAME = Eureka; - PRODUCT_NAME = Eureka; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Debug; - }; - CED55013787F3397AC938792D90D1C02 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 2E5D627D1ADFD545DE4D7CE2E37A83B5 /* Eureka.release.xcconfig */; - buildSettings = { - "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 1; - DYLIB_INSTALL_NAME_BASE = "@rpath"; - GCC_PREFIX_HEADER = "Target Support Files/Eureka/Eureka-prefix.pch"; - INFOPLIST_FILE = "Target Support Files/Eureka/Eureka-Info.plist"; - INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - MODULEMAP_FILE = "Target Support Files/Eureka/Eureka.modulemap"; - PRODUCT_MODULE_NAME = Eureka; - PRODUCT_NAME = Eureka; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - VERSIONING_SYSTEM = "apple-generic"; - VERSION_INFO_PREFIX = ""; - }; - name = Release; - }; D1AADB344ABE211C7A14EADDE0C68908 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 42CC27FD093CC4B3D0F27B928F7744AF /* ShareClient.debug.xcconfig */; @@ -2052,15 +1611,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 2BD6078BBF4ADE85FF441A7911BD9F20 /* Build configuration list for PBXNativeTarget "Eureka" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - AE59D8F42374C7745ABF1A1848E33758 /* Debug */, - CED55013787F3397AC938792D90D1C02 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 3C9F1E8CC2EB7C944989DFC3E98E9033 /* Build configuration list for PBXNativeTarget "Charts" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Pods/Target Support Files/Eureka/Eureka-Info.plist b/Pods/Target Support Files/Eureka/Eureka-Info.plist deleted file mode 100644 index 4a5e07045..000000000 --- a/Pods/Target Support Files/Eureka/Eureka-Info.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - ${PODS_DEVELOPMENT_LANGUAGE} - CFBundleExecutable - ${EXECUTABLE_NAME} - CFBundleIdentifier - ${PRODUCT_BUNDLE_IDENTIFIER} - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - ${PRODUCT_NAME} - CFBundlePackageType - FMWK - CFBundleShortVersionString - 5.3.6 - CFBundleSignature - ???? - CFBundleVersion - ${CURRENT_PROJECT_VERSION} - NSPrincipalClass - - - diff --git a/Pods/Target Support Files/Eureka/Eureka-dummy.m b/Pods/Target Support Files/Eureka/Eureka-dummy.m deleted file mode 100644 index 00a740105..000000000 --- a/Pods/Target Support Files/Eureka/Eureka-dummy.m +++ /dev/null @@ -1,5 +0,0 @@ -#import -@interface PodsDummy_Eureka : NSObject -@end -@implementation PodsDummy_Eureka -@end diff --git a/Pods/Target Support Files/Eureka/Eureka-prefix.pch b/Pods/Target Support Files/Eureka/Eureka-prefix.pch deleted file mode 100644 index beb2a2441..000000000 --- a/Pods/Target Support Files/Eureka/Eureka-prefix.pch +++ /dev/null @@ -1,12 +0,0 @@ -#ifdef __OBJC__ -#import -#else -#ifndef FOUNDATION_EXPORT -#if defined(__cplusplus) -#define FOUNDATION_EXPORT extern "C" -#else -#define FOUNDATION_EXPORT extern -#endif -#endif -#endif - diff --git a/Pods/Target Support Files/Eureka/Eureka-umbrella.h b/Pods/Target Support Files/Eureka/Eureka-umbrella.h deleted file mode 100644 index 5056e283b..000000000 --- a/Pods/Target Support Files/Eureka/Eureka-umbrella.h +++ /dev/null @@ -1,16 +0,0 @@ -#ifdef __OBJC__ -#import -#else -#ifndef FOUNDATION_EXPORT -#if defined(__cplusplus) -#define FOUNDATION_EXPORT extern "C" -#else -#define FOUNDATION_EXPORT extern -#endif -#endif -#endif - - -FOUNDATION_EXPORT double EurekaVersionNumber; -FOUNDATION_EXPORT const unsigned char EurekaVersionString[]; - diff --git a/Pods/Target Support Files/Eureka/Eureka.debug.xcconfig b/Pods/Target Support Files/Eureka/Eureka.debug.xcconfig deleted file mode 100644 index e57f7a71a..000000000 --- a/Pods/Target Support Files/Eureka/Eureka.debug.xcconfig +++ /dev/null @@ -1,15 +0,0 @@ -CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO -CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Eureka -GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 -LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift -OTHER_LDFLAGS = $(inherited) -framework "Foundation" -framework "UIKit" -OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS -PODS_BUILD_DIR = ${BUILD_DIR} -PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) -PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} -PODS_ROOT = ${SRCROOT} -PODS_TARGET_SRCROOT = ${PODS_ROOT}/Eureka -PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates -PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} -SKIP_INSTALL = YES -USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Pods/Target Support Files/Eureka/Eureka.modulemap b/Pods/Target Support Files/Eureka/Eureka.modulemap deleted file mode 100644 index 15b0d0671..000000000 --- a/Pods/Target Support Files/Eureka/Eureka.modulemap +++ /dev/null @@ -1,6 +0,0 @@ -framework module Eureka { - umbrella header "Eureka-umbrella.h" - - export * - module * { export * } -} diff --git a/Pods/Target Support Files/Eureka/Eureka.release.xcconfig b/Pods/Target Support Files/Eureka/Eureka.release.xcconfig deleted file mode 100644 index e57f7a71a..000000000 --- a/Pods/Target Support Files/Eureka/Eureka.release.xcconfig +++ /dev/null @@ -1,15 +0,0 @@ -CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO -CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Eureka -GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 -LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift -OTHER_LDFLAGS = $(inherited) -framework "Foundation" -framework "UIKit" -OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS -PODS_BUILD_DIR = ${BUILD_DIR} -PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) -PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} -PODS_ROOT = ${SRCROOT} -PODS_TARGET_SRCROOT = ${PODS_ROOT}/Eureka -PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates -PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} -SKIP_INSTALL = YES -USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-acknowledgements.markdown b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-acknowledgements.markdown index 6c75a824a..22a3d16f8 100644 --- a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-acknowledgements.markdown +++ b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-acknowledgements.markdown @@ -207,31 +207,6 @@ Apache License -## Eureka - -The MIT License (MIT) - -Copyright (c) 2019 XMARTLABS - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - ## ShareClient The MIT License (MIT) diff --git a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-acknowledgements.plist b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-acknowledgements.plist index 2cb117da4..3b4731ff9 100644 --- a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-acknowledgements.plist +++ b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-acknowledgements.plist @@ -240,37 +240,6 @@ furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - License - MIT - Title - Eureka - Type - PSGroupSpecifier - - - FooterText - The MIT License (MIT) - -Copyright (c) 2016 Mark Wilson - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE diff --git a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Debug-input-files.xcfilelist b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Debug-input-files.xcfilelist index 3d01d36e4..674c1e10e 100644 --- a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Debug-input-files.xcfilelist +++ b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Debug-input-files.xcfilelist @@ -1,5 +1,4 @@ ${PODS_ROOT}/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks.sh ${BUILT_PRODUCTS_DIR}/Charts/Charts.framework -${BUILT_PRODUCTS_DIR}/Eureka/Eureka.framework ${BUILT_PRODUCTS_DIR}/ShareClient/ShareClient.framework ${BUILT_PRODUCTS_DIR}/SwiftAlgorithms/Algorithms.framework \ No newline at end of file diff --git a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Debug-output-files.xcfilelist b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Debug-output-files.xcfilelist index e9ba878da..7ef5b39a5 100644 --- a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Debug-output-files.xcfilelist +++ b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Debug-output-files.xcfilelist @@ -1,4 +1,3 @@ ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Charts.framework -${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Eureka.framework ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ShareClient.framework ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Algorithms.framework \ No newline at end of file diff --git a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Release-input-files.xcfilelist b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Release-input-files.xcfilelist index 3d01d36e4..674c1e10e 100644 --- a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Release-input-files.xcfilelist +++ b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Release-input-files.xcfilelist @@ -1,5 +1,4 @@ ${PODS_ROOT}/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks.sh ${BUILT_PRODUCTS_DIR}/Charts/Charts.framework -${BUILT_PRODUCTS_DIR}/Eureka/Eureka.framework ${BUILT_PRODUCTS_DIR}/ShareClient/ShareClient.framework ${BUILT_PRODUCTS_DIR}/SwiftAlgorithms/Algorithms.framework \ No newline at end of file diff --git a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Release-output-files.xcfilelist b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Release-output-files.xcfilelist index e9ba878da..7ef5b39a5 100644 --- a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Release-output-files.xcfilelist +++ b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks-Release-output-files.xcfilelist @@ -1,4 +1,3 @@ ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Charts.framework -${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Eureka.framework ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ShareClient.framework ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Algorithms.framework \ No newline at end of file diff --git a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks.sh b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks.sh index 1388283cd..a25e47c80 100755 --- a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks.sh +++ b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow-frameworks.sh @@ -177,13 +177,11 @@ code_sign_if_enabled() { if [[ "$CONFIGURATION" == "Debug" ]]; then install_framework "${BUILT_PRODUCTS_DIR}/Charts/Charts.framework" - install_framework "${BUILT_PRODUCTS_DIR}/Eureka/Eureka.framework" install_framework "${BUILT_PRODUCTS_DIR}/ShareClient/ShareClient.framework" install_framework "${BUILT_PRODUCTS_DIR}/SwiftAlgorithms/Algorithms.framework" fi if [[ "$CONFIGURATION" == "Release" ]]; then install_framework "${BUILT_PRODUCTS_DIR}/Charts/Charts.framework" - install_framework "${BUILT_PRODUCTS_DIR}/Eureka/Eureka.framework" install_framework "${BUILT_PRODUCTS_DIR}/ShareClient/ShareClient.framework" install_framework "${BUILT_PRODUCTS_DIR}/SwiftAlgorithms/Algorithms.framework" fi diff --git a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow.debug.xcconfig b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow.debug.xcconfig index f2f239c38..88d7b4e2a 100644 --- a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow.debug.xcconfig +++ b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow.debug.xcconfig @@ -1,11 +1,11 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO -FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Charts" "${PODS_CONFIGURATION_BUILD_DIR}/Eureka" "${PODS_CONFIGURATION_BUILD_DIR}/ShareClient" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftAlgorithms" +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Charts" "${PODS_CONFIGURATION_BUILD_DIR}/ShareClient" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftAlgorithms" GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 -HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Charts/Charts.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Eureka/Eureka.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ShareClient/ShareClient.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftAlgorithms/Algorithms.framework/Headers" +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Charts/Charts.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ShareClient/ShareClient.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftAlgorithms/Algorithms.framework/Headers" LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks' LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift -OTHER_LDFLAGS = $(inherited) -framework "Algorithms" -framework "Charts" -framework "Eureka" -framework "Foundation" -framework "ShareClient" -framework "UIKit" +OTHER_LDFLAGS = $(inherited) -framework "Algorithms" -framework "Charts" -framework "Foundation" -framework "ShareClient" -framework "UIKit" OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) diff --git a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow.release.xcconfig b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow.release.xcconfig index f2f239c38..88d7b4e2a 100644 --- a/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow.release.xcconfig +++ b/Pods/Target Support Files/Pods-LoopFollow/Pods-LoopFollow.release.xcconfig @@ -1,11 +1,11 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO -FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Charts" "${PODS_CONFIGURATION_BUILD_DIR}/Eureka" "${PODS_CONFIGURATION_BUILD_DIR}/ShareClient" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftAlgorithms" +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Charts" "${PODS_CONFIGURATION_BUILD_DIR}/ShareClient" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftAlgorithms" GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 -HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Charts/Charts.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Eureka/Eureka.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ShareClient/ShareClient.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftAlgorithms/Algorithms.framework/Headers" +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Charts/Charts.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ShareClient/ShareClient.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftAlgorithms/Algorithms.framework/Headers" LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks' LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift -OTHER_LDFLAGS = $(inherited) -framework "Algorithms" -framework "Charts" -framework "Eureka" -framework "Foundation" -framework "ShareClient" -framework "UIKit" +OTHER_LDFLAGS = $(inherited) -framework "Algorithms" -framework "Charts" -framework "Foundation" -framework "ShareClient" -framework "UIKit" OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) From 5ffb69b8d5f65a71385a16f6671121f7e9ca58a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 27 May 2025 09:27:45 +0200 Subject: [PATCH 091/138] Bump SwiftFormat --- BuildTools/Package.swift | 2 +- LoopFollow/Helpers/Views/BGPicker.swift | 2 +- LoopFollow/Settings/GeneralSettingsView.swift | 2 +- Scripts/swiftformat.sh | 86 ------------------- .../BatteryConditionTests.swift | 2 +- Tests/AlarmConditions/Helpers.swift | 2 +- Tests/Tests.swift | 2 +- 7 files changed, 6 insertions(+), 92 deletions(-) diff --git a/BuildTools/Package.swift b/BuildTools/Package.swift index f65ebe50c..afff8edaf 100644 --- a/BuildTools/Package.swift +++ b/BuildTools/Package.swift @@ -9,7 +9,7 @@ let package = Package( dependencies: [ .package( url: "https://github.com/nicklockwood/SwiftFormat.git", - from: "0.41.2" + from: "0.56.1" ), ], targets: [ diff --git a/LoopFollow/Helpers/Views/BGPicker.swift b/LoopFollow/Helpers/Views/BGPicker.swift index 3460df072..ffd01ed96 100644 --- a/LoopFollow/Helpers/Views/BGPicker.swift +++ b/LoopFollow/Helpers/Views/BGPicker.swift @@ -1,6 +1,6 @@ // LoopFollow // BGPicker.swift -// Created by Jonas Björkert on 2025-05-26. +// Created by Jonas Björkert on 2025-05-14. import HealthKit import SwiftUI diff --git a/LoopFollow/Settings/GeneralSettingsView.swift b/LoopFollow/Settings/GeneralSettingsView.swift index 361a51a52..318997530 100644 --- a/LoopFollow/Settings/GeneralSettingsView.swift +++ b/LoopFollow/Settings/GeneralSettingsView.swift @@ -1,6 +1,6 @@ // LoopFollow // GeneralSettingsView.swift -// Created by Jonas Björkert on 2025-05-25. +// Created by Jonas Björkert on 2025-05-26. import SwiftUI diff --git a/Scripts/swiftformat.sh b/Scripts/swiftformat.sh index 9d5f1c2a4..2538a4e3b 100755 --- a/Scripts/swiftformat.sh +++ b/Scripts/swiftformat.sh @@ -14,89 +14,3 @@ unset SDKROOT swift run -c release --package-path BuildTools swiftformat "${SRCROOT}" \ --header "LoopFollow\n{file}\nCreated by {author.name} on {created}." \ --exclude Pods,Generated,R.generated.swift,fastlane/swift,Dependencies,dexcom-share-client-swift - -# andOperator,\ -# anyObjectProtocol,\ -# blankLinesAroundMark,\ -# blankLinesAtEndOfScope,\ -# blankLinesAtStartOfScope,\ -# blankLinesBetweenScopes,\ -# consecutiveBlankLines,\ -# consecutiveSpaces,\ -# duplicateImports,\ -# elseOnSameLine,\ -# emptyBraces,\ -# enumNamespaces,\ -# fileHeader,\ -# hoistPatternLet,\ -# indent,\ -# isEmpty,\ -# leadingDelimiters,\ -# linebreakAtEndOfFile,\ -# linebreaks,\ -# modifierOrder,\ -# numberFormatting,\ -# preferKeyPath,\ -# redundantBackticks,\ -# redundantBreak,\ -# redundantExtensionACL,\ -# redundantFileprivate,\ -# redundantGet,\ -# redundantLet,\ -# redundantLetError,\ -# redundantNilInit,\ -# redundantObjc,\ -# redundantParens,\ -# redundantPattern,\ -# redundantRawValues,\ -# redundantReturn,\ -# redundantSelf,\ -# redundantType,\ -# redundantVoidReturnType,\ -# semicolons,\ -# sortedImports,\ -# sortedSwitchCases,\ -# spaceAroundBraces,\ -# spaceAroundBrackets,\ -# spaceAroundComments,\ -# spaceAroundGenerics,\ -# spaceAroundOperators,\ -# spaceAroundParens,\ -# spaceInsideBraces,\ -# spaceInsideBrackets,\ -# spaceInsideComments,\ -# spaceInsideGenerics,\ -# spaceInsideParens,\ -# strongOutlets,\ -# strongifiedSelf,\ -# todos,\ -# trailingCommas,\ -# trailingSpace,\ -# typeSugar,\ -# unusedArguments,\ -# void,\ -# wrap,\ -# wrapArguments,\ -# wrapAttributes,\ -# wrapEnumCases,\ -# wrapMultilineStatementBraces,\ -# wrapSwitchCases \ -# --disable braces,\ -# redundantInit,\ -# trailingClosures \ -# --commas inline \ -# --exponentcase uppercase \ -# --header strip \ -# --hexliteralcase uppercase \ -# --ifdef indent \ -# --indent 4 \ -# --self remove \ -# --semicolons never \ -# --swiftversion 5.2 \ -# --trimwhitespace always \ -# --maxwidth 130 \ -# --wraparguments before-first \ -# --funcattributes same-line \ -# --typeattributes same-line \ -# --varattributes same-line \ -# --wrapcollections before-first \ diff --git a/Tests/AlarmConditions/BatteryConditionTests.swift b/Tests/AlarmConditions/BatteryConditionTests.swift index 77e813c65..1b754e037 100644 --- a/Tests/AlarmConditions/BatteryConditionTests.swift +++ b/Tests/AlarmConditions/BatteryConditionTests.swift @@ -1,6 +1,6 @@ // LoopFollow // BatteryConditionTests.swift -// Created by Jonas Björkert on 2025-05-21. +// Created by Jonas Björkert on 2025-05-23. @testable import LoopFollow import Testing diff --git a/Tests/AlarmConditions/Helpers.swift b/Tests/AlarmConditions/Helpers.swift index e20c9cf85..39df2d6a6 100644 --- a/Tests/AlarmConditions/Helpers.swift +++ b/Tests/AlarmConditions/Helpers.swift @@ -1,6 +1,6 @@ // LoopFollow // Helpers.swift -// Created by Jonas Björkert on 2025-05-21. +// Created by Jonas Björkert on 2025-05-23. // Tests/AlarmConditions/Helpers.swift import Foundation diff --git a/Tests/Tests.swift b/Tests/Tests.swift index c4d2f3273..4526460ec 100644 --- a/Tests/Tests.swift +++ b/Tests/Tests.swift @@ -1,6 +1,6 @@ // LoopFollow // Tests.swift -// Created by Jonas Björkert on 2025-05-21. +// Created by Jonas Björkert on 2025-05-23. import Testing From a86002e35e21efcbaea4ed555579f887b5916d10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 27 May 2025 13:07:47 +0200 Subject: [PATCH 092/138] Icon changes --- LoopFollow/Settings/SettingsMenuView.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/LoopFollow/Settings/SettingsMenuView.swift b/LoopFollow/Settings/SettingsMenuView.swift index d3734f54f..0a0ea40f5 100644 --- a/LoopFollow/Settings/SettingsMenuView.swift +++ b/LoopFollow/Settings/SettingsMenuView.swift @@ -62,7 +62,7 @@ struct SettingsMenuView: View { destination: .calendar) navRow(title: "Contact", - icon: "envelope", + icon: "person.circle", destination: .contact) } @@ -83,7 +83,7 @@ struct SettingsMenuView: View { } label: { Label("Share Logs", systemImage: "square.and.arrow.up") } - .accessibilityIdentifier("ShareLogsButton") + .buttonStyle(.plain) } // ────────────── Community ────────────── @@ -119,11 +119,11 @@ struct SettingsMenuView: View { .pickerStyle(.segmented) navRow(title: "Nightscout Settings", - icon: "antenna.radiowaves.left.and.right", + icon: "network", destination: .nightscout) navRow(title: "Dexcom Settings", - icon: "waveform.path.ecg", + icon: "sensor.tag.radiowaves.forward", destination: .dexcom) } .onAppear { From 52af22333a1c5261410c281b5918bdc986909fde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 27 May 2025 13:59:38 +0200 Subject: [PATCH 093/138] Glyph --- LoopFollow/Settings/SettingsMenuView.swift | 47 ++++++++++++++++++---- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/LoopFollow/Settings/SettingsMenuView.swift b/LoopFollow/Settings/SettingsMenuView.swift index 0a0ea40f5..4dded2a4c 100644 --- a/LoopFollow/Settings/SettingsMenuView.swift +++ b/LoopFollow/Settings/SettingsMenuView.swift @@ -3,6 +3,7 @@ // Created by Jonas Björkert on 2025-05-26. import SwiftUI +import UIKit struct SettingsMenuView: View { // MARK: – Call-backs ----------------------------------------------------- @@ -153,15 +154,27 @@ struct SettingsMenuView: View { // MARK: – Row helpers ---------------------------------------------------- /// Standard row with icon, chevron and sheet presentation - private func navRow(title: String, - icon: String, - destination: Sheet) -> some View - { - NavigationLink { - destination.destination + /// One tappable row, styled like the iOS Settings app + @ViewBuilder + private func navRow( + title: String, + icon: String, + tint: Color = .primary, + destination: Sheet + ) -> some View { + Button { + sheet = destination } label: { - Label(title, systemImage: icon) + HStack { + Glyph(symbol: icon, tint: tint) + Text(title) + Spacer() + Image(systemName: "chevron.right") + .foregroundColor(Color(uiColor: .tertiaryLabel)) + } + .contentShape(Rectangle()) } + .buttonStyle(.plain) } /// Simple key-value row @@ -258,3 +271,23 @@ extension UIViewController { present(a, animated: true) } } + +struct Glyph: View { + let symbol: String + let tint: Color + + @Environment(\.colorScheme) private var scheme + + var body: some View { + ZStack { + RoundedRectangle(cornerRadius: 8, style: .continuous) + .fill(Color(uiColor: .systemGray)) + .frame(width: 28, height: 28) + + Image(systemName: symbol) + .font(.system(size: 16, weight: .regular)) + .foregroundStyle(tint) + } + .frame(width: 36, height: 36) + } +} From 860712dbf8cdb5b7caa88a840b3c3f4532cf1ea3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 27 May 2025 14:08:18 +0200 Subject: [PATCH 094/138] SettingsMenu --- LoopFollow/Settings/SettingsMenuView.swift | 46 +++++++++++++++++----- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/LoopFollow/Settings/SettingsMenuView.swift b/LoopFollow/Settings/SettingsMenuView.swift index 4dded2a4c..c1da43c36 100644 --- a/LoopFollow/Settings/SettingsMenuView.swift +++ b/LoopFollow/Settings/SettingsMenuView.swift @@ -79,19 +79,15 @@ struct SettingsMenuView: View { icon: "doc.text.magnifyingglass", destination: .viewLog) - Button { - shareLogs() - } label: { - Label("Share Logs", systemImage: "square.and.arrow.up") - } - .buttonStyle(.plain) + actionRow(title: "Share Logs", + icon: "square.and.arrow.up") { shareLogs() } } // ────────────── Community ────────────── Section("Community") { - Link(destination: URL(string: "https://www.facebook.com/groups/loopfollowlnl")!) { - Label("LoopFollow Facebook Group", systemImage: "person.3") - } + linkRow(title: "LoopFollow Facebook Group", + icon: "person.2.fill", + url: URL(string: "https://www.facebook.com/groups/loopfollowlnl")!) } // ────────────── Build info ────────────── @@ -291,3 +287,35 @@ struct Glyph: View { .frame(width: 36, height: 36) } } + +@ViewBuilder +private func actionRow( + title: String, + icon: String, + tint: Color = .primary, + action: @escaping () -> Void +) -> some View { + Button { action() } label: { + HStack { + Glyph(symbol: icon, tint: tint) + Text(title) + Spacer() + Image(systemName: "chevron.right") + .foregroundColor(Color(uiColor: .tertiaryLabel)) + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) +} + +@ViewBuilder +private func linkRow( + title: String, + icon: String, + tint: Color = .primary, + url: URL +) -> some View { + actionRow(title: title, icon: icon, tint: tint) { + UIApplication.shared.open(url) + } +} From 4079dac44bfffab38510f33efc76fb76ae2e5008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 27 May 2025 14:11:58 +0200 Subject: [PATCH 095/138] SettingsMenu --- LoopFollow/Settings/SettingsMenuView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LoopFollow/Settings/SettingsMenuView.swift b/LoopFollow/Settings/SettingsMenuView.swift index c1da43c36..f0322f5f5 100644 --- a/LoopFollow/Settings/SettingsMenuView.swift +++ b/LoopFollow/Settings/SettingsMenuView.swift @@ -155,7 +155,7 @@ struct SettingsMenuView: View { private func navRow( title: String, icon: String, - tint: Color = .primary, + tint: Color = .white, destination: Sheet ) -> some View { Button { From 570d0a016c18aaad98ad6065fbf6e4afeaf17d39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 27 May 2025 15:42:52 +0200 Subject: [PATCH 096/138] SettingsMenu --- LoopFollow.xcodeproj/project.pbxproj | 16 ++ LoopFollow/Helpers/Views/ActionRow.swift | 25 ++ LoopFollow/Helpers/Views/Glyph.swift | 25 ++ LoopFollow/Helpers/Views/LinkRow.swift | 18 ++ LoopFollow/Helpers/Views/NavigationRow.swift | 26 ++ LoopFollow/Settings/SettingsMenuView.swift | 241 ++++++++----------- 6 files changed, 204 insertions(+), 147 deletions(-) create mode 100644 LoopFollow/Helpers/Views/ActionRow.swift create mode 100644 LoopFollow/Helpers/Views/Glyph.swift create mode 100644 LoopFollow/Helpers/Views/LinkRow.swift create mode 100644 LoopFollow/Helpers/Views/NavigationRow.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 357f7246a..104b47e58 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -192,6 +192,10 @@ DDDF6F472D479AD200884336 /* LoopOverrideViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDF6F462D479AD100884336 /* LoopOverrideViewModel.swift */; }; DDDF6F492D479AF000884336 /* NoRemoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDF6F482D479AEF00884336 /* NoRemoteView.swift */; }; DDE69ED22C7256260013EAEC /* RemoteType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE69ED12C7256260013EAEC /* RemoteType.swift */; }; + DDE75D232DE5E505007C1FC1 /* Glyph.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE75D222DE5E505007C1FC1 /* Glyph.swift */; }; + DDE75D272DE5E539007C1FC1 /* ActionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE75D262DE5E539007C1FC1 /* ActionRow.swift */; }; + DDE75D292DE5E56C007C1FC1 /* LinkRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE75D282DE5E56C007C1FC1 /* LinkRow.swift */; }; + DDE75D2B2DE5E613007C1FC1 /* NavigationRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE75D2A2DE5E613007C1FC1 /* NavigationRow.swift */; }; DDEF503A2D31615000999A5D /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDEF50392D31614200999A5D /* LogManager.swift */; }; DDEF503C2D31BE2D00999A5D /* TaskScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDEF503B2D31BE2A00999A5D /* TaskScheduler.swift */; }; DDEF503F2D32754F00999A5D /* ProfileTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDEF503E2D32754A00999A5D /* ProfileTask.swift */; }; @@ -558,6 +562,10 @@ DDDF6F462D479AD100884336 /* LoopOverrideViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopOverrideViewModel.swift; sourceTree = ""; }; DDDF6F482D479AEF00884336 /* NoRemoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoRemoteView.swift; sourceTree = ""; }; DDE69ED12C7256260013EAEC /* RemoteType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteType.swift; sourceTree = ""; }; + DDE75D222DE5E505007C1FC1 /* Glyph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Glyph.swift; sourceTree = ""; }; + DDE75D262DE5E539007C1FC1 /* ActionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionRow.swift; sourceTree = ""; }; + DDE75D282DE5E56C007C1FC1 /* LinkRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkRow.swift; sourceTree = ""; }; + DDE75D2A2DE5E613007C1FC1 /* NavigationRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRow.swift; sourceTree = ""; }; DDEF50392D31614200999A5D /* LogManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogManager.swift; sourceTree = ""; }; DDEF503B2D31BE2A00999A5D /* TaskScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskScheduler.swift; sourceTree = ""; }; DDEF503E2D32754A00999A5D /* ProfileTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileTask.swift; sourceTree = ""; }; @@ -1172,11 +1180,15 @@ DDF6999C2C5AAA4C0058A8D9 /* Views */ = { isa = PBXGroup; children = ( + DDE75D222DE5E505007C1FC1 /* Glyph.swift */, DD8316492DE4C504004467AA /* SettingsStepperRow.swift */, DD8316432DE47CA9004467AA /* BGPicker.swift */, DDF6999D2C5AAA640058A8D9 /* ErrorMessageView.swift */, DD16AF0E2C99592F00FB655A /* HKQuantityInputView.swift */, DD16AF102C997B4600FB655A /* LoadingButtonView.swift */, + DDE75D262DE5E539007C1FC1 /* ActionRow.swift */, + DDE75D282DE5E56C007C1FC1 /* LinkRow.swift */, + DDE75D2A2DE5E613007C1FC1 /* NavigationRow.swift */, ); path = Views; sourceTree = ""; @@ -1811,6 +1823,7 @@ DD4AFB492DB576C200BB593F /* AlarmSettingsView.swift in Sources */, DD7F4C032DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift in Sources */, DD7F4C1B2DD6501D00D449E9 /* COBCondition.swift in Sources */, + DDE75D232DE5E505007C1FC1 /* Glyph.swift in Sources */, DD4878202C7DAF890048F05C /* PushMessage.swift in Sources */, DD0247712DB4337700FCADF6 /* BuildExpireCondition.swift in Sources */, DDF6999B2C5AA32E0058A8D9 /* TempTargetPreset.swift in Sources */, @@ -1865,6 +1878,7 @@ DDB0AF522BB1A8BE00AFA48B /* BuildDetails.swift in Sources */, DDA9ACAA2D6A6B8300E6F1A9 /* ContactIncludeOption.swift in Sources */, DD7F4C1D2DD650D500D449E9 /* COBAlarmEditor.swift in Sources */, + DDE75D272DE5E539007C1FC1 /* ActionRow.swift in Sources */, DD0C0C622C4175FD00DBADDF /* NSProfile.swift in Sources */, DD8316182DE3633D004467AA /* GeneralSettingsView.swift in Sources */, DD58171E2D299FCA0041FB98 /* BluetoothDeviceDelegate.swift in Sources */, @@ -1884,6 +1898,7 @@ FCFEECA2248857A600402A7F /* SettingsViewController.swift in Sources */, DD7F4C232DD7A62200D449E9 /* AlarmType+SortDirection.swift in Sources */, DD0650F72DCFDA26004D3B41 /* InfoBanner.swift in Sources */, + DDE75D292DE5E56C007C1FC1 /* LinkRow.swift in Sources */, DD4AFB612DB68BBC00BB593F /* AlarmListView.swift in Sources */, DD7B0D442D730A3B0063DCB6 /* CycleHelper.swift in Sources */, DD7F4BA12DD2193F00D449E9 /* AlarmSnoozeSection.swift in Sources */, @@ -1951,6 +1966,7 @@ DD608A0C2C27415C00F91132 /* BackgroundAlertManager.swift in Sources */, DD50C7532D0828D10057AE6F /* ContactSettingsView.swift in Sources */, DD4878082C7B30BF0048F05C /* RemoteSettingsView.swift in Sources */, + DDE75D2B2DE5E613007C1FC1 /* NavigationRow.swift in Sources */, DD4AFB3B2DB55CB600BB593F /* TimeOfDay.swift in Sources */, DDEF503C2D31BE2D00999A5D /* TaskScheduler.swift in Sources */, DD0650EB2DCE8385004D3B41 /* LowBGCondition.swift in Sources */, diff --git a/LoopFollow/Helpers/Views/ActionRow.swift b/LoopFollow/Helpers/Views/ActionRow.swift new file mode 100644 index 000000000..86d28cb05 --- /dev/null +++ b/LoopFollow/Helpers/Views/ActionRow.swift @@ -0,0 +1,25 @@ +// LoopFollow +// ActionRow.swift +// Created by Jonas Björkert on 2025-05-27. + +import SwiftUI + +@ViewBuilder +func ActionRow( + title: String, + icon: String, + tint: Color = .white, + action: @escaping () -> Void +) -> some View { + Button { action() } label: { + HStack { + Glyph(symbol: icon, tint: tint) + Text(title) + Spacer() + Image(systemName: "chevron.right") + .foregroundColor(Color(uiColor: .tertiaryLabel)) + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) +} diff --git a/LoopFollow/Helpers/Views/Glyph.swift b/LoopFollow/Helpers/Views/Glyph.swift new file mode 100644 index 000000000..a5c166a54 --- /dev/null +++ b/LoopFollow/Helpers/Views/Glyph.swift @@ -0,0 +1,25 @@ +// LoopFollow +// Glyph.swift +// Created by Jonas Björkert on 2025-05-27. + +import SwiftUICore + +struct Glyph: View { + let symbol: String + let tint: Color + + @Environment(\.colorScheme) private var scheme + + var body: some View { + ZStack { + RoundedRectangle(cornerRadius: 8, style: .continuous) + .fill(Color(uiColor: .systemGray)) + .frame(width: 28, height: 28) + + Image(systemName: symbol) + .font(.system(size: 16, weight: .regular)) + .foregroundStyle(tint) + } + .frame(width: 36, height: 36) + } +} diff --git a/LoopFollow/Helpers/Views/LinkRow.swift b/LoopFollow/Helpers/Views/LinkRow.swift new file mode 100644 index 000000000..6470ba4fe --- /dev/null +++ b/LoopFollow/Helpers/Views/LinkRow.swift @@ -0,0 +1,18 @@ +// LoopFollow +// LinkRow.swift +// Created by Jonas Björkert on 2025-05-27. + +import Foundation +import SwiftUI + +@ViewBuilder +func LinkRow( + title: String, + icon: String, + tint: Color = .white, + url: URL +) -> some View { + ActionRow(title: title, icon: icon, tint: tint) { + UIApplication.shared.open(url) + } +} diff --git a/LoopFollow/Helpers/Views/NavigationRow.swift b/LoopFollow/Helpers/Views/NavigationRow.swift new file mode 100644 index 000000000..8e137b5d9 --- /dev/null +++ b/LoopFollow/Helpers/Views/NavigationRow.swift @@ -0,0 +1,26 @@ +// LoopFollow +// NavigationRow.swift +// Created by Jonas Björkert on 2025-05-27. + +import SwiftUI + +struct NavigationRow: View { + let title: String + let icon: String + var iconTint: Color = .white + let action: () -> Void + + var body: some View { + Button(action: action) { + HStack { + Glyph(symbol: icon, tint: iconTint) + Text(title) + Spacer() + Image(systemName: "chevron.right") + .foregroundColor(Color(uiColor: .tertiaryLabel)) + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } +} diff --git a/LoopFollow/Settings/SettingsMenuView.swift b/LoopFollow/Settings/SettingsMenuView.swift index f0322f5f5..37b7e204b 100644 --- a/LoopFollow/Settings/SettingsMenuView.swift +++ b/LoopFollow/Settings/SettingsMenuView.swift @@ -6,91 +6,114 @@ import SwiftUI import UIKit struct SettingsMenuView: View { - // MARK: – Call-backs ----------------------------------------------------- + // MARK: – Call-backs let onNightscoutVisibilityChange: (_ enabled: Bool) -> Void - // MARK: – Local state ---------------------------------------------------- + // MARK: – Local state @State private var sheet: Sheet? @State private var latestVersion: String? @State private var versionTint: Color = .secondary - // MARK: – Body ----------------------------------------------------------- + // MARK: – Body var body: some View { NavigationStack { List { - // ────────────── Data settings ────────────── + // ───────── Data settings ───────── dataSection - // ────────────── App settings ────────────── + // ───────── App settings ───────── Section("App Settings") { - navRow(title: "Background Refresh Settings", - icon: "arrow.clockwise", - destination: .backgroundRefresh) + NavigationRow(title: "Background Refresh Settings", + icon: "arrow.clockwise") + { + sheet = .backgroundRefresh + } - navRow(title: "General Settings", - icon: "gearshape", - destination: .general) + NavigationRow(title: "General Settings", + icon: "gearshape") + { + sheet = .general + } - navRow(title: "Graph Settings", - icon: "chart.xyaxis.line", - destination: .graph) + NavigationRow(title: "Graph Settings", + icon: "chart.xyaxis.line") + { + sheet = .graph + } if IsNightscoutEnabled() { - navRow(title: "Information Display Settings", - icon: "info.circle", - destination: .infoDisplay) + NavigationRow(title: "Information Display Settings", + icon: "info.circle") + { + sheet = .infoDisplay + } } } - // ────────────── Alarms ────────────── + // ───────── Alarms ───────── Section { - navRow(title: "Alarms", - icon: "bell", - destination: .alarmsList) + NavigationRow(title: "Alarms", + icon: "bell") + { + sheet = .alarmsList + } - navRow(title: "Alarm Settings", - icon: "bell.badge", - destination: .alarmSettings) + NavigationRow(title: "Alarm Settings", + icon: "bell.badge") + { + sheet = .alarmSettings + } } - // ────────────── Integrations ────────────── + // ───────── Integrations ───────── Section("Integrations") { - navRow(title: "Calendar", - icon: "calendar", - destination: .calendar) + NavigationRow(title: "Calendar", + icon: "calendar") + { + sheet = .calendar + } - navRow(title: "Contact", - icon: "person.circle", - destination: .contact) + NavigationRow(title: "Contact", + icon: "person.circle") + { + sheet = .contact + } } - // ────────────── Advanced / Logs ────────────── + // ───────── Advanced / Logs ───────── Section("Advanced Settings") { - navRow(title: "Advanced Settings", - icon: "exclamationmark.shield", - destination: .advanced) + NavigationRow(title: "Advanced Settings", + icon: "exclamationmark.shield") + { + sheet = .advanced + } } Section("Logging") { - navRow(title: "View Log", - icon: "doc.text.magnifyingglass", - destination: .viewLog) + NavigationRow(title: "View Log", + icon: "doc.text.magnifyingglass") + { + sheet = .viewLog + } - actionRow(title: "Share Logs", - icon: "square.and.arrow.up") { shareLogs() } + ActionRow(title: "Share Logs", + icon: "square.and.arrow.up") + { + shareLogs() + } } - // ────────────── Community ────────────── + // ───────── Community ───────── Section("Community") { - linkRow(title: "LoopFollow Facebook Group", + LinkRow(title: "LoopFollow Facebook Group", icon: "person.2.fill", url: URL(string: "https://www.facebook.com/groups/loopfollowlnl")!) } - // ────────────── Build info ────────────── + // ───────── Build info ───────── buildInfoSection } .navigationTitle("Settings") @@ -99,9 +122,8 @@ struct SettingsMenuView: View { .sheet(item: $sheet) { $0.destination } } - // MARK: – Section builders ---------------------------------------------- + // MARK: – Section builders - /// “Data Settings” @ViewBuilder private var dataSection: some View { Section("Data Settings") { @@ -115,20 +137,23 @@ struct SettingsMenuView: View { } .pickerStyle(.segmented) - navRow(title: "Nightscout Settings", - icon: "network", - destination: .nightscout) + NavigationRow(title: "Nightscout Settings", + icon: "network") + { + sheet = .nightscout + } - navRow(title: "Dexcom Settings", - icon: "sensor.tag.radiowaves.forward", - destination: .dexcom) + NavigationRow(title: "Dexcom Settings", + icon: "sensor.tag.radiowaves.forward") + { + sheet = .dexcom + } } .onAppear { onNightscoutVisibilityChange(IsNightscoutEnabled()) } } - /// version / build info @ViewBuilder private var buildInfoSection: some View { let build = BuildDetails.default @@ -137,45 +162,22 @@ struct SettingsMenuView: View { Section("Build Information") { keyValue("Version", ver, tint: versionTint) keyValue("Latest version", latestVersion ?? "Fetching…") - if !(build.isMacApp() || build.isSimulatorBuild()) { keyValue(build.expirationHeaderString, dateTimeUtils.formattedDate(from: build.calculateExpirationDate())) } - keyValue("Built", dateTimeUtils.formattedDate(from: build.buildDate())) + keyValue("Built", + dateTimeUtils.formattedDate(from: build.buildDate())) keyValue("Branch", build.branchAndSha) } } - // MARK: – Row helpers ---------------------------------------------------- + // MARK: – Helpers - /// Standard row with icon, chevron and sheet presentation - /// One tappable row, styled like the iOS Settings app - @ViewBuilder - private func navRow( - title: String, - icon: String, - tint: Color = .white, - destination: Sheet - ) -> some View { - Button { - sheet = destination - } label: { - HStack { - Glyph(symbol: icon, tint: tint) - Text(title) - Spacer() - Image(systemName: "chevron.right") - .foregroundColor(Color(uiColor: .tertiaryLabel)) - } - .contentShape(Rectangle()) - } - .buttonStyle(.plain) - } - - /// Simple key-value row - @ViewBuilder - private func keyValue(_ key: String, _ value: String, tint: Color = .secondary) -> some View { + private func keyValue(_ key: String, + _ value: String, + tint: Color = .secondary) -> some View + { HStack { Text(key) Spacer() @@ -183,22 +185,18 @@ struct SettingsMenuView: View { } } - // MARK: – Version check -------------------------------------------------- - private func refreshVersionInfo() async { - let manager = AppVersionManager() - let (latest, newer, blacklisted) = await manager.checkForNewVersionAsync() + let mgr = AppVersionManager() + let (latest, newer, blacklisted) = await mgr.checkForNewVersionAsync() latestVersion = latest ?? "Unknown" - // match old colour logic - let current = manager.version() - versionTint = blacklisted ? .red : - newer ? .orange : - latest == current ? .green : .secondary + let current = mgr.version() + versionTint = blacklisted ? .red + : newer ? .orange + : latest == current ? .green + : .secondary } - // MARK: – Share logs ----------------------------------------------------- - private func shareLogs() { let files = LogManager.shared.logFilesForTodayAndYesterday() guard !files.isEmpty else { @@ -208,12 +206,13 @@ struct SettingsMenuView: View { ) return } - let avc = UIActivityViewController(activityItems: files, applicationActivities: nil) + let avc = UIActivityViewController(activityItems: files, + applicationActivities: nil) UIApplication.shared.topMost?.present(avc, animated: true) } } -// MARK: – Sheet routing identical to earlier ------------------------------- +// MARK: – Sheet routing (unchanged) private enum Sheet: Identifiable { case nightscout, dexcom @@ -267,55 +266,3 @@ extension UIViewController { present(a, animated: true) } } - -struct Glyph: View { - let symbol: String - let tint: Color - - @Environment(\.colorScheme) private var scheme - - var body: some View { - ZStack { - RoundedRectangle(cornerRadius: 8, style: .continuous) - .fill(Color(uiColor: .systemGray)) - .frame(width: 28, height: 28) - - Image(systemName: symbol) - .font(.system(size: 16, weight: .regular)) - .foregroundStyle(tint) - } - .frame(width: 36, height: 36) - } -} - -@ViewBuilder -private func actionRow( - title: String, - icon: String, - tint: Color = .primary, - action: @escaping () -> Void -) -> some View { - Button { action() } label: { - HStack { - Glyph(symbol: icon, tint: tint) - Text(title) - Spacer() - Image(systemName: "chevron.right") - .foregroundColor(Color(uiColor: .tertiaryLabel)) - } - .contentShape(Rectangle()) - } - .buttonStyle(.plain) -} - -@ViewBuilder -private func linkRow( - title: String, - icon: String, - tint: Color = .primary, - url: URL -) -> some View { - actionRow(title: title, icon: icon, tint: tint) { - UIApplication.shared.open(url) - } -} From 514338fa7b3fdc7466a7ca8cdc1e17f710879be0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 27 May 2025 16:01:56 +0200 Subject: [PATCH 097/138] Dark Mode --- LoopFollow/Alarm/AlarmListView.swift | 1 + LoopFollow/Alarm/AlarmSettingsView.swift | 1 + LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift | 1 + LoopFollow/Contact/Settings/ContactSettingsView.swift | 1 + LoopFollow/Dexcom/DexcomSettingsView.swift | 1 + LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift | 1 + LoopFollow/Log/LogView.swift | 1 + LoopFollow/Nightscout/NightscoutSettingsView.swift | 1 + LoopFollow/Remote/Settings/RemoteSettingsView.swift | 1 + LoopFollow/Settings/AdvancedSettingsView.swift | 1 + LoopFollow/Settings/GeneralSettingsView.swift | 1 + LoopFollow/Settings/GraphSettingsView.swift | 1 + LoopFollow/Settings/WatchSettingsView.swift | 1 + 13 files changed, 13 insertions(+) diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index 12d25bafd..4611dcf46 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -142,6 +142,7 @@ struct AlarmListView: View { sheetContent(for: info) } } + .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) } private func handleSheetDismiss() { diff --git a/LoopFollow/Alarm/AlarmSettingsView.swift b/LoopFollow/Alarm/AlarmSettingsView.swift index 19d222fff..4beb59407 100644 --- a/LoopFollow/Alarm/AlarmSettingsView.swift +++ b/LoopFollow/Alarm/AlarmSettingsView.swift @@ -196,5 +196,6 @@ struct AlarmSettingsView: View { } } } + .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) } } diff --git a/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift b/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift index e26e26452..6bb03b3c8 100644 --- a/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift +++ b/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift @@ -37,6 +37,7 @@ struct BackgroundRefreshSettingsView: View { stopTimer() } } + .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) } // MARK: - Subviews / Computed Properties diff --git a/LoopFollow/Contact/Settings/ContactSettingsView.swift b/LoopFollow/Contact/Settings/ContactSettingsView.swift index 0d041aa4a..f7d58cc2d 100644 --- a/LoopFollow/Contact/Settings/ContactSettingsView.swift +++ b/LoopFollow/Contact/Settings/ContactSettingsView.swift @@ -89,6 +89,7 @@ struct ContactSettingsView: View { Alert(title: Text(alertTitle), message: Text(alertMessage), dismissButton: .default(Text("OK"))) } } + .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) } private func requestContactAccess() { diff --git a/LoopFollow/Dexcom/DexcomSettingsView.swift b/LoopFollow/Dexcom/DexcomSettingsView.swift index b4c2e63a1..91ba263dd 100644 --- a/LoopFollow/Dexcom/DexcomSettingsView.swift +++ b/LoopFollow/Dexcom/DexcomSettingsView.swift @@ -36,5 +36,6 @@ struct DexcomSettingsView: View { } } } + .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) } } diff --git a/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift index a220bde5c..a92732df8 100644 --- a/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift +++ b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift @@ -47,5 +47,6 @@ struct InfoDisplaySettingsView: View { NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil) } } + .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) } } diff --git a/LoopFollow/Log/LogView.swift b/LoopFollow/Log/LogView.swift index 8617e69be..cc5cd683d 100644 --- a/LoopFollow/Log/LogView.swift +++ b/LoopFollow/Log/LogView.swift @@ -46,5 +46,6 @@ struct LogView: View { viewModel.loadLogEntries() } } + .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) } } diff --git a/LoopFollow/Nightscout/NightscoutSettingsView.swift b/LoopFollow/Nightscout/NightscoutSettingsView.swift index db2343efb..e206917ed 100644 --- a/LoopFollow/Nightscout/NightscoutSettingsView.swift +++ b/LoopFollow/Nightscout/NightscoutSettingsView.swift @@ -27,6 +27,7 @@ struct NightscoutSettingsView: View { viewModel.dismiss() } } + .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) } // MARK: - Subviews / Computed Properties diff --git a/LoopFollow/Remote/Settings/RemoteSettingsView.swift b/LoopFollow/Remote/Settings/RemoteSettingsView.swift index 7f8707214..6e8a8b7f2 100644 --- a/LoopFollow/Remote/Settings/RemoteSettingsView.swift +++ b/LoopFollow/Remote/Settings/RemoteSettingsView.swift @@ -206,6 +206,7 @@ struct RemoteSettingsView: View { } } } + .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) } // MARK: - Custom Row for Remote Type Selection diff --git a/LoopFollow/Settings/AdvancedSettingsView.swift b/LoopFollow/Settings/AdvancedSettingsView.swift index 35fc31be6..993fed7a8 100644 --- a/LoopFollow/Settings/AdvancedSettingsView.swift +++ b/LoopFollow/Settings/AdvancedSettingsView.swift @@ -37,5 +37,6 @@ struct AdvancedSettingsView: View { } } } + .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) } } diff --git a/LoopFollow/Settings/GeneralSettingsView.swift b/LoopFollow/Settings/GeneralSettingsView.swift index 318997530..5b9ce5130 100644 --- a/LoopFollow/Settings/GeneralSettingsView.swift +++ b/LoopFollow/Settings/GeneralSettingsView.swift @@ -112,5 +112,6 @@ struct GeneralSettingsView: View { } } } + .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) } } diff --git a/LoopFollow/Settings/GraphSettingsView.swift b/LoopFollow/Settings/GraphSettingsView.swift index c3922de56..0caf6e885 100644 --- a/LoopFollow/Settings/GraphSettingsView.swift +++ b/LoopFollow/Settings/GraphSettingsView.swift @@ -143,6 +143,7 @@ struct GraphSettingsView: View { } } } + .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) } /// Marks the chart as needing a redraw diff --git a/LoopFollow/Settings/WatchSettingsView.swift b/LoopFollow/Settings/WatchSettingsView.swift index a3416716d..3422a4554 100644 --- a/LoopFollow/Settings/WatchSettingsView.swift +++ b/LoopFollow/Settings/WatchSettingsView.swift @@ -82,6 +82,7 @@ struct WatchSettingsView: View { await requestCalendarAccessAndLoad() } } + .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) } // MARK: - Helpers From 15f461bdc1a38adec1f4eb834a635f9837d4a377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 27 May 2025 20:29:10 +0200 Subject: [PATCH 098/138] SettingsMenu --- LoopFollow.xcodeproj/project.pbxproj | 32 ++----- .../Alarm/AlarmEditing/AlarmEditor.swift | 12 +-- LoopFollow/Alarm/AlarmListView.swift | 84 ++++++++----------- LoopFollow/Alarm/AlarmSettingsView.swift | 10 +-- .../BackgroundRefreshSettingsView.swift | 10 +-- .../InfoDisplaySettingsView.swift | 5 +- LoopFollow/Log/LogView.swift | 10 +-- .../Nightscout/NightscoutSettingsView.swift | 10 +-- .../Settings/AdvancedSettingsView.swift | 10 +-- ...sView.swift => CalendarSettingsView.swift} | 14 +--- .../Settings/ContactSettingsView.swift | 10 +-- .../Settings/ContactSettingsViewModel.swift | 2 +- .../DexcomSettingsView.swift | 12 +-- .../DexcomSettingsViewModel.swift | 0 LoopFollow/Settings/GeneralSettingsView.swift | 13 +-- LoopFollow/Settings/GraphSettingsView.swift | 13 +-- LoopFollow/Settings/SettingsMenuView.swift | 52 ++++++------ 17 files changed, 95 insertions(+), 204 deletions(-) rename LoopFollow/Settings/{WatchSettingsView.swift => CalendarSettingsView.swift} (91%) rename LoopFollow/{Contact => }/Settings/ContactSettingsView.swift (93%) rename LoopFollow/{Contact => }/Settings/ContactSettingsViewModel.swift (98%) rename LoopFollow/{Dexcom => Settings}/DexcomSettingsView.swift (70%) rename LoopFollow/{Dexcom => Settings}/DexcomSettingsViewModel.swift (100%) diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 104b47e58..726dda3d7 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -125,7 +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 */; }; + DD83164E2DE4E093004467AA /* CalendarSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD83164D2DE4E093004467AA /* CalendarSettingsView.swift */; }; DD8316502DE4E635004467AA /* SettingsMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD83164F2DE4E635004467AA /* SettingsMenuView.swift */; }; DD85E9952D739CFE001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */; }; DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */; }; @@ -492,7 +492,7 @@ DD8316472DE49EE5004467AA /* Storage+Migrate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+Migrate.swift"; sourceTree = ""; }; DD8316492DE4C504004467AA /* SettingsStepperRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsStepperRow.swift; sourceTree = ""; }; DD83164B2DE4DB3A004467AA /* BinaryFloatingPoint+localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BinaryFloatingPoint+localized.swift"; sourceTree = ""; }; - DD83164D2DE4E093004467AA /* WatchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchSettingsView.swift; sourceTree = ""; }; + DD83164D2DE4E093004467AA /* CalendarSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarSettingsView.swift; sourceTree = ""; }; DD83164F2DE4E635004467AA /* SettingsMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsMenuView.swift; sourceTree = ""; }; DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnipodDashHeartbeatBluetoothTransmitter.swift; sourceTree = ""; }; DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseConversion.swift; sourceTree = ""; }; @@ -852,11 +852,15 @@ isa = PBXGroup; children = ( DD83164F2DE4E635004467AA /* SettingsMenuView.swift */, - DD83164D2DE4E093004467AA /* WatchSettingsView.swift */, + DD2C2E552D3C3913006413A5 /* DexcomSettingsView.swift */, + DD2C2E532D3C37D7006413A5 /* DexcomSettingsViewModel.swift */, + DD83164D2DE4E093004467AA /* CalendarSettingsView.swift */, DD8316452DE49B09004467AA /* GraphSettingsView.swift */, DD1A97152D4294B2000DDC11 /* AdvancedSettingsViewModel.swift */, DD1A97132D4294A4000DDC11 /* AdvancedSettingsView.swift */, DD8316172DE3633D004467AA /* GeneralSettingsView.swift */, + DD50C74F2D0828800057AE6F /* ContactSettingsViewModel.swift */, + DD50C7522D0828D10057AE6F /* ContactSettingsView.swift */, ); path = Settings; sourceTree = ""; @@ -870,15 +874,6 @@ path = Nightscout; sourceTree = ""; }; - DD2C2E522D3C36A8006413A5 /* Dexcom */ = { - isa = PBXGroup; - children = ( - DD2C2E552D3C3913006413A5 /* DexcomSettingsView.swift */, - DD2C2E532D3C37D7006413A5 /* DexcomSettingsViewModel.swift */, - ); - path = Dexcom; - sourceTree = ""; - }; DD4878062C7B2E9E0048F05C /* Settings */ = { isa = PBXGroup; children = ( @@ -932,21 +927,11 @@ DDA9ACAB2D6B316F00E6F1A9 /* ContactType.swift */, DDA9ACA92D6A6B8200E6F1A9 /* ContactIncludeOption.swift */, DDA9ACA72D6A66DD00E6F1A9 /* ContactColorOption.swift */, - DD50C7512D0828B40057AE6F /* Settings */, DD50C7542D0862770057AE6F /* ContactImageUpdater.swift */, ); path = Contact; sourceTree = ""; }; - DD50C7512D0828B40057AE6F /* Settings */ = { - isa = PBXGroup; - children = ( - DD50C74F2D0828800057AE6F /* ContactSettingsViewModel.swift */, - DD50C7522D0828D10057AE6F /* ContactSettingsView.swift */, - ); - path = Settings; - sourceTree = ""; - }; DD5334252C61667700062F9D /* InfoDisplaySettings */ = { isa = PBXGroup; children = ( @@ -1382,7 +1367,6 @@ DDC7E5142DBCE1B900EB1127 /* Snoozer */, DDCF9A7E2D85FCE6004DF4DD /* Alarm */, DD1A97122D429495000DDC11 /* Settings */, - DD2C2E522D3C36A8006413A5 /* Dexcom */, DD2C2E4D2D3B8ACF006413A5 /* Nightscout */, DD9ED0C62D355225000D2A63 /* Log */, DDEF503D2D32753A00999A5D /* Task */, @@ -1995,7 +1979,7 @@ DDC6CA432DD8CED20060EE25 /* SensorAgeCondition.swift in Sources */, DD7F4C212DD66BB200D449E9 /* FastRiseAlarmEditor.swift in Sources */, FC97881A2485969B00A7906C /* SceneDelegate.swift in Sources */, - DD83164E2DE4E093004467AA /* WatchSettingsView.swift in Sources */, + DD83164E2DE4E093004467AA /* CalendarSettingsView.swift in Sources */, DD0C0C662C46E54C00DBADDF /* InfoDataSeparator.swift in Sources */, DD58171C2D299F940041FB98 /* BluetoothDevice.swift in Sources */, DD7E198A2ACDA62600DBD158 /* SensorStart.swift in Sources */, diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index 58ecdeb1f..dcb516af6 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -17,13 +17,13 @@ struct AlarmEditor: View { innerEditor() .navigationBarTitleDisplayMode(.inline) .toolbar { - if isNew { - ToolbarItem(placement: .confirmationAction) { - Button("Done") { - onDone() - dismiss() - } + ToolbarItem(placement: .confirmationAction) { + Button("Done") { + onDone() + dismiss() } + } + if isNew { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { onCancel() diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index 4611dcf46..5eea487f5 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -89,57 +89,50 @@ private enum SheetInfo: Identifiable { struct AlarmListView: View { @ObservedObject private var store = Storage.shared.alarms - @Environment(\.dismiss) private var dismiss - @State private var sheetInfo: SheetInfo? @State private var deleteAfterDismiss: UUID? + @State private var selectedAlarm: Alarm? var body: some View { - NavigationStack { - List { - ForEach(store.value) { alarm in - NavigationLink { - AlarmEditor(alarm: binding(for: alarm)) - } label: { - HStack(spacing: 12) { - ZStack { - Image(systemName: alarm.type.icon) + List { + ForEach(store.value) { alarm in + Button(action: { + selectedAlarm = alarm + sheetInfo = .editor(id: alarm.id, isNew: false) + }) { + HStack(spacing: 12) { + ZStack { + Image(systemName: alarm.type.icon) + .font(.title3) + .symbolRenderingMode(.hierarchical) + .foregroundStyle(alarm.isEnabled ? Color.accentColor : Color.secondary) + .opacity(iconOpacity(for: alarm)) + + if let until = alarm.snoozedUntil, until > Date() { + Image(systemName: "zzz") .font(.title3) - .symbolRenderingMode(.hierarchical) - .foregroundStyle(alarm.isEnabled ? Color.accentColor : Color.secondary) - .opacity(iconOpacity(for: alarm)) - - if let until = alarm.snoozedUntil, until > Date() { - Image(systemName: "zzz") - .font(.title3) - .foregroundStyle(Color.secondary) - .shadow(color: .black.opacity(1), radius: 2, x: 0, y: 0) - .blendMode(.screen) - .offset(x: 6, y: 6) - } + .foregroundStyle(Color.secondary) + .shadow(color: .black.opacity(1), radius: 2, x: 0, y: 0) + .blendMode(.screen) + .offset(x: 6, y: 6) } - .frame(width: 26, height: 26) - - Text(alarm.name) - .frame(maxWidth: .infinity, alignment: .leading) } + .frame(width: 26, height: 26) + + Text(alarm.name) + .frame(maxWidth: .infinity, alignment: .leading) } } - .onDelete { store.value.remove(atOffsets: $0) } - } - .navigationTitle("Alarms") - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button("Done") { dismiss() } - } - ToolbarItem(placement: .navigationBarTrailing) { - Button { sheetInfo = .picker } label: { Image(systemName: "plus") } - } } - .sheet(item: $sheetInfo, - onDismiss: handleSheetDismiss) - { info in - sheetContent(for: info) + .onDelete { store.value.remove(atOffsets: $0) } + } + .sheet(item: $sheetInfo, onDismiss: handleSheetDismiss) { info in + sheetContent(for: info) + } + .navigationBarTitle("Alarms", displayMode: .inline) + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button { sheetInfo = .picker } label: { Image(systemName: "plus") } } } .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) @@ -171,7 +164,7 @@ struct AlarmListView: View { isNew: isNew, onDone: { sheetInfo = nil }, onCancel: { - deleteAfterDismiss = id + if isNew { deleteAfterDismiss = id } sheetInfo = nil } ) @@ -181,13 +174,6 @@ struct AlarmListView: View { } } - private func binding(for alarm: Alarm) -> Binding { - guard let idx = store.value.firstIndex(where: { $0.id == alarm.id }) else { - fatalError("Alarm not found") - } - return $store.value[idx] - } - private func iconOpacity(for alarm: Alarm) -> Double { if !alarm.isEnabled { return 0.35 } if let until = alarm.snoozedUntil, until > Date() { return 0.55 } diff --git a/LoopFollow/Alarm/AlarmSettingsView.swift b/LoopFollow/Alarm/AlarmSettingsView.swift index 4beb59407..90bb66754 100644 --- a/LoopFollow/Alarm/AlarmSettingsView.swift +++ b/LoopFollow/Alarm/AlarmSettingsView.swift @@ -6,7 +6,6 @@ import SwiftUI struct AlarmSettingsView: View { @ObservedObject private var cfgStore = Storage.shared.alarmConfiguration - @Environment(\.presentationMode) var presentationMode /// Helper to bind an optional Date? into a non‑optional Date for DatePicker private func optDateBinding(_ b: Binding) -> Binding { @@ -187,15 +186,8 @@ struct AlarmSettingsView: View { ) } } - .navigationTitle("Alarm Settings") - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button("Done") { - presentationMode.wrappedValue.dismiss() - } - } - } } .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .navigationBarTitle("Alarm Settings", displayMode: .inline) } } diff --git a/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift b/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift index 6bb03b3c8..18b66b2d3 100644 --- a/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift +++ b/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift @@ -6,7 +6,6 @@ import SwiftUI struct BackgroundRefreshSettingsView: View { @ObservedObject var viewModel: BackgroundRefreshSettingsViewModel - @Environment(\.presentationMode) var presentationMode @State private var forceRefresh = false @State private var timer: Timer? @@ -22,14 +21,6 @@ struct BackgroundRefreshSettingsView: View { availableDevicesSection } } - .navigationBarTitle("Background Refresh Settings", displayMode: .inline) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button("Done") { - presentationMode.wrappedValue.dismiss() - } - } - } .onAppear { startTimer() } @@ -38,6 +29,7 @@ struct BackgroundRefreshSettingsView: View { } } .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .navigationBarTitle("Background Refresh Settings", displayMode: .inline) } // MARK: - Subviews / Computed Properties diff --git a/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift index a92732df8..c96d2e3df 100644 --- a/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift +++ b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift @@ -6,7 +6,6 @@ import SwiftUI struct InfoDisplaySettingsView: View { @ObservedObject var viewModel: InfoDisplaySettingsViewModel - @Environment(\.presentationMode) var presentationMode var body: some View { NavigationView { @@ -40,13 +39,11 @@ struct InfoDisplaySettingsView: View { .environment(\.editMode, .constant(.active)) } } - .navigationBarItems(trailing: Button("Done") { - presentationMode.wrappedValue.dismiss() - }) .onDisappear { NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil) } } .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .navigationBarTitle("Information Display Settings", displayMode: .inline) } } diff --git a/LoopFollow/Log/LogView.swift b/LoopFollow/Log/LogView.swift index cc5cd683d..63e5a42f3 100644 --- a/LoopFollow/Log/LogView.swift +++ b/LoopFollow/Log/LogView.swift @@ -6,7 +6,6 @@ import SwiftUI struct LogView: View { @ObservedObject var viewModel = LogViewModel() - @Environment(\.presentationMode) var presentationMode var body: some View { NavigationView { @@ -34,18 +33,11 @@ struct LogView: View { .padding(.horizontal) } } - .navigationBarTitle("Today's Logs", displayMode: .inline) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button("Done") { - presentationMode.wrappedValue.dismiss() - } - } - } .onAppear { viewModel.loadLogEntries() } } .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .navigationBarTitle("Today's Logs", displayMode: .inline) } } diff --git a/LoopFollow/Nightscout/NightscoutSettingsView.swift b/LoopFollow/Nightscout/NightscoutSettingsView.swift index e206917ed..61f0b180c 100644 --- a/LoopFollow/Nightscout/NightscoutSettingsView.swift +++ b/LoopFollow/Nightscout/NightscoutSettingsView.swift @@ -6,7 +6,6 @@ import SwiftUI struct NightscoutSettingsView: View { @ObservedObject var viewModel: NightscoutSettingsViewModel - @Environment(\.presentationMode) var presentationMode var body: some View { NavigationView { @@ -15,19 +14,12 @@ struct NightscoutSettingsView: View { tokenSection statusSection } - .navigationBarTitle("Nightscout Settings", displayMode: .inline) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button("Done") { - presentationMode.wrappedValue.dismiss() - } - } - } .onDisappear { viewModel.dismiss() } } .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .navigationBarTitle("Nightscout Settings", displayMode: .inline) } // MARK: - Subviews / Computed Properties diff --git a/LoopFollow/Settings/AdvancedSettingsView.swift b/LoopFollow/Settings/AdvancedSettingsView.swift index 993fed7a8..30dd43336 100644 --- a/LoopFollow/Settings/AdvancedSettingsView.swift +++ b/LoopFollow/Settings/AdvancedSettingsView.swift @@ -6,7 +6,6 @@ import SwiftUI struct AdvancedSettingsView: View { @ObservedObject var viewModel: AdvancedSettingsViewModel - @Environment(\.presentationMode) var presentationMode var body: some View { NavigationView { @@ -28,15 +27,8 @@ struct AdvancedSettingsView: View { Toggle("Debug Log Level", isOn: $viewModel.debugLogLevel) } } - .navigationBarTitle("Advanced Settings", displayMode: .inline) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button("Done") { - presentationMode.wrappedValue.dismiss() - } - } - } } .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .navigationBarTitle("Advanced Settings", displayMode: .inline) } } diff --git a/LoopFollow/Settings/WatchSettingsView.swift b/LoopFollow/Settings/CalendarSettingsView.swift similarity index 91% rename from LoopFollow/Settings/WatchSettingsView.swift rename to LoopFollow/Settings/CalendarSettingsView.swift index 3422a4554..739576a24 100644 --- a/LoopFollow/Settings/WatchSettingsView.swift +++ b/LoopFollow/Settings/CalendarSettingsView.swift @@ -1,11 +1,11 @@ // LoopFollow -// WatchSettingsView.swift +// CalendarSettingsView.swift // Created by Jonas Björkert on 2025-05-26. import EventKit import SwiftUI -struct WatchSettingsView: View { +struct CalendarSettingsView: View { // MARK: Storage bindings @ObservedObject private var writeCalendarEvent = Storage.shared.writeCalendarEvent @@ -15,14 +15,13 @@ struct WatchSettingsView: View { // 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 { + NavigationView { Form { // ------------- Calendar write ------------- Section { @@ -72,17 +71,12 @@ struct WatchSettingsView: View { } } } - .navigationTitle("Calendar") - .toolbar { - ToolbarItem(placement: .confirmationAction) { - Button("Done") { presentationMode.wrappedValue.dismiss() } - } - } .task { // runs once on appear await requestCalendarAccessAndLoad() } } .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .navigationBarTitle("Calendar", displayMode: .inline) } // MARK: - Helpers diff --git a/LoopFollow/Contact/Settings/ContactSettingsView.swift b/LoopFollow/Settings/ContactSettingsView.swift similarity index 93% rename from LoopFollow/Contact/Settings/ContactSettingsView.swift rename to LoopFollow/Settings/ContactSettingsView.swift index f7d58cc2d..c2b34c7e6 100644 --- a/LoopFollow/Contact/Settings/ContactSettingsView.swift +++ b/LoopFollow/Settings/ContactSettingsView.swift @@ -7,7 +7,6 @@ import SwiftUI struct ContactSettingsView: View { @ObservedObject var viewModel: ContactSettingsViewModel - @Environment(\.presentationMode) var presentationMode @State private var showAlert: Bool = false @State private var alertTitle: String = "" @@ -77,19 +76,12 @@ struct ContactSettingsView: View { } } } - .navigationBarTitle("Contact Settings", displayMode: .inline) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button("Done") { - presentationMode.wrappedValue.dismiss() - } - } - } .alert(isPresented: $showAlert) { Alert(title: Text(alertTitle), message: Text(alertMessage), dismissButton: .default(Text("OK"))) } } .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .navigationBarTitle("Contact", displayMode: .inline) } private func requestContactAccess() { diff --git a/LoopFollow/Contact/Settings/ContactSettingsViewModel.swift b/LoopFollow/Settings/ContactSettingsViewModel.swift similarity index 98% rename from LoopFollow/Contact/Settings/ContactSettingsViewModel.swift rename to LoopFollow/Settings/ContactSettingsViewModel.swift index a9ee26a52..f2af24c9a 100644 --- a/LoopFollow/Contact/Settings/ContactSettingsViewModel.swift +++ b/LoopFollow/Settings/ContactSettingsViewModel.swift @@ -1,6 +1,6 @@ // LoopFollow // ContactSettingsViewModel.swift -// Created by Jonas Björkert on 2024-12-10. +// Created by Jonas Björkert on 2025-05-23. import Combine import Foundation diff --git a/LoopFollow/Dexcom/DexcomSettingsView.swift b/LoopFollow/Settings/DexcomSettingsView.swift similarity index 70% rename from LoopFollow/Dexcom/DexcomSettingsView.swift rename to LoopFollow/Settings/DexcomSettingsView.swift index 91ba263dd..29d6fb909 100644 --- a/LoopFollow/Dexcom/DexcomSettingsView.swift +++ b/LoopFollow/Settings/DexcomSettingsView.swift @@ -1,12 +1,11 @@ // LoopFollow // DexcomSettingsView.swift -// Created by Jonas Björkert on 2025-01-18. +// Created by Jonas Björkert on 2025-05-23. import SwiftUI struct DexcomSettingsView: View { @ObservedObject var viewModel: DexcomSettingsViewModel - @Environment(\.presentationMode) var presentationMode var body: some View { NavigationView { @@ -27,15 +26,8 @@ struct DexcomSettingsView: View { .pickerStyle(SegmentedPickerStyle()) } } - .navigationBarTitle("Dexcom Settings", displayMode: .inline) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button("Done") { - presentationMode.wrappedValue.dismiss() - } - } - } } .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .navigationBarTitle("Dexcom Settings", displayMode: .inline) } } diff --git a/LoopFollow/Dexcom/DexcomSettingsViewModel.swift b/LoopFollow/Settings/DexcomSettingsViewModel.swift similarity index 100% rename from LoopFollow/Dexcom/DexcomSettingsViewModel.swift rename to LoopFollow/Settings/DexcomSettingsViewModel.swift diff --git a/LoopFollow/Settings/GeneralSettingsView.swift b/LoopFollow/Settings/GeneralSettingsView.swift index 5b9ce5130..56910c714 100644 --- a/LoopFollow/Settings/GeneralSettingsView.swift +++ b/LoopFollow/Settings/GeneralSettingsView.swift @@ -5,8 +5,6 @@ import SwiftUI struct GeneralSettingsView: View { - @Environment(\.presentationMode) var presentationMode - @ObservedObject var colorBGText = Storage.shared.colorBGText @ObservedObject var appBadge = Storage.shared.appBadge @ObservedObject var forceDarkMode = Storage.shared.forceDarkMode @@ -29,7 +27,7 @@ struct GeneralSettingsView: View { @ObservedObject var speakHighBGLimit = Storage.shared.speakHighBGLimit var body: some View { - NavigationStack { + NavigationView { Form { Section("App Settings") { Toggle("Display App Badge", isOn: $appBadge.value) @@ -103,15 +101,8 @@ struct GeneralSettingsView: View { } } } - .navigationTitle("General Settings") - .toolbar { - ToolbarItem(placement: .confirmationAction) { - Button("Done") { - presentationMode.wrappedValue.dismiss() - } - } - } } .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .navigationBarTitle("General Settings", displayMode: .inline) } } diff --git a/LoopFollow/Settings/GraphSettingsView.swift b/LoopFollow/Settings/GraphSettingsView.swift index 0caf6e885..835acb379 100644 --- a/LoopFollow/Settings/GraphSettingsView.swift +++ b/LoopFollow/Settings/GraphSettingsView.swift @@ -5,9 +5,6 @@ import SwiftUI struct GraphSettingsView: View { - @Environment(\.presentationMode) private var presentationMode - - // ── Stored settings ────────────────────────────────────────────────────── @ObservedObject private var showDots = Storage.shared.showDots @ObservedObject private var showLines = Storage.shared.showLines @ObservedObject private var showValues = Storage.shared.showValues @@ -26,11 +23,10 @@ struct GraphSettingsView: View { @ObservedObject private var highLine = Storage.shared.highLine @ObservedObject private var downloadDays = Storage.shared.downloadDays - // ───────────────────────────────────────────────────────────────────────── private var nightscoutEnabled: Bool { IsNightscoutEnabled() } var body: some View { - NavigationStack { + NavigationView { Form { // ── Graph Display ──────────────────────────────────────────── Section("Graph Display") { @@ -136,14 +132,9 @@ struct GraphSettingsView: View { } } } - .navigationTitle("Graph Settings") - .toolbar { - ToolbarItem(placement: .confirmationAction) { - Button("Done") { presentationMode.wrappedValue.dismiss() } - } - } } .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .navigationBarTitle("Graph Settings", displayMode: .inline) } /// Marks the chart as needing a redraw diff --git a/LoopFollow/Settings/SettingsMenuView.swift b/LoopFollow/Settings/SettingsMenuView.swift index 37b7e204b..420c0375c 100644 --- a/LoopFollow/Settings/SettingsMenuView.swift +++ b/LoopFollow/Settings/SettingsMenuView.swift @@ -12,14 +12,15 @@ struct SettingsMenuView: View { // MARK: – Local state - @State private var sheet: Sheet? + @State private var path = NavigationPath() @State private var latestVersion: String? @State private var versionTint: Color = .secondary + @State private var navPath = NavigationPath() // MARK: – Body var body: some View { - NavigationStack { + NavigationStack(path: $path) { List { // ───────── Data settings ───────── dataSection @@ -29,26 +30,26 @@ struct SettingsMenuView: View { NavigationRow(title: "Background Refresh Settings", icon: "arrow.clockwise") { - sheet = .backgroundRefresh + path.append(Sheet.backgroundRefresh) } NavigationRow(title: "General Settings", icon: "gearshape") { - sheet = .general + path.append(Sheet.general) } NavigationRow(title: "Graph Settings", icon: "chart.xyaxis.line") { - sheet = .graph + path.append(Sheet.graph) } if IsNightscoutEnabled() { NavigationRow(title: "Information Display Settings", icon: "info.circle") { - sheet = .infoDisplay + path.append(Sheet.infoDisplay) } } } @@ -58,13 +59,13 @@ struct SettingsMenuView: View { NavigationRow(title: "Alarms", icon: "bell") { - sheet = .alarmsList + path.append(Sheet.alarmsList) } NavigationRow(title: "Alarm Settings", icon: "bell.badge") { - sheet = .alarmSettings + path.append(Sheet.alarmSettings) } } @@ -73,13 +74,13 @@ struct SettingsMenuView: View { NavigationRow(title: "Calendar", icon: "calendar") { - sheet = .calendar + path.append(Sheet.calendar) } NavigationRow(title: "Contact", icon: "person.circle") { - sheet = .contact + path.append(Sheet.contact) } } @@ -88,7 +89,7 @@ struct SettingsMenuView: View { NavigationRow(title: "Advanced Settings", icon: "exclamationmark.shield") { - sheet = .advanced + path.append(Sheet.advanced) } } @@ -96,14 +97,12 @@ struct SettingsMenuView: View { NavigationRow(title: "View Log", icon: "doc.text.magnifyingglass") { - sheet = .viewLog + path.append(Sheet.viewLog) } ActionRow(title: "Share Logs", - icon: "square.and.arrow.up") - { - shareLogs() - } + icon: "square.and.arrow.up", + action: shareLogs) } // ───────── Community ───────── @@ -117,9 +116,9 @@ struct SettingsMenuView: View { buildInfoSection } .navigationTitle("Settings") + .navigationDestination(for: Sheet.self) { $0.destination } } .task { await refreshVersionInfo() } - .sheet(item: $sheet) { $0.destination } } // MARK: – Section builders @@ -140,13 +139,13 @@ struct SettingsMenuView: View { NavigationRow(title: "Nightscout Settings", icon: "network") { - sheet = .nightscout + path.append(Sheet.nightscout) } NavigationRow(title: "Dexcom Settings", icon: "sensor.tag.radiowaves.forward") { - sheet = .dexcom + path.append(Sheet.dexcom) } } .onAppear { @@ -162,6 +161,7 @@ struct SettingsMenuView: View { Section("Build Information") { keyValue("Version", ver, tint: versionTint) keyValue("Latest version", latestVersion ?? "Fetching…") + if !(build.isMacApp() || build.isSimulatorBuild()) { keyValue(build.expirationHeaderString, dateTimeUtils.formattedDate(from: build.calculateExpirationDate())) @@ -212,9 +212,9 @@ struct SettingsMenuView: View { } } -// MARK: – Sheet routing (unchanged) +// MARK: – Sheet routing -private enum Sheet: Identifiable { +private enum Sheet: Hashable, Identifiable { case nightscout, dexcom case backgroundRefresh case general, graph @@ -225,7 +225,7 @@ private enum Sheet: Identifiable { case advanced case viewLog - var id: Int { hashValue } + var id: Self { self } @ViewBuilder var destination: some View { @@ -239,7 +239,7 @@ private enum Sheet: Identifiable { case .alarmsList: AlarmListView() case .alarmSettings: AlarmSettingsView() case .remote: RemoteSettingsView(viewModel: .init()) - case .calendar: WatchSettingsView() + case .calendar: CalendarSettingsView() case .contact: ContactSettingsView(viewModel: .init()) case .advanced: AdvancedSettingsView(viewModel: .init()) case .viewLog: LogView(viewModel: .init()) @@ -247,6 +247,8 @@ private enum Sheet: Identifiable { } } +// MARK: – UIKit helpers (unchanged) + import UIKit extension UIApplication { @@ -261,7 +263,9 @@ extension UIApplication { extension UIViewController { func presentSimpleAlert(title: String, message: String) { - let a = UIAlertController(title: title, message: message, preferredStyle: .alert) + let a = UIAlertController(title: title, + message: message, + preferredStyle: .alert) a.addAction(UIAlertAction(title: "OK", style: .default)) present(a, animated: true) } From 1a0c37650ce0cfc4510078387b6d126ca591c230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 28 May 2025 07:56:52 +0200 Subject: [PATCH 099/138] Glyph on alarm list --- LoopFollow/Alarm/AlarmListView.swift | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index 5eea487f5..533e6ff16 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -96,28 +96,24 @@ struct AlarmListView: View { var body: some View { List { ForEach(store.value) { alarm in - Button(action: { + Button { selectedAlarm = alarm sheetInfo = .editor(id: alarm.id, isNew: false) - }) { + } label: { HStack(spacing: 12) { - ZStack { - Image(systemName: alarm.type.icon) - .font(.title3) - .symbolRenderingMode(.hierarchical) - .foregroundStyle(alarm.isEnabled ? Color.accentColor : Color.secondary) - .opacity(iconOpacity(for: alarm)) - + Glyph( + symbol: alarm.type.icon, + tint: alarm.isEnabled ? .white : Color(uiColor: .darkGray) + ) + .overlay { if let until = alarm.snoozedUntil, until > Date() { Image(systemName: "zzz") - .font(.title3) - .foregroundStyle(Color.secondary) - .shadow(color: .black.opacity(1), radius: 2, x: 0, y: 0) - .blendMode(.screen) - .offset(x: 6, y: 6) + .font(.caption.bold()) + .foregroundColor(.secondary) + .shadow(color: .black, radius: 2) + .offset(x: 8, y: 8) } } - .frame(width: 26, height: 26) Text(alarm.name) .frame(maxWidth: .infinity, alignment: .leading) From e548b51c0c4ed8e19784f66a67058e10b611200f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 28 May 2025 08:24:24 +0200 Subject: [PATCH 100/138] Alarm list --- LoopFollow/Alarm/AlarmListView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index 533e6ff16..e6c1990f8 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -117,6 +117,7 @@ struct AlarmListView: View { Text(alarm.name) .frame(maxWidth: .infinity, alignment: .leading) + .foregroundColor(.primary) } } } From 82b381b8c7777ca7c16c78eba0d6a43478f226aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 28 May 2025 10:39:58 +0200 Subject: [PATCH 101/138] Restore Rermote Settings --- LoopFollow/Alarm/Alarm.swift | 8 +------- .../Remote/Settings/RemoteSettingsView.swift | 14 +++----------- LoopFollow/Settings/SettingsMenuView.swift | 6 ++++++ 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index dd17a040a..0daf9ee9b 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -197,16 +197,10 @@ struct Alarm: Identifiable, Codable, Equatable { let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger) UNUserNotificationCenter.current().add(request, withCompletionHandler: nil) - let action = UNNotificationAction(identifier: "snooze", title: "Snooze", options: []) + let action = UNNotificationAction(identifier: "snooze", title: snoozeDuration == 0 ? "Acknowledge" : "Snooze", options: []) let category = UNNotificationCategory(identifier: "category", actions: [action], intentIdentifiers: [], options: []) UNUserNotificationCenter.current().setNotificationCategories([category]) - /* TODO: när vi gör bg alarm sätt timestamp/datum för denna readings tid så vi inte larmar på samma igen, se isBGBased - if snooozedBGReadingTime != nil { - UserDefaultsRepository.snoozedBGReadingTime.value = snooozedBGReadingTime - } - */ - if playSound { AlarmSound.setSoundFile(str: soundFile.rawValue) AlarmSound.play(repeating: shouldRepeat) diff --git a/LoopFollow/Remote/Settings/RemoteSettingsView.swift b/LoopFollow/Remote/Settings/RemoteSettingsView.swift index 6e8a8b7f2..3f8993ca3 100644 --- a/LoopFollow/Remote/Settings/RemoteSettingsView.swift +++ b/LoopFollow/Remote/Settings/RemoteSettingsView.swift @@ -7,7 +7,6 @@ import SwiftUI struct RemoteSettingsView: View { @ObservedObject var viewModel: RemoteSettingsViewModel - @Environment(\.presentationMode) var presentationMode @State private var showAlert: Bool = false @State private var alertType: AlertType? = nil @@ -58,7 +57,7 @@ struct RemoteSettingsView: View { Section(header: Text("Trio Remote Control Settings")) { HStack { Text("Shared Secret") - TextField("Enter Shared Secret", text: $viewModel.sharedSecret) + SecureField("Enter Shared Secret", text: $viewModel.sharedSecret) .autocapitalization(.none) .disableAutocorrection(true) .multilineTextAlignment(.trailing) @@ -66,7 +65,7 @@ struct RemoteSettingsView: View { HStack { Text("APNS Key ID") - TextField("Enter APNS Key ID", text: $viewModel.keyId) + SecureField("Enter APNS Key ID", text: $viewModel.keyId) .autocapitalization(.none) .disableAutocorrection(true) .multilineTextAlignment(.trailing) @@ -185,14 +184,6 @@ struct RemoteSettingsView: View { } } } - .navigationBarTitle("Remote Settings", displayMode: .inline) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button("Done") { - presentationMode.wrappedValue.dismiss() - } - } - } .alert(isPresented: $showAlert) { switch alertType { case .validation: @@ -207,6 +198,7 @@ struct RemoteSettingsView: View { } } .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .navigationBarTitle("Remote Settings", displayMode: .inline) } // MARK: - Custom Row for Remote Type Selection diff --git a/LoopFollow/Settings/SettingsMenuView.swift b/LoopFollow/Settings/SettingsMenuView.swift index 420c0375c..b480055fa 100644 --- a/LoopFollow/Settings/SettingsMenuView.swift +++ b/LoopFollow/Settings/SettingsMenuView.swift @@ -51,6 +51,12 @@ struct SettingsMenuView: View { { path.append(Sheet.infoDisplay) } + + NavigationRow(title: "Remote Settings", + icon: "antenna.radiowaves.left.and.right") + { + path.append(Sheet.remote) + } } } From df9211c8e6da978eff1f232227986ecdb2d57011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 28 May 2025 11:55:57 +0200 Subject: [PATCH 102/138] TogglableSecureInput --- LoopFollow.xcodeproj/project.pbxproj | 4 ++ .../Helpers/Views/TogglableSecureInput.swift | 67 +++++++++++++++++++ .../Remote/Settings/RemoteSettingsView.swift | 32 ++++----- 3 files changed, 87 insertions(+), 16 deletions(-) create mode 100644 LoopFollow/Helpers/Views/TogglableSecureInput.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 726dda3d7..6fc89cf14 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -196,6 +196,7 @@ DDE75D272DE5E539007C1FC1 /* ActionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE75D262DE5E539007C1FC1 /* ActionRow.swift */; }; DDE75D292DE5E56C007C1FC1 /* LinkRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE75D282DE5E56C007C1FC1 /* LinkRow.swift */; }; DDE75D2B2DE5E613007C1FC1 /* NavigationRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE75D2A2DE5E613007C1FC1 /* NavigationRow.swift */; }; + DDE75D2D2DE71401007C1FC1 /* TogglableSecureInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE75D2C2DE71401007C1FC1 /* TogglableSecureInput.swift */; }; DDEF503A2D31615000999A5D /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDEF50392D31614200999A5D /* LogManager.swift */; }; DDEF503C2D31BE2D00999A5D /* TaskScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDEF503B2D31BE2A00999A5D /* TaskScheduler.swift */; }; DDEF503F2D32754F00999A5D /* ProfileTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDEF503E2D32754A00999A5D /* ProfileTask.swift */; }; @@ -566,6 +567,7 @@ DDE75D262DE5E539007C1FC1 /* ActionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionRow.swift; sourceTree = ""; }; DDE75D282DE5E56C007C1FC1 /* LinkRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkRow.swift; sourceTree = ""; }; DDE75D2A2DE5E613007C1FC1 /* NavigationRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationRow.swift; sourceTree = ""; }; + DDE75D2C2DE71401007C1FC1 /* TogglableSecureInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TogglableSecureInput.swift; sourceTree = ""; }; DDEF50392D31614200999A5D /* LogManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogManager.swift; sourceTree = ""; }; DDEF503B2D31BE2A00999A5D /* TaskScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskScheduler.swift; sourceTree = ""; }; DDEF503E2D32754A00999A5D /* ProfileTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileTask.swift; sourceTree = ""; }; @@ -1165,6 +1167,7 @@ DDF6999C2C5AAA4C0058A8D9 /* Views */ = { isa = PBXGroup; children = ( + DDE75D2C2DE71401007C1FC1 /* TogglableSecureInput.swift */, DDE75D222DE5E505007C1FC1 /* Glyph.swift */, DD8316492DE4C504004467AA /* SettingsStepperRow.swift */, DD8316432DE47CA9004467AA /* BGPicker.swift */, @@ -1834,6 +1837,7 @@ DD0650ED2DCE9371004D3B41 /* HighBgAlarmEditor.swift in Sources */, DD7F4C172DD63FA700D449E9 /* RecBolusCondition.swift in Sources */, FCC6886F2489A53800A0279D /* AppConstants.swift in Sources */, + DDE75D2D2DE71401007C1FC1 /* TogglableSecureInput.swift in Sources */, DD7E19842ACDA50C00DBD158 /* Overrides.swift in Sources */, DD0B9D562DE1EC8A0090C337 /* AlarmType+Snooze.swift in Sources */, DDF2C0102BEFA991007A20E6 /* GitHubService.swift in Sources */, diff --git a/LoopFollow/Helpers/Views/TogglableSecureInput.swift b/LoopFollow/Helpers/Views/TogglableSecureInput.swift new file mode 100644 index 000000000..f41ea4b77 --- /dev/null +++ b/LoopFollow/Helpers/Views/TogglableSecureInput.swift @@ -0,0 +1,67 @@ +// LoopFollow +// TogglableSecureInput.swift +// Created by Jonas Björkert on 2025-05-28. + +import SwiftUI + +struct TogglableSecureInput: View { + enum Style { case singleLine, multiLine } + + let placeholder: String + @Binding var text: String + let style: Style + + @State private var isVisible = false + @FocusState private var isFocused: Bool + + var body: some View { + HStack(alignment: .top) { + Group { + switch style { + case .singleLine: + if isVisible { + TextField(placeholder, text: $text).multilineTextAlignment(.trailing) + } else { + SecureField(placeholder, text: $text).multilineTextAlignment(.trailing) + } + + case .multiLine: + ZStack(alignment: .topLeading) { + TextEditor(text: $text) + .opacity(isVisible ? 1 : 0) + .focused($isFocused) + .frame(minHeight: 100) + + if !isVisible { + Text(maskString) + .font(.body.monospaced()) + .foregroundColor(.primary) + .frame(maxWidth: .infinity, + maxHeight: .infinity, + alignment: .topLeading) + .padding(.top, 8) + .padding(.leading, 5) + } + } + } + } + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .privacySensitive() + .submitLabel(.done) + + Button { isVisible.toggle() } label: { + Image(systemName: isVisible ? "eye.slash" : "eye") + .foregroundColor(.secondary) + } + .buttonStyle(.plain) + .padding(.leading, 4) + } + .contentShape(Rectangle()) + .onTapGesture { isFocused = true } + } + + private var maskString: String { + text.map { $0.isNewline ? "\n" : "•" }.joined() + } +} diff --git a/LoopFollow/Remote/Settings/RemoteSettingsView.swift b/LoopFollow/Remote/Settings/RemoteSettingsView.swift index 3f8993ca3..e1d9adfde 100644 --- a/LoopFollow/Remote/Settings/RemoteSettingsView.swift +++ b/LoopFollow/Remote/Settings/RemoteSettingsView.swift @@ -57,30 +57,30 @@ struct RemoteSettingsView: View { Section(header: Text("Trio Remote Control Settings")) { HStack { Text("Shared Secret") - SecureField("Enter Shared Secret", text: $viewModel.sharedSecret) - .autocapitalization(.none) - .disableAutocorrection(true) - .multilineTextAlignment(.trailing) + TogglableSecureInput( + placeholder: "Enter Shared Secret", + text: $viewModel.sharedSecret, + style: .singleLine + ) } HStack { Text("APNS Key ID") - SecureField("Enter APNS Key ID", text: $viewModel.keyId) - .autocapitalization(.none) - .disableAutocorrection(true) - .multilineTextAlignment(.trailing) + TogglableSecureInput( + placeholder: "Enter APNS Key ID", + text: $viewModel.keyId, + style: .singleLine + ) } VStack(alignment: .leading) { Text("APNS Key") - TextEditor(text: $viewModel.apnsKey) - .frame(height: 100) - .autocapitalization(.none) - .disableAutocorrection(true) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(Color.gray.opacity(0.5), lineWidth: 1) - ) + TogglableSecureInput( + placeholder: "Paste APNS Key", + text: $viewModel.apnsKey, + style: .multiLine + ) + .frame(minHeight: 110) } } From 73e2bfb4117403fb10901e1852a7cebd58bb05d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 28 May 2025 13:54:14 +0200 Subject: [PATCH 103/138] Migrate general alarm settings --- LoopFollow/Controllers/AlarmSound.swift | 24 +---- LoopFollow/Controllers/Nightscout/SAge.swift | 19 +++- LoopFollow/Storage/Storage+Migrate.swift | 100 +++++++++++++++++++ LoopFollow/Storage/UserDefaults.swift | 26 ----- 4 files changed, 117 insertions(+), 52 deletions(-) diff --git a/LoopFollow/Controllers/AlarmSound.swift b/LoopFollow/Controllers/AlarmSound.swift index 3755d15e2..5907355b2 100644 --- a/LoopFollow/Controllers/AlarmSound.swift +++ b/LoopFollow/Controllers/AlarmSound.swift @@ -23,10 +23,6 @@ class AlarmSound { static var soundFile = "Indeed" static var isTesting: Bool = false - // static let volumeChangeDetector = VolumeChangeDetector() - - static let vibrate = UserDefaultsRepository.vibrate - fileprivate static var systemOutputVolumeBeforeOverride: Float? fileprivate static var playingTimer: Timer? @@ -70,11 +66,7 @@ class AlarmSound { * Sets the volume of the alarm back to the volume before it has been muted. */ static func unmuteVolume() { - if UserDefaultsRepository.fadeInTimeInterval.value > 0 { - audioPlayer?.setVolume(1.0, fadeDuration: UserDefaultsRepository.fadeInTimeInterval.value) - } else { - audioPlayer?.volume = 1.0 - } + audioPlayer?.volume = 1.0 muted = false } @@ -102,10 +94,6 @@ class AlarmSound { audioPlayer?.numberOfLoops = 0 - // init volume before start playing (mute if fade-in) - - // self.audioPlayer!.volume = (self.muted || (UserDefaultsRepository.fadeInTimeInterval.value > 0)) ? 0.0 : 1.0 - if !audioPlayer!.prepareToPlay() { LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed preparing to play") } @@ -145,9 +133,6 @@ class AlarmSound { systemOutputVolumeBeforeOverride = AVAudioSession.sharedInstance().outputVolume } - // init volume before start playing (mute if fade-in) - // self.audioPlayer!.volume = (self.muted || (UserDefaultsRepository.fadeInTimeInterval.value > 0)) ? 0.0 : 1.0 - if !audioPlayer!.prepareToPlay() { LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed preparing to play") } @@ -161,11 +146,6 @@ class AlarmSound { LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed to play") } - // do fade-in - // if !self.muted && (UserDefaultsRepository.fadeInTimeInterval.value > 0) { - // self.audioPlayer!.setVolume(1.0, fadeDuration: UserDefaultsRepository.fadeInTimeInterval.value) - // } - if Storage.shared.alarmConfiguration.value.overrideSystemOutputVolume { MPVolumeView.setVolume(Storage.shared.alarmConfiguration.value.forcedOutputVolume) } @@ -215,7 +195,7 @@ class AlarmSound { } fileprivate static func restoreSystemOutputVolume() { - guard UserDefaultsRepository.overrideSystemOutputVolume.value else { + guard Storage.shared.alarmConfiguration.value.overrideSystemOutputVolume else { return } diff --git a/LoopFollow/Controllers/Nightscout/SAge.swift b/LoopFollow/Controllers/Nightscout/SAge.swift index d2f040a47..92607391e 100644 --- a/LoopFollow/Controllers/Nightscout/SAge.swift +++ b/LoopFollow/Controllers/Nightscout/SAge.swift @@ -46,10 +46,21 @@ extension MainViewController { .withColonSeparatorInTime] UserDefaultsRepository.alertSageInsertTime.value = formatter.date(from: lastSageString)?.timeIntervalSince1970 as! TimeInterval - if UserDefaultsRepository.alertAutoSnoozeCGMStart.value, dateTimeUtils.getNowTimeIntervalUTC() - UserDefaultsRepository.alertSageInsertTime.value < 7200 { - let snoozeTime = Date(timeIntervalSince1970: UserDefaultsRepository.alertSageInsertTime.value + 7200) - UserDefaultsRepository.alertSnoozeAllTime.value = snoozeTime - UserDefaultsRepository.alertSnoozeAllIsSnoozed.value = true + // -- Auto-snooze CGM start ──────────────────────────────────────────────── + let now = Date() + + // 1. Do we *want* the automatic global snooze? + if Storage.shared.alarmConfiguration.value.autoSnoozeCGMStart { + // 2. When did the sensor start? + let insertTime = UserDefaultsRepository.alertSageInsertTime.value + + // 3. If the start is less than 2 h ago, snooze *all* alarms for the + // remainder of that 2-hour window. + if now.timeIntervalSince1970 - insertTime < 7200 { + var cfg = Storage.shared.alarmConfiguration.value + cfg.snoozeUntil = Date(timeIntervalSince1970: insertTime + 7200) + Storage.shared.alarmConfiguration.value = cfg + } } if let sageTime = formatter.date(from: (lastSageString as! String))?.timeIntervalSince1970 { diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index 8f1096af3..c3028e9f9 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -182,5 +182,105 @@ extension Storage { move(UserDefaultsValue(key: "calendarIdentifier", default: ""), into: calendarIdentifier) move(UserDefaultsValue(key: "watchLine1", default: "%BG% %DIRECTION% %DELTA% %MINAGO%"), into: watchLine1) move(UserDefaultsValue(key: "watchLine2", default: "C:%COB% I:%IOB% B:%BASAL%"), into: watchLine2) + + // Migration of generic alarm settings + // ── AlarmConfiguration migration ───────────────────────────────────────── + do { + // Work on a mutable copy, then write the whole thing back once. + var cfg = Storage.shared.alarmConfiguration.value + let cal = Calendar.current + + /// Copy *one* legacy value → struct field → delete old key + func move( + _ legacy: @autoclosure () -> UserDefaultsValue, + write: (inout AlarmConfiguration, T) -> Void + ) { + let item = legacy() + guard item.exists else { return } + write(&cfg, item.value) + item.setNil(key: item.key) + } + + // 1. Override-volume toggle + move(UserDefaultsValue(key: "overrideSystemOutputVolume", + default: cfg.overrideSystemOutputVolume)) + { + $0.overrideSystemOutputVolume = $1 + } + + // 2. Forced output volume itself. + // Prefer newer key (“forcedOutputVolume”); otherwise fall back. + if UserDefaultsValue(key: "forcedOutputVolume", + default: cfg.forcedOutputVolume).exists + { + move(UserDefaultsValue(key: "forcedOutputVolume", + default: cfg.forcedOutputVolume)) + { + $0.forcedOutputVolume = $1 + } + } else { + move(UserDefaultsValue(key: "systemOutputVolume", + default: cfg.forcedOutputVolume)) + { + $0.forcedOutputVolume = $1 + } + } + + // 3. Play audio during phone calls + move(UserDefaultsValue(key: "alertAudioDuringPhone", + default: cfg.audioDuringCalls)) + { + $0.audioDuringCalls = $1 + } + + // 4. Auto-snooze CGM-start alarm + move(UserDefaultsValue(key: "alertAutoSnoozeCGMStart", + default: cfg.autoSnoozeCGMStart)) + { + $0.autoSnoozeCGMStart = $1 + } + + // 5. Global “Snooze all” → snoozeUntil + move(UserDefaultsValue(key: "alertSnoozeAllTime", + default: cfg.snoozeUntil)) + { + $0.snoozeUntil = $1 + } + + // 6. Global “Mute all” → muteUntil + move(UserDefaultsValue(key: "alertMuteAllTime", + default: cfg.muteUntil)) + { + $0.muteUntil = $1 + } + + // 7 & 8. Legacy quiet-hours → day/night start + // (only if both dates exist and are on the same “reference” day) + let qStart = UserDefaultsValue(key: "quietHourStart", default: nil) + let qEnd = UserDefaultsValue(key: "quietHourEnd", default: nil) + if let s = qStart.value, let e = qEnd.value { + let compsStart = cal.dateComponents([.hour, .minute], from: s) + let compsEnd = cal.dateComponents([.hour, .minute], from: e) + + if let sh = compsStart.hour, let sm = compsStart.minute, + let eh = compsEnd.hour, let em = compsEnd.minute + { + cfg.nightStart = TimeOfDay(hour: sh, minute: sm) + cfg.dayStart = TimeOfDay(hour: eh, minute: em) + qStart.setNil(key: qStart.key) + qEnd.setNil(key: qEnd.key) + } + } + + // 9. Legacy “ignore zero BG” flag → ignoreZeroBG + move(UserDefaultsValue(key: "alertIgnoreZero", + default: cfg.ignoreZeroBG)) + { + $0.ignoreZeroBG = $1 + } + + // finally persist the whole struct + Storage.shared.alarmConfiguration.value = cfg + } } } diff --git a/LoopFollow/Storage/UserDefaults.swift b/LoopFollow/Storage/UserDefaults.swift index 347b57795..50aabdc65 100644 --- a/LoopFollow/Storage/UserDefaults.swift +++ b/LoopFollow/Storage/UserDefaults.swift @@ -81,7 +81,6 @@ class UserDefaultsRepository { // Graph Settings static let chartScaleX = UserDefaultsValue(key: "chartScaleX", default: 18.0) - static let hoursToLoad = UserDefaultsValue(key: "hoursToLoad", default: 24) // Deprecated, used to detect if backgroundRefresh was set to off. TODO: Remove in the beginning of 2026 static let backgroundRefresh = UserDefaultsValue(key: "backgroundRefresh", default: true) @@ -95,34 +94,9 @@ class UserDefaultsRepository { static let graphCarbs = UserDefaultsValue(key: "graphCarbs", default: true) static let bgUpdateDelay = UserDefaultsValue(key: "bgUpdateDelay", default: 10) - // Alarm Settings - static let systemOutputVolume = UserDefaultsValue(key: "systemOutputVolume", default: 0.5) - static let fadeInTimeInterval = UserDefaultsValue(key: "fadeInTimeInterval", default: 0) - static let vibrate = UserDefaultsValue(key: "vibrate", default: true) - static let overrideSystemOutputVolume = UserDefaultsValue(key: "overrideSystemOutputVolume", default: true) - static let forcedOutputVolume = UserDefaultsValue(key: "forcedOutputVolume", default: 0.5) - - // Alerts - - let components = DateComponents(hour: 20, minute: 0) - static let quietHourStart = UserDefaultsValue(key: "quietHourStart", default: nil) // eventually need to adjust this to night time instead of quiet hour to clean up - static let quietHourEnd = UserDefaultsValue(key: "quietHourEnd", default: nil) // eventually need to adjust this to night time instead of quiet hour to clean up - static let nightTime = UserDefaultsValue(key: "nightTime", default: false) - - static let snoozedBGReadingTime = UserDefaultsValue(key: "snoozedBGReadingTime", default: 0) - - static let alertIgnoreZero = UserDefaultsValue(key: "alertIgnoreZero", default: true) - static let alertAudioDuringPhone = UserDefaultsValue(key: "alertAudioDuringPhone", default: true) - static let alertAutoSnoozeCGMStart = UserDefaultsValue(key: "alertAutoSnoozeCGMStart", default: false) - static let alertCageInsertTime = UserDefaultsValue(key: "alertCageInsertTime", default: 0) static let alertSageInsertTime = UserDefaultsValue(key: "alertSageInsertTime", default: 0) - static let alertSnoozeAllTime = UserDefaultsValue(key: "alertSnoozeAllTime", default: nil) - static let alertSnoozeAllIsSnoozed = UserDefaultsValue(key: "alertSnoozeAllIsSnoozed", default: false) - static let alertMuteAllTime = UserDefaultsValue(key: "alertMuteAllTime", default: nil) - static let alertMuteAllIsMuted = UserDefaultsValue(key: "alertMuteAllIsMuted", default: false) - static let alertUrgentLowActive = UserDefaultsValue(key: "alertUrgentLowActive", default: false) static let alertUrgentLowBG = UserDefaultsValue(key: "alertUrgentLowBG", default: 55.0) static let alertUrgentLowPredictiveMinutes = UserDefaultsValue(key: "alertUrgentLowPredictiveMinutes", default: 0) From 42506a7ae662bcb84878c84b50d8ce2f9b099610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 28 May 2025 21:15:26 +0200 Subject: [PATCH 104/138] Migrate urgent low --- LoopFollow/Storage/Storage+Migrate.swift | 57 ++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index c3028e9f9..43cf203b4 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -282,5 +282,62 @@ extension Storage { // finally persist the whole struct Storage.shared.alarmConfiguration.value = cfg } + + migrateUrgentLowAlarm() + } + + // MARK: - One-off alarm migrations + + /// Reads *all* `alertUrgentLow*` keys, converts them into a single `Alarm`, + /// saves it to the modern `[Alarm]` store, then deletes the legacy keys. + private func migrateUrgentLowAlarm() { + // Did the user ever change that alert? (No key ⇒ nothing to do.) + guard UserDefaultsValue(key: "alertUrgentLowActive", default: false).exists else { return } + + /// Helper: fetch-then-delete a legacy value in one line. + func take(_ key: String, default def: V) -> V { + let box = UserDefaultsValue(key: key, default: def) + defer { box.setNil(key: key) } + return box.value + } + + // Build the new Alarm ------------------------------------------------ + var alarm = Alarm(type: .low) + alarm.name = "Urgent Low" + alarm.isEnabled = take("alertUrgentLowActive", default: false) + alarm.belowBG = Double(take("alertUrgentLowBG", default: 55.0)) + alarm.predictiveMinutes = take("alertUrgentLowPredictiveMinutes", default: 0) + alarm.snoozeDuration = take("alertUrgentLowSnooze", default: 5) + alarm.snoozedUntil = take("alertUrgentLowSnoozedTime", default: nil as Date?) + alarm.soundFile = SoundFile( + rawValue: take("alertUrgentLowSound", + default: "Emergency_Alarm_Siren")) + ?? .emergencyAlarmSiren + + alarm.playSoundOption = PlaySoundOption( + rawValue: take("alertUrgentLowAudible", + default: "Always").lowercased()) ?? .always + + alarm.repeatSoundOption = RepeatSoundOption( + rawValue: take("alertUrgentLowRepeat", + default: "Always").lowercased()) ?? .always + + // Day / Night active-window (“Pre-Snooze”) + let autoStr = take("alertUrgentLowAutosnooze", default: "Never").lowercased() + let dayFlag = take("alertUrgentLowAutosnoozeDay", default: false) + let nightFlag = take("alertUrgentLowAutosnoozeNight", default: false) + + alarm.activeOption = { + if dayFlag, !nightFlag { return .day } + if !dayFlag, nightFlag { return .night } + switch autoStr { + case "day", "at day": return .day + case "night", "at night": return .night + default: return .always // “Never” → always active + } + }() + + // Persist ----------------------------------------------------------------- + Storage.shared.alarms.value.append(alarm) } } From 9eba9b8cd1437e325e25889663f9acd6039dd4a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 28 May 2025 21:15:56 +0200 Subject: [PATCH 105/138] Migrate urgent low --- LoopFollow/Storage/UserDefaults.swift | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/LoopFollow/Storage/UserDefaults.swift b/LoopFollow/Storage/UserDefaults.swift index 50aabdc65..8d18fb806 100644 --- a/LoopFollow/Storage/UserDefaults.swift +++ b/LoopFollow/Storage/UserDefaults.swift @@ -97,23 +97,6 @@ class UserDefaultsRepository { static let alertCageInsertTime = UserDefaultsValue(key: "alertCageInsertTime", default: 0) static let alertSageInsertTime = UserDefaultsValue(key: "alertSageInsertTime", default: 0) - static let alertUrgentLowActive = UserDefaultsValue(key: "alertUrgentLowActive", default: false) - static let alertUrgentLowBG = UserDefaultsValue(key: "alertUrgentLowBG", default: 55.0) - static let alertUrgentLowPredictiveMinutes = UserDefaultsValue(key: "alertUrgentLowPredictiveMinutes", default: 0) - static let alertUrgentLowSnooze = UserDefaultsValue(key: "alertUrgentLowSnooze", default: 5) - static let alertUrgentLowSnoozedTime = UserDefaultsValue(key: "alertUrgentLowSnoozedTime", default: nil) - static let alertUrgentLowIsSnoozed = UserDefaultsValue(key: "alertUrgentLowIsSnoozed", default: false) - static let alertUrgentLowRepeat = UserDefaultsValue(key: "alertUrgentLowRepeat", default: "Always") - static let alertUrgentLowDayTime = UserDefaultsValue(key: "alertUrgentLowDayTime", default: true) // need to change all DayTime to DayTimeRepeat - static let alertUrgentLowNightTime = UserDefaultsValue(key: "alertUrgentLowNightTime", default: true) // need to change all NightTime to NightTimeRepeat - static let alertUrgentLowSound = UserDefaultsValue(key: "alertUrgentLowSound", default: "Emergency_Alarm_Siren") - static let alertUrgentLowAudible = UserDefaultsValue(key: "alertUrgentLowAudible", default: "Always") - static let alertUrgentLowDayTimeAudible = UserDefaultsValue(key: "alertUrgentLowDayTimeAudible", default: true) - static let alertUrgentLowNightTimeAudible = UserDefaultsValue(key: "alertUrgentLowNightTimeAudible", default: true) - static let alertUrgentLowAutosnooze = UserDefaultsValue(key: "alertUrgentLowAutosnooze", default: "Never") - static let alertUrgentLowAutosnoozeDay = UserDefaultsValue(key: "alertUrgentLowAutosnoozeDay", default: false) - static let alertUrgentLowAutosnoozeNight = UserDefaultsValue(key: "alertUrgentLowAutosnoozeNight", default: false) - static let alertLowActive = UserDefaultsValue(key: "alertLowActive", default: false) static let alertLowBG = UserDefaultsValue(key: "alertLowBG", default: 70.0) static let alertLowPersistent = UserDefaultsValue(key: "alertLowPersistent", default: 0) From 5f1dc05b0dae6f94fe637a3c0418b5d34d388b6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 28 May 2025 21:32:09 +0200 Subject: [PATCH 106/138] Migrate low --- LoopFollow/Storage/Storage+Migrate.swift | 61 ++++++++++++++++++++++++ LoopFollow/Storage/UserDefaults.swift | 18 ------- 2 files changed, 61 insertions(+), 18 deletions(-) diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index 43cf203b4..752e6dbbd 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -284,6 +284,7 @@ extension Storage { } migrateUrgentLowAlarm() + migrateLowAlarm() } // MARK: - One-off alarm migrations @@ -340,4 +341,64 @@ extension Storage { // Persist ----------------------------------------------------------------- Storage.shared.alarms.value.append(alarm) } + + // MARK: - Low-BG alarm ------------------------------------------------------- + + private func migrateLowAlarm() { + // Bail if the old keys were never written + guard UserDefaultsValue(key: "alertLowActive", default: false).exists else { return } + + // tiny helper: fetch value → erase key + func take(_ key: String, default def: V) -> V { + let box = UserDefaultsValue(key: key, default: def) + defer { box.setNil(key: key) } + return box.value + } + + // Build the new Alarm ---------------------------------------------------- + var alarm = Alarm(type: .low) + alarm.name = "Low" + alarm.isEnabled = take("alertLowActive", default: false) + alarm.belowBG = Double(take("alertLowBG", default: 70.0)) + + // “Persistent ≥ X min” → `persistentMinutes` + alarm.persistentMinutes = take("alertLowPersistent", default: 0) + + // “Persistence max BG drop” -- ignoring this for now + // let persistentLowTriggerImmediatelyBG = UserDefaultsRepository.alertLowBG.value - UserDefaultsRepository.alertLowPersistenceMax.value + // (Float(persistentLowBG) <= UserDefaultsRepository.alertLowBG.value || Float(currentBG) <= persistentLowTriggerImmediatelyBG) + _ = Double(take("alertLowPersistenceMax", default: 5.0)) + + alarm.snoozeDuration = take("alertLowSnooze", default: 5) + alarm.snoozedUntil = take("alertLowSnoozedTime", default: nil as Date?) + alarm.soundFile = SoundFile( + rawValue: take("alertLowSound", + default: "Indeed")) ?? .indeed + + alarm.playSoundOption = PlaySoundOption( + rawValue: take("alertLowAudible", + default: "Always").lowercased()) ?? .always + + alarm.repeatSoundOption = RepeatSoundOption( + rawValue: take("alertLowRepeat", + default: "Always").lowercased()) ?? .always + + // activeOption ← legacy “Pre-Snooze” flags / picker + let autoStr = take("alertLowAutosnooze", default: "Never").lowercased() + let dayFlag = take("alertLowAutosnoozeDay", default: false) + let nightFlag = take("alertLowAutosnoozeNight", default: false) + + alarm.activeOption = { + if dayFlag, !nightFlag { return .day } + if !dayFlag, nightFlag { return .night } + switch autoStr { + case "day", "at day": return .day + case "night", "at night": return .night + default: return .always + } + }() + + // Done → append to the modern store + Storage.shared.alarms.value.append(alarm) + } } diff --git a/LoopFollow/Storage/UserDefaults.swift b/LoopFollow/Storage/UserDefaults.swift index 8d18fb806..bbf9be154 100644 --- a/LoopFollow/Storage/UserDefaults.swift +++ b/LoopFollow/Storage/UserDefaults.swift @@ -97,24 +97,6 @@ class UserDefaultsRepository { static let alertCageInsertTime = UserDefaultsValue(key: "alertCageInsertTime", default: 0) static let alertSageInsertTime = UserDefaultsValue(key: "alertSageInsertTime", default: 0) - static let alertLowActive = UserDefaultsValue(key: "alertLowActive", default: false) - static let alertLowBG = UserDefaultsValue(key: "alertLowBG", default: 70.0) - static let alertLowPersistent = UserDefaultsValue(key: "alertLowPersistent", default: 0) - static let alertLowPersistenceMax = UserDefaultsValue(key: "alertLowPersistenceMax", default: 5.0) - static let alertLowSnooze = UserDefaultsValue(key: "alertLowSnooze", default: 5) - static let alertLowSnoozedTime = UserDefaultsValue(key: "alertLowSnoozedTime", default: nil) - static let alertLowIsSnoozed = UserDefaultsValue(key: "alertLowIsSnoozed", default: false) - static let alertLowRepeat = UserDefaultsValue(key: "alertLowRepeat", default: "Always") - static let alertLowDayTime = UserDefaultsValue(key: "alertLowDayTime", default: true) - static let alertLowNightTime = UserDefaultsValue(key: "alertLowNightTime", default: true) - static let alertLowSound = UserDefaultsValue(key: "alertLowSound", default: "Indeed") - static let alertLowAudible = UserDefaultsValue(key: "alertLowAudible", default: "Always") - static let alertLowDayTimeAudible = UserDefaultsValue(key: "alertLowDayTimeAudible", default: true) - static let alertLowNightTimeAudible = UserDefaultsValue(key: "alertLowNightTimeAudible", default: true) - static let alertLowAutosnooze = UserDefaultsValue(key: "alertLowAutosnooze", default: "Never") - static let alertLowAutosnoozeDay = UserDefaultsValue(key: "alertLowAutosnoozeDay", default: false) - static let alertLowAutosnoozeNight = UserDefaultsValue(key: "alertLowAutosnoozeNight", default: false) - static let alertHighActive = UserDefaultsValue(key: "alertHighActive", default: false) static let alertHighBG = UserDefaultsValue(key: "alertHighBG", default: 180.0) static let alertHighPersistent = UserDefaultsValue(key: "alertHighPersistent", default: 60) From b5ca80109c9339289c5f382578e9a0d26c3e03c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 28 May 2025 21:35:29 +0200 Subject: [PATCH 107/138] Migrate high --- LoopFollow/Storage/Storage+Migrate.swift | 109 +++++++++++++++++++++++ LoopFollow/Storage/UserDefaults.swift | 33 ------- 2 files changed, 109 insertions(+), 33 deletions(-) diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index 752e6dbbd..5dc0d82ad 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -285,6 +285,8 @@ extension Storage { migrateUrgentLowAlarm() migrateLowAlarm() + migrateHighAlarm() + migrateUrgentHighAlarm() } // MARK: - One-off alarm migrations @@ -401,4 +403,111 @@ extension Storage { // Done → append to the modern store Storage.shared.alarms.value.append(alarm) } + + // MARK: - High-BG alarm ----------------------------------------------------- + + private func migrateHighAlarm() { + // Only run if the legacy key ever existed + guard UserDefaultsValue(key: "alertHighActive", default: false).exists else { return } + + /// Fetch → erase helper + func take(_ key: String, default def: V) -> V { + let box = UserDefaultsValue(key: key, default: def) + defer { box.setNil(key: key) } // remove legacy value + return box.value + } + + // ---------- Build Alarm ----------------------------------------------- + var alarm = Alarm(type: .high) + alarm.name = "High" + alarm.isEnabled = take("alertHighActive", default: false) + alarm.aboveBG = Double(take("alertHighBG", default: 180.0)) + + alarm.persistentMinutes = take("alertHighPersistent", default: 60) + alarm.snoozeDuration = take("alertHighSnooze", default: 60) + alarm.snoozedUntil = take("alertHighSnoozedTime", default: nil as Date?) + + alarm.soundFile = SoundFile( + rawValue: take("alertHighSound", + default: "Time_Has_Come")) ?? .timeHasCome + + alarm.playSoundOption = PlaySoundOption( + rawValue: take("alertHighAudible", + default: "Always").lowercased()) ?? .always + + alarm.repeatSoundOption = RepeatSoundOption( + rawValue: take("alertHighRepeat", + default: "Always").lowercased()) ?? .always + + // ── activeOption derived from “Pre-Snooze” picker & flags + let autoStr = take("alertHighAutosnooze", default: "Never").lowercased() + let dayFlag = take("alertHighAutosnoozeDay", default: false) + let nightFlag = take("alertHighAutosnoozeNight", default: false) + + alarm.activeOption = { + if dayFlag, !nightFlag { return .day } + if !dayFlag, nightFlag { return .night } + switch autoStr { + case "day", "at day": return .day + case "night", "at night": return .night + default: return .always + } + }() + + // ---------- Persist & we’re done -------------------------------------- + Storage.shared.alarms.value.append(alarm) + } + + // MARK: - Urgent-High alarm -------------------------------------------------- + + private func migrateUrgentHighAlarm() { + // run only once, only if the user ever changed that toggle + guard UserDefaultsValue(key: "alertUrgentHighActive", default: false).exists else { return } + + // helper: read-then-erase a legacy value + func take(_ key: String, default def: V) -> V { + let box = UserDefaultsValue(key: key, default: def) + defer { box.setNil(key: key) } // wipe legacy key + return box.value + } + + // ───────── Build the Alarm ──────────────────────────────────────────── + var alarm = Alarm(type: .high) // we map to the existing `.high` type + alarm.name = "Urgent High" + alarm.isEnabled = take("alertUrgentHighActive", default: false) + alarm.aboveBG = Double(take("alertUrgentHighBG", default: 250.0)) + + alarm.snoozeDuration = take("alertUrgentHighSnooze", default: 30) + alarm.snoozedUntil = take("alertUrgentHighSnoozedTime", default: nil as Date?) + + alarm.soundFile = SoundFile( + rawValue: take("alertUrgentHighSound", + default: "Pager_Beeps")) ?? .pagerBeeps + + alarm.playSoundOption = PlaySoundOption( + rawValue: take("alertUrgentHighAudible", + default: "Always").lowercased()) ?? .always + + alarm.repeatSoundOption = RepeatSoundOption( + rawValue: take("alertUrgentHighRepeat", + default: "Always").lowercased()) ?? .always + + // activeOption comes from the old “Pre-Snooze” picker + its day/night flags + let autoStr = take("alertUrgentHighAutosnooze", default: "Never").lowercased() + let dayFlag = take("alertUrgentHighAutosnoozeDay", default: false) + let nightFlag = take("alertUrgentHighAutosnoozeNight", default: false) + + alarm.activeOption = { + if dayFlag, !nightFlag { return .day } + if !dayFlag, nightFlag { return .night } + switch autoStr { // fall back to picker value + case "day", "at day": return .day + case "night", "at night": return .night + default: return .always + } + }() + + // ───────── Persist in new storage ───────────────────────────────────── + Storage.shared.alarms.value.append(alarm) + } } diff --git a/LoopFollow/Storage/UserDefaults.swift b/LoopFollow/Storage/UserDefaults.swift index bbf9be154..6018b7a61 100644 --- a/LoopFollow/Storage/UserDefaults.swift +++ b/LoopFollow/Storage/UserDefaults.swift @@ -97,39 +97,6 @@ class UserDefaultsRepository { static let alertCageInsertTime = UserDefaultsValue(key: "alertCageInsertTime", default: 0) static let alertSageInsertTime = UserDefaultsValue(key: "alertSageInsertTime", default: 0) - static let alertHighActive = UserDefaultsValue(key: "alertHighActive", default: false) - static let alertHighBG = UserDefaultsValue(key: "alertHighBG", default: 180.0) - static let alertHighPersistent = UserDefaultsValue(key: "alertHighPersistent", default: 60) - static let alertHighSnooze = UserDefaultsValue(key: "alertHighSnooze", default: 60) - static let alertHighSnoozedTime = UserDefaultsValue(key: "alertHighSnoozedTime", default: nil) - static let alertHighIsSnoozed = UserDefaultsValue(key: "alertHighIsSnoozed", default: false) - static let alertHighRepeat = UserDefaultsValue(key: "alertHighRepeat", default: "Always") - static let alertHighDayTime = UserDefaultsValue(key: "alertHighDayTime", default: true) - static let alertHighNightTime = UserDefaultsValue(key: "alertHighNightTime", default: true) - static let alertHighSound = UserDefaultsValue(key: "alertHighSound", default: "Time_Has_Come") - static let alertHighAudible = UserDefaultsValue(key: "alertHighAudible", default: "Always") - static let alertHighDayTimeAudible = UserDefaultsValue(key: "alertHighDayTimeAudible", default: true) - static let alertHighNightTimeAudible = UserDefaultsValue(key: "alertHighNightTimeAudible", default: true) - static let alertHighAutosnooze = UserDefaultsValue(key: "alertHighAutosnooze", default: "Never") - static let alertHighAutosnoozeDay = UserDefaultsValue(key: "alertHighAutosnoozeDay", default: false) - static let alertHighAutosnoozeNight = UserDefaultsValue(key: "alertHighAutosnoozeNight", default: false) - - static let alertUrgentHighActive = UserDefaultsValue(key: "alertUrgentHighActive", default: false) - static let alertUrgentHighBG = UserDefaultsValue(key: "alertUrgentHighBG", default: 250.0) - static let alertUrgentHighSnooze = UserDefaultsValue(key: "alertUrgentHighSnooze", default: 30) - static let alertUrgentHighSnoozedTime = UserDefaultsValue(key: "alertUrgentHighSnoozedTime", default: nil) - static let alertUrgentHighIsSnoozed = UserDefaultsValue(key: "alertUrgentHighIsSnoozed", default: false) - static let alertUrgentHighRepeat = UserDefaultsValue(key: "alertUrgentHighRepeat", default: "Always") - static let alertUrgentHighDayTime = UserDefaultsValue(key: "alertUrgentHighDayTime", default: true) - static let alertUrgentHighNightTime = UserDefaultsValue(key: "alertUrgentHighNightTime", default: true) - static let alertUrgentHighSound = UserDefaultsValue(key: "alertUrgentHighSound", default: "Pager_Beeps") - static let alertUrgentHighAudible = UserDefaultsValue(key: "alertUrgentHighAudible", default: "Always") - static let alertUrgentHighDayTimeAudible = UserDefaultsValue(key: "alertUrgentHighDayTimeAudible", default: true) - static let alertUrgentHighNightTimeAudible = UserDefaultsValue(key: "alertUrgentHighNightTimeAudible", default: true) - static let alertUrgentHighAutosnooze = UserDefaultsValue(key: "alertUrgentHighAutosnooze", default: "Never") - static let alertUrgentHighAutosnoozeDay = UserDefaultsValue(key: "alertUrgentHighAutosnoozeDay", default: false) - static let alertUrgentHighAutosnoozeNight = UserDefaultsValue(key: "alertUrgentHighAutosnoozeNight", default: false) - static let alertFastDropActive = UserDefaultsValue(key: "alertFastDropDeltaActive", default: false) static let alertFastDropSnooze = UserDefaultsValue(key: "alertFastDropDeltaSnooze", default: 10) static let alertFastDropDelta = UserDefaultsValue(key: "alertFastDropDelta", default: 10.0) From e52bef49dfece4696c660d3a66112291114df632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 28 May 2025 21:45:02 +0200 Subject: [PATCH 108/138] drop and rise migration --- LoopFollow/Storage/Storage+Migrate.swift | 112 +++++++++++++++++++++++ LoopFollow/Storage/UserDefaults.swift | 37 -------- 2 files changed, 112 insertions(+), 37 deletions(-) diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index 5dc0d82ad..ae902d790 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -510,4 +510,116 @@ extension Storage { // ───────── Persist in new storage ───────────────────────────────────── Storage.shared.alarms.value.append(alarm) } + + // MARK: - Fast-Drop alarm ---------------------------------------------------- + private func migrateFastDropAlarm() { + + guard UserDefaultsValue(key: "alertFastDropDeltaActive", + default: false).exists else { return } + + // helper: read-then-erase + func take(_ k: String, default d: V) -> V { + let box = UserDefaultsValue(key: k, default: d) + defer { box.setNil(key: k) } + return box.value + } + + var alarm = Alarm(type: .fastDrop) + alarm.name = "Fast Drop" + alarm.isEnabled = take("alertFastDropDeltaActive", default: false) + + // core trigger parameters + alarm.delta = Double(take("alertFastDropDelta", default: 10.0)) + alarm.monitoringWindow = take("alertFastDropReadings", default: 3) // store #readings + if take("alertFastDropUseLimit", default: false) { + alarm.belowBG = Double(take("alertFastDropBelowBG", default: 120.0)) + } + + // snoozing + alarm.snoozeDuration = take("alertFastDropDeltaSnooze", default: 10) + alarm.snoozedUntil = take("alertFastDropSnoozedTime", default: nil as Date?) + + // sound + options + alarm.soundFile = SoundFile( + rawValue: take("alertFastDropSound", default: "Big_Clock_Ticking") + ) ?? .bigClockTicking + + alarm.playSoundOption = PlaySoundOption( + rawValue: take("alertFastDropAudible", default: "Always").lowercased() + ) ?? .always + + alarm.repeatSoundOption = RepeatSoundOption( + rawValue: take("alertFastDropRepeat", default: "Never").lowercased() + ) ?? .never + + // activeOption from old “Pre-Snooze” picker + day/night flags + let autoStr = take("alertFastDropAutosnooze", default: "Never").lowercased() + let dayFlag = take("alertFastDropAutosnoozeDay", default: false) + let nightFlag = take("alertFastDropAutosnoozeNight", default: false) + alarm.activeOption = { + if dayFlag, !nightFlag { return .day } + if !dayFlag, nightFlag { return .night } + switch autoStr { + case "day", "at day": return .day + case "night", "at night": return .night + default: return .always + } + }() + + Storage.shared.alarms.value.append(alarm) + } + + // MARK: - Fast-Rise alarm ---------------------------------------------------- + private func migrateFastRiseAlarm() { + + guard UserDefaultsValue(key: "alertFastRiseDeltaActive", + default: false).exists else { return } + + func take(_ k: String, default d: V) -> V { + let box = UserDefaultsValue(key: k, default: d) + defer { box.setNil(key: k) } + return box.value + } + + var alarm = Alarm(type: .fastRise) + alarm.name = "Fast Rise" + alarm.isEnabled = take("alertFastRiseDeltaActive", default: false) + + alarm.delta = Double(take("alertFastRiseDelta", default: 10.0)) + alarm.monitoringWindow = take("alertFastRiseReadings", default: 3) + if take("alertFastRiseUseLimit", default: false) { + alarm.aboveBG = Double(take("alertFastRiseAboveBG", default: 200.0)) + } + + alarm.snoozeDuration = take("alertFastRiseDeltaSnooze", default: 10) + alarm.snoozedUntil = take("alertFastRiseSnoozedTime", default: nil as Date?) + + alarm.soundFile = SoundFile( + rawValue: take("alertFastRiseSound", + default: "Cartoon_Fail_Strings_Trumpet") + ) ?? .cartoonFailStringsTrumpet + + alarm.playSoundOption = PlaySoundOption( + rawValue: take("alertFastRiseAudible", default: "Always").lowercased() + ) ?? .always + + alarm.repeatSoundOption = RepeatSoundOption( + rawValue: take("alertFastRiseRepeat", default: "Never").lowercased() + ) ?? .never + + let autoStr = take("alertFastRiseAutosnooze", default: "Never").lowercased() + let dayFlag = take("alertFastRiseAutosnoozeDay", default: false) + let nightFlag = take("alertFastRiseAutosnoozeNight", default: false) + alarm.activeOption = { + if dayFlag, !nightFlag { return .day } + if !dayFlag, nightFlag { return .night } + switch autoStr { + case "day", "at day": return .day + case "night", "at night": return .night + default: return .always + } + }() + + Storage.shared.alarms.value.append(alarm) + } } diff --git a/LoopFollow/Storage/UserDefaults.swift b/LoopFollow/Storage/UserDefaults.swift index 6018b7a61..c77ab69ce 100644 --- a/LoopFollow/Storage/UserDefaults.swift +++ b/LoopFollow/Storage/UserDefaults.swift @@ -97,43 +97,6 @@ class UserDefaultsRepository { static let alertCageInsertTime = UserDefaultsValue(key: "alertCageInsertTime", default: 0) static let alertSageInsertTime = UserDefaultsValue(key: "alertSageInsertTime", default: 0) - static let alertFastDropActive = UserDefaultsValue(key: "alertFastDropDeltaActive", default: false) - static let alertFastDropSnooze = UserDefaultsValue(key: "alertFastDropDeltaSnooze", default: 10) - static let alertFastDropDelta = UserDefaultsValue(key: "alertFastDropDelta", default: 10.0) - static let alertFastDropReadings = UserDefaultsValue(key: "alertFastDropReadings", default: 3) - static let alertFastDropUseLimit = UserDefaultsValue(key: "alertFastDropUseLimit", default: false) - static let alertFastDropBelowBG = UserDefaultsValue(key: "alertFastDropBelowBG", default: 120.0) - static let alertFastDropSnoozedTime = UserDefaultsValue(key: "alertFastDropSnoozedTime", default: nil) - static let alertFastDropIsSnoozed = UserDefaultsValue(key: "alertFastDropIsSnoozed", default: false) - static let alertFastDropRepeat = UserDefaultsValue(key: "alertFastDropRepeat", default: "Never") - static let alertFastDropDayTime = UserDefaultsValue(key: "alertFastDropDayTime", default: false) - static let alertFastDropNightTime = UserDefaultsValue(key: "alertFastDropNightTime", default: false) - static let alertFastDropSound = UserDefaultsValue(key: "alertFastDropSound", default: "Big_Clock_Ticking") - static let alertFastDropAudible = UserDefaultsValue(key: "alertFastDropAudible", default: "Always") - static let alertFastDropDayTimeAudible = UserDefaultsValue(key: "alertFastDropDayTimeAudible", default: true) - static let alertFastDropNightTimeAudible = UserDefaultsValue(key: "alertFastDropNightTimeAudible", default: true) - static let alertFastDropAutosnooze = UserDefaultsValue(key: "alertFastDropAutosnooze", default: "Never") - static let alertFastDropAutosnoozeDay = UserDefaultsValue(key: "alertFastDropAutosnoozeDay", default: false) - static let alertFastDropAutosnoozeNight = UserDefaultsValue(key: "alertFastDropAutosnoozeNight", default: false) - - static let alertFastRiseActive = UserDefaultsValue(key: "alertFastRiseDeltaActive", default: false) - static let alertFastRiseSnooze = UserDefaultsValue(key: "alertFastRiseDeltaSnooze", default: 10) - static let alertFastRiseDelta = UserDefaultsValue(key: "alertFastRiseDelta", default: 10.0) - static let alertFastRiseReadings = UserDefaultsValue(key: "alertFastRiseReadings", default: 3) - static let alertFastRiseUseLimit = UserDefaultsValue(key: "alertFastRiseUseLimit", default: false) - static let alertFastRiseAboveBG = UserDefaultsValue(key: "alertFastRiseAboveBG", default: 200.0) - static let alertFastRiseSnoozedTime = UserDefaultsValue(key: "alertFastRiseSnoozedTime", default: nil) - static let alertFastRiseIsSnoozed = UserDefaultsValue(key: "alertFastRiseIsSnoozed", default: false) - static let alertFastRiseRepeat = UserDefaultsValue(key: "alertFastRiseRepeat", default: "Never") - static let alertFastRiseDayTime = UserDefaultsValue(key: "alertFastRiseDayTime", default: false) - static let alertFastRiseNightTime = UserDefaultsValue(key: "alertFastRiseNightTime", default: false) - static let alertFastRiseSound = UserDefaultsValue(key: "alertFastRiseSound", default: "Cartoon_Fail_Strings_Trumpet") - static let alertFastRiseAudible = UserDefaultsValue(key: "alertFastRiseAudible", default: "Always") - static let alertFastRiseDayTimeAudible = UserDefaultsValue(key: "alertFastRiseDayTimeAudible", default: true) - static let alertFastRiseNightTimeAudible = UserDefaultsValue(key: "alertFastRiseNightTimeAudible", default: true) - static let alertFastRiseAutosnooze = UserDefaultsValue(key: "alertFastRiseAutosnooze", default: "Never") - static let alertFastRiseAutosnoozeDay = UserDefaultsValue(key: "alertFastRiseAutosnoozeDay", default: false) - static let alertFastRiseAutosnoozeNight = UserDefaultsValue(key: "alertFastRiseAutosnoozeNight", default: false) static let alertMissedReadingActive = UserDefaultsValue(key: "alertMissedReadingActive", default: false) static let alertMissedReading = UserDefaultsValue(key: "alertMissedReading", default: 31) From dd3f61fdee280f2fd5fe827e92f5824bf79e2aa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 28 May 2025 21:47:17 +0200 Subject: [PATCH 109/138] drop and rise migration --- LoopFollow/Storage/Storage+Migrate.swift | 54 ++++++++++++------------ LoopFollow/Storage/UserDefaults.swift | 15 ------- 2 files changed, 27 insertions(+), 42 deletions(-) diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index ae902d790..b3be834f0 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -512,8 +512,8 @@ extension Storage { } // MARK: - Fast-Drop alarm ---------------------------------------------------- - private func migrateFastDropAlarm() { + private func migrateFastDropAlarm() { guard UserDefaultsValue(key: "alertFastDropDeltaActive", default: false).exists else { return } @@ -525,22 +525,22 @@ extension Storage { } var alarm = Alarm(type: .fastDrop) - alarm.name = "Fast Drop" - alarm.isEnabled = take("alertFastDropDeltaActive", default: false) + alarm.name = "Fast Drop" + alarm.isEnabled = take("alertFastDropDeltaActive", default: false) // core trigger parameters - alarm.delta = Double(take("alertFastDropDelta", default: 10.0)) - alarm.monitoringWindow = take("alertFastDropReadings", default: 3) // store #readings + alarm.delta = Double(take("alertFastDropDelta", default: 10.0)) + alarm.monitoringWindow = take("alertFastDropReadings", default: 3) // store #readings if take("alertFastDropUseLimit", default: false) { - alarm.belowBG = Double(take("alertFastDropBelowBG", default: 120.0)) + alarm.belowBG = Double(take("alertFastDropBelowBG", default: 120.0)) } // snoozing alarm.snoozeDuration = take("alertFastDropDeltaSnooze", default: 10) - alarm.snoozedUntil = take("alertFastDropSnoozedTime", default: nil as Date?) + alarm.snoozedUntil = take("alertFastDropSnoozedTime", default: nil as Date?) // sound + options - alarm.soundFile = SoundFile( + alarm.soundFile = SoundFile( rawValue: take("alertFastDropSound", default: "Big_Clock_Ticking") ) ?? .bigClockTicking @@ -553,16 +553,16 @@ extension Storage { ) ?? .never // activeOption from old “Pre-Snooze” picker + day/night flags - let autoStr = take("alertFastDropAutosnooze", default: "Never").lowercased() - let dayFlag = take("alertFastDropAutosnoozeDay", default: false) + let autoStr = take("alertFastDropAutosnooze", default: "Never").lowercased() + let dayFlag = take("alertFastDropAutosnoozeDay", default: false) let nightFlag = take("alertFastDropAutosnoozeNight", default: false) alarm.activeOption = { - if dayFlag, !nightFlag { return .day } - if !dayFlag, nightFlag { return .night } + if dayFlag, !nightFlag { return .day } + if !dayFlag, nightFlag { return .night } switch autoStr { - case "day", "at day": return .day + case "day", "at day": return .day case "night", "at night": return .night - default: return .always + default: return .always } }() @@ -570,8 +570,8 @@ extension Storage { } // MARK: - Fast-Rise alarm ---------------------------------------------------- - private func migrateFastRiseAlarm() { + private func migrateFastRiseAlarm() { guard UserDefaultsValue(key: "alertFastRiseDeltaActive", default: false).exists else { return } @@ -582,19 +582,19 @@ extension Storage { } var alarm = Alarm(type: .fastRise) - alarm.name = "Fast Rise" - alarm.isEnabled = take("alertFastRiseDeltaActive", default: false) + alarm.name = "Fast Rise" + alarm.isEnabled = take("alertFastRiseDeltaActive", default: false) - alarm.delta = Double(take("alertFastRiseDelta", default: 10.0)) + alarm.delta = Double(take("alertFastRiseDelta", default: 10.0)) alarm.monitoringWindow = take("alertFastRiseReadings", default: 3) if take("alertFastRiseUseLimit", default: false) { - alarm.aboveBG = Double(take("alertFastRiseAboveBG", default: 200.0)) + alarm.aboveBG = Double(take("alertFastRiseAboveBG", default: 200.0)) } alarm.snoozeDuration = take("alertFastRiseDeltaSnooze", default: 10) - alarm.snoozedUntil = take("alertFastRiseSnoozedTime", default: nil as Date?) + alarm.snoozedUntil = take("alertFastRiseSnoozedTime", default: nil as Date?) - alarm.soundFile = SoundFile( + alarm.soundFile = SoundFile( rawValue: take("alertFastRiseSound", default: "Cartoon_Fail_Strings_Trumpet") ) ?? .cartoonFailStringsTrumpet @@ -607,16 +607,16 @@ extension Storage { rawValue: take("alertFastRiseRepeat", default: "Never").lowercased() ) ?? .never - let autoStr = take("alertFastRiseAutosnooze", default: "Never").lowercased() - let dayFlag = take("alertFastRiseAutosnoozeDay", default: false) + let autoStr = take("alertFastRiseAutosnooze", default: "Never").lowercased() + let dayFlag = take("alertFastRiseAutosnoozeDay", default: false) let nightFlag = take("alertFastRiseAutosnoozeNight", default: false) alarm.activeOption = { - if dayFlag, !nightFlag { return .day } - if !dayFlag, nightFlag { return .night } + if dayFlag, !nightFlag { return .day } + if !dayFlag, nightFlag { return .night } switch autoStr { - case "day", "at day": return .day + case "day", "at day": return .day case "night", "at night": return .night - default: return .always + default: return .always } }() diff --git a/LoopFollow/Storage/UserDefaults.swift b/LoopFollow/Storage/UserDefaults.swift index c77ab69ce..c3a9d3acc 100644 --- a/LoopFollow/Storage/UserDefaults.swift +++ b/LoopFollow/Storage/UserDefaults.swift @@ -98,21 +98,6 @@ class UserDefaultsRepository { static let alertSageInsertTime = UserDefaultsValue(key: "alertSageInsertTime", default: 0) - static let alertMissedReadingActive = UserDefaultsValue(key: "alertMissedReadingActive", default: false) - static let alertMissedReading = UserDefaultsValue(key: "alertMissedReading", default: 31) - static let alertMissedReadingSnooze = UserDefaultsValue(key: "alertMissedReadingSnooze", default: 30) - static let alertMissedReadingSnoozedTime = UserDefaultsValue(key: "alertMissedReadingSnoozedTime", default: nil) - static let alertMissedReadingIsSnoozed = UserDefaultsValue(key: "alertMissedReadingIsSnoozed", default: false) - static let alertMissedReadingRepeat = UserDefaultsValue(key: "alertMissedReadingRepeat", default: "Never") - static let alertMissedReadingDayTime = UserDefaultsValue(key: "alertMissedReadingDayTime", default: false) - static let alertMissedReadingNightTime = UserDefaultsValue(key: "alertMissedReadingNightTime", default: false) - static let alertMissedReadingSound = UserDefaultsValue(key: "alertMissedReadingSound", default: "Cartoon_Tip_Toe_Sneaky_Walk") - static let alertMissedReadingAudible = UserDefaultsValue(key: "alertMissedReadingAudible", default: "Always") - static let alertMissedReadingDayTimeAudible = UserDefaultsValue(key: "alertMissedReadingDayTimeAudible", default: true) - static let alertMissedReadingNightTimeAudible = UserDefaultsValue(key: "alertMissedReadingNightTimeAudible", default: true) - static let alertMissedReadingAutosnooze = UserDefaultsValue(key: "alertMissedReadingAutosnooze", default: "Never") - static let alertMissedReadingAutosnoozeDay = UserDefaultsValue(key: "alertMissedReadingAutosnoozeDay", default: false) - static let alertMissedReadingAutosnoozeNight = UserDefaultsValue(key: "alertMissedReadingAutosnoozeNight", default: false) static let alertNotLoopingActive = UserDefaultsValue(key: "alertNotLoopingActive", default: false) static let alertNotLooping = UserDefaultsValue(key: "alertNotLooping", default: 31) From 2171f11e6bb3b4d97f608707dce5dc9219062899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 28 May 2025 21:53:09 +0200 Subject: [PATCH 110/138] migrateMissedReadingAlarm --- LoopFollow/Storage/Storage+Migrate.swift | 67 ++++++++++++++++++++++++ LoopFollow/Storage/UserDefaults.swift | 2 - 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index b3be834f0..603eb71bf 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -287,6 +287,9 @@ extension Storage { migrateLowAlarm() migrateHighAlarm() migrateUrgentHighAlarm() + migrateFastDropAlarm() + migrateFastRiseAlarm() + migrateMissedReadingAlarm() } // MARK: - One-off alarm migrations @@ -622,4 +625,68 @@ extension Storage { Storage.shared.alarms.value.append(alarm) } + + // MARK: - Missed-Reading alarm --------------------------------------------- + + private func migrateMissedReadingAlarm() { + // Run only when the user has ever modified those settings + guard UserDefaultsValue(key: "alertMissedReadingActive", + default: false).exists else { return } + + // read-then-erase helper + func take(_ k: String, default d: V) -> V { + let b = UserDefaultsValue(key: k, default: d) + defer { b.setNil(key: k) } + return b.value + } + + var alarm = Alarm(type: .missedReading) + alarm.name = "Missed Reading" + alarm.isEnabled = take("alertMissedReadingActive", default: false) + + // “No CGM data for X minutes” + alarm.persistentMinutes = take("alertMissedReading", default: 31) + + // snoozing + alarm.snoozeDuration = take("alertMissedReadingSnooze", default: 30) + alarm.snoozedUntil = take("alertMissedReadingSnoozedTime", + default: nil as Date?) + // (legacy “is-snoozed” flag is implicit in snoozedUntil) + + // sound + alarm.soundFile = SoundFile( + rawValue: take("alertMissedReadingSound", + default: "Cartoon_Tip_Toe_Sneaky_Walk") + ) ?? .cartoonTipToeSneakyWalk + + // play / repeat options + alarm.playSoundOption = PlaySoundOption( + rawValue: take("alertMissedReadingAudible", + default: "Always").lowercased() + ) ?? .always + + alarm.repeatSoundOption = RepeatSoundOption( + rawValue: take("alertMissedReadingRepeat", + default: "Never").lowercased() + ) ?? .never + + // activeOption ← “Pre-Snooze” picker + day/night flags + let autoStr = take("alertMissedReadingAutosnooze", default: "Never") + .lowercased() + let dayFlag = take("alertMissedReadingAutosnoozeDay", default: false) + let nightFlag = take("alertMissedReadingAutosnoozeNight", default: false) + + alarm.activeOption = { + if dayFlag, !nightFlag { return .day } + if !dayFlag, nightFlag { return .night } + switch autoStr { + case "day", "at day": return .day + case "night", "at night": return .night + default: return .always // “Never” → always on + } + }() + + // store in the new world + Storage.shared.alarms.value.append(alarm) + } } diff --git a/LoopFollow/Storage/UserDefaults.swift b/LoopFollow/Storage/UserDefaults.swift index c3a9d3acc..409bcdef7 100644 --- a/LoopFollow/Storage/UserDefaults.swift +++ b/LoopFollow/Storage/UserDefaults.swift @@ -97,8 +97,6 @@ class UserDefaultsRepository { static let alertCageInsertTime = UserDefaultsValue(key: "alertCageInsertTime", default: 0) static let alertSageInsertTime = UserDefaultsValue(key: "alertSageInsertTime", default: 0) - - static let alertNotLoopingActive = UserDefaultsValue(key: "alertNotLoopingActive", default: false) static let alertNotLooping = UserDefaultsValue(key: "alertNotLooping", default: 31) static let alertNotLoopingSnooze = UserDefaultsValue(key: "alertNotLoopingSnooze", default: 30) From f1b2e3a531fbf47b70e9ec8e2784a5a27870448d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 6 Jun 2025 14:31:22 +0200 Subject: [PATCH 111/138] Not looping migration --- .../Controllers/Nightscout/DeviceStatus.swift | 15 ++-- .../Nightscout/DeviceStatusLoop.swift | 4 +- .../Nightscout/DeviceStatusOpenAPS.swift | 4 +- LoopFollow/Storage/Observable.swift | 2 + LoopFollow/Storage/Storage+Migrate.swift | 77 +++++++++++++++++++ LoopFollow/Storage/UserDefaults.swift | 20 ----- LoopFollow/Task/AlarmTask.swift | 2 +- .../ViewControllers/MainViewController.swift | 5 -- 8 files changed, 89 insertions(+), 40 deletions(-) diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift index 0817b818d..5eaf3fbea 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift @@ -35,17 +35,12 @@ extension MainViewController { func evaluateNotLooping() { guard let statusStackView = LoopStatusLabel.superview as? UIStackView else { return } + guard let lastLoopTime = Observable.shared.alertLastLoopTime.value, lastLoopTime > 0 else { + return + } let now = TimeInterval(Date().timeIntervalSince1970) - let lastLoopTime = UserDefaultsRepository.alertLastLoopTime.value - let isAlarmEnabled = UserDefaultsRepository.alertNotLoopingActive.value - let nonLoopingTimeThreshold: TimeInterval - - if isAlarmEnabled { - nonLoopingTimeThreshold = Double(UserDefaultsRepository.alertNotLooping.value * 60) - } else { - nonLoopingTimeThreshold = 15 * 60 - } + let nonLoopingTimeThreshold: TimeInterval = 15 * 60 if IsNightscoutEnabled(), (now - lastLoopTime) >= nonLoopingTimeThreshold, lastLoopTime > 0 { IsNotLooping = true @@ -165,7 +160,7 @@ extension MainViewController { // Start the timer based on the timestamp let now = dateTimeUtils.getNowTimeIntervalUTC() - let secondsAgo = now - UserDefaultsRepository.alertLastLoopTime.value + let secondsAgo = now - (Observable.shared.alertLastLoopTime.value ?? 0) DispatchQueue.main.async { if secondsAgo >= (20 * 60) { diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift index 0a5376ac4..d6735e463 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift @@ -16,8 +16,8 @@ extension MainViewController { } if let lastLoopTime = formatter.date(from: (lastLoopRecord["timestamp"] as! String))?.timeIntervalSince1970 { - let previousLastLoopTime = UserDefaultsRepository.alertLastLoopTime.value - UserDefaultsRepository.alertLastLoopTime.value = lastLoopTime + let previousLastLoopTime = Observable.shared.alertLastLoopTime.value ?? 0 + Observable.shared.alertLastLoopTime.value = lastLoopTime if let failure = lastLoopRecord["failureReason"] { LoopStatusLabel.text = "X" latestLoopStatusString = "X" diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift index 4b8c72455..38d32e014 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift @@ -25,11 +25,11 @@ extension MainViewController { if let timestampString = enacted["timestamp"] as? String, let lastLoopTime = formatter.date(from: timestampString)?.timeIntervalSince1970 { - let storedTime = UserDefaultsRepository.alertLastLoopTime.value + let storedTime = Observable.shared.alertLastLoopTime.value ?? 0 if lastLoopTime < storedTime { LogManager.shared.log(category: .deviceStatus, message: "Received an old timestamp for enacted: \(lastLoopTime) is older than last stored time \(storedTime), ignoring update.", isDebug: false) } else { - UserDefaultsRepository.alertLastLoopTime.value = lastLoopTime + Observable.shared.alertLastLoopTime.value = lastLoopTime LogManager.shared.log(category: .deviceStatus, message: "New LastLoopTime: \(lastLoopTime)", isDebug: true) } } diff --git a/LoopFollow/Storage/Observable.swift b/LoopFollow/Storage/Observable.swift index d42e793f7..869bde4b7 100644 --- a/LoopFollow/Storage/Observable.swift +++ b/LoopFollow/Storage/Observable.swift @@ -30,5 +30,7 @@ class Observable { var chartSettingsChanged = ObservableValue(default: false) + var alertLastLoopTime = ObservableValue(default: nil) + private init() {} } diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index 603eb71bf..d212fb670 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -290,6 +290,7 @@ extension Storage { migrateFastDropAlarm() migrateFastRiseAlarm() migrateMissedReadingAlarm() + migrateNotLoopingAlarm() } // MARK: - One-off alarm migrations @@ -689,4 +690,80 @@ extension Storage { // store in the new world Storage.shared.alarms.value.append(alarm) } + + // MARK: - “Not Looping” (legacy → Storage.shared.alarms) + + private func migrateNotLoopingAlarm() { + // Check if the user ever configured this alarm + let activeFlag = UserDefaultsValue(key: "alertNotLoopingActive", default: false) + guard activeFlag.exists else { return } // nothing to migrate + + // Convenience: read-then-erase + func take(_ key: String, default def: V) -> V { + let box = UserDefaultsValue(key: key, default: def) + defer { box.setNil(key: key) } // wipe after reading + return box.value + } + + // Build the new Alarm --------------------------------------------------- + var alarm = Alarm(type: .notLooping) + alarm.name = "Not Looping Alert" + alarm.isEnabled = take("alertNotLoopingActive", default: false) + alarm.threshold = Double(take("alertNotLooping", default: 31)) // minutes + alarm.snoozeDuration = take("alertNotLoopingSnooze", default: 30) + alarm.snoozedUntil = take("alertNotLoopingSnoozedTime", default: nil as Date?) + alarm.soundFile = SoundFile(rawValue: + take("alertNotLoopingSound", + default: "Sci-Fi_Engine_Shut_Down")) ?? .sciFiEngineShutDown + + // ── ACTIVE-DURING (day/night) ← old **Pre-Snooze** flags -------------- + let actDay = take("alertNotLoopingAutosnoozeDay", default: false) + let actNight = take("alertNotLoopingAutosnoozeNight", default: false) + alarm.activeOption = { + switch (actDay, actNight) { + case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .always // “Never” in old UI + } + }() + + // ── PLAY-SOUND option --------------------------------------------------- + let playStr = take("alertNotLoopingAudible", default: "Always").lowercased() + let playDay = take("alertNotLoopingDayTimeAudible", default: true) + let playNight = take("alertNotLoopingNightTimeAudible", default: true) + alarm.playSoundOption = { + if !playDay, !playNight { return .never } + else if playDay, playNight { return .always } + else if playDay { return .day } + else { return .night } + }() + + // ── REPEAT-SOUND option ------------------------------------------------- + let repStr = take("alertNotLoopingRepeat", default: "Never").lowercased() + let repDay = take("alertNotLoopingDayTime", default: false) + let repNight = take("alertNotLoopingNightTime", default: false) + alarm.repeatSoundOption = { + if repDay, repNight { return .always } + else if repDay, !repNight { return .day } + else if repNight, !repDay { return .night } + else { return .never } + }() + + // ── BG-limit guard ------------------------------------------------------ + if take("alertNotLoopingUseLimits", default: false) { + alarm.belowBG = Double(take("alertNotLoopingLowerLimit", default: 100.0)) + alarm.aboveBG = Double(take("alertNotLoopingUpperLimit", default: 160.0)) + } + + // ── Per-alarm snooze state --------------------------------------------- + if !take("alertNotLoopingIsSnoozed", default: false) { + alarm.snoozedUntil = nil // ignore stored date if flag isn’t set + } + + // Persist & finish ------------------------------------------------------- + var list = Storage.shared.alarms.value + list.append(alarm) + Storage.shared.alarms.value = list + } } diff --git a/LoopFollow/Storage/UserDefaults.swift b/LoopFollow/Storage/UserDefaults.swift index 409bcdef7..5653ae444 100644 --- a/LoopFollow/Storage/UserDefaults.swift +++ b/LoopFollow/Storage/UserDefaults.swift @@ -97,26 +97,6 @@ class UserDefaultsRepository { static let alertCageInsertTime = UserDefaultsValue(key: "alertCageInsertTime", default: 0) static let alertSageInsertTime = UserDefaultsValue(key: "alertSageInsertTime", default: 0) - static let alertNotLoopingActive = UserDefaultsValue(key: "alertNotLoopingActive", default: false) - static let alertNotLooping = UserDefaultsValue(key: "alertNotLooping", default: 31) - static let alertNotLoopingSnooze = UserDefaultsValue(key: "alertNotLoopingSnooze", default: 30) - static let alertNotLoopingUseLimits = UserDefaultsValue(key: "alertNotLoopingUseLimits", default: false) - static let alertNotLoopingLowerLimit = UserDefaultsValue(key: "alertNotLoopingBelowBG", default: 100.0) - static let alertNotLoopingUpperLimit = UserDefaultsValue(key: "alertNotLoopingAboveBG", default: 160.0) - static let alertNotLoopingSnoozedTime = UserDefaultsValue(key: "alertNotLoopingSnoozedTime", default: nil) - static let alertNotLoopingIsSnoozed = UserDefaultsValue(key: "alertNotLoopingIsSnoozed", default: false) - static let alertNotLoopingRepeat = UserDefaultsValue(key: "alertNotLoopingRepeat", default: "Never") - static let alertNotLoopingDayTime = UserDefaultsValue(key: "alertNotLoopingDayTime", default: false) - static let alertNotLoopingNightTime = UserDefaultsValue(key: "alertNotLoopingNightTime", default: false) - static let alertNotLoopingSound = UserDefaultsValue(key: "alertNotLoopingSound", default: "Sci-Fi_Engine_Shut_Down") - static let alertLastLoopTime = UserDefaultsValue(key: "alertLastLoopTime", default: 0) - static let alertNotLoopingAudible = UserDefaultsValue(key: "alertNotLoopingAudible", default: "Always") - static let alertNotLoopingDayTimeAudible = UserDefaultsValue(key: "alertNotLoopingDayTimeAudible", default: true) - static let alertNotLoopingNightTimeAudible = UserDefaultsValue(key: "alertNotLoopingNightTimeAudible", default: true) - static let alertNotLoopingAutosnooze = UserDefaultsValue(key: "alertNotLoopingAutosnooze", default: "Never") - static let alertNotLoopingAutosnoozeDay = UserDefaultsValue(key: "alertNotLoopingAutosnoozeDay", default: false) - static let alertNotLoopingAutosnoozeNight = UserDefaultsValue(key: "alertNotLoopingAutosnoozeNight", default: false) - static let alertMissedBolusActive = UserDefaultsValue(key: "alertMissedBolusActive", default: false) static let alertMissedBolus = UserDefaultsValue(key: "alertMissedBolus", default: 10) static let alertMissedBolusSnooze = UserDefaultsValue(key: "alertMissedBolusSnooze", default: 10) diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index 3a2df4fb7..073dfcbe4 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -37,7 +37,7 @@ extension MainViewController { .prefix(12) .map { GlucoseValue(sgv: $0.sgv, date: Date(timeIntervalSince1970: $0.date)) }, /// These are oldest .. newest, Predictions not currently available for Trio expireDate: Storage.shared.expirationDate.value, - lastLoopTime: UserDefaultsRepository.alertLastLoopTime.value, + lastLoopTime: Observable.shared.alertLastLoopTime.value, latestOverrideStart: latestOverrideStart, latestOverrideEnd: latestOverrideEnd, latestTempTargetStart: latestTempTargetStart, diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index e84286c5c..1919e3f4a 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -125,11 +125,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele Storage.shared.migrate() - // Ensure alertNotLooping has a minimum value of 16. - if UserDefaultsRepository.alertNotLooping.value < 16 { - UserDefaultsRepository.alertNotLooping.value = 16 - } - // Synchronize info types to ensure arrays are the correct size UserDefaultsRepository.synchronizeInfoTypes() From 1f67ea25b7ae66a742ac171ee96ec7d2a5442c1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 6 Jun 2025 16:57:10 +0200 Subject: [PATCH 112/138] Missed bolus alert migration --- LoopFollow/Storage/Storage+Migrate.swift | 92 ++++++++++++++++++++++++ LoopFollow/Storage/UserDefaults.swift | 21 ------ 2 files changed, 92 insertions(+), 21 deletions(-) diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index d212fb670..b1b8ece99 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -291,6 +291,7 @@ extension Storage { migrateFastRiseAlarm() migrateMissedReadingAlarm() migrateNotLoopingAlarm() + migrateMissedBolusAlarm() } // MARK: - One-off alarm migrations @@ -766,4 +767,95 @@ extension Storage { list.append(alarm) Storage.shared.alarms.value = list } + + // MARK: – Missed-Bolus alarm ------------------------------------------------- + + private func migrateMissedBolusAlarm() { + // Was the old alarm ever configured? + let legacyActive = UserDefaultsValue(key: "alertMissedBolusActive", + default: false) + guard legacyActive.exists else { return } // nothing to do + + // helper: read-then-delete ------------------------------------------------ + func take(_ k: String, + _ def: V) -> V + { + let box = UserDefaultsValue(key: k, default: def) + defer { box.setNil(key: k) } // wipe after reading + return box.value + } + + // ──────────────────────────────────────────────────────────────────────── + // Build the new Alarm + // ──────────────────────────────────────────────────────────────────────── + var alarm = Alarm(type: .missedBolus) + alarm.name = "Missed Bolus Alert" + alarm.isEnabled = take("alertMissedBolusActive", false) + + // core timings + alarm.monitoringWindow = take("alertMissedBolus", 10) // delay + alarm.predictiveMinutes = take("alertMissedBolusPrebolus", 20) // pre-bolus window + alarm.snoozeDuration = take("alertMissedBolusSnooze", 10) + + // snooze-state + if take("alertMissedBolusIsSnoozed", false) { + alarm.snoozedUntil = take("alertMissedBolusSnoozedTime", + nil as Date?) + } + + // carb / bolus filters + alarm.delta = take("alertMissedBolusIgnoreBolus", 0.5) + alarm.threshold = Double( + take("alertMissedBolusLowGrams", 10)) + alarm.aboveBG = Double( + take("alertMissedBolusLowGramsBG", 70.0)) + + // sound & tone + alarm.soundFile = SoundFile(rawValue: + take("alertMissedBolusSound", + "Dhol_Shuffleloop")) ?? .dholShuffleloop + + // ── ACTIVE-DURING ← old “Pre-Snooze” flags + let actDay = take("alertMissedBolusAutosnoozeDay", false) + let actNight = take("alertMissedBolusAutosnoozeNight", false) + alarm.activeOption = { + switch (actDay, actNight) { + case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .always // “Never” → always + } + }() + + // ── PLAY-SOUND ← old “PlaySound” picker + let playDay = take("alertMissedBolusDayTimeAudible", true) + let playNight = take("alertMissedBolusNightTimeAudible", true) + alarm.playSoundOption = { + switch (playDay, playNight) { + case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .never + } + }() + + // ── REPEAT-SOUND ← old “Repeat Sound” picker + let repDay = take("alertMissedBolusDayTime", false) + let repNight = take("alertMissedBolusNightTime", false) + alarm.repeatSoundOption = { + switch (repDay, repNight) { + case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .never + } + }() + + // (The deprecated “alertMissedBolusQuiet” key is ignored.) + + // ── Store & finish ------------------------------------------------------ + var list = Storage.shared.alarms.value + list.append(alarm) + Storage.shared.alarms.value = list + } } diff --git a/LoopFollow/Storage/UserDefaults.swift b/LoopFollow/Storage/UserDefaults.swift index 5653ae444..7fd4728ae 100644 --- a/LoopFollow/Storage/UserDefaults.swift +++ b/LoopFollow/Storage/UserDefaults.swift @@ -97,27 +97,6 @@ class UserDefaultsRepository { static let alertCageInsertTime = UserDefaultsValue(key: "alertCageInsertTime", default: 0) static let alertSageInsertTime = UserDefaultsValue(key: "alertSageInsertTime", default: 0) - static let alertMissedBolusActive = UserDefaultsValue(key: "alertMissedBolusActive", default: false) - static let alertMissedBolus = UserDefaultsValue(key: "alertMissedBolus", default: 10) - static let alertMissedBolusSnooze = UserDefaultsValue(key: "alertMissedBolusSnooze", default: 10) - static let alertMissedBolusPrebolus = UserDefaultsValue(key: "alertMissedBolusPrebolus", default: 20) - static let alertMissedBolusIgnoreBolus = UserDefaultsValue(key: "alertMissedBolusIgnoreBolus", default: 0.5) - static let alertMissedBolusLowGrams = UserDefaultsValue(key: "alertMissedBolusLowGrams", default: 10) - static let alertMissedBolusLowGramsBG = UserDefaultsValue(key: "alertMissedBolusLowGramsBG", default: 70.0) - static let alertMissedBolusSnoozedTime = UserDefaultsValue(key: "alertMissedBolusSnoozedTime", default: nil) - static let alertMissedBolusIsSnoozed = UserDefaultsValue(key: "alertMissedBolusIsSnoozed", default: false) - static let alertMissedBolusQuiet = UserDefaultsValue(key: "alertMissedBolusQuiet", default: false) - static let alertMissedBolusRepeat = UserDefaultsValue(key: "alertMissedBolusRepeat", default: "Never") - static let alertMissedBolusDayTime = UserDefaultsValue(key: "alertMissedBolusDayTime", default: false) - static let alertMissedBolusNightTime = UserDefaultsValue(key: "alertMissedBolusNightTime", default: false) - static let alertMissedBolusSound = UserDefaultsValue(key: "alertMissedBolusSound", default: "Dhol_Shuffleloop") - static let alertMissedBolusAudible = UserDefaultsValue(key: "alertMissedBolusAudible", default: "Always") - static let alertMissedBolusDayTimeAudible = UserDefaultsValue(key: "alertMissedBolusDayTimeAudible", default: true) - static let alertMissedBolusNightTimeAudible = UserDefaultsValue(key: "alertMissedBolusNightTimeAudible", default: true) - static let alertMissedBolusAutosnooze = UserDefaultsValue(key: "alertMissedBolusAutosnooze", default: "Never") - static let alertMissedBolusAutosnoozeDay = UserDefaultsValue(key: "alertMissedBolusAutosnoozeDay", default: false) - static let alertMissedBolusAutosnoozeNight = UserDefaultsValue(key: "alertMissedBolusAutosnoozeNight", default: false) - static let alertSAGEActive = UserDefaultsValue(key: "alertSAGEActive", default: false) static let alertSAGE = UserDefaultsValue(key: "alertSAGE", default: 8) // Hours static let alertSAGEQuiet = UserDefaultsValue(key: "alertSAGEQuiet", default: false) From 0527e9caa4c57da5a0588061abad39582cad52f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 6 Jun 2025 17:03:28 +0200 Subject: [PATCH 113/138] CAGE & SAGE --- LoopFollow/Storage/Storage+Migrate.swift | 139 +++++++++++++++++++++++ LoopFollow/Storage/UserDefaults.swift | 34 ------ 2 files changed, 139 insertions(+), 34 deletions(-) diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index b1b8ece99..ce75123c7 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -292,6 +292,8 @@ extension Storage { migrateMissedReadingAlarm() migrateNotLoopingAlarm() migrateMissedBolusAlarm() + migrateSensorChangeAlarm() + migratePumpChangeAlarm() } // MARK: - One-off alarm migrations @@ -858,4 +860,141 @@ extension Storage { list.append(alarm) Storage.shared.alarms.value = list } + + // ───────────────────────────────────────────────────────────────────────────── + + // MARK: SAGE → .sensorChange + + // ───────────────────────────────────────────────────────────────────────────── + private func migrateSensorChangeAlarm() { + // Was the old setting ever stored? + let flag = UserDefaultsValue(key: "alertSAGEActive", default: false) + guard flag.exists else { return } + + // tiny helper that *reads + wipes* a legacy key + func take(_ k: String, _ def: V) -> V { + let b = UserDefaultsValue(key: k, default: def) + defer { b.setNil(key: k) } + return b.value + } + + var alarm = Alarm(type: .sensorChange) + alarm.name = "Sensor Change Reminder" + alarm.isEnabled = take("alertSAGEActive", false) + alarm.threshold = Double(take("alertSAGE", 8)) // hours + alarm.snoozeDuration = take("alertSAGESnooze", 2) // hours + if take("alertSAGEIsSnoozed", false) { + alarm.snoozedUntil = take("alertSAGESnoozedTime", nil as Date?) + } + alarm.soundFile = SoundFile(rawValue: + take("alertSAGESound", "Wake_Up_Will_You")) ?? .wakeUpWillYou + + // ACTIVE (day / night) + let actDay = take("alertSAGEAutosnoozeDay", false) + let actNight = take("alertSAGEAutosnoozeNight", true) + alarm.activeOption = { + switch (actDay, actNight) { + case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .always + } + }() + + // PLAY sound + let playDay = take("alertSAGEDayTimeAudible", true) + let playNight = take("alertSAGENightTimeAudible", true) + alarm.playSoundOption = { + switch (playDay, playNight) { + case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .never + } + }() + + // REPEAT sound + let repDay = take("alertSAGEDayTime", false) + let repNight = take("alertSAGENightTime", false) + alarm.repeatSoundOption = { + switch (repDay, repNight) { + case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .never + } + }() + + // Persist + var list = Storage.shared.alarms.value + list.append(alarm) + Storage.shared.alarms.value = list + } + + // ───────────────────────────────────────────────────────────────────────────── + + // MARK: CAGE → .pumpChange + + // ───────────────────────────────────────────────────────────────────────────── + private func migratePumpChangeAlarm() { + let flag = UserDefaultsValue(key: "alertCAGEActive", default: false) + guard flag.exists else { return } + + func take(_ k: String, _ def: V) -> V { + let b = UserDefaultsValue(key: k, default: def) + defer { b.setNil(key: k) } + return b.value + } + + var alarm = Alarm(type: .pumpChange) + alarm.name = "Pump / Cannula Change" + alarm.isEnabled = take("alertCAGEActive", false) + alarm.threshold = Double(take("alertCAGE", 4)) // hours + alarm.snoozeDuration = take("alertCAGESnooze", 2) // hours + if take("alertCAGEIsSnoozed", false) { + alarm.snoozedUntil = take("alertCAGESnoozedTime", nil as Date?) + } + alarm.soundFile = SoundFile(rawValue: + take("alertCAGESound", "Wake_Up_Will_You")) ?? .wakeUpWillYou + + // ACTIVE + let actDay = take("alertCAGEAutosnoozeDay", false) + let actNight = take("alertCAGEAutosnoozeNight", true) + alarm.activeOption = { + switch (actDay, actNight) { + case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .always + } + }() + + // PLAY + let playDay = take("alertCAGEDayTimeAudible", true) + let playNight = take("alertCAGENightTimeAudible", true) + alarm.playSoundOption = { + switch (playDay, playNight) { + case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .never + } + }() + + // REPEAT + let repDay = take("alertCAGEDayTime", false) + let repNight = take("alertCAGENightTime", false) + alarm.repeatSoundOption = { + switch (repDay, repNight) { + case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .never + } + }() + + var list = Storage.shared.alarms.value + list.append(alarm) + Storage.shared.alarms.value = list + } } diff --git a/LoopFollow/Storage/UserDefaults.swift b/LoopFollow/Storage/UserDefaults.swift index 7fd4728ae..b6baf5b8a 100644 --- a/LoopFollow/Storage/UserDefaults.swift +++ b/LoopFollow/Storage/UserDefaults.swift @@ -97,40 +97,6 @@ class UserDefaultsRepository { static let alertCageInsertTime = UserDefaultsValue(key: "alertCageInsertTime", default: 0) static let alertSageInsertTime = UserDefaultsValue(key: "alertSageInsertTime", default: 0) - static let alertSAGEActive = UserDefaultsValue(key: "alertSAGEActive", default: false) - static let alertSAGE = UserDefaultsValue(key: "alertSAGE", default: 8) // Hours - static let alertSAGEQuiet = UserDefaultsValue(key: "alertSAGEQuiet", default: false) - static let alertSAGERepeat = UserDefaultsValue(key: "alertSAGERepeat", default: "Never") - static let alertSAGEDayTime = UserDefaultsValue(key: "alertSAGEDayTime", default: false) - static let alertSAGENightTime = UserDefaultsValue(key: "alertSAGENightTime", default: false) - static let alertSAGEAudible = UserDefaultsValue(key: "alertSAGEAudible", default: "Always") - static let alertSAGEDayTimeAudible = UserDefaultsValue(key: "alertSAGEDayTimeAudible", default: true) - static let alertSAGENightTimeAudible = UserDefaultsValue(key: "alertSAGENightTimeAudible", default: true) - static let alertSAGESnooze = UserDefaultsValue(key: "alertSAGESnooze", default: 2) // Hours - static let alertSAGESnoozedTime = UserDefaultsValue(key: "alertSAGESnoozedTime", default: nil) - static let alertSAGEIsSnoozed = UserDefaultsValue(key: "alertSAGEIsSnoozed", default: false) - static let alertSAGESound = UserDefaultsValue(key: "alertSAGESound", default: "Wake_Up_Will_You") - static let alertSAGEAutosnooze = UserDefaultsValue(key: "alertSAGEAutosnooze", default: "At night") - static let alertSAGEAutosnoozeDay = UserDefaultsValue(key: "alertSAGEAutosnoozeDay", default: false) - static let alertSAGEAutosnoozeNight = UserDefaultsValue(key: "alertSAGEAutosnoozeNight", default: true) - - static let alertCAGEActive = UserDefaultsValue(key: "alertCAGEActive", default: false) - static let alertCAGE = UserDefaultsValue(key: "alertCAGE", default: 4) // Hours - static let alertCAGEQuiet = UserDefaultsValue(key: "alertCAGEQuiet", default: false) - static let alertCAGERepeat = UserDefaultsValue(key: "alertCAGERepeat", default: "Never") - static let alertCAGEDayTime = UserDefaultsValue(key: "alertCAGEDayTime", default: false) - static let alertCAGENightTime = UserDefaultsValue(key: "alertCAGENightTime", default: false) - static let alertCAGEAudible = UserDefaultsValue(key: "alertCAGEAudible", default: "Always") - static let alertCAGEDayTimeAudible = UserDefaultsValue(key: "alertCAGEDayTimeAudible", default: true) - static let alertCAGENightTimeAudible = UserDefaultsValue(key: "alertCAGENightTimeAudible", default: true) - static let alertCAGESnooze = UserDefaultsValue(key: "alertCAGESnooze", default: 2) // Hours - static let alertCAGESnoozedTime = UserDefaultsValue(key: "alertCAGESnoozedTime", default: nil) - static let alertCAGEIsSnoozed = UserDefaultsValue(key: "alertCAGEIsSnoozed", default: false) - static let alertCAGESound = UserDefaultsValue(key: "alertCAGESound", default: "Wake_Up_Will_You") - static let alertCAGEAutosnooze = UserDefaultsValue(key: "alertCAGEAutosnooze", default: "At night") - static let alertCAGEAutosnoozeDay = UserDefaultsValue(key: "alertCAGEAutosnoozeDay", default: false) - static let alertCAGEAutosnoozeNight = UserDefaultsValue(key: "alertCAGEAutosnoozeNight", default: true) - static let alertAppInactive = UserDefaultsValue(key: "alertAppInactive", default: false) static let alertTemporaryActive = UserDefaultsValue(key: "alertTemporaryActive", default: false) From 628200661bb432f245fd6660f8e8cf36ef24039a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 6 Jun 2025 17:13:52 +0200 Subject: [PATCH 114/138] Override alarm migrate --- LoopFollow/Storage/Storage+Migrate.swift | 150 +++++++++++++++++++++++ LoopFollow/Storage/UserDefaults.swift | 30 ----- 2 files changed, 150 insertions(+), 30 deletions(-) diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index ce75123c7..05e8fad27 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -294,6 +294,8 @@ extension Storage { migrateMissedBolusAlarm() migrateSensorChangeAlarm() migratePumpChangeAlarm() + migrateOverrideStartAlarm() + migrateOverrideEndAlarm() } // MARK: - One-off alarm migrations @@ -997,4 +999,152 @@ extension Storage { list.append(alarm) Storage.shared.alarms.value = list } + + // ───────────────────────────────────────────────────────────────────────────── + + // MARK: Override-Start → .overrideStart + + // ───────────────────────────────────────────────────────────────────────────── + private func migrateOverrideStartAlarm() { + let exists = UserDefaultsValue(key: "alertOverrideStart", default: false) + guard exists.exists else { return } // user never touched it + + func take(_ k: String, _ def: V) -> V { + let box = UserDefaultsValue(key: k, default: def) + defer { box.setNil(key: k) } // wipe after reading + return box.value + } + + var alarm = Alarm(type: .overrideStart) + alarm.name = "Override Started" + alarm.disableAfterFiring = true // fire once, then stay off + + alarm.isEnabled = take("alertOverrideStart", false) + alarm.snoozeDuration = 5 // legacy UI had no stepper + if take("alertOverrideStartIsSnoozed", false) { + alarm.snoozedUntil = take("alertOverrideStartSnoozedTime", nil as Date?) + } + + alarm.soundFile = SoundFile( + rawValue: take("alertOverrideStartSound", "Ending_Reached") + ) ?? .endingReached + + // ── ACTIVE (legacy “Pre-Snooze” day/night flags) ────────────── + let actDay = take("alertOverrideStartAutosnoozeDay", false) + let actNight = take("alertOverrideStartAutosnoozeNight", false) + alarm.activeOption = { + switch (actDay, actNight) { + case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .always + } + }() + + // ── PLAY (legacy “Play Sound” day/night flags) ─────────────── + let playDay = take("alertOverrideStartDayTimeAudible", true) + let playNight = take("alertOverrideStartNightTimeAudible", true) + alarm.playSoundOption = { + switch (playDay, playNight) { + case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .never + } + }() + + // ── REPEAT (legacy “Repeat Sound” day/night flags) ─────────── + let repDay = take("alertOverrideStartDayTime", false) + let repNight = take("alertOverrideStartNightTime", false) + alarm.repeatSoundOption = { + switch (repDay, repNight) { + case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .never + } + }() + + // ignore & wipe unused keys + _ = take("alertOverrideStartQuiet", false as Bool) + _ = take("alertOverrideStartRepeatAudible", "Always" as String) + + var list = Storage.shared.alarms.value + list.append(alarm) + Storage.shared.alarms.value = list + } + + // ───────────────────────────────────────────────────────────────────────────── + + // MARK: Override-End → .overrideEnd + + // ───────────────────────────────────────────────────────────────────────────── + private func migrateOverrideEndAlarm() { + let exists = UserDefaultsValue(key: "alertOverrideEnd", default: false) + guard exists.exists else { return } + + func take(_ k: String, _ def: V) -> V { + let box = UserDefaultsValue(key: k, default: def) + defer { box.setNil(key: k) } + return box.value + } + + var alarm = Alarm(type: .overrideEnd) + alarm.name = "Override Ended" + alarm.disableAfterFiring = true + + alarm.isEnabled = take("alertOverrideEnd", false) + alarm.snoozeDuration = 5 + if take("alertOverrideEndIsSnoozed", false) { + alarm.snoozedUntil = take("alertOverrideEndSnoozedTime", nil as Date?) + } + + alarm.soundFile = SoundFile( + rawValue: take("alertOverrideEndSound", "Alert_Tone_Busy") + ) ?? .alertToneBusy + + // ACTIVE + let actDay = take("alertOverrideEndAutosnoozeDay", false) + let actNight = take("alertOverrideEndAutosnoozeNight", false) + alarm.activeOption = { + switch (actDay, actNight) { + case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .always + } + }() + + // PLAY + let playDay = take("alertOverrideEndDayTimeAudible", true) + let playNight = take("alertOverrideEndNightTimeAudible", true) + alarm.playSoundOption = { + switch (playDay, playNight) { + case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .never + } + }() + + // REPEAT + let repDay = take("alertOverrideEndDayTime", false) + let repNight = take("alertOverrideEndNightTime", false) + alarm.repeatSoundOption = { + switch (repDay, repNight) { + case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .never + } + }() + + // wipe unused keys + _ = take("alertOverrideEndQuiet", false as Bool) + _ = take("alertOverrideEndRepeatAudible", "Always" as String) + + var list = Storage.shared.alarms.value + list.append(alarm) + Storage.shared.alarms.value = list + } } diff --git a/LoopFollow/Storage/UserDefaults.swift b/LoopFollow/Storage/UserDefaults.swift index b6baf5b8a..93fb4ee80 100644 --- a/LoopFollow/Storage/UserDefaults.swift +++ b/LoopFollow/Storage/UserDefaults.swift @@ -106,36 +106,6 @@ class UserDefaultsRepository { static let alertTemporaryBGAudible = UserDefaultsValue(key: "alertTemporaryBGRepeatAudible", default: true) static let alertTemporarySound = UserDefaultsValue(key: "alertTemporarySound", default: "Indeed") - static let alertOverrideStart = UserDefaultsValue(key: "alertOverrideStart", default: false) - static let alertOverrideStartQuiet = UserDefaultsValue(key: "alertOverrideStartQuiet", default: false) - static let alertOverrideStartRepeat = UserDefaultsValue(key: "alertOverrideStartRepeat", default: "Never") - static let alertOverrideStartDayTime = UserDefaultsValue(key: "alertOverrideStartDayTime", default: false) - static let alertOverrideStartNightTime = UserDefaultsValue(key: "alertOverrideStartNightTime", default: false) - static let alertOverrideStartAudible = UserDefaultsValue(key: "alertOverrideStartRepeatAudible", default: "Always") - static let alertOverrideStartDayTimeAudible = UserDefaultsValue(key: "alertOverrideStartDayTimeAudible", default: true) - static let alertOverrideStartNightTimeAudible = UserDefaultsValue(key: "alertOverrideStartNightTimeAudible", default: true) - static let alertOverrideStartSound = UserDefaultsValue(key: "alertOverrideStartSound", default: "Ending_Reached") - static let alertOverrideStartSnoozedTime = UserDefaultsValue(key: "alertOverrideStartSnoozedTime", default: nil) - static let alertOverrideStartIsSnoozed = UserDefaultsValue(key: "alertOverrideStartIsSnoozed", default: false) - static let alertOverrideStartAutosnooze = UserDefaultsValue(key: "alertOverrideStartAutosnooze", default: "Never") - static let alertOverrideStartAutosnoozeDay = UserDefaultsValue(key: "alertOverrideStartAutosnoozeDay", default: false) - static let alertOverrideStartAutosnoozeNight = UserDefaultsValue(key: "alertOverrideStartAutosnoozeNight", default: false) - - static let alertOverrideEnd = UserDefaultsValue(key: "alertOverrideEnd", default: false) - static let alertOverrideEndQuiet = UserDefaultsValue(key: "alertOverrideEndQuiet", default: false) - static let alertOverrideEndRepeat = UserDefaultsValue(key: "alertOverrideEndRepeat", default: "Never") - static let alertOverrideEndDayTime = UserDefaultsValue(key: "alertOverrideEndDayTime", default: false) - static let alertOverrideEndNightTime = UserDefaultsValue(key: "alertOverrideEndNightTime", default: false) - static let alertOverrideEndAudible = UserDefaultsValue(key: "alertOverrideEndRepeatAudible", default: "Always") - static let alertOverrideEndDayTimeAudible = UserDefaultsValue(key: "alertOverrideEndDayTimeAudible", default: true) - static let alertOverrideEndNightTimeAudible = UserDefaultsValue(key: "alertOverrideEndNightTimeAudible", default: true) - static let alertOverrideEndSound = UserDefaultsValue(key: "alertOverrideEndSound", default: "Alert_Tone_Busy") - static let alertOverrideEndSnoozedTime = UserDefaultsValue(key: "alertOverrideEndSnoozedTime", default: nil) - static let alertOverrideEndIsSnoozed = UserDefaultsValue(key: "alertOverrideEndIsSnoozed", default: false) - static let alertOverrideEndAutosnooze = UserDefaultsValue(key: "alertOverrideEndAutosnooze", default: "Never") - static let alertOverrideEndAutosnoozeDay = UserDefaultsValue(key: "alertOverrideEndAutosnoozeDay", default: false) - static let alertOverrideEndAutosnoozeNight = UserDefaultsValue(key: "alertOverrideEndAutosnoozeNight", default: false) - static let alertTempTargetStart = UserDefaultsValue(key: "alertTempTargetStart", default: false) static let alertTempTargetStartQuiet = UserDefaultsValue(key: "alertTempTargetStartQuiet", default: false) static let alertTempTargetStartRepeat = UserDefaultsValue(key: "alertTempTargetStartRepeat", default: "Never") From d165f130e32c2919d60adae641076fab05915797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 6 Jun 2025 17:19:11 +0200 Subject: [PATCH 115/138] Temp target alarm migration --- LoopFollow/Storage/Storage+Migrate.swift | 128 ++++++++++++++++++++++- LoopFollow/Storage/UserDefaults.swift | 30 ------ 2 files changed, 126 insertions(+), 32 deletions(-) diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index 05e8fad27..2ae79454a 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -296,6 +296,8 @@ extension Storage { migratePumpChangeAlarm() migrateOverrideStartAlarm() migrateOverrideEndAlarm() + migrateTempTargetStartAlarm() + migrateTempTargetEndAlarm() } // MARK: - One-off alarm migrations @@ -1017,7 +1019,6 @@ extension Storage { var alarm = Alarm(type: .overrideStart) alarm.name = "Override Started" - alarm.disableAfterFiring = true // fire once, then stay off alarm.isEnabled = take("alertOverrideStart", false) alarm.snoozeDuration = 5 // legacy UI had no stepper @@ -1091,7 +1092,6 @@ extension Storage { var alarm = Alarm(type: .overrideEnd) alarm.name = "Override Ended" - alarm.disableAfterFiring = true alarm.isEnabled = take("alertOverrideEnd", false) alarm.snoozeDuration = 5 @@ -1147,4 +1147,128 @@ extension Storage { list.append(alarm) Storage.shared.alarms.value = list } + + // MARK: ––––– Temp-Target START → .tempTargetStart ––––– + + private func migrateTempTargetStartAlarm() { + let touched = UserDefaultsValue(key: "alertTempTargetStart", default: false) + guard touched.exists else { return } + + func take(_ k: String, _ def: V) -> V { + let box = UserDefaultsValue(key: k, default: def) + defer { box.setNil(key: k) } // scrub after read + return box.value + } + + var alarm = Alarm(type: .tempTargetStart) + alarm.name = "Temp Target Started" + + alarm.isEnabled = take("alertTempTargetStart", false) + alarm.snoozeDuration = 5 + if take("alertTempTargetStartIsSnoozed", false) { + alarm.snoozedUntil = take("alertTempTargetStartSnoozedTime", nil as Date?) + } + + alarm.soundFile = SoundFile( + rawValue: take("alertTempTargetStartSound", "Ending_Reached") + ) ?? .endingReached + + // ACTIVE ← legacy Pre-Snooze day/night flags + alarm.activeOption = { + let d = take("alertTempTargetStartAutosnoozeDay", false) + let n = take("alertTempTargetStartAutosnoozeNight", false) + switch (d, n) { case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .always } + }() + + // PLAY + alarm.playSoundOption = { + let d = take("alertTempTargetStartDayTimeAudible", true) + let n = take("alertTempTargetStartNightTimeAudible", true) + switch (d, n) { case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .never } + }() + + // REPEAT + alarm.repeatSoundOption = { + let d = take("alertTempTargetStartDayTime", false) + let n = take("alertTempTargetStartNightTime", false) + switch (d, n) { case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .never } + }() + + // wipe “quiet / RepeatAudible” extras + _ = take("alertTempTargetStartQuiet", false as Bool) + _ = take("alertTempTargetStartRepeatAudible", "Always" as String) + + var list = Storage.shared.alarms.value + list.append(alarm) + Storage.shared.alarms.value = list + } + + // MARK: ––––– Temp-Target END → .tempTargetEnd ––––– + + private func migrateTempTargetEndAlarm() { + let touched = UserDefaultsValue(key: "alertTempTargetEnd", default: false) + guard touched.exists else { return } + + func take(_ k: String, _ def: V) -> V { + let box = UserDefaultsValue(key: k, default: def) + defer { box.setNil(key: k) } + return box.value + } + + var alarm = Alarm(type: .tempTargetEnd) + alarm.name = "Temp Target Ended" + + alarm.isEnabled = take("alertTempTargetEnd", false) + alarm.snoozeDuration = 5 + if take("alertTempTargetEndIsSnoozed", false) { + alarm.snoozedUntil = take("alertTempTargetEndSnoozedTime", nil as Date?) + } + + alarm.soundFile = SoundFile( + rawValue: take("alertTempTargetEndSound", "Alert_Tone_Busy") + ) ?? .alertToneBusy + + alarm.activeOption = { + let d = take("alertTempTargetEndAutosnoozeDay", false) + let n = take("alertTempTargetEndAutosnoozeNight", false) + switch (d, n) { case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .always } + }() + + alarm.playSoundOption = { + let d = take("alertTempTargetEndDayTimeAudible", true) + let n = take("alertTempTargetEndNightTimeAudible", true) + switch (d, n) { case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .never } + }() + + alarm.repeatSoundOption = { + let d = take("alertTempTargetEndDayTime", false) + let n = take("alertTempTargetEndNightTime", false) + switch (d, n) { case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .never } + }() + + _ = take("alertTempTargetEndQuiet", false as Bool) + _ = take("alertTempTargetEndRepeatAudible", "Always" as String) + + var list = Storage.shared.alarms.value + list.append(alarm) + Storage.shared.alarms.value = list + } } diff --git a/LoopFollow/Storage/UserDefaults.swift b/LoopFollow/Storage/UserDefaults.swift index 93fb4ee80..d7eeb6b52 100644 --- a/LoopFollow/Storage/UserDefaults.swift +++ b/LoopFollow/Storage/UserDefaults.swift @@ -106,36 +106,6 @@ class UserDefaultsRepository { static let alertTemporaryBGAudible = UserDefaultsValue(key: "alertTemporaryBGRepeatAudible", default: true) static let alertTemporarySound = UserDefaultsValue(key: "alertTemporarySound", default: "Indeed") - static let alertTempTargetStart = UserDefaultsValue(key: "alertTempTargetStart", default: false) - static let alertTempTargetStartQuiet = UserDefaultsValue(key: "alertTempTargetStartQuiet", default: false) - static let alertTempTargetStartRepeat = UserDefaultsValue(key: "alertTempTargetStartRepeat", default: "Never") - static let alertTempTargetStartDayTime = UserDefaultsValue(key: "alertTempTargetStartDayTime", default: false) - static let alertTempTargetStartNightTime = UserDefaultsValue(key: "alertTempTargetStartNightTime", default: false) - static let alertTempTargetStartAudible = UserDefaultsValue(key: "alertTempTargetStartRepeatAudible", default: "Always") - static let alertTempTargetStartDayTimeAudible = UserDefaultsValue(key: "alertTempTargetStartDayTimeAudible", default: true) - static let alertTempTargetStartNightTimeAudible = UserDefaultsValue(key: "alertTempTargetStartNightTimeAudible", default: true) - static let alertTempTargetStartSound = UserDefaultsValue(key: "alertTempTargetStartSound", default: "Ending_Reached") - static let alertTempTargetStartSnoozedTime = UserDefaultsValue(key: "alertTempTargetStartSnoozedTime", default: nil) - static let alertTempTargetStartIsSnoozed = UserDefaultsValue(key: "alertTempTargetStartIsSnoozed", default: false) - static let alertTempTargetStartAutosnooze = UserDefaultsValue(key: "alertTempTargetStartAutosnooze", default: "Never") - static let alertTempTargetStartAutosnoozeDay = UserDefaultsValue(key: "alertTempTargetStartAutosnoozeDay", default: false) - static let alertTempTargetStartAutosnoozeNight = UserDefaultsValue(key: "alertTempTargetStartAutosnoozeNight", default: false) - - static let alertTempTargetEnd = UserDefaultsValue(key: "alertTempTargetEnd", default: false) - static let alertTempTargetEndQuiet = UserDefaultsValue(key: "alertTempTargetEndQuiet", default: false) - static let alertTempTargetEndRepeat = UserDefaultsValue(key: "alertTempTargetEndRepeat", default: "Never") - static let alertTempTargetEndDayTime = UserDefaultsValue(key: "alertTempTargetEndDayTime", default: false) - static let alertTempTargetEndNightTime = UserDefaultsValue(key: "alertTempTargetEndNightTime", default: false) - static let alertTempTargetEndAudible = UserDefaultsValue(key: "alertTempTargetEndRepeatAudible", default: "Always") - static let alertTempTargetEndDayTimeAudible = UserDefaultsValue(key: "alertTempTargetEndDayTimeAudible", default: true) - static let alertTempTargetEndNightTimeAudible = UserDefaultsValue(key: "alertTempTargetEndNightTimeAudible", default: true) - static let alertTempTargetEndSound = UserDefaultsValue(key: "alertTempTargetEndSound", default: "Alert_Tone_Busy") - static let alertTempTargetEndSnoozedTime = UserDefaultsValue(key: "alertTempTargetEndSnoozedTime", default: nil) - static let alertTempTargetEndIsSnoozed = UserDefaultsValue(key: "alertTempTargetEndIsSnoozed", default: false) - static let alertTempTargetEndAutosnooze = UserDefaultsValue(key: "alertTempTargetEndAutosnooze", default: "Never") - static let alertTempTargetEndAutosnoozeDay = UserDefaultsValue(key: "alertTempTargetEndAutosnoozeDay", default: false) - static let alertTempTargetEndAutosnoozeNight = UserDefaultsValue(key: "alertTempTargetEndAutosnoozeNight", default: false) - static let alertPump = UserDefaultsValue(key: "alertPump", default: false) static let alertPumpAt = UserDefaultsValue(key: "alertPumpAt", default: 10) // Units static let alertPumpQuiet = UserDefaultsValue(key: "alertPumpQuiet", default: false) From 21505ba5388fb617f69e7ba29f9c2fc94ca9be9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 6 Jun 2025 17:25:43 +0200 Subject: [PATCH 116/138] Migration --- LoopFollow/Alarm/Alarm.swift | 3 - .../AlarmCondition/PumpChangeCondition.swift | 1 - LoopFollow/Application/AppDelegate.swift | 4 - LoopFollow/Storage/Storage+Migrate.swift | 133 ++++++++++++++++-- LoopFollow/Storage/UserDefaults.swift | 2 - 5 files changed, 120 insertions(+), 23 deletions(-) diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index 0daf9ee9b..71b83c4b1 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -94,9 +94,6 @@ struct Alarm: Identifiable, Codable, Equatable { /// When is the alarm active var activeOption: ActiveOption = .always - /// For temporary alerts, it will trigger once and then disable itself - var disableAfterFiring: Bool = false - // ───────────────────────────────────────────────────────────── // Missed‑Bolus‑specific settings // ───────────────────────────────────────────────────────────── diff --git a/LoopFollow/Alarm/AlarmCondition/PumpChangeCondition.swift b/LoopFollow/Alarm/AlarmCondition/PumpChangeCondition.swift index cf74101ab..2858a984c 100644 --- a/LoopFollow/Alarm/AlarmCondition/PumpChangeCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/PumpChangeCondition.swift @@ -32,7 +32,6 @@ struct PumpChangeCondition: AlarmCondition { let expiry = insertedAt.addingTimeInterval(lifetime) let trigger = expiry.addingTimeInterval(-warnAheadHrs * 3600) - // 2. nothing else to track – disableAfterFiring=true handles repeats return Date() >= trigger } } diff --git a/LoopFollow/Application/AppDelegate.swift b/LoopFollow/Application/AppDelegate.swift index a1f02a910..eab24524f 100644 --- a/LoopFollow/Application/AppDelegate.swift +++ b/LoopFollow/Application/AppDelegate.swift @@ -44,10 +44,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } func applicationWillTerminate(_: UIApplication) { - if UserDefaultsRepository.alertAppInactive.value { - AlarmSound.setSoundFile(str: "Alarm_Buzzer") - AlarmSound.playTerminated() - } } // MARK: UISceneSession Lifecycle diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index 2ae79454a..300a1a33b 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -298,6 +298,8 @@ extension Storage { migrateOverrideEndAlarm() migrateTempTargetStartAlarm() migrateTempTargetEndAlarm() + migrateTemporaryBGAlarm() + migratePumpVolumeAlarm() } // MARK: - One-off alarm migrations @@ -326,7 +328,7 @@ extension Storage { alarm.soundFile = SoundFile( rawValue: take("alertUrgentLowSound", default: "Emergency_Alarm_Siren")) - ?? .emergencyAlarmSiren + ?? .emergencyAlarmSiren alarm.playSoundOption = PlaySoundOption( rawValue: take("alertUrgentLowAudible", @@ -720,8 +722,8 @@ extension Storage { alarm.snoozeDuration = take("alertNotLoopingSnooze", default: 30) alarm.snoozedUntil = take("alertNotLoopingSnoozedTime", default: nil as Date?) alarm.soundFile = SoundFile(rawValue: - take("alertNotLoopingSound", - default: "Sci-Fi_Engine_Shut_Down")) ?? .sciFiEngineShutDown + take("alertNotLoopingSound", + default: "Sci-Fi_Engine_Shut_Down")) ?? .sciFiEngineShutDown // ── ACTIVE-DURING (day/night) ← old **Pre-Snooze** flags -------------- let actDay = take("alertNotLoopingAutosnoozeDay", default: false) @@ -818,8 +820,8 @@ extension Storage { // sound & tone alarm.soundFile = SoundFile(rawValue: - take("alertMissedBolusSound", - "Dhol_Shuffleloop")) ?? .dholShuffleloop + take("alertMissedBolusSound", + "Dhol_Shuffleloop")) ?? .dholShuffleloop // ── ACTIVE-DURING ← old “Pre-Snooze” flags let actDay = take("alertMissedBolusAutosnoozeDay", false) @@ -891,7 +893,7 @@ extension Storage { alarm.snoozedUntil = take("alertSAGESnoozedTime", nil as Date?) } alarm.soundFile = SoundFile(rawValue: - take("alertSAGESound", "Wake_Up_Will_You")) ?? .wakeUpWillYou + take("alertSAGESound", "Wake_Up_Will_You")) ?? .wakeUpWillYou // ACTIVE (day / night) let actDay = take("alertSAGEAutosnoozeDay", false) @@ -959,7 +961,7 @@ extension Storage { alarm.snoozedUntil = take("alertCAGESnoozedTime", nil as Date?) } alarm.soundFile = SoundFile(rawValue: - take("alertCAGESound", "Wake_Up_Will_You")) ?? .wakeUpWillYou + take("alertCAGESound", "Wake_Up_Will_You")) ?? .wakeUpWillYou // ACTIVE let actDay = take("alertCAGEAutosnoozeDay", false) @@ -1180,7 +1182,7 @@ extension Storage { switch (d, n) { case (true, true): return .always case (true, false): return .day case (false, true): return .night - default: return .always } + default: return .always } }() // PLAY @@ -1190,7 +1192,7 @@ extension Storage { switch (d, n) { case (true, true): return .always case (true, false): return .day case (false, true): return .night - default: return .never } + default: return .never } }() // REPEAT @@ -1200,7 +1202,7 @@ extension Storage { switch (d, n) { case (true, true): return .always case (true, false): return .day case (false, true): return .night - default: return .never } + default: return .never } }() // wipe “quiet / RepeatAudible” extras @@ -1243,7 +1245,7 @@ extension Storage { switch (d, n) { case (true, true): return .always case (true, false): return .day case (false, true): return .night - default: return .always } + default: return .always } }() alarm.playSoundOption = { @@ -1252,7 +1254,7 @@ extension Storage { switch (d, n) { case (true, true): return .always case (true, false): return .day case (false, true): return .night - default: return .never } + default: return .never } }() alarm.repeatSoundOption = { @@ -1261,7 +1263,7 @@ extension Storage { switch (d, n) { case (true, true): return .always case (true, false): return .day case (false, true): return .night - default: return .never } + default: return .never } }() _ = take("alertTempTargetEndQuiet", false as Bool) @@ -1271,4 +1273,109 @@ extension Storage { list.append(alarm) Storage.shared.alarms.value = list } + + // MARK: ––––– TEMPORARY BG LIMIT → .temporary ––––– + private func migrateTemporaryBGAlarm() { + let flag = UserDefaultsValue(key: "alertTemporaryActive", default: false) + guard flag.exists else { return } + + func take(_ k: String, _ d: V) -> V { + let box = UserDefaultsValue(key: k, default: d) + defer { box.setNil(key: k) } + return box.value + } + + var alarm = Alarm(type: .temporary) + alarm.name = "Temporary BG Limit" + alarm.isEnabled = take("alertTemporaryActive", false) + + // limit direction ↓ / ↑ + let limit = Double(take("alertTemporaryBG", 90.0 as Float)) + if take("alertTemporaryBelow", true) { + alarm.belowBG = limit + } else { + alarm.aboveBG = limit + } + + // audio & repeat + alarm.soundFile = SoundFile(rawValue: + take("alertTemporarySound", "Indeed")) ?? .indeed + + alarm.playSoundOption = take("alertTemporaryBGAudible", true) ? .always : .never + alarm.repeatSoundOption = take("alertTemporaryBGRepeat", false) ? .always : .never + + Storage.shared.alarms.value.append(alarm) + } + + // MARK: ––––– PUMP RESERVOIR LEVEL → .pump ––––– + private func migratePumpVolumeAlarm() { + let flag = UserDefaultsValue(key: "alertPump", default: false) + guard flag.exists else { return } + + func take(_ k: String, _ d: V) -> V { + let box = UserDefaultsValue(key: k, default: d) + defer { box.setNil(key: k) } + return box.value + } + + var alarm = Alarm(type: .pump) + alarm.name = "Pump Reservoir" + alarm.isEnabled = take("alertPump", false) + alarm.threshold = Double(take("alertPumpAt", 10)) // units left + + // Snooze — stored in hours, so keep as-is. + alarm.snoozeDuration = take("alertPumpSnoozeHours", 5) + + if take("alertPumpIsSnoozed", false) { + alarm.snoozedUntil = take("alertPumpSnoozedTime", nil as Date?) + } + + alarm.soundFile = SoundFile(rawValue: + take("alertPumpSound", "Marimba_Descend")) ?? .marimbaDescend + + // PLAY-sound option (day / night / never) + alarm.playSoundOption = { + let d = take("alertPumpDayTimeAudible", true) + let n = take("alertPumpNightTimeAudible",true) + switch (d,n) { + case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .never + } + }() + + // REPEAT-sound option – derived from legacy picker flags + alarm.repeatSoundOption = { + let d = take("alertPumpDayTime", false) + let n = take("alertPumpNightTime",false) + switch (d,n) { + case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .never + } + }() + + // ACTIVE day/night ← old “Pre-Snooze” flags + alarm.activeOption = { + let d = take("alertPumpAutosnoozeDay", false) + let n = take("alertPumpAutosnoozeNight", false) + switch (d,n) { + case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .always + } + }() + + // Discard no-longer-needed extras + _ = take("alertPumpQuiet", false as Bool) + _ = take("alertPumpRepeat", "Never" as String) + _ = take("alertPumpAudible", "Always" as String) + _ = take("alertPumpAutosnooze", "Never" as String) + + Storage.shared.alarms.value.append(alarm) + } + } diff --git a/LoopFollow/Storage/UserDefaults.swift b/LoopFollow/Storage/UserDefaults.swift index d7eeb6b52..60b9bef37 100644 --- a/LoopFollow/Storage/UserDefaults.swift +++ b/LoopFollow/Storage/UserDefaults.swift @@ -97,8 +97,6 @@ class UserDefaultsRepository { static let alertCageInsertTime = UserDefaultsValue(key: "alertCageInsertTime", default: 0) static let alertSageInsertTime = UserDefaultsValue(key: "alertSageInsertTime", default: 0) - static let alertAppInactive = UserDefaultsValue(key: "alertAppInactive", default: false) - static let alertTemporaryActive = UserDefaultsValue(key: "alertTemporaryActive", default: false) static let alertTemporaryBelow = UserDefaultsValue(key: "alertTemporaryBelow", default: true) static let alertTemporaryBG = UserDefaultsValue(key: "alertTemporaryBG", default: 90.0) From f4f1472d2279f230d0ce34cc48442879b61f9ae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 6 Jun 2025 17:34:47 +0200 Subject: [PATCH 117/138] IOB/COB Migration --- LoopFollow/Application/AppDelegate.swift | 3 +- LoopFollow/Storage/Storage+Migrate.swift | 235 ++++++++++++++++++----- LoopFollow/Storage/UserDefaults.swift | 61 ------ 3 files changed, 191 insertions(+), 108 deletions(-) diff --git a/LoopFollow/Application/AppDelegate.swift b/LoopFollow/Application/AppDelegate.swift index eab24524f..832111e36 100644 --- a/LoopFollow/Application/AppDelegate.swift +++ b/LoopFollow/Application/AppDelegate.swift @@ -43,8 +43,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } - func applicationWillTerminate(_: UIApplication) { - } + func applicationWillTerminate(_: UIApplication) {} // MARK: UISceneSession Lifecycle diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index 300a1a33b..4232fbfaf 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -300,6 +300,8 @@ extension Storage { migrateTempTargetEndAlarm() migrateTemporaryBGAlarm() migratePumpVolumeAlarm() + migrateIOBAlarm() + migrateCOBAlarm() } // MARK: - One-off alarm migrations @@ -328,7 +330,7 @@ extension Storage { alarm.soundFile = SoundFile( rawValue: take("alertUrgentLowSound", default: "Emergency_Alarm_Siren")) - ?? .emergencyAlarmSiren + ?? .emergencyAlarmSiren alarm.playSoundOption = PlaySoundOption( rawValue: take("alertUrgentLowAudible", @@ -722,8 +724,8 @@ extension Storage { alarm.snoozeDuration = take("alertNotLoopingSnooze", default: 30) alarm.snoozedUntil = take("alertNotLoopingSnoozedTime", default: nil as Date?) alarm.soundFile = SoundFile(rawValue: - take("alertNotLoopingSound", - default: "Sci-Fi_Engine_Shut_Down")) ?? .sciFiEngineShutDown + take("alertNotLoopingSound", + default: "Sci-Fi_Engine_Shut_Down")) ?? .sciFiEngineShutDown // ── ACTIVE-DURING (day/night) ← old **Pre-Snooze** flags -------------- let actDay = take("alertNotLoopingAutosnoozeDay", default: false) @@ -820,8 +822,8 @@ extension Storage { // sound & tone alarm.soundFile = SoundFile(rawValue: - take("alertMissedBolusSound", - "Dhol_Shuffleloop")) ?? .dholShuffleloop + take("alertMissedBolusSound", + "Dhol_Shuffleloop")) ?? .dholShuffleloop // ── ACTIVE-DURING ← old “Pre-Snooze” flags let actDay = take("alertMissedBolusAutosnoozeDay", false) @@ -893,7 +895,7 @@ extension Storage { alarm.snoozedUntil = take("alertSAGESnoozedTime", nil as Date?) } alarm.soundFile = SoundFile(rawValue: - take("alertSAGESound", "Wake_Up_Will_You")) ?? .wakeUpWillYou + take("alertSAGESound", "Wake_Up_Will_You")) ?? .wakeUpWillYou // ACTIVE (day / night) let actDay = take("alertSAGEAutosnoozeDay", false) @@ -961,7 +963,7 @@ extension Storage { alarm.snoozedUntil = take("alertCAGESnoozedTime", nil as Date?) } alarm.soundFile = SoundFile(rawValue: - take("alertCAGESound", "Wake_Up_Will_You")) ?? .wakeUpWillYou + take("alertCAGESound", "Wake_Up_Will_You")) ?? .wakeUpWillYou // ACTIVE let actDay = take("alertCAGEAutosnoozeDay", false) @@ -1182,7 +1184,7 @@ extension Storage { switch (d, n) { case (true, true): return .always case (true, false): return .day case (false, true): return .night - default: return .always } + default: return .always } }() // PLAY @@ -1192,7 +1194,7 @@ extension Storage { switch (d, n) { case (true, true): return .always case (true, false): return .day case (false, true): return .night - default: return .never } + default: return .never } }() // REPEAT @@ -1202,7 +1204,7 @@ extension Storage { switch (d, n) { case (true, true): return .always case (true, false): return .day case (false, true): return .night - default: return .never } + default: return .never } }() // wipe “quiet / RepeatAudible” extras @@ -1245,7 +1247,7 @@ extension Storage { switch (d, n) { case (true, true): return .always case (true, false): return .day case (false, true): return .night - default: return .always } + default: return .always } }() alarm.playSoundOption = { @@ -1254,7 +1256,7 @@ extension Storage { switch (d, n) { case (true, true): return .always case (true, false): return .day case (false, true): return .night - default: return .never } + default: return .never } }() alarm.repeatSoundOption = { @@ -1263,7 +1265,7 @@ extension Storage { switch (d, n) { case (true, true): return .always case (true, false): return .day case (false, true): return .night - default: return .never } + default: return .never } }() _ = take("alertTempTargetEndQuiet", false as Bool) @@ -1275,6 +1277,7 @@ extension Storage { } // MARK: ––––– TEMPORARY BG LIMIT → .temporary ––––– + private func migrateTemporaryBGAlarm() { let flag = UserDefaultsValue(key: "alertTemporaryActive", default: false) guard flag.exists else { return } @@ -1286,8 +1289,8 @@ extension Storage { } var alarm = Alarm(type: .temporary) - alarm.name = "Temporary BG Limit" - alarm.isEnabled = take("alertTemporaryActive", false) + alarm.name = "Temporary BG Limit" + alarm.isEnabled = take("alertTemporaryActive", false) // limit direction ↓ / ↑ let limit = Double(take("alertTemporaryBG", 90.0 as Float)) @@ -1299,15 +1302,16 @@ extension Storage { // audio & repeat alarm.soundFile = SoundFile(rawValue: - take("alertTemporarySound", "Indeed")) ?? .indeed + take("alertTemporarySound", "Indeed")) ?? .indeed - alarm.playSoundOption = take("alertTemporaryBGAudible", true) ? .always : .never + alarm.playSoundOption = take("alertTemporaryBGAudible", true) ? .always : .never alarm.repeatSoundOption = take("alertTemporaryBGRepeat", false) ? .always : .never Storage.shared.alarms.value.append(alarm) } // MARK: ––––– PUMP RESERVOIR LEVEL → .pump ––––– + private func migratePumpVolumeAlarm() { let flag = UserDefaultsValue(key: "alertPump", default: false) guard flag.exists else { return } @@ -1319,9 +1323,9 @@ extension Storage { } var alarm = Alarm(type: .pump) - alarm.name = "Pump Reservoir" - alarm.isEnabled = take("alertPump", false) - alarm.threshold = Double(take("alertPumpAt", 10)) // units left + alarm.name = "Pump Reservoir" + alarm.isEnabled = take("alertPump", false) + alarm.threshold = Double(take("alertPumpAt", 10)) // units left // Snooze — stored in hours, so keep as-is. alarm.snoozeDuration = take("alertPumpSnoozeHours", 5) @@ -1331,51 +1335,192 @@ extension Storage { } alarm.soundFile = SoundFile(rawValue: - take("alertPumpSound", "Marimba_Descend")) ?? .marimbaDescend + take("alertPumpSound", "Marimba_Descend")) ?? .marimbaDescend // PLAY-sound option (day / night / never) alarm.playSoundOption = { - let d = take("alertPumpDayTimeAudible", true) - let n = take("alertPumpNightTimeAudible",true) - switch (d,n) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never + let d = take("alertPumpDayTimeAudible", true) + let n = take("alertPumpNightTimeAudible", true) + switch (d, n) { + case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .never } }() // REPEAT-sound option – derived from legacy picker flags alarm.repeatSoundOption = { - let d = take("alertPumpDayTime", false) - let n = take("alertPumpNightTime",false) - switch (d,n) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .never + let d = take("alertPumpDayTime", false) + let n = take("alertPumpNightTime", false) + switch (d, n) { + case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .never } }() // ACTIVE day/night ← old “Pre-Snooze” flags alarm.activeOption = { - let d = take("alertPumpAutosnoozeDay", false) + let d = take("alertPumpAutosnoozeDay", false) let n = take("alertPumpAutosnoozeNight", false) - switch (d,n) { - case (true, true): return .always - case (true, false): return .day - case (false, true): return .night - default: return .always + switch (d, n) { + case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .always } }() // Discard no-longer-needed extras - _ = take("alertPumpQuiet", false as Bool) - _ = take("alertPumpRepeat", "Never" as String) - _ = take("alertPumpAudible", "Always" as String) - _ = take("alertPumpAutosnooze", "Never" as String) + _ = take("alertPumpQuiet", false as Bool) + _ = take("alertPumpRepeat", "Never" as String) + _ = take("alertPumpAudible", "Always" as String) + _ = take("alertPumpAutosnooze", "Never" as String) + + Storage.shared.alarms.value.append(alarm) + } + + // ----------------------------------------------------------------------------- + + // MARK: - Helpers (place them once, near the top of the migrate() file) + + // ----------------------------------------------------------------------------- + + /// Legacy picker + day/night flags → PlaySoundOption + private func playOption(from picker: String, + dayFlag: Bool, + nightFlag: Bool) -> PlaySoundOption + { + let p = picker.lowercased() + if p == "never" { return .never } + if p == "always" { return .always } + + switch (dayFlag, nightFlag) { + case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .never + } + } + + /// Legacy picker + day/night flags → RepeatSoundOption + private func repeatOption(from picker: String, + dayFlag: Bool, + nightFlag: Bool) -> RepeatSoundOption + { + let p = picker.lowercased() + if p == "never" { return .never } + if p == "always" { return .always } + + switch (dayFlag, nightFlag) { + case (true, true): return .always + case (true, false): return .day + case (false, true): return .night + default: return .never + } + } + + /// Convenience: load-then-erase a legacy `UserDefaultsValue` + private func take( + _ key: String, + _ defaultValue: V + ) -> V { + let box = UserDefaultsValue(key: key, default: defaultValue) + defer { box.setNil(key: key) } + return box.value + } + + // ----------------------------------------------------------------------------- + + // MARK: - IOB Alarm migration + + // ----------------------------------------------------------------------------- + private func migrateIOBAlarm() { + // Only migrate if the user ever touched IOB alarm settings + guard UserDefaultsValue(key: "alertIOB", default: false).exists else { + return + } + + var alarm = Alarm(type: .iob) + alarm.name = "High IOB" + alarm.isEnabled = take("alertIOB", false) + alarm.aboveBG = nil // BG band not used + alarm.threshold = Double(take("alertIOBAt", 1.5)) // units threshold + alarm.bolusCountThreshold = take("alertIOBNumber", 3) + alarm.bolusWindowMinutes = take("alertIOBBolusesWithin", 60) + alarm.delta = Double(take("alertIOBMaxBoluses", 10)) // max total bolus + alarm.snoozeDuration = take("alertIOBSnoozeHours", 1) * 60 // hours → minutes + alarm.snoozedUntil = take("alertIOBSnoozedTime", nil as Date?) + alarm.soundFile = SoundFile(rawValue: + take("alertIOBSound", "Alert_Tone_Ringtone_1")) + ?? .alertToneRingtone1 + + // Audio options + alarm.playSoundOption = playOption( + from: take("alertIOBAudible", "Always"), + dayFlag: take("alertIOBDayTimeAudible", true), + nightFlag: take("alertIOBNightTimeAudible", true) + ) + alarm.repeatSoundOption = repeatOption( + from: take("alertIOBRepeat", "Always"), + dayFlag: take("alertIOBDayTime", true), + nightFlag: take("alertIOBNightTime", true) + ) + + // Active (day/night) option comes from the legacy “Pre-Snooze” picker, + // but the IOB alarm never had one, so treat it as always-on. + alarm.activeOption = .always + + // Persist Storage.shared.alarms.value.append(alarm) } + // ----------------------------------------------------------------------------- + + // MARK: - COB Alarm migration + + // ----------------------------------------------------------------------------- + + private func migrateCOBAlarm() { + // Only migrate if the user ever touched COB alarm settings + guard UserDefaultsValue(key: "alertCOB", default: false).exists else { + return + } + + var alarm = Alarm(type: .cob) + alarm.name = "High COB" + alarm.isEnabled = take("alertCOB", false) + alarm.threshold = Double(take("alertCOBAt", 50)) // grams threshold + alarm.snoozeDuration = take("alertCOBSnoozeHours", 1) * 60 // hours → minutes + alarm.snoozedUntil = take("alertCOBSnoozedTime", nil as Date?) + alarm.soundFile = SoundFile(rawValue: + take("alertCOBSound", "Alert_Tone_Ringtone_2")) + ?? .alertToneRingtone2 + + // Audio options + alarm.playSoundOption = playOption( + from: take("alertCOBAudible", "Always"), + dayFlag: take("alertCOBDayTimeAudible", true), + nightFlag: take("alertCOBNightTimeAudible", true) + ) + alarm.repeatSoundOption = repeatOption( + from: take("alertCOBRepeat", "Always"), + dayFlag: take("alertCOBDayTime", true), + nightFlag: take("alertCOBNightTime", true) + ) + + alarm.activeOption = .always // same reason as above + + // Persist + Storage.shared.alarms.value.append(alarm) + } + + // ----------------------------------------------------------------------------- + + // MARK: - Inject into main migrate() entry point + + // ----------------------------------------------------------------------------- } diff --git a/LoopFollow/Storage/UserDefaults.swift b/LoopFollow/Storage/UserDefaults.swift index 60b9bef37..28f403196 100644 --- a/LoopFollow/Storage/UserDefaults.swift +++ b/LoopFollow/Storage/UserDefaults.swift @@ -97,67 +97,6 @@ class UserDefaultsRepository { static let alertCageInsertTime = UserDefaultsValue(key: "alertCageInsertTime", default: 0) static let alertSageInsertTime = UserDefaultsValue(key: "alertSageInsertTime", default: 0) - static let alertTemporaryActive = UserDefaultsValue(key: "alertTemporaryActive", default: false) - static let alertTemporaryBelow = UserDefaultsValue(key: "alertTemporaryBelow", default: true) - static let alertTemporaryBG = UserDefaultsValue(key: "alertTemporaryBG", default: 90.0) - static let alertTemporaryBGRepeat = UserDefaultsValue(key: "alertTemporaryBGRepeat", default: false) - static let alertTemporaryBGAudible = UserDefaultsValue(key: "alertTemporaryBGRepeatAudible", default: true) - static let alertTemporarySound = UserDefaultsValue(key: "alertTemporarySound", default: "Indeed") - - static let alertPump = UserDefaultsValue(key: "alertPump", default: false) - static let alertPumpAt = UserDefaultsValue(key: "alertPumpAt", default: 10) // Units - static let alertPumpQuiet = UserDefaultsValue(key: "alertPumpQuiet", default: false) - static let alertPumpRepeat = UserDefaultsValue(key: "alertPumpRepeat", default: "Never") - static let alertPumpDayTime = UserDefaultsValue(key: "alertPumpDayTime", default: false) - static let alertPumpNightTime = UserDefaultsValue(key: "alertPumpNightTime", default: false) - static let alertPumpAudible = UserDefaultsValue(key: "alertPumpAudible", default: "Always") - static let alertPumpDayTimeAudible = UserDefaultsValue(key: "alertPumpDayTimeAudible", default: true) - static let alertPumpNightTimeAudible = UserDefaultsValue(key: "alertPumpNightTimeAudible", default: true) - static let alertPumpSound = UserDefaultsValue(key: "alertPumpSound", default: "Marimba_Descend") - static let alertPumpSnoozeHours = UserDefaultsValue(key: "alertPumpSnoozeHours", default: 5) // Hours - static let alertPumpIsSnoozed = UserDefaultsValue(key: "alertPumpIsSnoozed", default: false) - static let alertPumpSnoozedTime = UserDefaultsValue(key: "alertPumpSnoozedTime", default: nil) - static let alertPumpAutosnooze = UserDefaultsValue(key: "alertPumpAutosnooze", default: "Never") - static let alertPumpAutosnoozeDay = UserDefaultsValue(key: "alertPumpAutosnoozeDay", default: false) - static let alertPumpAutosnoozeNight = UserDefaultsValue(key: "alertPumpAutosnoozeNight", default: false) - - static let alertIOB = UserDefaultsValue(key: "alertIOB", default: false) - static let alertIOBAt = UserDefaultsValue(key: "alertIOBAt", default: 1.5) // Units - static let alertIOBNumber = UserDefaultsValue(key: "alertIOBNumber", default: 3) // Number - static let alertIOBBolusesWithin = UserDefaultsValue(key: "alertIOBBolusesWithin", default: 60) // Minutes - static let alertIOBMaxBoluses = UserDefaultsValue(key: "alertIOBMaxBoluses", default: 10) // Units - static let alertIOBQuiet = UserDefaultsValue(key: "alertIOBQuiet", default: false) - static let alertIOBRepeat = UserDefaultsValue(key: "alertIOBRepeat", default: "Always") - static let alertIOBDayTime = UserDefaultsValue(key: "alertIOBDayTime", default: true) - static let alertIOBNightTime = UserDefaultsValue(key: "alertIOBNightTime", default: true) - static let alertIOBAudible = UserDefaultsValue(key: "alertIOBAudible", default: "Always") - static let alertIOBDayTimeAudible = UserDefaultsValue(key: "alertIOBDayTimeAudible", default: true) - static let alertIOBNightTimeAudible = UserDefaultsValue(key: "alertIOBNightTimeAudible", default: true) - static let alertIOBSound = UserDefaultsValue(key: "alertIOBSound", default: "Alert_Tone_Ringtone_1") - static let alertIOBSnoozeHours = UserDefaultsValue(key: "alertIOBSnoozeHours", default: 1) // Hours - static let alertIOBIsSnoozed = UserDefaultsValue(key: "alertIOBIsSnoozed", default: false) - static let alertIOBSnoozedTime = UserDefaultsValue(key: "alertIOBSnoozedTime", default: nil) - static let alertIOBAutosnooze = UserDefaultsValue(key: "alertIOBAutosnooze", default: "Never") - static let alertIOBAutosnoozeDay = UserDefaultsValue(key: "alertIOBAutosnoozeDay", default: false) - static let alertIOBAutosnoozeNight = UserDefaultsValue(key: "alertIOBAutosnoozeNight", default: false) - - static let alertCOB = UserDefaultsValue(key: "alertCOB", default: false) - static let alertCOBAt = UserDefaultsValue(key: "alertCOBAt", default: 50) // Units - static let alertCOBQuiet = UserDefaultsValue(key: "alertCOBQuiet", default: false) - static let alertCOBRepeat = UserDefaultsValue(key: "alertCOBRepeat", default: "Always") - static let alertCOBDayTime = UserDefaultsValue(key: "alertCOBDayTime", default: true) - static let alertCOBNightTime = UserDefaultsValue(key: "alertCOBNightTime", default: true) - static let alertCOBAudible = UserDefaultsValue(key: "alertCOBAudible", default: "Always") - static let alertCOBDayTimeAudible = UserDefaultsValue(key: "alertCOBDayTimeAudible", default: true) - static let alertCOBNightTimeAudible = UserDefaultsValue(key: "alertCOBNightTimeAudible", default: true) - static let alertCOBSound = UserDefaultsValue(key: "alertCOBSound", default: "Alert_Tone_Ringtone_2") - static let alertCOBSnoozeHours = UserDefaultsValue(key: "alertCOBSnoozeHours", default: 1) // Hours - static let alertCOBIsSnoozed = UserDefaultsValue(key: "alertCOBIsSnoozed", default: false) - static let alertCOBSnoozedTime = UserDefaultsValue(key: "alertCOBSnoozedTime", default: nil) - static let alertCOBAutosnooze = UserDefaultsValue(key: "alertCOBAutosnooze", default: "Never") - static let alertCOBAutosnoozeDay = UserDefaultsValue(key: "alertCOBAutosnoozeDay", default: false) - static let alertCOBAutosnoozeNight = UserDefaultsValue(key: "alertCOBAutosnoozeNight", default: false) - static let alertBatteryActive = UserDefaultsValue(key: "alertBatteryActive", default: false) static let alertBatteryLevel = UserDefaultsValue(key: "alertBatteryLevel", default: 25) static let alertBatterySound = UserDefaultsValue(key: "alertBatterySound", default: "Machine_Charge") From cdff6db7d9a7b8f298f9587f83cd8e9c4fb2672e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 6 Jun 2025 18:01:23 +0200 Subject: [PATCH 118/138] Alarm migration --- .../Controllers/Nightscout/DeviceStatus.swift | 2 +- .../Nightscout/DeviceStatusLoop.swift | 2 +- .../Nightscout/DeviceStatusOpenAPS.swift | 4 +- LoopFollow/Storage/Observable.swift | 2 + LoopFollow/Storage/Storage+Migrate.swift | 80 ++++++++++++++++++- LoopFollow/Storage/UserDefaults.swift | 27 ------- LoopFollow/Task/AlarmTask.swift | 4 +- 7 files changed, 85 insertions(+), 36 deletions(-) diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift index 5eaf3fbea..f9840de7e 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift @@ -109,7 +109,7 @@ extension MainViewController { batteryText = String(format: "%.0f", upbat) + "%" } infoManager.updateInfoData(type: .battery, value: batteryText) - UserDefaultsRepository.deviceBatteryLevel.value = upbat + Observable.shared.deviceBatteryLevel.value = upbat let timestamp = uploader["timestamp"] as? Date ?? Date() let currentBattery = DataStructs.batteryStruct(batteryLevel: upbat, timestamp: timestamp) diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift index d6735e463..813371ba6 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift @@ -101,7 +101,7 @@ extension MainViewController { if let recBolus = lastLoopRecord["recommendedBolus"] as? Double { let formattedRecBolus = String(format: "%.2fU", recBolus) infoManager.updateInfoData(type: .recBolus, value: formattedRecBolus) - UserDefaultsRepository.deviceRecBolus.value = recBolus + Observable.shared.deviceRecBolus.value = recBolus } if let loopStatus = lastLoopRecord["recommendedTempBasal"] as? [String: AnyObject] { if let tempBasalTime = formatter.date(from: (loopStatus["timestamp"] as! String))?.timeIntervalSince1970 { diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift index 38d32e014..ebe0c1725 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift @@ -120,9 +120,9 @@ extension MainViewController { // Insulin Required if let insulinReqMetric = InsulinMetric(from: enactedOrSuggested, key: "insulinReq") { infoManager.updateInfoData(type: .recBolus, value: insulinReqMetric) - UserDefaultsRepository.deviceRecBolus.value = insulinReqMetric.value + Observable.shared.deviceRecBolus.value = insulinReqMetric.value } else { - UserDefaultsRepository.deviceRecBolus.value = 0 + Observable.shared.deviceRecBolus.value = 0 } // Autosens diff --git a/LoopFollow/Storage/Observable.swift b/LoopFollow/Storage/Observable.swift index 869bde4b7..6787a1461 100644 --- a/LoopFollow/Storage/Observable.swift +++ b/LoopFollow/Storage/Observable.swift @@ -31,6 +31,8 @@ class Observable { var chartSettingsChanged = ObservableValue(default: false) var alertLastLoopTime = ObservableValue(default: nil) + var deviceRecBolus = ObservableValue(default: nil) + var deviceBatteryLevel = ObservableValue(default: nil) private init() {} } diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index 4232fbfaf..e5e845459 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -302,6 +302,9 @@ extension Storage { migratePumpVolumeAlarm() migrateIOBAlarm() migrateCOBAlarm() + migrateBatteryAlarm() + migrateBatteryDropAlarm() + migrateRecBolusAlarm() } // MARK: - One-off alarm migrations @@ -1518,9 +1521,80 @@ extension Storage { Storage.shared.alarms.value.append(alarm) } - // ----------------------------------------------------------------------------- + // ============================================================================= + // BATTERY-LEVEL alarm (old keys → .battery) + // ============================================================================= + private func migrateBatteryAlarm() { + guard UserDefaultsValue(key: "alertBatteryActive", + default: false).exists else { return } - // MARK: - Inject into main migrate() entry point + var alarm = Alarm(type: .battery) + alarm.name = "Low Battery" + alarm.isEnabled = take("alertBatteryActive", false) + alarm.threshold = Double(take("alertBatteryLevel", 25)) // % + alarm.snoozeDuration = take("alertBatterySnoozeHours", 1) * 60 + alarm.snoozedUntil = take("alertBatterySnoozedTime", nil as Date?) + alarm.soundFile = SoundFile(rawValue: + take("alertBatterySound", "Machine_Charge")) + ?? .machineCharge - // ----------------------------------------------------------------------------- + // ── audio – legacy had a simple Bool “repeat / no-repeat” + let rpt = take("alertBatteryRepeat", true) + alarm.playSoundOption = .always // no day/night picker in legacy UI + alarm.repeatSoundOption = rpt ? .always : .never + + alarm.activeOption = .always // no day/night activation picker + Storage.shared.alarms.value.append(alarm) + } + + // ============================================================================= + // BATTERY-DROP alarm (old keys → .batteryDrop) + // ============================================================================= + private func migrateBatteryDropAlarm() { + guard UserDefaultsValue(key: "alertBatteryDropActive", + default: false).exists else { return } + + var alarm = Alarm(type: .batteryDrop) + alarm.name = "Battery Drop" + alarm.isEnabled = take("alertBatteryDropActive", false) + alarm.delta = Double(take("alertBatteryDropPercentage", 5)) // % drop + alarm.monitoringWindow = take("alertBatteryDropPeriod", 15) // min + alarm.snoozeDuration = take("alertBatteryDropSnoozeHours", 1) * 60 + alarm.snoozedUntil = take("alertBatteryDropSnoozedTime", nil as Date?) + alarm.soundFile = SoundFile(rawValue: + take("alertBatteryDropSound", "Machine_Charge")) + ?? .machineCharge + + let rpt = take("alertBatteryDropRepeat", true) + alarm.playSoundOption = .always + alarm.repeatSoundOption = rpt ? .always : .never + alarm.activeOption = .always + + Storage.shared.alarms.value.append(alarm) + } + + // ============================================================================= + // REC-BOLUS alarm (old keys → .recBolus) + // ============================================================================= + private func migrateRecBolusAlarm() { + guard UserDefaultsValue(key: "alertRecBolusActive", + default: false).exists else { return } + + var alarm = Alarm(type: .recBolus) + alarm.name = "Recommended Bolus" + alarm.isEnabled = take("alertRecBolusActive", false) + alarm.delta = take("alertRecBolusLevel", 1.0) // units + alarm.snoozeDuration = take("alertRecBolusSnooze", 5) // min + alarm.snoozedUntil = take("alertRecBolusSnoozedTime", nil as Date?) + alarm.soundFile = SoundFile(rawValue: + take("alertRecBolusSound", "Dhol_Shuffleloop")) + ?? .dholShuffleloop + + let rpt = take("alertRecBolusRepeat", false) + alarm.playSoundOption = .always + alarm.repeatSoundOption = rpt ? .always : .never + alarm.activeOption = .always + + Storage.shared.alarms.value.append(alarm) + } } diff --git a/LoopFollow/Storage/UserDefaults.swift b/LoopFollow/Storage/UserDefaults.swift index 28f403196..9b55309e6 100644 --- a/LoopFollow/Storage/UserDefaults.swift +++ b/LoopFollow/Storage/UserDefaults.swift @@ -97,33 +97,6 @@ class UserDefaultsRepository { static let alertCageInsertTime = UserDefaultsValue(key: "alertCageInsertTime", default: 0) static let alertSageInsertTime = UserDefaultsValue(key: "alertSageInsertTime", default: 0) - static let alertBatteryActive = UserDefaultsValue(key: "alertBatteryActive", default: false) - static let alertBatteryLevel = UserDefaultsValue(key: "alertBatteryLevel", default: 25) - static let alertBatterySound = UserDefaultsValue(key: "alertBatterySound", default: "Machine_Charge") - static let alertBatteryRepeat = UserDefaultsValue(key: "alertBatteryRepeat", default: true) - static let alertBatteryIsSnoozed = UserDefaultsValue(key: "alertBatteryIsSnoozed", default: false) - static let alertBatterySnoozedTime = UserDefaultsValue(key: "alertBatterySnoozedTime", default: nil) - static let alertBatterySnoozeHours = UserDefaultsValue(key: "alertBatterySnoozeHours", default: 1) - static var deviceBatteryLevel: UserDefaultsValue = UserDefaultsValue(key: "deviceBatteryLevel", default: 100.0) - - static let alertBatteryDropActive = UserDefaultsValue(key: "alertBatteryDropActive", default: false) - static let alertBatteryDropPercentage = UserDefaultsValue(key: "alertBatteryDropPercentage", default: 5) - static let alertBatteryDropPeriod = UserDefaultsValue(key: "alertBatteryDropPeriod", default: 15) - static let alertBatteryDropSound = UserDefaultsValue(key: "alertBatteryDropSound", default: "Machine_Charge") - static let alertBatteryDropRepeat = UserDefaultsValue(key: "alertBatteryDropRepeat", default: true) - static let alertBatteryDropIsSnoozed = UserDefaultsValue(key: "alertBatteryDropIsSnoozed", default: false) - static let alertBatteryDropSnoozedTime = UserDefaultsValue(key: "alertBatteryDropSnoozedTime", default: nil) - static let alertBatteryDropSnoozeHours = UserDefaultsValue(key: "alertBatteryDropSnoozeHours", default: 1) - - static let alertRecBolusActive = UserDefaultsValue(key: "alertRecBolusActive", default: false) - static let alertRecBolusLevel = UserDefaultsValue(key: "alertRecBolusLevel", default: 1) // Unit[s] - static let alertRecBolusSound = UserDefaultsValue(key: "alertRecBolusSound", default: "Dhol_Shuffleloop") - static let alertRecBolusRepeat = UserDefaultsValue(key: "alertRecBolusRepeat", default: false) - static let alertRecBolusIsSnoozed = UserDefaultsValue(key: "alertRecBolusIsSnoozed", default: false) - static let alertRecBolusSnooze = UserDefaultsValue(key: "alertRecBolusSnooze", default: 5) - static let alertRecBolusSnoozedTime = UserDefaultsValue(key: "alertRecBolusSnoozedTime", default: nil) - static var deviceRecBolus: UserDefaultsValue = UserDefaultsValue(key: "deviceRecBolus", default: 0.0) - // What version is the cache valid for static let cachedForVersion = UserDefaultsValue(key: "cachedForVersion", default: nil) diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index 073dfcbe4..c987fc1ac 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -20,13 +20,13 @@ extension MainViewController { let latestOverrideEnd = self.overrideGraphData.last { $0.endDate <= now }?.endDate let latestTempTargetStart = self.tempTargetGraphData.last { $0.date <= now }?.date let latestTempTargetEnd = self.tempTargetGraphData.last { $0.endDate <= now }?.endDate - let recBolus = UserDefaultsRepository.deviceRecBolus.value + let recBolus = Observable.shared.deviceRecBolus.value let COB = self.latestCOB?.value let sensorInsertedAt = UserDefaultsRepository.alertSageInsertTime.value let pumpInsertTime = UserDefaultsRepository.alertCageInsertTime.value let latestPumpVol = self.latestPumpVolume let bolusEntries = self.bolusData.map { BolusEntry(units: $0.value, date: Date(timeIntervalSince1970: $0.date)) } - let latestBattery = UserDefaultsRepository.deviceBatteryLevel.value + let latestBattery = Observable.shared.deviceBatteryLevel.value let recentCarbs: [CarbSample] = self.carbData.map { CarbSample(grams: $0.value, date: Date(timeIntervalSince1970: $0.date)) } let alarmData = AlarmData( From 52c4b2c1324d48c3f677ee88c205b9572f21b52f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 6 Jun 2025 22:04:22 +0200 Subject: [PATCH 119/138] Remove UserDefaults.swift --- LoopFollow.xcodeproj/project.pbxproj | 4 - .../BackgroundRefresh/BT/BLEManager.swift | 2 +- LoopFollow/Contact/ContactImageUpdater.swift | 2 +- LoopFollow/Controllers/Graphs.swift | 10 +- .../Controllers/Nightscout/BGData.swift | 2 +- LoopFollow/Controllers/Nightscout/CAge.swift | 2 +- .../Nightscout/DeviceStatusLoop.swift | 2 +- .../Controllers/Nightscout/Profile.swift | 2 +- LoopFollow/Controllers/Nightscout/SAge.swift | 4 +- .../Controllers/Nightscout/Treatments.swift | 2 +- .../Nightscout/Treatments/BGCheck.swift | 2 +- .../Nightscout/Treatments/Basals.swift | 2 +- .../Nightscout/Treatments/Bolus.swift | 2 +- .../Nightscout/Treatments/Carbs.swift | 2 +- .../Treatments/InsulinCartridgeChange.swift | 2 +- .../Nightscout/Treatments/Notes.swift | 2 +- .../Nightscout/Treatments/Overrides.swift | 2 +- .../Nightscout/Treatments/ResumePump.swift | 2 +- .../Nightscout/Treatments/SMB.swift | 2 +- .../Nightscout/Treatments/SensorStart.swift | 2 +- .../Nightscout/Treatments/SuspendPump.swift | 2 +- .../Treatments/TemporaryTarget.swift | 2 +- LoopFollow/Controllers/Stats.swift | 2 +- LoopFollow/Helpers/AppVersionManager.swift | 20 +-- LoopFollow/Helpers/Localizer.swift | 18 ++- LoopFollow/Helpers/NightscoutUtils.swift | 8 +- LoopFollow/Helpers/Views/BGPicker.swift | 2 +- .../InfoDisplaySettingsView.swift | 4 +- .../InfoDisplaySettingsViewModel.swift | 8 +- LoopFollow/InfoTable/InfoManager.swift | 4 +- .../NightscoutSettingsViewModel.swift | 6 +- LoopFollow/Remote/Loop/LoopOverrideView.swift | 2 +- .../Nightscout/TrioNightscoutRemoteView.swift | 14 +-- LoopFollow/Remote/TRC/OverrideView.swift | 2 +- LoopFollow/Remote/TRC/TempTargetView.swift | 10 +- .../Settings/AdvancedSettingsViewModel.swift | 28 ++--- .../Settings/DexcomSettingsViewModel.swift | 12 +- LoopFollow/Settings/SettingsMenuView.swift | 4 +- LoopFollow/Snoozer/SnoozerView.swift | 4 +- LoopFollow/Storage/Storage+Migrate.swift | 71 ++++++++++- LoopFollow/Storage/Storage.swift | 42 +++++++ LoopFollow/Storage/UserDefaults.swift | 116 ------------------ LoopFollow/Task/AlarmTask.swift | 4 +- LoopFollow/Task/BGTask.swift | 8 +- LoopFollow/Task/TreatmentsTask.swift | 2 +- .../ViewControllers/MainViewController.swift | 62 ++++++++-- .../NightScoutViewController.swift | 2 +- 47 files changed, 270 insertions(+), 241 deletions(-) delete mode 100644 LoopFollow/Storage/UserDefaults.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 6fc89cf14..ca5511348 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -350,7 +350,6 @@ FCC0FAC224922A22003E610E /* DictionaryKeyPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC0FAC124922A22003E610E /* DictionaryKeyPath.swift */; }; FCC6885C2489559400A0279D /* blank.wav in Resources */ = {isa = PBXBuildFile; fileRef = FCC6885B2489559400A0279D /* blank.wav */; }; FCC6885E24896A6C00A0279D /* silence.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = FCC6885D24896A6C00A0279D /* silence.mp3 */; }; - FCC6886524898EEE00A0279D /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC6886424898EEE00A0279D /* UserDefaults.swift */; }; FCC6886724898F8000A0279D /* UserDefaultsValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC6886624898F8000A0279D /* UserDefaultsValue.swift */; }; FCC6886924898FB100A0279D /* UserDefaultsValueGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC6886824898FB100A0279D /* UserDefaultsValueGroups.swift */; }; FCC6886B24898FD800A0279D /* ObservationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCC6886A24898FD800A0279D /* ObservationToken.swift */; }; @@ -724,7 +723,6 @@ FCC688592489554800A0279D /* BackgroundTaskAudio.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundTaskAudio.swift; sourceTree = ""; }; FCC6885B2489559400A0279D /* blank.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = blank.wav; sourceTree = ""; }; FCC6885D24896A6C00A0279D /* silence.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = silence.mp3; sourceTree = ""; }; - FCC6886424898EEE00A0279D /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = ""; }; FCC6886624898F8000A0279D /* UserDefaultsValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsValue.swift; sourceTree = ""; }; FCC6886824898FB100A0279D /* UserDefaultsValueGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsValueGroups.swift; sourceTree = ""; }; FCC6886A24898FD800A0279D /* ObservationToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservationToken.swift; sourceTree = ""; }; @@ -1085,7 +1083,6 @@ isa = PBXGroup; children = ( FCC688512489363F00A0279D /* Framework */, - FCC6886424898EEE00A0279D /* UserDefaults.swift */, DDD10F002C510C6B00D76A8E /* ObservableUserDefaults.swift */, DDD10F062C529DE800D76A8E /* Observable.swift */, DD4878042C7B2C970048F05C /* Storage.swift */, @@ -1910,7 +1907,6 @@ FCEF87AC24A141A700AE6FA0 /* Localizer.swift in Sources */, FC1BDD3224A2585C001B652C /* DataStructs.swift in Sources */, DDF6999E2C5AAA640058A8D9 /* ErrorMessageView.swift in Sources */, - FCC6886524898EEE00A0279D /* UserDefaults.swift in Sources */, DD4878152C7B75230048F05C /* MealView.swift in Sources */, FC16A97F249969E2003D6245 /* Graphs.swift in Sources */, FC8589BF252B54F500C8FC73 /* Mobileprovision.swift in Sources */, diff --git a/LoopFollow/BackgroundRefresh/BT/BLEManager.swift b/LoopFollow/BackgroundRefresh/BT/BLEManager.swift index 11f83522a..598dca29a 100644 --- a/LoopFollow/BackgroundRefresh/BT/BLEManager.swift +++ b/LoopFollow/BackgroundRefresh/BT/BLEManager.swift @@ -248,7 +248,7 @@ extension BLEManager { return nil } - let pollingDelay: TimeInterval = Double(UserDefaultsRepository.bgUpdateDelay.value) + let pollingDelay: TimeInterval = Double(Storage.shared.bgUpdateDelay.value) let expectedOffset = sensorOffset + pollingDelay diff --git a/LoopFollow/Contact/ContactImageUpdater.swift b/LoopFollow/Contact/ContactImageUpdater.swift index 96477ab83..dbeaee0b4 100644 --- a/LoopFollow/Contact/ContactImageUpdater.swift +++ b/LoopFollow/Contact/ContactImageUpdater.swift @@ -85,7 +85,7 @@ class ContactImageUpdater { paragraphStyle.alignment = .center // Format extraDelta based on the user's unit preference - let unitPreference = UserDefaultsRepository.units.value + let unitPreference = Storage.shared.units.value let yOffset: CGFloat = 48 if contactType == .Trend, Storage.shared.contactTrend.value == .separate { let trendRect = CGRect(x: 0, y: 46, width: size.width, height: size.height - 80) diff --git a/LoopFollow/Controllers/Graphs.swift b/LoopFollow/Controllers/Graphs.swift index 61e7f3783..9671e43aa 100644 --- a/LoopFollow/Controllers/Graphs.swift +++ b/LoopFollow/Controllers/Graphs.swift @@ -200,7 +200,7 @@ class TempTargetRenderer: LineChartRenderer { } } -let ScaleXMax: Float = 150.0 +let ScaleXMax: Double = 150.0 extension MainViewController { func updateChartRenderers() { let tempTargetDataIndex = GraphDataIndex.tempTarget.rawValue @@ -230,11 +230,11 @@ extension MainViewController { func chartScaled(_: ChartViewBase, scaleX _: CGFloat, scaleY _: CGFloat) { // dont store huge values - var scale = Float(BGChart.scaleX) + var scale = Double(BGChart.scaleX) if scale > ScaleXMax { scale = ScaleXMax } - UserDefaultsRepository.chartScaleX.value = Float(scale) + Storage.shared.chartScaleX.value = scale } func createGraph() { @@ -853,10 +853,10 @@ extension MainViewController { BGChartFull.notifyDataSetChanged() if firstGraphLoad { - var scaleX = CGFloat(UserDefaultsRepository.chartScaleX.value) + var scaleX = CGFloat(Storage.shared.chartScaleX.value) if scaleX > CGFloat(ScaleXMax) { scaleX = CGFloat(ScaleXMax) - UserDefaultsRepository.chartScaleX.value = ScaleXMax + Storage.shared.chartScaleX.value = ScaleXMax } BGChart.zoom(scaleX: scaleX, scaleY: 1, x: 1, y: 1) firstGraphLoad = false diff --git a/LoopFollow/Controllers/Nightscout/BGData.swift b/LoopFollow/Controllers/Nightscout/BGData.swift index cc799fa02..ae8868dac 100644 --- a/LoopFollow/Controllers/Nightscout/BGData.swift +++ b/LoopFollow/Controllers/Nightscout/BGData.swift @@ -167,7 +167,7 @@ extension MainViewController { message: "Reading is close to 5 minutes old (\(secondsAgo) sec). Scheduling next fetch in 5 seconds.", isDebug: true) } else { - delayToSchedule = 300 - secondsAgo + Double(UserDefaultsRepository.bgUpdateDelay.value) + delayToSchedule = 300 - secondsAgo + Double(Storage.shared.bgUpdateDelay.value) LogManager.shared.log(category: .nightscout, message: "Fresh reading. Scheduling next fetch in \(delayToSchedule) seconds.", isDebug: true) diff --git a/LoopFollow/Controllers/Nightscout/CAge.swift b/LoopFollow/Controllers/Nightscout/CAge.swift index 224040894..d6c986133 100644 --- a/LoopFollow/Controllers/Nightscout/CAge.swift +++ b/LoopFollow/Controllers/Nightscout/CAge.swift @@ -40,7 +40,7 @@ extension MainViewController { .withTime, .withDashSeparatorInDate, .withColonSeparatorInTime] - UserDefaultsRepository.alertCageInsertTime.value = formatter.date(from: lastCageString)?.timeIntervalSince1970 as! TimeInterval + Storage.shared.cageInsertTime.value = formatter.date(from: lastCageString)?.timeIntervalSince1970 as! TimeInterval if let cageTime = formatter.date(from: lastCageString)?.timeIntervalSince1970 { let now = dateTimeUtils.getNowTimeIntervalUTC() let secondsAgo = now - cageTime diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift index 813371ba6..b3fdf3416 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift @@ -66,7 +66,7 @@ extension MainViewController { let prediction = predictdata["values"] as! [Double] PredictionLabel.text = Localizer.toDisplayUnits(String(Int(prediction.last!))) PredictionLabel.textColor = UIColor.systemPurple - if UserDefaultsRepository.downloadPrediction.value, previousLastLoopTime < lastLoopTime { + if Storage.shared.downloadPrediction.value, previousLastLoopTime < lastLoopTime { predictionData.removeAll() var predictionTime = lastLoopTime let toLoad = Int(Storage.shared.predictionToLoad.value * 12) diff --git a/LoopFollow/Controllers/Nightscout/Profile.swift b/LoopFollow/Controllers/Nightscout/Profile.swift index fdc444ce5..e019a7104 100644 --- a/LoopFollow/Controllers/Nightscout/Profile.swift +++ b/LoopFollow/Controllers/Nightscout/Profile.swift @@ -110,7 +110,7 @@ extension MainViewController { } } - if UserDefaultsRepository.graphBasal.value { + if Storage.shared.graphBasal.value { updateBasalScheduledGraph() } } diff --git a/LoopFollow/Controllers/Nightscout/SAge.swift b/LoopFollow/Controllers/Nightscout/SAge.swift index 92607391e..c45ea81a6 100644 --- a/LoopFollow/Controllers/Nightscout/SAge.swift +++ b/LoopFollow/Controllers/Nightscout/SAge.swift @@ -44,7 +44,7 @@ extension MainViewController { .withTime, .withDashSeparatorInDate, .withColonSeparatorInTime] - UserDefaultsRepository.alertSageInsertTime.value = formatter.date(from: lastSageString)?.timeIntervalSince1970 as! TimeInterval + Storage.shared.sageInsertTime.value = formatter.date(from: lastSageString)?.timeIntervalSince1970 as! TimeInterval // -- Auto-snooze CGM start ──────────────────────────────────────────────── let now = Date() @@ -52,7 +52,7 @@ extension MainViewController { // 1. Do we *want* the automatic global snooze? if Storage.shared.alarmConfiguration.value.autoSnoozeCGMStart { // 2. When did the sensor start? - let insertTime = UserDefaultsRepository.alertSageInsertTime.value + let insertTime = Storage.shared.sageInsertTime.value // 3. If the start is less than 2 h ago, snooze *all* alarms for the // remainder of that 2-hour window. diff --git a/LoopFollow/Controllers/Nightscout/Treatments.swift b/LoopFollow/Controllers/Nightscout/Treatments.swift index aba121c37..1376224e9 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments.swift @@ -8,7 +8,7 @@ extension MainViewController { // NS Treatments Web Call // Downloads Basal, Bolus, Carbs, BG Check, Notes, Overrides func WebLoadNSTreatments() { - if !UserDefaultsRepository.downloadTreatments.value { return } + if !Storage.shared.downloadTreatments.value { return } let startTimeString = dateTimeUtils.getDateTimeString(addingDays: -1 * Storage.shared.downloadDays.value) let currentTimeString = dateTimeUtils.getDateTimeString(addingHours: 6) diff --git a/LoopFollow/Controllers/Nightscout/Treatments/BGCheck.swift b/LoopFollow/Controllers/Nightscout/Treatments/BGCheck.swift index 2592dd6a5..00ec21c39 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/BGCheck.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/BGCheck.swift @@ -29,7 +29,7 @@ extension MainViewController { } } - if UserDefaultsRepository.graphOtherTreatments.value { + if Storage.shared.graphOtherTreatments.value { updateBGCheckGraph() } } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Basals.swift b/LoopFollow/Controllers/Nightscout/Treatments/Basals.swift index 49a847bea..bfaf8c9ed 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Basals.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Basals.swift @@ -138,7 +138,7 @@ extension MainViewController { basalData.append(endDot) } - if UserDefaultsRepository.graphBasal.value { + if Storage.shared.graphBasal.value { updateBasalGraph() } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Bolus.swift b/LoopFollow/Controllers/Nightscout/Treatments/Bolus.swift index 3ae15e894..6ea898ce9 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Bolus.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Bolus.swift @@ -35,7 +35,7 @@ extension MainViewController { } } - if UserDefaultsRepository.graphBolus.value { + if Storage.shared.graphBolus.value { updateBolusGraph() } } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift b/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift index 3bb3e0b38..05cc044a1 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift @@ -46,7 +46,7 @@ extension MainViewController { } } - if UserDefaultsRepository.graphCarbs.value { + if Storage.shared.graphCarbs.value { updateCarbGraph() } } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/InsulinCartridgeChange.swift b/LoopFollow/Controllers/Nightscout/Treatments/InsulinCartridgeChange.swift index 3a64fe6c8..3a3aa0e20 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/InsulinCartridgeChange.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/InsulinCartridgeChange.swift @@ -10,7 +10,7 @@ extension MainViewController { updateIage(data: entries) } else if let iage = currentIage { updateIage(data: [iage]) - } else if UserDefaultsRepository.infoVisible.value[InfoType.iage.rawValue] { + } else if Storage.shared.infoVisible.value[InfoType.iage.rawValue] { webLoadNSIage() } } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Notes.swift b/LoopFollow/Controllers/Nightscout/Treatments/Notes.swift index 2e1b4d6e0..2e009644f 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Notes.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Notes.swift @@ -40,7 +40,7 @@ extension MainViewController { } } - if UserDefaultsRepository.graphOtherTreatments.value { + if Storage.shared.graphOtherTreatments.value { updateNotes() } } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift b/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift index 83fcacb42..b5c41f0b9 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift @@ -79,7 +79,7 @@ extension MainViewController { infoManager.clearInfoData(type: .override) } } - if UserDefaultsRepository.graphOtherTreatments.value { + if Storage.shared.graphOtherTreatments.value { updateOverrideGraph() } } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/ResumePump.swift b/LoopFollow/Controllers/Nightscout/Treatments/ResumePump.swift index 7d1dfb720..8eee01618 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/ResumePump.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/ResumePump.swift @@ -28,7 +28,7 @@ extension MainViewController { } } - if UserDefaultsRepository.graphOtherTreatments.value { + if Storage.shared.graphOtherTreatments.value { updateResumeGraph() } } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/SMB.swift b/LoopFollow/Controllers/Nightscout/Treatments/SMB.swift index 6e9438929..7ab3b7a48 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/SMB.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/SMB.swift @@ -34,7 +34,7 @@ extension MainViewController { } } - if UserDefaultsRepository.graphBolus.value { + if Storage.shared.graphBolus.value { updateSmbGraph() updateChartRenderers() } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/SensorStart.swift b/LoopFollow/Controllers/Nightscout/Treatments/SensorStart.swift index f22509800..aa0646421 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/SensorStart.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/SensorStart.swift @@ -35,7 +35,7 @@ extension MainViewController { print("Failed to parse date") } } - if UserDefaultsRepository.graphOtherTreatments.value { + if Storage.shared.graphOtherTreatments.value { updateSensorStart() } } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/SuspendPump.swift b/LoopFollow/Controllers/Nightscout/Treatments/SuspendPump.swift index 6cef5fa90..2e1137930 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/SuspendPump.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/SuspendPump.swift @@ -28,7 +28,7 @@ extension MainViewController { } } - if UserDefaultsRepository.graphOtherTreatments.value { + if Storage.shared.graphOtherTreatments.value { updateSuspendGraph() } } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/TemporaryTarget.swift b/LoopFollow/Controllers/Nightscout/Treatments/TemporaryTarget.swift index 3e13df678..529b0de8a 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/TemporaryTarget.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/TemporaryTarget.swift @@ -63,7 +63,7 @@ extension MainViewController { } } - if UserDefaultsRepository.graphOtherTreatments.value { + if Storage.shared.graphOtherTreatments.value { updateTempTargetGraph() updateChartRenderers() } diff --git a/LoopFollow/Controllers/Stats.swift b/LoopFollow/Controllers/Stats.swift index 10d74a0fa..4fb31906a 100644 --- a/LoopFollow/Controllers/Stats.swift +++ b/LoopFollow/Controllers/Stats.swift @@ -62,7 +62,7 @@ class StatsData { } stdDev = sqrt(partialSum / Float(bgData.count)) - if UserDefaultsRepository.units.value != "mg/dL" { + if Storage.shared.units.value != "mg/dL" { stdDev = stdDev * Float(GlucoseConversion.mgDlToMmolL) } diff --git a/LoopFollow/Helpers/AppVersionManager.swift b/LoopFollow/Helpers/AppVersionManager.swift index c42a9c2d3..077789fb6 100644 --- a/LoopFollow/Helpers/AppVersionManager.swift +++ b/LoopFollow/Helpers/AppVersionManager.swift @@ -23,15 +23,15 @@ class AppVersionManager { let now = Date() // Retrieve cache - let latestVersionChecked = UserDefaultsRepository.latestVersionChecked.value ?? Date.distantPast - let latestVersion = UserDefaultsRepository.latestVersion.value - let currentVersionBlackListed = UserDefaultsRepository.currentVersionBlackListed.value - let cachedForVersion = UserDefaultsRepository.cachedForVersion.value + let latestVersionChecked = Storage.shared.latestVersionChecked.value ?? Date.distantPast + let latestVersion = Storage.shared.latestVersion.value + let currentVersionBlackListed = Storage.shared.currentVersionBlackListed.value + let cachedForVersion = Storage.shared.cachedForVersion.value // Reset notifications if version has changed if let cachedVersion = cachedForVersion, cachedVersion != currentVersion { - UserDefaultsRepository.lastBlacklistNotificationShown.value = Date.distantPast - UserDefaultsRepository.lastVersionUpdateNotificationShown.value = Date.distantPast + Storage.shared.lastBlacklistNotificationShown.value = Date.distantPast + Storage.shared.lastVersionUpdateNotificationShown.value = Date.distantPast } // Check if the cache is still valid @@ -59,10 +59,10 @@ class AppVersionManager { .map { $0.blacklistedVersions.map { $0.version }.contains(currentVersion) } ?? false // Update cache with new data - UserDefaultsRepository.latestVersion.value = fetchedVersion - UserDefaultsRepository.latestVersionChecked.value = Date() - UserDefaultsRepository.currentVersionBlackListed.value = isBlacklisted - UserDefaultsRepository.cachedForVersion.value = currentVersion + Storage.shared.latestVersion.value = fetchedVersion + Storage.shared.latestVersionChecked.value = Date() + Storage.shared.currentVersionBlackListed.value = isBlacklisted + Storage.shared.cachedForVersion.value = currentVersion // Call completion with new data completion(fetchedVersion, isNewer, isBlacklisted) diff --git a/LoopFollow/Helpers/Localizer.swift b/LoopFollow/Helpers/Localizer.swift index 58a7389f1..a3c2dbf45 100644 --- a/LoopFollow/Helpers/Localizer.swift +++ b/LoopFollow/Helpers/Localizer.swift @@ -6,6 +6,16 @@ import Foundation import HealthKit class Localizer { + static func getPreferredUnit() -> HKUnit { + let unitString = Storage.shared.units.value + switch unitString { + case "mmol/L": + return .millimolesPerLiter + default: + return .milligramsPerDeciliter + } + } + static func formatToLocalizedString(_ value: Double, maxFractionDigits: Int = 1, minFractionDigits: Int = 0) -> String { let numberFormatter = NumberFormatter() numberFormatter.numberStyle = .decimal @@ -18,7 +28,7 @@ class Localizer { } static func formatQuantity(_ quantity: HKQuantity) -> String { - let unit: HKUnit = UserDefaultsRepository.getPreferredUnit() + let unit: HKUnit = getPreferredUnit() let value = quantity.doubleValue(for: unit) return formatToLocalizedString(value, maxFractionDigits: unit.preferredFractionDigits, minFractionDigits: unit.preferredFractionDigits) @@ -41,7 +51,7 @@ class Localizer { let numberFormatter = NumberFormatter() numberFormatter.numberStyle = .decimal - let units = unit ?? UserDefaultsRepository.units.value + let units = unit ?? Storage.shared.units.value if units == "mg/dL" { numberFormatter.maximumFractionDigits = 0 // No decimal places for mg/dL @@ -60,7 +70,7 @@ class Localizer { let numberFormatter = NumberFormatter() numberFormatter.numberStyle = .decimal - if UserDefaultsRepository.units.value == "mg/dL" { + if Storage.shared.units.value == "mg/dL" { numberFormatter.maximumFractionDigits = 0 // No decimal places for mg/dL } else { numberFormatter.maximumFractionDigits = 1 // Always one decimal place for mmol/L @@ -70,7 +80,7 @@ class Localizer { numberFormatter.locale = Locale.current if let number = Float(value) { - if UserDefaultsRepository.units.value == "mg/dL" { + if Storage.shared.units.value == "mg/dL" { let numberValue = NSNumber(value: number) return numberFormatter.string(from: numberValue) ?? value } else { diff --git a/LoopFollow/Helpers/NightscoutUtils.swift b/LoopFollow/Helpers/NightscoutUtils.swift index b24c5ee3d..f0741ed11 100644 --- a/LoopFollow/Helpers/NightscoutUtils.swift +++ b/LoopFollow/Helpers/NightscoutUtils.swift @@ -64,7 +64,7 @@ class NightscoutUtils { static func executeRequest(eventType: EventType, parameters: [String: String], completion: @escaping (Result) -> Void) { let baseURL = ObservableUserDefaults.shared.url.value - let token = UserDefaultsRepository.token.value + let token = Storage.shared.token.value guard let url = NightscoutUtils.constructURL(baseURL: baseURL, token: token, endpoint: eventType.endpoint, parameters: parameters) else { completion(.failure(NSError(domain: "NightscoutUtils", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to construct URL"]))) @@ -114,7 +114,7 @@ class NightscoutUtils { static func executeDynamicRequest(eventType: EventType, parameters: [String: String], completion: @escaping (Result) -> Void) { let baseURL = ObservableUserDefaults.shared.url.value - let token = UserDefaultsRepository.token.value + let token = Storage.shared.token.value guard let url = NightscoutUtils.constructURL(baseURL: baseURL, token: token, endpoint: eventType.endpoint, parameters: parameters) else { completion(.failure(NSError(domain: "NightscoutUtils", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to construct URL"]))) @@ -187,7 +187,7 @@ class NightscoutUtils { static func verifyURLAndToken(completion: @escaping (NightscoutError?, String?, Bool, Bool) -> Void) { let urlUser = ObservableUserDefaults.shared.url.value - let token = UserDefaultsRepository.token.value + let token = Storage.shared.token.value if urlUser.isEmpty { completion(.emptyAddress, nil, false, false) @@ -285,7 +285,7 @@ class NightscoutUtils { static func retrieveJWTToken() async throws -> String { let urlUser = ObservableUserDefaults.shared.url.value - let token = UserDefaultsRepository.token.value + let token = Storage.shared.token.value if urlUser.isEmpty { throw NightscoutError.emptyAddress diff --git a/LoopFollow/Helpers/Views/BGPicker.swift b/LoopFollow/Helpers/Views/BGPicker.swift index ffd01ed96..09bd06adf 100644 --- a/LoopFollow/Helpers/Views/BGPicker.swift +++ b/LoopFollow/Helpers/Views/BGPicker.swift @@ -13,7 +13,7 @@ struct BGPicker: View { // MARK: – Helpers - private var unit: HKUnit { UserDefaultsRepository.getPreferredUnit() } + private var unit: HKUnit { Localizer.getPreferredUnit() } private var allValues: [Double] { if unit == .millimolesPerLiter { diff --git a/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift index c96d2e3df..33cbc5580 100644 --- a/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift +++ b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift @@ -12,8 +12,8 @@ struct InfoDisplaySettingsView: View { Form { Section(header: Text("General")) { Toggle(isOn: Binding( - get: { UserDefaultsRepository.hideInfoTable.value }, - set: { UserDefaultsRepository.hideInfoTable.value = $0 } + get: { Storage.shared.hideInfoTable.value }, + set: { Storage.shared.hideInfoTable.value = $0 } )) { Text("Hide Information Table") } diff --git a/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsViewModel.swift b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsViewModel.swift index 2e4502124..b1ef78e74 100644 --- a/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsViewModel.swift +++ b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsViewModel.swift @@ -10,18 +10,18 @@ class InfoDisplaySettingsViewModel: ObservableObject { @Published var infoVisible: [Bool] init() { - infoSort = UserDefaultsRepository.infoSort.value - infoVisible = UserDefaultsRepository.infoVisible.value + infoSort = Storage.shared.infoSort.value + infoVisible = Storage.shared.infoVisible.value } func toggleVisibility(for sortedIndex: Int) { infoVisible[sortedIndex].toggle() - UserDefaultsRepository.infoVisible.value = infoVisible + Storage.shared.infoVisible.value = infoVisible } func move(from source: IndexSet, to destination: Int) { infoSort.move(fromOffsets: source, toOffset: destination) - UserDefaultsRepository.infoSort.value = infoSort + Storage.shared.infoSort.value = infoSort } func getName(for index: Int) -> String { diff --git a/LoopFollow/InfoTable/InfoManager.swift b/LoopFollow/InfoTable/InfoManager.swift index a876d6df0..4a285294e 100644 --- a/LoopFollow/InfoTable/InfoManager.swift +++ b/LoopFollow/InfoTable/InfoManager.swift @@ -67,11 +67,11 @@ class InfoManager { } func numberOfRows() -> Int { - return UserDefaultsRepository.infoSort.value.filter { UserDefaultsRepository.infoVisible.value[$0] }.count + return Storage.shared.infoSort.value.filter { Storage.shared.infoVisible.value[$0] }.count } func dataForIndexPath(_ indexPath: IndexPath) -> InfoData? { - let sortedAndVisibleIndexes = UserDefaultsRepository.infoSort.value.filter { UserDefaultsRepository.infoVisible.value[$0] } + let sortedAndVisibleIndexes = Storage.shared.infoSort.value.filter { Storage.shared.infoVisible.value[$0] } guard indexPath.row < sortedAndVisibleIndexes.count else { return nil diff --git a/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift index 3c6fccc24..1131f8fd3 100644 --- a/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift +++ b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift @@ -24,10 +24,10 @@ class NightscoutSettingsViewModel: ObservableObject { } } - @Published var nightscoutToken: String = UserDefaultsRepository.token.value { + @Published var nightscoutToken: String = Storage.shared.token.value { willSet { if newValue != nightscoutToken { - UserDefaultsRepository.token.value = newValue + Storage.shared.token.value = newValue triggerCheckStatus() } } @@ -41,7 +41,7 @@ class NightscoutSettingsViewModel: ObservableObject { init() { initialURL = ObservableUserDefaults.shared.url.value - initialToken = UserDefaultsRepository.token.value + initialToken = Storage.shared.token.value setupDebounce() checkNightscoutStatus() diff --git a/LoopFollow/Remote/Loop/LoopOverrideView.swift b/LoopFollow/Remote/Loop/LoopOverrideView.swift index ee33c2e90..b69bebba8 100644 --- a/LoopFollow/Remote/Loop/LoopOverrideView.swift +++ b/LoopFollow/Remote/Loop/LoopOverrideView.swift @@ -99,7 +99,7 @@ struct LoopOverrideView: View { .foregroundColor(.secondary) if !override.targetRange.isEmpty { let range = override.targetRange.map { Localizer.formatQuantity($0) }.joined(separator: " - ") - Text("Target Range: \(range) \(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString)") + Text("Target Range: \(range) \(Localizer.getPreferredUnit().localizedShortUnitString)") .font(.subheadline) .foregroundColor(.secondary) } diff --git a/LoopFollow/Remote/Nightscout/TrioNightscoutRemoteView.swift b/LoopFollow/Remote/Nightscout/TrioNightscoutRemoteView.swift index d0fed3488..e71a95757 100644 --- a/LoopFollow/Remote/Nightscout/TrioNightscoutRemoteView.swift +++ b/LoopFollow/Remote/Nightscout/TrioNightscoutRemoteView.swift @@ -58,7 +58,7 @@ struct TrioNightscoutRemoteView: View { Text("Current Target") Spacer() Text(Localizer.formatQuantity(tempTargetValue)) - Text(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString).foregroundColor(.secondary) + Text(Localizer.getPreferredUnit().localizedShortUnitString).foregroundColor(.secondary) } Button { alertType = .confirmCancellation @@ -81,7 +81,7 @@ struct TrioNightscoutRemoteView: View { TextFieldWithToolBar( quantity: $newHKTarget, maxLength: 4, - unit: UserDefaultsRepository.getPreferredUnit(), + unit: Localizer.getPreferredUnit(), minValue: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 80), maxValue: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 200), onValidationError: { message in @@ -89,7 +89,7 @@ struct TrioNightscoutRemoteView: View { } ) .focused($targetFieldIsFocused) - Text(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString).foregroundColor(.secondary) + Text(Localizer.getPreferredUnit().localizedShortUnitString).foregroundColor(.secondary) } HStack { Text("Duration") @@ -181,7 +181,7 @@ struct TrioNightscoutRemoteView: View { case .confirmCommand: return Alert( title: Text("Confirm Command"), - message: Text("New Target: \(Localizer.formatQuantity(newHKTarget)) \(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString)\nDuration: \(Int(duration.doubleValue(for: HKUnit.minute()))) minutes"), + message: Text("New Target: \(Localizer.formatQuantity(newHKTarget)) \(Localizer.getPreferredUnit().localizedShortUnitString)\nDuration: \(Int(duration.doubleValue(for: HKUnit.minute()))) minutes"), primaryButton: .default(Text("Confirm"), action: { enactTempTarget() }), @@ -244,7 +244,7 @@ struct TrioNightscoutRemoteView: View { } private var isButtonDisabled: Bool { - return newHKTarget.doubleValue(for: UserDefaultsRepository.getPreferredUnit()) == 0 || + return newHKTarget.doubleValue(for: Localizer.getPreferredUnit()) == 0 || duration.doubleValue(for: HKUnit.minute()) == 0 || isLoading } @@ -257,13 +257,13 @@ struct TrioNightscoutRemoteView: View { self.statusMessage = "Command successfully sent to Nightscout." LogManager.shared.log( category: .nightscout, - message: "sendTempTarget succeeded - New Target: \(Localizer.formatQuantity(newHKTarget)) \(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString), Duration: \(Int(duration.doubleValue(for: HKUnit.minute()))) minutes" + message: "sendTempTarget succeeded - New Target: \(Localizer.formatQuantity(newHKTarget)) \(Localizer.getPreferredUnit().localizedShortUnitString), Duration: \(Int(duration.doubleValue(for: HKUnit.minute()))) minutes" ) } else { self.statusMessage = "Failed to enact target." LogManager.shared.log( category: .nightscout, - message: "sendTempTarget failed - New Target: \(Localizer.formatQuantity(newHKTarget)) \(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString), Duration: \(Int(duration.doubleValue(for: HKUnit.minute()))) minutes" + message: "sendTempTarget failed - New Target: \(Localizer.formatQuantity(newHKTarget)) \(Localizer.getPreferredUnit().localizedShortUnitString), Duration: \(Int(duration.doubleValue(for: HKUnit.minute()))) minutes" ) } self.alertType = .status diff --git a/LoopFollow/Remote/TRC/OverrideView.swift b/LoopFollow/Remote/TRC/OverrideView.swift index fef4d8170..583d9407c 100644 --- a/LoopFollow/Remote/TRC/OverrideView.swift +++ b/LoopFollow/Remote/TRC/OverrideView.swift @@ -92,7 +92,7 @@ struct OverrideView: View { } if let target = override.target { - Text("Target: \(Localizer.formatQuantity(target)) \(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString)") + Text("Target: \(Localizer.formatQuantity(target)) \(Localizer.getPreferredUnit().localizedShortUnitString)") .font(.subheadline) .foregroundColor(.secondary) } diff --git a/LoopFollow/Remote/TRC/TempTargetView.swift b/LoopFollow/Remote/TRC/TempTargetView.swift index 7fe68617a..e79cdb24d 100644 --- a/LoopFollow/Remote/TRC/TempTargetView.swift +++ b/LoopFollow/Remote/TRC/TempTargetView.swift @@ -50,7 +50,7 @@ struct TempTargetView: View { Text("Current Target") Spacer() Text(Localizer.formatQuantity(tempTargetValue)) - Text(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString).foregroundColor(.secondary) + Text(Localizer.getPreferredUnit().localizedShortUnitString).foregroundColor(.secondary) } Button { alertType = .confirmCancellation @@ -73,7 +73,7 @@ struct TempTargetView: View { TextFieldWithToolBar( quantity: $newHKTarget, maxLength: 4, - unit: UserDefaultsRepository.getPreferredUnit(), + unit: Localizer.getPreferredUnit(), minValue: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 80), maxValue: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 200), onValidationError: { message in @@ -81,7 +81,7 @@ struct TempTargetView: View { } ) .focused($targetFieldIsFocused) - Text(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString).foregroundColor(.secondary) + Text(Localizer.getPreferredUnit().localizedShortUnitString).foregroundColor(.secondary) } HStack { Text("Duration") @@ -173,7 +173,7 @@ struct TempTargetView: View { case .confirmCommand: return Alert( title: Text("Confirm Command"), - message: Text("New Target: \(Localizer.formatQuantity(newHKTarget)) \(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString)\nDuration: \(Int(duration.doubleValue(for: HKUnit.minute()))) minutes"), + message: Text("New Target: \(Localizer.formatQuantity(newHKTarget)) \(Localizer.getPreferredUnit().localizedShortUnitString)\nDuration: \(Int(duration.doubleValue(for: HKUnit.minute()))) minutes"), primaryButton: .default(Text("Confirm"), action: { enactTempTarget() }), @@ -242,7 +242,7 @@ struct TempTargetView: View { } private var isButtonDisabled: Bool { - return newHKTarget.doubleValue(for: UserDefaultsRepository.getPreferredUnit()) == 0 || + return newHKTarget.doubleValue(for: Localizer.getPreferredUnit()) == 0 || duration.doubleValue(for: HKUnit.minute()) == 0 || isLoading } diff --git a/LoopFollow/Settings/AdvancedSettingsViewModel.swift b/LoopFollow/Settings/AdvancedSettingsViewModel.swift index 363302470..0766c5a05 100644 --- a/LoopFollow/Settings/AdvancedSettingsViewModel.swift +++ b/LoopFollow/Settings/AdvancedSettingsViewModel.swift @@ -7,43 +7,43 @@ import Foundation class AdvancedSettingsViewModel: ObservableObject { @Published var downloadTreatments: Bool { didSet { - UserDefaultsRepository.downloadTreatments.value = downloadTreatments + Storage.shared.downloadTreatments.value = downloadTreatments } } @Published var downloadPrediction: Bool { didSet { - UserDefaultsRepository.downloadPrediction.value = downloadPrediction + Storage.shared.downloadPrediction.value = downloadPrediction } } @Published var graphBasal: Bool { didSet { - UserDefaultsRepository.graphBasal.value = graphBasal + Storage.shared.graphBasal.value = graphBasal } } @Published var graphBolus: Bool { didSet { - UserDefaultsRepository.graphBolus.value = graphBolus + Storage.shared.graphBolus.value = graphBolus } } @Published var graphCarbs: Bool { didSet { - UserDefaultsRepository.graphCarbs.value = graphCarbs + Storage.shared.graphCarbs.value = graphCarbs } } @Published var graphOtherTreatments: Bool { didSet { - UserDefaultsRepository.graphOtherTreatments.value = graphOtherTreatments + Storage.shared.graphOtherTreatments.value = graphOtherTreatments } } @Published var bgUpdateDelay: Int { didSet { - UserDefaultsRepository.bgUpdateDelay.value = bgUpdateDelay + Storage.shared.bgUpdateDelay.value = bgUpdateDelay } } @@ -54,13 +54,13 @@ class AdvancedSettingsViewModel: ObservableObject { } init() { - downloadTreatments = UserDefaultsRepository.downloadTreatments.value - downloadPrediction = UserDefaultsRepository.downloadPrediction.value - graphBasal = UserDefaultsRepository.graphBasal.value - graphBolus = UserDefaultsRepository.graphBolus.value - graphCarbs = UserDefaultsRepository.graphCarbs.value - graphOtherTreatments = UserDefaultsRepository.graphOtherTreatments.value - bgUpdateDelay = UserDefaultsRepository.bgUpdateDelay.value + downloadTreatments = Storage.shared.downloadTreatments.value + downloadPrediction = Storage.shared.downloadPrediction.value + graphBasal = Storage.shared.graphBasal.value + graphBolus = Storage.shared.graphBolus.value + graphCarbs = Storage.shared.graphCarbs.value + graphOtherTreatments = Storage.shared.graphOtherTreatments.value + bgUpdateDelay = Storage.shared.bgUpdateDelay.value debugLogLevel = Storage.shared.debugLogLevel.value } } diff --git a/LoopFollow/Settings/DexcomSettingsViewModel.swift b/LoopFollow/Settings/DexcomSettingsViewModel.swift index 3bac0ce52..e6fbb64df 100644 --- a/LoopFollow/Settings/DexcomSettingsViewModel.swift +++ b/LoopFollow/Settings/DexcomSettingsViewModel.swift @@ -6,26 +6,26 @@ import Combine import Foundation class DexcomSettingsViewModel: ObservableObject { - @Published var userName: String = UserDefaultsRepository.shareUserName.value { + @Published var userName: String = Storage.shared.shareUserName.value { willSet { if newValue != userName { - UserDefaultsRepository.shareUserName.value = newValue + Storage.shared.shareUserName.value = newValue } } } - @Published var password: String = UserDefaultsRepository.sharePassword.value { + @Published var password: String = Storage.shared.sharePassword.value { willSet { if newValue != password { - UserDefaultsRepository.sharePassword.value = newValue + Storage.shared.sharePassword.value = newValue } } } - @Published var server: String = UserDefaultsRepository.shareServer.value { + @Published var server: String = Storage.shared.shareServer.value { willSet { if newValue != server { - UserDefaultsRepository.shareServer.value = newValue + Storage.shared.shareServer.value = newValue } } } diff --git a/LoopFollow/Settings/SettingsMenuView.swift b/LoopFollow/Settings/SettingsMenuView.swift index b480055fa..678f7e611 100644 --- a/LoopFollow/Settings/SettingsMenuView.swift +++ b/LoopFollow/Settings/SettingsMenuView.swift @@ -134,8 +134,8 @@ struct SettingsMenuView: View { Section("Data Settings") { Picker("Units", selection: Binding( - get: { UserDefaultsRepository.units.value }, - set: { UserDefaultsRepository.units.value = $0 } + get: { Storage.shared.units.value }, + set: { Storage.shared.units.value = $0 } )) { Text("mg/dL").tag("mg/dL") Text("mmol/L").tag("mmol/L") diff --git a/LoopFollow/Snoozer/SnoozerView.swift b/LoopFollow/Snoozer/SnoozerView.swift index b4fc5ec96..8d2717e8b 100644 --- a/LoopFollow/Snoozer/SnoozerView.swift +++ b/LoopFollow/Snoozer/SnoozerView.swift @@ -154,11 +154,11 @@ struct SnoozerView: View { return "🤷" } - if UserDefaultsRepository.getPreferredUnit() == .millimolesPerLiter, Localizer.removePeriodAndCommaForBadge(bgText.value) == "55" { + if Localizer.getPreferredUnit() == .millimolesPerLiter, Localizer.removePeriodAndCommaForBadge(bgText.value) == "55" { return "🦄" } - if UserDefaultsRepository.getPreferredUnit() == .milligramsPerDeciliter, bg == 100 { + if Localizer.getPreferredUnit() == .milligramsPerDeciliter, bg == 100 { return "🦄" } diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index e5e845459..d8227229b 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -28,11 +28,6 @@ extension Storage { item.setNil(key: item.key) } - if !UserDefaultsRepository.backgroundRefresh.value { - Storage.shared.backgroundRefreshType.value = .none - UserDefaultsRepository.backgroundRefresh.value = true - } - // Remove this in a year later than the release of the new Alarms [BEGIN] let legacyColorBGText = UserDefaultsValue(key: "colorBGText", default: true) if legacyColorBGText.exists { @@ -283,6 +278,72 @@ extension Storage { Storage.shared.alarmConfiguration.value = cfg } + // ── Dexcom Share -------------------------------------------------------- + move(UserDefaultsValue(key: "shareUserName", default: ""), + into: Storage.shared.shareUserName) + + move(UserDefaultsValue(key: "sharePassword", default: ""), + into: Storage.shared.sharePassword) + + move(UserDefaultsValue(key: "shareServer", default: "US"), + into: Storage.shared.shareServer) + + // ── Graph --------------------------------------------------------------- + moveFloatToDouble( + UserDefaultsValue(key: "chartScaleX", default: 18.0), + into: Storage.shared.chartScaleX + ) + + // ── Advanced settings --------------------------------------------------- + move(UserDefaultsValue(key: "downloadTreatments", default: true), + into: Storage.shared.downloadTreatments) + move(UserDefaultsValue(key: "downloadPrediction", default: true), + into: Storage.shared.downloadPrediction) + move(UserDefaultsValue(key: "graphOtherTreatments", default: true), + into: Storage.shared.graphOtherTreatments) + move(UserDefaultsValue(key: "graphBasal", default: true), + into: Storage.shared.graphBasal) + move(UserDefaultsValue(key: "graphBolus", default: true), + into: Storage.shared.graphBolus) + move(UserDefaultsValue(key: "graphCarbs", default: true), + into: Storage.shared.graphCarbs) + move(UserDefaultsValue(key: "bgUpdateDelay", default: 10), + into: Storage.shared.bgUpdateDelay) + + // ── Insert times -------------------------------------------------------- + move(UserDefaultsValue(key: "alertCageInsertTime", default: 0), + into: Storage.shared.cageInsertTime) + move(UserDefaultsValue(key: "alertSageInsertTime", default: 0), + into: Storage.shared.sageInsertTime) + + // ── Version-cache / notification bookkeeping --------------------------- + move(UserDefaultsValue(key: "cachedForVersion", default: nil), + into: Storage.shared.cachedForVersion) + move(UserDefaultsValue(key: "latestVersion", default: nil), + into: Storage.shared.latestVersion) + move(UserDefaultsValue(key: "latestVersionChecked", default: nil), + into: Storage.shared.latestVersionChecked) + move(UserDefaultsValue(key: "currentVersionBlackListed", default: false), + into: Storage.shared.currentVersionBlackListed) + move(UserDefaultsValue(key: "lastBlacklistNotificationShown", default: nil), + into: Storage.shared.lastBlacklistNotificationShown) + move(UserDefaultsValue(key: "lastVersionUpdateNotificationShown", default: nil), + into: Storage.shared.lastVersionUpdateNotificationShown) + move(UserDefaultsValue(key: "lastExpirationNotificationShown", default: nil), + into: Storage.shared.lastExpirationNotificationShown) + + move(UserDefaultsValue(key: "hideInfoTable", default: false), into: Storage.shared.hideInfoTable) + move(UserDefaultsValue(key: "token", default: ""), into: Storage.shared.token) + move(UserDefaultsValue(key: "units", default: "mg/dL"), into: Storage.shared.units) + + move(UserDefaultsValue<[Int]>(key: "infoSort", + default: InfoType.allCases.map { $0.sortOrder }), + into: Storage.shared.infoSort) + + move(UserDefaultsValue<[Bool]>(key: "infoVisible", + default: InfoType.allCases.map { $0.defaultVisible }), + into: Storage.shared.infoVisible) + migrateUrgentLowAlarm() migrateLowAlarm() migrateHighAlarm() diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index a1d257f02..289d06d1f 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -107,6 +107,48 @@ class Storage { var watchLine2 = StorageValue(key: "watchLine2", defaultValue: "C:%COB% I:%IOB% B:%BASAL%") // Calendar entries [END] + // MARK: - Dexcom Share -------------------------------------------------------- + + var shareUserName = StorageValue(key: "shareUserName", defaultValue: "") + var sharePassword = StorageValue(key: "sharePassword", defaultValue: "") + var shareServer = StorageValue(key: "shareServer", defaultValue: "US") + + // MARK: - Graph --------------------------------------------------------------- + + var chartScaleX = StorageValue(key: "chartScaleX", defaultValue: 18.0) + + // MARK: - Advanced settings --------------------------------------------------- + + var downloadTreatments = StorageValue(key: "downloadTreatments", defaultValue: true) + var downloadPrediction = StorageValue(key: "downloadPrediction", defaultValue: true) + var graphOtherTreatments = StorageValue(key: "graphOtherTreatments", defaultValue: true) + var graphBasal = StorageValue(key: "graphBasal", defaultValue: true) + var graphBolus = StorageValue(key: "graphBolus", defaultValue: true) + var graphCarbs = StorageValue(key: "graphCarbs", defaultValue: true) + var bgUpdateDelay = StorageValue(key: "bgUpdateDelay", defaultValue: 10) + + // MARK: - Insert times (sensor / pump) --------------------------------------- + + var cageInsertTime = StorageValue(key: "cageInsertTime", defaultValue: 0) + var sageInsertTime = StorageValue(key: "sageInsertTime", defaultValue: 0) + + // MARK: - Version-info --------------------------- + + var cachedForVersion = StorageValue(key: "cachedForVersion", defaultValue: nil) + var latestVersion = StorageValue(key: "latestVersion", defaultValue: nil) + var latestVersionChecked = StorageValue(key: "latestVersionChecked", defaultValue: nil) + var currentVersionBlackListed = StorageValue(key: "currentVersionBlackListed", defaultValue: false) + var lastBlacklistNotificationShown = StorageValue(key: "lastBlacklistNotificationShown", defaultValue: nil) + var lastVersionUpdateNotificationShown = StorageValue(key: "lastVersionUpdateNotificationShown", defaultValue: nil) + var lastExpirationNotificationShown = StorageValue(key: "lastExpirationNotificationShown", defaultValue: nil) + + var hideInfoTable = StorageValue(key: "hideInfoTable", defaultValue: false) + var token = StorageValue(key: "token", defaultValue: "") + var units = StorageValue(key: "units", defaultValue: "mg/dL") + + var infoSort = StorageValue<[Int]>(key: "infoSort", defaultValue: InfoType.allCases.map { $0.sortOrder }) + var infoVisible = StorageValue<[Bool]>(key: "infoVisible", defaultValue: InfoType.allCases.map { $0.defaultVisible }) + static let shared = Storage() private init() {} } diff --git a/LoopFollow/Storage/UserDefaults.swift b/LoopFollow/Storage/UserDefaults.swift deleted file mode 100644 index 9b55309e6..000000000 --- a/LoopFollow/Storage/UserDefaults.swift +++ /dev/null @@ -1,116 +0,0 @@ -// LoopFollow -// UserDefaults.swift -// Created by Jon Fawcett on 2020-06-05. - -import Foundation -import HealthKit -import UIKit - -/* - Legacy storage, we are moving away from this - */ - -class UserDefaultsRepository { - static let infoSort = UserDefaultsValue<[Int]>(key: "infoSort", default: InfoType.allCases.map { $0.sortOrder }) - static let infoVisible = UserDefaultsValue<[Bool]>(key: "infoVisible", default: InfoType.allCases.map { $0.defaultVisible }) - - static func synchronizeInfoTypes() { - var sortArray = infoSort.value - var visibleArray = infoVisible.value - - // Current valid indices based on InfoType - let currentValidIndices = InfoType.allCases.map { $0.rawValue } - - // Add missing indices to sortArray - for index in currentValidIndices { - if !sortArray.contains(index) { - sortArray.append(index) - // print("Added missing index \(index) to sortArray") - } - } - - // Remove deprecated indices - sortArray = sortArray.filter { currentValidIndices.contains($0) } - - // Ensure visibleArray is updated with new entries - if visibleArray.count < currentValidIndices.count { - for i in visibleArray.count ..< currentValidIndices.count { - visibleArray.append(InfoType(rawValue: i)?.defaultVisible ?? false) - // print("Added default visibility for new index \(i)") - } - } - - // Trim excess elements if there are more than needed - if visibleArray.count > currentValidIndices.count { - visibleArray = Array(visibleArray.prefix(currentValidIndices.count)) - // print("Trimmed visibleArray to match current valid indices") - } - - infoSort.value = sortArray - infoVisible.value = visibleArray - } - - static let hideInfoTable = UserDefaultsValue(key: "hideInfoTable", default: false) - - // Nightscout Settings - static let token = UserDefaultsValue(key: "token", default: "") - static let units = UserDefaultsValue(key: "units", default: "mg/dL") - - static func getPreferredUnit() -> HKUnit { - let unitString = units.value - switch unitString { - case "mmol/L": - return .millimolesPerLiter - default: - return .milligramsPerDeciliter - } - } - - static func setPreferredUnit(_ unit: HKUnit) { - var unitString = "mg/dL" - if unit == .millimolesPerLiter { - unitString = "mmol/L" - } - units.value = unitString - } - - // Dexcom Share Settings - static let shareUserName = UserDefaultsValue(key: "shareUserName", default: "") - static let sharePassword = UserDefaultsValue(key: "sharePassword", default: "") - static let shareServer = UserDefaultsValue(key: "shareServer", default: "US") - - // Graph Settings - static let chartScaleX = UserDefaultsValue(key: "chartScaleX", default: 18.0) - - // Deprecated, used to detect if backgroundRefresh was set to off. TODO: Remove in the beginning of 2026 - static let backgroundRefresh = UserDefaultsValue(key: "backgroundRefresh", default: true) - - // Advanced Settings - static let downloadTreatments = UserDefaultsValue(key: "downloadTreatments", default: true) - static let downloadPrediction = UserDefaultsValue(key: "downloadPrediction", default: true) - static let graphOtherTreatments = UserDefaultsValue(key: "graphOtherTreatments", default: true) - static let graphBasal = UserDefaultsValue(key: "graphBasal", default: true) - static let graphBolus = UserDefaultsValue(key: "graphBolus", default: true) - static let graphCarbs = UserDefaultsValue(key: "graphCarbs", default: true) - static let bgUpdateDelay = UserDefaultsValue(key: "bgUpdateDelay", default: 10) - - static let alertCageInsertTime = UserDefaultsValue(key: "alertCageInsertTime", default: 0) - static let alertSageInsertTime = UserDefaultsValue(key: "alertSageInsertTime", default: 0) - - // What version is the cache valid for - static let cachedForVersion = UserDefaultsValue(key: "cachedForVersion", default: nil) - - // Caching of latest version - static let latestVersion = UserDefaultsValue(key: "latestVersion", default: nil) - static let latestVersionChecked = UserDefaultsValue(key: "latestVersionChecked", default: nil) - - // Caching of blacklisted version - static let currentVersionBlackListed = UserDefaultsValue(key: "currentVersionBlackListed", default: false) - - // Tracking notifications to manage frequency - static let lastBlacklistNotificationShown = UserDefaultsValue(key: "lastBlacklistNotificationShown", default: nil) - static let lastVersionUpdateNotificationShown = UserDefaultsValue(key: "lastVersionUpdateNotificationShown", default: nil) - - // Tracking the last time the expiration notification was shown - static let lastExpirationNotificationShown = UserDefaultsValue(key: "lastExpirationNotificationShown", default: nil) -} diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index c987fc1ac..ac7122f56 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -22,8 +22,8 @@ extension MainViewController { let latestTempTargetEnd = self.tempTargetGraphData.last { $0.endDate <= now }?.endDate let recBolus = Observable.shared.deviceRecBolus.value let COB = self.latestCOB?.value - let sensorInsertedAt = UserDefaultsRepository.alertSageInsertTime.value - let pumpInsertTime = UserDefaultsRepository.alertCageInsertTime.value + let sensorInsertedAt = Storage.shared.sageInsertTime.value + let pumpInsertTime = Storage.shared.cageInsertTime.value let latestPumpVol = self.latestPumpVolume let bolusEntries = self.bolusData.map { BolusEntry(units: $0.value, date: Date(timeIntervalSince1970: $0.date)) } let latestBattery = Observable.shared.deviceBatteryLevel.value diff --git a/LoopFollow/Task/BGTask.swift b/LoopFollow/Task/BGTask.swift index 238b2bd41..f12d71049 100644 --- a/LoopFollow/Task/BGTask.swift +++ b/LoopFollow/Task/BGTask.swift @@ -21,16 +21,16 @@ extension MainViewController { ) // If no Dexcom credentials and no Nightscout, schedule a retry in 60 seconds. - if UserDefaultsRepository.shareUserName.value == "", - UserDefaultsRepository.sharePassword.value == "", + if Storage.shared.shareUserName.value == "", + Storage.shared.sharePassword.value == "", !IsNightscoutEnabled() { return } // If Dexcom credentials exist, fetch from DexShare - if UserDefaultsRepository.shareUserName.value != "", - UserDefaultsRepository.sharePassword.value != "" + if Storage.shared.shareUserName.value != "", + Storage.shared.sharePassword.value != "" { webLoadDexShare() } else { diff --git a/LoopFollow/Task/TreatmentsTask.swift b/LoopFollow/Task/TreatmentsTask.swift index d735d8beb..deba1d791 100644 --- a/LoopFollow/Task/TreatmentsTask.swift +++ b/LoopFollow/Task/TreatmentsTask.swift @@ -15,7 +15,7 @@ extension MainViewController { func treatmentsTaskAction() { // If Nightscout not enabled, wait 60s and try again - guard IsNightscoutEnabled(), UserDefaultsRepository.downloadTreatments.value else { + guard IsNightscoutEnabled(), Storage.shared.downloadTreatments.value else { TaskScheduler.shared.rescheduleTask(id: .treatments, to: Date().addingTimeInterval(60)) return } diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 1919e3f4a..3142b7384 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -126,7 +126,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele Storage.shared.migrate() // Synchronize info types to ensure arrays are the correct size - UserDefaultsRepository.synchronizeInfoTypes() + synchronizeInfoTypes() infoTable.rowHeight = 21 infoTable.dataSource = self @@ -139,9 +139,9 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele smallGraphHeightConstraint.constant = CGFloat(Storage.shared.smallGraphHeight.value) view.layoutIfNeeded() - let shareUserName = UserDefaultsRepository.shareUserName.value - let sharePassword = UserDefaultsRepository.sharePassword.value - let shareServer = UserDefaultsRepository.shareServer.value == "US" ?KnownShareServers.US.rawValue : KnownShareServers.NON_US.rawValue + let shareUserName = Storage.shared.shareUserName.value + let sharePassword = Storage.shared.sharePassword.value + let shareServer = Storage.shared.shareServer.value == "US" ?KnownShareServers.US.rawValue : KnownShareServers.NON_US.rawValue dexShare = ShareClient(username: shareUserName, password: sharePassword, shareServer: shareServer) // setup show/hide small graph and stats @@ -420,17 +420,17 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele // Check if the current version is blacklisted, or if there is a newer version available if isBlacklisted { - let lastBlacklistShown = UserDefaultsRepository.lastBlacklistNotificationShown.value ?? Date.distantPast + let lastBlacklistShown = Storage.shared.lastBlacklistNotificationShown.value ?? Date.distantPast if now.timeIntervalSince(lastBlacklistShown) > 86400 { // 24 hours self.versionAlert(message: "The current version has a critical issue and should be updated as soon as possible.") - UserDefaultsRepository.lastBlacklistNotificationShown.value = now - UserDefaultsRepository.lastVersionUpdateNotificationShown.value = now + Storage.shared.lastBlacklistNotificationShown.value = now + Storage.shared.lastVersionUpdateNotificationShown.value = now } } else if isNewer { - let lastVersionUpdateShown = UserDefaultsRepository.lastVersionUpdateNotificationShown.value ?? Date.distantPast + let lastVersionUpdateShown = Storage.shared.lastVersionUpdateNotificationShown.value ?? Date.distantPast if now.timeIntervalSince(lastVersionUpdateShown) > 1_209_600 { // 2 weeks self.versionAlert(message: "A new version is available: \(latestVersion ?? "Unknown"). It is recommended to update.") - UserDefaultsRepository.lastVersionUpdateNotificationShown.value = now + Storage.shared.lastVersionUpdateNotificationShown.value = now } } } @@ -450,10 +450,10 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele let weekBeforeExpiration = Calendar.current.date(byAdding: .day, value: -7, to: expirationDate)! if now >= weekBeforeExpiration { - let lastExpirationShown = UserDefaultsRepository.lastExpirationNotificationShown.value ?? Date.distantPast + let lastExpirationShown = Storage.shared.lastExpirationNotificationShown.value ?? Date.distantPast if now.timeIntervalSince(lastExpirationShown) > 86400 { // 24 hours expirationAlert() - UserDefaultsRepository.lastExpirationNotificationShown.value = now + Storage.shared.lastExpirationNotificationShown.value = now } } } @@ -493,7 +493,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } infoTable.isHidden = isHidden - if UserDefaultsRepository.hideInfoTable.value { + if Storage.shared.hideInfoTable.value { infoTable.isHidden = true } @@ -706,11 +706,47 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } if let token = debugData.token { - UserDefaultsRepository.token.value = token + Storage.shared.token.value = token } } catch { LogManager.shared.log(category: .alarm, message: "Failed to load DebugData: \(error)", isDebug: true) } } } + + private func synchronizeInfoTypes() { + var sortArray = Storage.shared.infoSort.value + var visibleArray = Storage.shared.infoVisible.value + + // Current valid indices based on InfoType + let currentValidIndices = InfoType.allCases.map { $0.rawValue } + + // Add missing indices to sortArray + for index in currentValidIndices { + if !sortArray.contains(index) { + sortArray.append(index) + // print("Added missing index \(index) to sortArray") + } + } + + // Remove deprecated indices + sortArray = sortArray.filter { currentValidIndices.contains($0) } + + // Ensure visibleArray is updated with new entries + if visibleArray.count < currentValidIndices.count { + for i in visibleArray.count ..< currentValidIndices.count { + visibleArray.append(InfoType(rawValue: i)?.defaultVisible ?? false) + // print("Added default visibility for new index \(i)") + } + } + + // Trim excess elements if there are more than needed + if visibleArray.count > currentValidIndices.count { + visibleArray = Array(visibleArray.prefix(currentValidIndices.count)) + // print("Trimmed visibleArray to match current valid indices") + } + + Storage.shared.infoSort.value = sortArray + Storage.shared.infoVisible.value = visibleArray + } } diff --git a/LoopFollow/ViewControllers/NightScoutViewController.swift b/LoopFollow/ViewControllers/NightScoutViewController.swift index 869508bb2..19fd4cb64 100644 --- a/LoopFollow/ViewControllers/NightScoutViewController.swift +++ b/LoopFollow/ViewControllers/NightScoutViewController.swift @@ -15,7 +15,7 @@ class NightscoutViewController: UIViewController { } var url = ObservableUserDefaults.shared.url.value - let token = UserDefaultsRepository.token.value + let token = Storage.shared.token.value if token != "" { url = url + "?token=" + token From 0b118befd67a0a2e60d1661824d18cc5ade3770a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 7 Jun 2025 09:44:23 +0200 Subject: [PATCH 120/138] Data migration --- .../Controllers/Nightscout/DeviceStatusLoop.swift | 2 +- .../Nightscout/DeviceStatusOpenAPS.swift | 2 +- .../Nightscout/Treatments/Overrides.swift | 2 +- LoopFollow/Helpers/NightscoutUtils.swift | 12 ++++++------ .../Nightscout/NightscoutSettingsViewModel.swift | 14 +++++++------- .../Remote/Loop/LoopNightscoutRemoteView.swift | 2 +- LoopFollow/Remote/Loop/LoopOverrideView.swift | 4 ++-- .../Nightscout/TrioNightscoutRemoteView.swift | 6 +++--- LoopFollow/Remote/RemoteViewController.swift | 12 ++++++------ .../Remote/Settings/RemoteSettingsViewModel.swift | 4 ++-- LoopFollow/Remote/TRC/OverrideView.swift | 2 +- LoopFollow/Remote/TRC/TempTargetView.swift | 2 +- LoopFollow/Storage/ObservableUserDefaults.swift | 8 ++++---- LoopFollow/Storage/Storage+Migrate.swift | 7 ++++++- LoopFollow/Storage/Storage.swift | 7 +++++++ .../ViewControllers/MainViewController.swift | 11 +++++------ .../ViewControllers/NightScoutViewController.swift | 2 +- 17 files changed, 55 insertions(+), 44 deletions(-) diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift index b3fdf3416..386850013 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift @@ -9,7 +9,7 @@ import UIKit extension MainViewController { func DeviceStatusLoop(formatter: ISO8601DateFormatter, lastLoopRecord: [String: AnyObject]) { - ObservableUserDefaults.shared.device.value = "Loop" + Storage.shared.device.value = "Loop" if Storage.shared.remoteType.value == .trc { Storage.shared.remoteType.value = .none diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift index ebe0c1725..94a55d1f8 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift @@ -8,7 +8,7 @@ import UIKit extension MainViewController { func DeviceStatusOpenAPS(formatter: ISO8601DateFormatter, lastDeviceStatus: [String: AnyObject]?, lastLoopRecord: [String: AnyObject]) { - ObservableUserDefaults.shared.device.value = lastDeviceStatus?["device"] as? String ?? "" + Storage.shared.device.value = lastDeviceStatus?["device"] as? String ?? "" if lastLoopRecord["failureReason"] != nil { LoopStatusLabel.text = "X" latestLoopStatusString = "X" diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift b/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift index b5c41f0b9..99c03bb66 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift @@ -72,7 +72,7 @@ extension MainViewController { } Observable.shared.override.value = activeOverrideNote - if ObservableUserDefaults.shared.device.value == "Trio" { + if Storage.shared.device.value == "Trio" { if let note = activeOverrideNote { infoManager.updateInfoData(type: .override, value: note) } else { diff --git a/LoopFollow/Helpers/NightscoutUtils.swift b/LoopFollow/Helpers/NightscoutUtils.swift index f0741ed11..435ba57e2 100644 --- a/LoopFollow/Helpers/NightscoutUtils.swift +++ b/LoopFollow/Helpers/NightscoutUtils.swift @@ -63,7 +63,7 @@ class NightscoutUtils { } static func executeRequest(eventType: EventType, parameters: [String: String], completion: @escaping (Result) -> Void) { - let baseURL = ObservableUserDefaults.shared.url.value + let baseURL = Storage.shared.url.value let token = Storage.shared.token.value guard let url = NightscoutUtils.constructURL(baseURL: baseURL, token: token, endpoint: eventType.endpoint, parameters: parameters) else { @@ -113,7 +113,7 @@ class NightscoutUtils { } static func executeDynamicRequest(eventType: EventType, parameters: [String: String], completion: @escaping (Result) -> Void) { - let baseURL = ObservableUserDefaults.shared.url.value + let baseURL = Storage.shared.url.value let token = Storage.shared.token.value guard let url = NightscoutUtils.constructURL(baseURL: baseURL, token: token, endpoint: eventType.endpoint, parameters: parameters) else { @@ -186,7 +186,7 @@ class NightscoutUtils { } static func verifyURLAndToken(completion: @escaping (NightscoutError?, String?, Bool, Bool) -> Void) { - let urlUser = ObservableUserDefaults.shared.url.value + let urlUser = Storage.shared.url.value let token = Storage.shared.token.value if urlUser.isEmpty { @@ -284,7 +284,7 @@ class NightscoutUtils { } static func retrieveJWTToken() async throws -> String { - let urlUser = ObservableUserDefaults.shared.url.value + let urlUser = Storage.shared.url.value let token = Storage.shared.token.value if urlUser.isEmpty { @@ -327,7 +327,7 @@ class NightscoutUtils { static func executePostRequest(eventType: EventType, body: [String: Any]) async throws -> T { let jwtToken = try await retrieveJWTToken() - let baseURL = ObservableUserDefaults.shared.url.value + let baseURL = Storage.shared.url.value guard let url = URL(string: "\(baseURL)\(eventType.endpoint)") else { throw NightscoutError.invalidURL @@ -357,7 +357,7 @@ class NightscoutUtils { static func executePostRequest(eventType: EventType, body: [String: Any]) async throws -> String { let jwtToken = try await retrieveJWTToken() - let baseURL = ObservableUserDefaults.shared.url.value + let baseURL = Storage.shared.url.value guard let url = URL(string: "\(baseURL)\(eventType.endpoint)") else { throw NightscoutError.invalidURL diff --git a/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift index 1131f8fd3..76884310e 100644 --- a/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift +++ b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift @@ -15,10 +15,10 @@ class NightscoutSettingsViewModel: ObservableObject { private var initialURL: String private var initialToken: String - @Published var nightscoutURL: String = ObservableUserDefaults.shared.url.value { + @Published var nightscoutURL: String = Storage.shared.url.value { willSet { if newValue != nightscoutURL { - ObservableUserDefaults.shared.url.value = newValue + Storage.shared.url.value = newValue triggerCheckStatus() } } @@ -40,7 +40,7 @@ class NightscoutSettingsViewModel: ObservableObject { private var checkStatusWorkItem: DispatchWorkItem? init() { - initialURL = ObservableUserDefaults.shared.url.value + initialURL = Storage.shared.url.value initialToken = Storage.shared.token.value setupDebounce() @@ -98,8 +98,8 @@ class NightscoutSettingsViewModel: ObservableObject { func checkNightscoutStatus() { NightscoutUtils.verifyURLAndToken { error, _, nsWriteAuth, nsAdminAuth in DispatchQueue.main.async { - ObservableUserDefaults.shared.nsWriteAuth.value = nsWriteAuth - ObservableUserDefaults.shared.nsAdminAuth.value = nsAdminAuth + Storage.shared.nsWriteAuth.value = nsWriteAuth + Storage.shared.nsAdminAuth.value = nsAdminAuth self.updateStatusLabel(error: error) } @@ -126,10 +126,10 @@ class NightscoutSettingsViewModel: ObservableObject { } } else { let authStatus: String - if ObservableUserDefaults.shared.nsAdminAuth.value { + if Storage.shared.nsAdminAuth.value { authStatus = "Admin" } else { - authStatus = "Read" + (ObservableUserDefaults.shared.nsWriteAuth.value ? " & Write" : "") + authStatus = "Read" + (Storage.shared.nsWriteAuth.value ? " & Write" : "") } nightscoutStatus = "OK (\(authStatus))" diff --git a/LoopFollow/Remote/Loop/LoopNightscoutRemoteView.swift b/LoopFollow/Remote/Loop/LoopNightscoutRemoteView.swift index fcf3a29f7..8b44fff2a 100644 --- a/LoopFollow/Remote/Loop/LoopNightscoutRemoteView.swift +++ b/LoopFollow/Remote/Loop/LoopNightscoutRemoteView.swift @@ -6,7 +6,7 @@ import SwiftUI struct LoopNightscoutRemoteView: View { @Environment(\.presentationMode) var presentationMode - @ObservedObject var nsAdmin = ObservableUserDefaults.shared.nsWriteAuth + @ObservedObject var nsAdmin = Storage.shared.nsWriteAuth var body: some View { NavigationView { diff --git a/LoopFollow/Remote/Loop/LoopOverrideView.swift b/LoopFollow/Remote/Loop/LoopOverrideView.swift index b69bebba8..2fcedb002 100644 --- a/LoopFollow/Remote/Loop/LoopOverrideView.swift +++ b/LoopFollow/Remote/Loop/LoopOverrideView.swift @@ -8,9 +8,9 @@ import SwiftUI struct LoopOverrideView: View { @Environment(\.presentationMode) private var presentationMode - @ObservedObject var device = ObservableUserDefaults.shared.device + @ObservedObject var device = Storage.shared.device @ObservedObject var overrideNote = Observable.shared.override - @ObservedObject var nsAdmin = ObservableUserDefaults.shared.nsWriteAuth + @ObservedObject var nsAdmin = Storage.shared.nsWriteAuth @StateObject private var viewModel = LoopOverrideViewModel() diff --git a/LoopFollow/Remote/Nightscout/TrioNightscoutRemoteView.swift b/LoopFollow/Remote/Nightscout/TrioNightscoutRemoteView.swift index e71a95757..2be504fcb 100644 --- a/LoopFollow/Remote/Nightscout/TrioNightscoutRemoteView.swift +++ b/LoopFollow/Remote/Nightscout/TrioNightscoutRemoteView.swift @@ -8,9 +8,9 @@ import SwiftUI struct TrioNightscoutRemoteView: View { private let remoteController = TrioNightscoutRemoteController() - @ObservedObject var nightscoutURL = ObservableUserDefaults.shared.url - @ObservedObject var device = ObservableUserDefaults.shared.device - @ObservedObject var nsWriteAuth = ObservableUserDefaults.shared.nsWriteAuth + @ObservedObject var nightscoutURL = Storage.shared.url + @ObservedObject var device = Storage.shared.device + @ObservedObject var nsWriteAuth = Storage.shared.nsWriteAuth @ObservedObject var tempTarget = Observable.shared.tempTarget @State private var newHKTarget = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 0.0) diff --git a/LoopFollow/Remote/RemoteViewController.swift b/LoopFollow/Remote/RemoteViewController.swift index 4095d76a3..b1b8f1b58 100644 --- a/LoopFollow/Remote/RemoteViewController.swift +++ b/LoopFollow/Remote/RemoteViewController.swift @@ -17,7 +17,7 @@ class RemoteViewController: UIViewController { cancellable = Publishers.CombineLatest( Storage.shared.remoteType.$value, - ObservableUserDefaults.shared.device.$value + Storage.shared.device.$value ) .sink { [weak self] _, _ in DispatchQueue.main.async { @@ -40,7 +40,7 @@ class RemoteViewController: UIViewController { if remoteType == .nightscout { var remoteView: AnyView - switch ObservableUserDefaults.shared.device.value { + switch Storage.shared.device.value { case "Trio": remoteView = AnyView(TrioNightscoutRemoteView()) case "Loop": @@ -51,7 +51,7 @@ class RemoteViewController: UIViewController { hostingController = UIHostingController(rootView: remoteView) } else if remoteType == .trc { - if ObservableUserDefaults.shared.device.value != "Trio" { + if Storage.shared.device.value != "Trio" { hostingController = UIHostingController( rootView: AnyView( Text("Trio Remote Control is only supported for 'Trio'") @@ -81,11 +81,11 @@ class RemoteViewController: UIViewController { hostingController.didMove(toParent: self) } - if remoteType == .nightscout, !ObservableUserDefaults.shared.nsWriteAuth.value { + if remoteType == .nightscout, !Storage.shared.nsWriteAuth.value { NightscoutUtils.verifyURLAndToken { _, _, nsWriteAuth, nsAdminAuth in DispatchQueue.main.async { - ObservableUserDefaults.shared.nsWriteAuth.value = nsWriteAuth - ObservableUserDefaults.shared.nsAdminAuth.value = nsAdminAuth + Storage.shared.nsWriteAuth.value = nsWriteAuth + Storage.shared.nsAdminAuth.value = nsAdminAuth } } } diff --git a/LoopFollow/Remote/Settings/RemoteSettingsViewModel.swift b/LoopFollow/Remote/Settings/RemoteSettingsViewModel.swift index b966bdc54..5eac3ca4e 100644 --- a/LoopFollow/Remote/Settings/RemoteSettingsViewModel.swift +++ b/LoopFollow/Remote/Settings/RemoteSettingsViewModel.swift @@ -19,7 +19,7 @@ class RemoteSettingsViewModel: ObservableObject { @Published var maxFat: HKQuantity @Published var mealWithBolus: Bool @Published var mealWithFatProtein: Bool - @Published var isTrioDevice: Bool = (ObservableUserDefaults.shared.device.value == "Trio") + @Published var isTrioDevice: Bool = (Storage.shared.device.value == "Trio") private var storage = Storage.shared private var cancellables = Set() @@ -85,7 +85,7 @@ class RemoteSettingsViewModel: ObservableObject { .sink { [weak self] in self?.storage.mealWithFatProtein.value = $0 } .store(in: &cancellables) - ObservableUserDefaults.shared.device.$value + Storage.shared.device.$value .receive(on: DispatchQueue.main) .sink { [weak self] newValue in self?.isTrioDevice = (newValue == "Trio") diff --git a/LoopFollow/Remote/TRC/OverrideView.swift b/LoopFollow/Remote/TRC/OverrideView.swift index 583d9407c..8ccff7620 100644 --- a/LoopFollow/Remote/TRC/OverrideView.swift +++ b/LoopFollow/Remote/TRC/OverrideView.swift @@ -9,7 +9,7 @@ struct OverrideView: View { @Environment(\.presentationMode) private var presentationMode private let pushNotificationManager = PushNotificationManager() - @ObservedObject var device = ObservableUserDefaults.shared.device + @ObservedObject var device = Storage.shared.device @ObservedObject var overrideNote = Observable.shared.override @State private var showAlert: Bool = false diff --git a/LoopFollow/Remote/TRC/TempTargetView.swift b/LoopFollow/Remote/TRC/TempTargetView.swift index e79cdb24d..19302076e 100644 --- a/LoopFollow/Remote/TRC/TempTargetView.swift +++ b/LoopFollow/Remote/TRC/TempTargetView.swift @@ -9,7 +9,7 @@ struct TempTargetView: View { @Environment(\.presentationMode) private var presentationMode private let pushNotificationManager = PushNotificationManager() - @ObservedObject var device = ObservableUserDefaults.shared.device + @ObservedObject var device = Storage.shared.device @ObservedObject var tempTarget = Observable.shared.tempTarget @State private var newHKTarget = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: 0.0) diff --git a/LoopFollow/Storage/ObservableUserDefaults.swift b/LoopFollow/Storage/ObservableUserDefaults.swift index 8a24f3662..ea4e7d353 100644 --- a/LoopFollow/Storage/ObservableUserDefaults.swift +++ b/LoopFollow/Storage/ObservableUserDefaults.swift @@ -12,10 +12,10 @@ import Foundation class ObservableUserDefaults { static let shared = ObservableUserDefaults() - var url = ObservableUserDefaultsValue(key: "url", default: "") - var device = ObservableUserDefaultsValue(key: "device", default: "") - var nsWriteAuth = ObservableUserDefaultsValue(key: "nsWriteAuth", default: false) - var nsAdminAuth = ObservableUserDefaultsValue(key: "nsAdminAuth", default: false) + var old_url = ObservableUserDefaultsValue(key: "url", default: "") + var old_device = ObservableUserDefaultsValue(key: "device", default: "") + var old_nsWriteAuth = ObservableUserDefaultsValue(key: "nsWriteAuth", default: false) + var old_nsAdminAuth = ObservableUserDefaultsValue(key: "nsAdminAuth", default: false) private init() {} } diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index d8227229b..9af26d51c 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -5,7 +5,12 @@ import Foundation extension Storage { - func migrate() { + func migrateStep1() { + Storage.shared.url.value = ObservableUserDefaults.shared.old_url.value + Storage.shared.device.value = ObservableUserDefaults.shared.old_device.value + Storage.shared.nsWriteAuth.value = ObservableUserDefaults.shared.old_nsWriteAuth.value + Storage.shared.nsAdminAuth.value = ObservableUserDefaults.shared.old_nsAdminAuth.value + // Helper: 1-to-1 type ----------------------------------------------------------------- func move( _ legacy: @autoclosure () -> UserDefaultsValue, diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index 289d06d1f..80a50cdcc 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -149,6 +149,13 @@ class Storage { var infoSort = StorageValue<[Int]>(key: "infoSort", defaultValue: InfoType.allCases.map { $0.sortOrder }) var infoVisible = StorageValue<[Bool]>(key: "infoVisible", defaultValue: InfoType.allCases.map { $0.defaultVisible }) + var url = StorageValue(key: "url", defaultValue: "") + var device = StorageValue(key: "device", defaultValue: "") + var nsWriteAuth = StorageValue(key: "nsWriteAuth", defaultValue: false) + var nsAdminAuth = StorageValue(key: "nsAdminAuth", defaultValue: false) + + var migrationStep = StorageValue(key: "migrationStep", defaultValue: 0) + static let shared = Storage() private init() {} } diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 3142b7384..801564a10 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -13,7 +13,7 @@ import UIKit import UserNotifications func IsNightscoutEnabled() -> Bool { - return !ObservableUserDefaults.shared.url.value.isEmpty + return !Storage.shared.url.value.isEmpty } class MainViewController: UIViewController, UITableViewDataSource, ChartViewDelegate, UNUserNotificationCenterDelegate, UIScrollViewDelegate { @@ -119,12 +119,11 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele loadDebugData() - if ObservableUserDefaults.shared.device.value != "Trio" && Storage.shared.remoteType.value == .trc { - Storage.shared.remoteType.value = .none + if Storage.shared.migrationStep.value < 1 { + Storage.shared.migrateStep1() + Storage.shared.migrationStep.value = 1 } - Storage.shared.migrate() - // Synchronize info types to ensure arrays are the correct size synchronizeInfoTypes() @@ -702,7 +701,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } if let url = debugData.url { - ObservableUserDefaults.shared.url.value = url + Storage.shared.url.value = url } if let token = debugData.token { diff --git a/LoopFollow/ViewControllers/NightScoutViewController.swift b/LoopFollow/ViewControllers/NightScoutViewController.swift index 19fd4cb64..ad12bcb1d 100644 --- a/LoopFollow/ViewControllers/NightScoutViewController.swift +++ b/LoopFollow/ViewControllers/NightScoutViewController.swift @@ -14,7 +14,7 @@ class NightscoutViewController: UIViewController { overrideUserInterfaceStyle = .dark } - var url = ObservableUserDefaults.shared.url.value + var url = Storage.shared.url.value let token = Storage.shared.token.value if token != "" { From bc025eb45945968bea1a10f15137d1db962fa52a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 7 Jun 2025 09:55:04 +0200 Subject: [PATCH 121/138] High/low limits for not looping --- .../Editors/NotLoopingAlarmEditor.swift | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift index f58d068e6..543ff2393 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift @@ -7,6 +7,8 @@ import SwiftUI struct NotLoopingAlarmEditor: View { @Binding var alarm: Alarm + private let bgRange: ClosedRange = 40 ... 300 + var body: some View { Form { InfoBanner( @@ -30,6 +32,24 @@ struct NotLoopingAlarmEditor: View { ) ) + AlarmBGLimitSection( + header: "Low Limit", + footer: "Alert only if BG is equal to or below this value.", + toggleText: "Enable low limit", + pickerTitle: "Below", + range: bgRange, + value: $alarm.belowBG + ) + + AlarmBGLimitSection( + header: "High Limit", + footer: "Alert only if BG is equal to or above this value.", + toggleText: "Enable high limit", + pickerTitle: "Above", + range: bgRange, + value: $alarm.aboveBG + ) + AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection(alarm: $alarm) From a89a65441d28f703a01ab7a466ed4673977a5f1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 7 Jun 2025 12:30:36 +0200 Subject: [PATCH 122/138] Default value for bglimitqs --- .../Components/AlarmBGLimitSection.swift | 32 ++++++++++++------- .../Editors/FastDropAlarmEditor.swift | 1 + .../Editors/FastRiseAlarmEditor.swift | 1 + .../Editors/MissedBolusAlarmEditor.swift | 1 + .../Editors/NotLoopingAlarmEditor.swift | 2 ++ 5 files changed, 26 insertions(+), 11 deletions(-) diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift index fb1f0055e..15442d87c 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift @@ -1,16 +1,16 @@ -// LoopFollow -// AlarmBGLimitSection.swift -// Created by Jonas Björkert on 2025-05-14. - import SwiftUI struct AlarmBGLimitSection: View { + // ────────── Public API ────────── let header: String? let footer: String? let toggleText: String let pickerTitle: String let range: ClosedRange + let defaultOnValue: Double + @Binding var value: Double? + // ──────────────────────────────── init( header: String? = nil, @@ -18,21 +18,29 @@ struct AlarmBGLimitSection: View { toggleText: String, pickerTitle: String, range: ClosedRange, + defaultOnValue: Double? = nil, value: Binding ) { - self.header = header - self.footer = footer - self.toggleText = toggleText - self.pickerTitle = pickerTitle - self.range = range + self.header = header + self.footer = footer + self.toggleText = toggleText + self.pickerTitle = pickerTitle + self.range = range + if let v = defaultOnValue, range.contains(v) { + self.defaultOnValue = v + } else { + self.defaultOnValue = range.lowerBound + } _value = value } + // MARK: - Private bindings + private var isOn: Binding { Binding( get: { value != nil }, set: { on in - if on, value == nil { value = range.lowerBound } + if on, value == nil { value = defaultOnValue } if !on { value = nil } } ) @@ -40,11 +48,13 @@ struct AlarmBGLimitSection: View { private var pickerValue: Binding { Binding( - get: { value ?? range.lowerBound }, + get: { value ?? defaultOnValue }, set: { newVal in value = newVal } ) } + // MARK: - Body + var body: some View { Section( header: header.map(Text.init), diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift index c53ba74d3..05265e31f 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift @@ -46,6 +46,7 @@ struct FastDropAlarmEditor: View { toggleText: "Use BG Limit", pickerTitle: "Dropping below", range: 40 ... 300, + defaultOnValue: 120, value: $alarm.belowBG ) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift index e0aec3a86..1e8c6aafc 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift @@ -49,6 +49,7 @@ struct FastRiseAlarmEditor: View { toggleText: "Use BG Limit", pickerTitle: "Rising above", range: 40 ... 300, + defaultOnValue: 200, value: $alarm.aboveBG ) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift index 64cefb58d..4004d8686 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift @@ -78,6 +78,7 @@ struct MissedBolusAlarmEditor: View { toggleText: "Use BG Limit", pickerTitle: "Above", range: 40 ... 140, + defaultOnValue: 70, value: $alarm.aboveBG ) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift index 543ff2393..d710161f2 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift @@ -38,6 +38,7 @@ struct NotLoopingAlarmEditor: View { toggleText: "Enable low limit", pickerTitle: "Below", range: bgRange, + defaultOnValue: 100, value: $alarm.belowBG ) @@ -47,6 +48,7 @@ struct NotLoopingAlarmEditor: View { toggleText: "Enable high limit", pickerTitle: "Above", range: bgRange, + defaultOnValue: 160, value: $alarm.aboveBG ) From d5c34bb308d7349e01a321b2dfdd7131c1676bbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sat, 7 Jun 2025 12:32:29 +0200 Subject: [PATCH 123/138] Default value for bglimit --- .../Components/AlarmBGLimitSection.swift | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift index 15442d87c..2ed32e6a6 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift @@ -1,3 +1,7 @@ +// LoopFollow +// AlarmBGLimitSection.swift +// Created by Jonas Björkert on 2025-05-14. + import SwiftUI struct AlarmBGLimitSection: View { @@ -21,11 +25,11 @@ struct AlarmBGLimitSection: View { defaultOnValue: Double? = nil, value: Binding ) { - self.header = header - self.footer = footer - self.toggleText = toggleText - self.pickerTitle = pickerTitle - self.range = range + self.header = header + self.footer = footer + self.toggleText = toggleText + self.pickerTitle = pickerTitle + self.range = range if let v = defaultOnValue, range.contains(v) { self.defaultOnValue = v } else { From 13905210bcc488d1f3abdc5b79a17fc807e6ca53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 9 Jun 2025 10:04:51 +0200 Subject: [PATCH 124/138] Default values --- LoopFollow/Alarm/Alarm.swift | 27 +++++++++ .../Components/AlarmStepperSection.swift | 58 +++++++++++++++++-- .../Editors/BatteryAlarmEditor.swift | 5 +- .../Editors/BatteryDropAlarmEditor.swift | 10 +--- .../Editors/BuildExpireAlarmEditor.swift | 5 +- .../AlarmEditing/Editors/COBAlarmEditor.swift | 5 +- .../Editors/FastDropAlarmEditor.swift | 8 +-- .../Editors/FastRiseAlarmEditor.swift | 7 +-- .../Editors/HighBgAlarmEditor.swift | 7 +-- .../AlarmEditing/Editors/IOBAlarmEditor.swift | 20 ++----- .../Editors/LowBgAlarmEditor.swift | 10 +--- .../Editors/MissedBolusAlarmEditor.swift | 20 ++----- .../Editors/MissedReadingEditor.swift | 5 +- .../Editors/NotLoopingAlarmEditor.swift | 5 +- .../Editors/PumpChangeAlarmEditor.swift | 5 +- .../Editors/PumpVolumeAlarmEditor.swift | 5 +- .../Editors/RecBolusAlarmEditor.swift | 5 +- .../Editors/SensorAgeAlarmEditor.swift | 5 +- LoopFollow/Storage/Storage+Migrate.swift | 4 +- LoopFollow/Task/AlarmTask.swift | 1 + 20 files changed, 110 insertions(+), 107 deletions(-) diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index 71b83c4b1..d986b3bf3 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -217,34 +217,61 @@ struct Alarm: Identifiable, Codable, Equatable { repeatSoundOption = .always case .low: soundFile = .indeed + belowBG = 80 + persistentMinutes = 0 + predictiveMinutes = 0 case .iob: soundFile = .alertToneRingtone1 + delta = 1 + monitoringWindow = 2 + predictiveMinutes = 30 + threshold = 6 case .cob: soundFile = .alertToneRingtone2 + threshold = 20 case .high: soundFile = .timeHasCome + aboveBG = 180 + persistentMinutes = 0 case .fastDrop: soundFile = .bigClockTicking + delta = 18 + monitoringWindow = 2 case .fastRise: soundFile = .cartoonFailStringsTrumpet + delta = 10 + monitoringWindow = 3 case .missedReading: soundFile = .cartoonTipToeSneakyWalk + threshold = 16 case .notLooping: soundFile = .sciFiEngineShutDown + threshold = 31 case .missedBolus: soundFile = .dholShuffleloop + monitoringWindow = 15 + predictiveMinutes = 15 + delta = 0.1 + threshold = 4 case .sensorChange: soundFile = .wakeUpWillYou + threshold = 12 case .pumpChange: soundFile = .wakeUpWillYou + threshold = 12 case .pump: soundFile = .marimbaDescend + threshold = 20 case .battery: soundFile = .machineCharge + threshold = 20 case .batteryDrop: soundFile = .machineCharge + delta = 10 + monitoringWindow = 15 case .recBolus: soundFile = .dholShuffleloop + threshold = 1 case .overrideStart: soundFile = .endingReached repeatSoundOption = .never diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift index b5f7829ce..0c222bd81 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift @@ -5,13 +5,21 @@ import SwiftUI struct AlarmStepperSection: View { + // MARK: – public parameters + let header: String? let footer: String? let title: String let range: ClosedRange let step: Double let unitLabel: String? - @Binding var value: Double + + // MARK: – private binding (always Double?) + + @Binding + private var value: Double? + + // MARK: – designated initialiser (Double?) init( header: String? = nil, @@ -20,7 +28,7 @@ struct AlarmStepperSection: View { range: ClosedRange, step: Double, unitLabel: String? = nil, - value: Binding + value: Binding ) { self.header = header self.footer = footer @@ -31,17 +39,57 @@ struct AlarmStepperSection: View { _value = value } + // MARK: – convenience initialiser (Int?) + + /// Same API but for **`Binding`** — it bridges to Double internally. + init( + header: String? = nil, + footer: String? = nil, + title: String, + range: ClosedRange, + step: Double, + unitLabel: String? = nil, + value intValue: Binding + ) { + self.init( + header: header, + footer: footer, + title: title, + range: range, + step: step, + unitLabel: unitLabel, + value: Binding( + get: { intValue.wrappedValue.map(Double.init) }, + set: { newVal in intValue.wrappedValue = newVal.map(Int.init) } + ) + ) + } + + // MARK: – derived non-optional Binding + + private var nonOptional: Binding { + Binding( + get: { value ?? range.lowerBound }, + set: { newVal in value = newVal } + ) + } + + // MARK: – view + var body: some View { Section( header: header.map(Text.init), footer: footer.map(Text.init) ) { - Stepper(value: $value, in: range, step: step) { + Stepper(value: nonOptional, in: range, step: step) { HStack { Text(title) Spacer() - Text("\(Int(value))\(unitLabel.map { " \($0)" } ?? "")") - .foregroundColor(.secondary) + Text( + "\(Int(nonOptional.wrappedValue))" + + (unitLabel.map { " \($0)" } ?? "") + ) + .foregroundColor(.secondary) } } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift index e85f6c8a7..71eac5b69 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift @@ -23,10 +23,7 @@ struct BatteryAlarmEditor: View { range: 0 ... 100, step: 5, unitLabel: "%", - value: Binding( - get: { alarm.threshold ?? 20 }, - set: { alarm.threshold = $0 } - ) + value: $alarm.threshold ) AlarmActiveSection(alarm: $alarm) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift index 676e27b80..2aa2e3fad 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift @@ -23,10 +23,7 @@ struct BatteryDropAlarmEditor: View { range: 5 ... 100, step: 5, unitLabel: "%", - value: Binding( - get: { alarm.delta ?? 10 }, - set: { alarm.delta = $0 } - ) + value: $alarm.delta ) AlarmStepperSection( @@ -36,10 +33,7 @@ struct BatteryDropAlarmEditor: View { range: 5 ... 30, step: 5, unitLabel: "min", - value: Binding( - get: { Double(alarm.monitoringWindow ?? 15) }, - set: { alarm.monitoringWindow = Int($0) } - ) + value: $alarm.monitoringWindow ) AlarmActiveSection(alarm: $alarm) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift index 44d0b96d3..8fe05955b 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift @@ -22,10 +22,7 @@ struct BuildExpireAlarmEditor: View { range: 1 ... 14, step: 1, unitLabel: alarm.type.snoozeTimeUnit.label, - value: Binding( - get: { alarm.threshold ?? 1 }, - set: { alarm.threshold = $0 } - ) + value: $alarm.threshold ) AlarmActiveSection(alarm: $alarm) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift index 5dd514d62..e6ebd07c9 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift @@ -23,10 +23,7 @@ struct COBAlarmEditor: View { range: 1 ... 200, step: 1, unitLabel: "g", - value: Binding( - get: { alarm.threshold ?? 20 }, - set: { alarm.threshold = $0 } - ) + value: $alarm.threshold ) AlarmActiveSection(alarm: $alarm) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift index 05265e31f..47fb69a7f 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift @@ -22,22 +22,18 @@ struct FastDropAlarmEditor: View { title: "Falls by", range: 3 ... 54, value: Binding( - get: { alarm.delta ?? 18 }, + get: { alarm.delta ?? 18 }, // This value is not used, the default value is set on the alarm set: { alarm.delta = $0 } ) ) - // TODO: In the migration script, use 1 value less than stored since we are switching from readings to drops AlarmStepperSection( header: "Consecutive Drops", footer: "Number of drops—each meeting the rate above—required before an alert fires.", title: "Number of Drops", range: 1 ... 3, step: 1, - value: Binding( - get: { Double(alarm.monitoringWindow ?? 2) }, - set: { alarm.monitoringWindow = Int($0) } - ) + value: $alarm.monitoringWindow ) AlarmBGLimitSection( diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift index 1e8c6aafc..f348e945b 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift @@ -24,7 +24,7 @@ struct FastRiseAlarmEditor: View { title: "Rises by", range: 3 ... 54, value: Binding( - get: { alarm.delta ?? 3 }, + get: { alarm.delta ?? 10 }, // This value has not effect since it is set as default on the alarm set: { alarm.delta = $0 } ) ) @@ -36,10 +36,7 @@ struct FastRiseAlarmEditor: View { title: "Rises in a row", range: 1 ... 3, step: 1, - value: Binding( - get: { Double(alarm.monitoringWindow ?? 2) }, - set: { alarm.monitoringWindow = Int($0) } - ) + value: $alarm.monitoringWindow ) AlarmBGLimitSection( diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift index 6c31fd530..786aea9ac 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift @@ -22,7 +22,7 @@ struct HighBgAlarmEditor: View { title: "BG", range: 120 ... 350, value: Binding( - get: { alarm.aboveBG ?? 180 }, + get: { alarm.aboveBG ?? 180 }, // This value is not used, default is set on the alarm type set: { alarm.aboveBG = $0 } ) ) @@ -35,10 +35,7 @@ struct HighBgAlarmEditor: View { range: 0 ... 120, step: 5, unitLabel: alarm.type.snoozeTimeUnit.label, - value: Binding( - get: { Double(alarm.persistentMinutes ?? 0) }, - set: { alarm.persistentMinutes = Int($0) } - ) + value: $alarm.persistentMinutes ) AlarmActiveSection(alarm: $alarm) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift index d6d85b44a..6f9593898 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift @@ -24,10 +24,7 @@ struct IOBAlarmEditor: View { range: 0.1 ... 20, step: 0.1, unitLabel: "Units", - value: Binding( - get: { alarm.delta ?? 1.0 }, - set: { alarm.delta = $0 } - ) + value: $alarm.delta ) AlarmStepperSection( @@ -37,10 +34,7 @@ struct IOBAlarmEditor: View { range: 1 ... 10, step: 1, unitLabel: "Boluses", - value: Binding( - get: { Double(alarm.monitoringWindow ?? 2) }, - set: { alarm.monitoringWindow = Int($0) } - ) + value: $alarm.monitoringWindow ) AlarmStepperSection( @@ -50,10 +44,7 @@ struct IOBAlarmEditor: View { range: 5 ... 120, step: 5, unitLabel: "min", - value: Binding( - get: { Double(alarm.predictiveMinutes ?? 30) }, - set: { alarm.predictiveMinutes = Int($0) } - ) + value: $alarm.predictiveMinutes ) AlarmStepperSection( @@ -63,10 +54,7 @@ struct IOBAlarmEditor: View { range: 1 ... 20, step: 0.5, unitLabel: "Units", - value: Binding( - get: { alarm.threshold ?? 6 }, - set: { alarm.threshold = $0 } - ) + value: $alarm.threshold ) AlarmActiveSection(alarm: $alarm) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift index 0439db0fa..381456f00 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift @@ -32,10 +32,7 @@ struct LowBgAlarmEditor: View { range: 0 ... 120, step: 5, unitLabel: alarm.type.snoozeTimeUnit.label, - value: Binding( - get: { Double(alarm.persistentMinutes ?? 0) }, - set: { alarm.persistentMinutes = Int($0) } - ) + value: $alarm.persistentMinutes ) AlarmStepperSection( @@ -47,10 +44,7 @@ struct LowBgAlarmEditor: View { range: 0 ... 60, step: 5, unitLabel: alarm.type.snoozeTimeUnit.label, - value: Binding( - get: { Double(alarm.predictiveMinutes ?? 0) }, - set: { alarm.predictiveMinutes = Int($0) } - ) + value: $alarm.predictiveMinutes ) AlarmActiveSection(alarm: $alarm) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift index 4004d8686..e03b52e60 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift @@ -26,10 +26,7 @@ struct MissedBolusAlarmEditor: View { range: 5 ... 60, step: 5, unitLabel: "min", - value: Binding( - get: { Double(alarm.monitoringWindow ?? 15) }, - set: { alarm.monitoringWindow = Int($0) } - ) + value: $alarm.monitoringWindow ) AlarmStepperSection( @@ -40,10 +37,7 @@ struct MissedBolusAlarmEditor: View { range: 0 ... 45, step: 5, unitLabel: "min", - value: Binding( - get: { Double(alarm.predictiveMinutes ?? 15) }, - set: { alarm.predictiveMinutes = Int($0) } - ) + value: $alarm.predictiveMinutes ) AlarmStepperSection( @@ -53,10 +47,7 @@ struct MissedBolusAlarmEditor: View { range: 0.05 ... 2, step: 0.05, unitLabel: "Units", - value: Binding( - get: { alarm.delta ?? 0.1 }, - set: { alarm.delta = $0 } - ) + value: $alarm.delta ) AlarmStepperSection( @@ -66,10 +57,7 @@ struct MissedBolusAlarmEditor: View { range: 0 ... 15, step: 1, unitLabel: "Grams", - value: Binding( - get: { alarm.threshold ?? 4 }, - set: { alarm.threshold = $0 } - ) + value: $alarm.threshold ) AlarmBGLimitSection( diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift index 525bcf715..4a6430efe 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift @@ -20,10 +20,7 @@ struct MissedReadingEditor: View { range: 11 ... 121, step: 5, unitLabel: alarm.type.snoozeTimeUnit.label, - value: Binding( - get: { alarm.threshold ?? 16 }, - set: { alarm.threshold = $0 } - ) + value: $alarm.threshold ) AlarmActiveSection(alarm: $alarm) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift index d710161f2..649dad7df 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift @@ -26,10 +26,7 @@ struct NotLoopingAlarmEditor: View { range: 16 ... 61, step: 5, unitLabel: alarm.type.snoozeTimeUnit.label, - value: Binding( - get: { alarm.threshold ?? 31 }, - set: { alarm.threshold = $0 } - ) + value: $alarm.threshold ) AlarmBGLimitSection( diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift index 4d7f47ac7..090b38062 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift @@ -25,10 +25,7 @@ struct PumpChangeAlarmEditor: View { range: 1 ... 24, step: 1, unitLabel: "Hours", - value: Binding( - get: { alarm.threshold ?? 12 }, - set: { alarm.threshold = $0 } - ) + value: $alarm.threshold ) AlarmActiveSection(alarm: $alarm) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift index 0a0bb5378..a2e375b46 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift @@ -24,10 +24,7 @@ struct PumpVolumeAlarmEditor: View { range: 1 ... 50, step: 1, unitLabel: "Units", - value: Binding( - get: { alarm.threshold ?? 20 }, - set: { alarm.threshold = $0 } - ) + value: $alarm.threshold ) AlarmActiveSection(alarm: $alarm) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift index f31692c15..61785b42a 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift @@ -24,10 +24,7 @@ struct RecBolusAlarmEditor: View { range: 0.1 ... 50, step: 0.1, unitLabel: "Units", - value: Binding( - get: { alarm.threshold ?? 1.0 }, - set: { alarm.threshold = $0 } - ) + value: $alarm.threshold ) AlarmActiveSection(alarm: $alarm) diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift index f2282ad42..c0d23e75b 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift @@ -24,10 +24,7 @@ struct SensorAgeAlarmEditor: View { range: 1 ... 24, step: 1, unitLabel: "hours", - value: Binding( - get: { alarm.threshold ?? 12 }, - set: { alarm.threshold = $0 } - ) + value: $alarm.threshold ) AlarmActiveSection(alarm: $alarm) diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index 9af26d51c..09d53dc2c 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -614,7 +614,7 @@ extension Storage { // core trigger parameters alarm.delta = Double(take("alertFastDropDelta", default: 10.0)) - alarm.monitoringWindow = take("alertFastDropReadings", default: 3) // store #readings + alarm.monitoringWindow = take("alertFastDropReadings", default: 3) - 1// store #readings if take("alertFastDropUseLimit", default: false) { alarm.belowBG = Double(take("alertFastDropBelowBG", default: 120.0)) } @@ -726,7 +726,7 @@ extension Storage { alarm.isEnabled = take("alertMissedReadingActive", default: false) // “No CGM data for X minutes” - alarm.persistentMinutes = take("alertMissedReading", default: 31) + alarm.threshold = take("alertMissedReading", default: 31) // snoozing alarm.snoozeDuration = take("alertMissedReadingSnooze", default: 30) diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index ac7122f56..db2c35170 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -63,6 +63,7 @@ extension MainViewController { } LogManager.shared.log(category: .alarm, message: "Checking alarms based on \(finalAlarmData)", isDebug: true) + LogManager.shared.log(category: .alarm, message: "Alarms \(Storage.shared.alarms.value)", isDebug: true) AlarmManager.shared.checkAlarms(data: finalAlarmData) From e0daae6b7b13d3573ddb509b799daa9eec85c983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 9 Jun 2025 12:06:56 +0200 Subject: [PATCH 125/138] Default values --- .../Components/AlarmBGSection.swift | 24 ++++++++++++++++--- .../Editors/FastDropAlarmEditor.swift | 5 +--- .../Editors/FastRiseAlarmEditor.swift | 5 +--- .../Editors/HighBgAlarmEditor.swift | 5 +--- .../Editors/LowBgAlarmEditor.swift | 5 +--- LoopFollow/Storage/Storage+Migrate.swift | 2 +- 6 files changed, 26 insertions(+), 20 deletions(-) diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift index 16066bcab..bd5ffb705 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift @@ -6,18 +6,25 @@ import HealthKit import SwiftUI struct AlarmBGSection: View { + // MARK: – public parameters + let header: String? let footer: String? let title: String let range: ClosedRange - @Binding var value: Double + + // MARK: – underlying optional binding + + @Binding private var value: Double? + + // MARK: – designated initialiser init( header: String? = nil, footer: String? = nil, title: String, range: ClosedRange, - value: Binding + value: Binding ) { self.header = header self.footer = footer @@ -26,6 +33,17 @@ struct AlarmBGSection: View { _value = value } + // MARK: – derived non-optional binding + + private var nonOptional: Binding { + Binding( + get: { value ?? range.lowerBound }, + set: { newVal in value = newVal } + ) + } + + // MARK: – view + var body: some View { Section( header: header.map(Text.init), @@ -34,7 +52,7 @@ struct AlarmBGSection: View { BGPicker( title: title, range: range, - value: $value + value: nonOptional ) } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift index 47fb69a7f..88bcf732b 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift @@ -21,10 +21,7 @@ struct FastDropAlarmEditor: View { footer: "This is how much the glucose must drop to be considered a fast drop.", title: "Falls by", range: 3 ... 54, - value: Binding( - get: { alarm.delta ?? 18 }, // This value is not used, the default value is set on the alarm - set: { alarm.delta = $0 } - ) + value: $alarm.delta ) AlarmStepperSection( diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift index f348e945b..a907212a0 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift @@ -23,10 +23,7 @@ struct FastRiseAlarmEditor: View { footer: "This is how much the glucose must rise to be considered a fast rise.", title: "Rises by", range: 3 ... 54, - value: Binding( - get: { alarm.delta ?? 10 }, // This value has not effect since it is set as default on the alarm - set: { alarm.delta = $0 } - ) + value: $alarm.delta ) AlarmStepperSection( diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift index 786aea9ac..bb02ff6e5 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift @@ -21,10 +21,7 @@ struct HighBgAlarmEditor: View { footer: "The alert becomes eligible once any reading is at or above this value.", title: "BG", range: 120 ... 350, - value: Binding( - get: { alarm.aboveBG ?? 180 }, // This value is not used, default is set on the alarm type - set: { alarm.aboveBG = $0 } - ) + value: $alarm.aboveBG ) AlarmStepperSection( diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift index 381456f00..69b7a1f81 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift @@ -18,10 +18,7 @@ struct LowBgAlarmEditor: View { footer: "Alert when any reading or prediction is at or below this value.", title: "BG", range: 40 ... 150, - value: Binding( - get: { alarm.belowBG ?? 80 }, - set: { alarm.belowBG = $0 } - ) + value: $alarm.belowBG ) AlarmStepperSection( diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index 09d53dc2c..78478937c 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -614,7 +614,7 @@ extension Storage { // core trigger parameters alarm.delta = Double(take("alertFastDropDelta", default: 10.0)) - alarm.monitoringWindow = take("alertFastDropReadings", default: 3) - 1// store #readings + alarm.monitoringWindow = take("alertFastDropReadings", default: 3) - 1 // store #readings if take("alertFastDropUseLimit", default: false) { alarm.belowBG = Double(take("alertFastDropBelowBG", default: 120.0)) } From 57232f4e88dc9e8d9ccc784f009f79d00873657a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 9 Jun 2025 12:35:47 +0200 Subject: [PATCH 126/138] Task scheduling of alarms --- LoopFollow/Controllers/Nightscout/BGData.swift | 1 + LoopFollow/Controllers/Nightscout/DeviceStatus.swift | 1 + LoopFollow/Task/TaskScheduler.swift | 8 ++++---- LoopFollow/Task/TreatmentsTask.swift | 1 + 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/LoopFollow/Controllers/Nightscout/BGData.swift b/LoopFollow/Controllers/Nightscout/BGData.swift index ae8868dac..a2788fecb 100644 --- a/LoopFollow/Controllers/Nightscout/BGData.swift +++ b/LoopFollow/Controllers/Nightscout/BGData.swift @@ -171,6 +171,7 @@ extension MainViewController { LogManager.shared.log(category: .nightscout, message: "Fresh reading. Scheduling next fetch in \(delayToSchedule) seconds.", isDebug: true) + TaskScheduler.shared.rescheduleTask(id: .alarmCheck, to: Date()) } TaskScheduler.shared.rescheduleTask(id: .fetchBG, to: Date().addingTimeInterval(delayToSchedule)) diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift index f9840de7e..ff54135be 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift @@ -192,6 +192,7 @@ extension MainViewController { id: .deviceStatus, to: Date().addingTimeInterval(interval) ) + TaskScheduler.shared.rescheduleTask(id: .alarmCheck, to: Date()) } } diff --git a/LoopFollow/Task/TaskScheduler.swift b/LoopFollow/Task/TaskScheduler.swift index ed258e8a0..86d11e52b 100644 --- a/LoopFollow/Task/TaskScheduler.swift +++ b/LoopFollow/Task/TaskScheduler.swift @@ -43,7 +43,7 @@ class TaskScheduler { } func rescheduleTask(id: TaskID, to newRunDate: Date) { - let timeString = formatTime(newRunDate) + // let timeString = formatTime(newRunDate) // LogManager.shared.log(category: .taskScheduler, message: "Reschedule Task \(id): next run = \(timeString)", isDebug: true) queue.async { @@ -97,9 +97,9 @@ class TaskScheduler { continue } - // Skip alarm checks if data-fetching tasks (deviceStatus, treatments, fetchBG) are currently due or just executed. + // Skip alarm checks if data-fetching tasks (deviceStatus, treatments, fetchBG) are currently due. // This ensures alarms are evaluated with the latest data, avoiding premature or incorrect triggers. - // If skipped, reschedule alarmCheck 5 seconds later to retry after data updates. + // If skipped, reschedule alarmCheck 1 second later to retry after data updates. if taskID == .alarmCheck { let shouldSkip = tasksToSkipAlarmCheck.contains { guard let checkTask = tasks[$0] else { return false } @@ -107,7 +107,7 @@ class TaskScheduler { } if shouldSkip { guard var existingTask = tasks[taskID] else { continue } - existingTask.nextRun = Date().addingTimeInterval(5) + existingTask.nextRun = Date().addingTimeInterval(1) tasks[taskID] = existingTask continue } diff --git a/LoopFollow/Task/TreatmentsTask.swift b/LoopFollow/Task/TreatmentsTask.swift index deba1d791..40c3954cd 100644 --- a/LoopFollow/Task/TreatmentsTask.swift +++ b/LoopFollow/Task/TreatmentsTask.swift @@ -23,5 +23,6 @@ extension MainViewController { WebLoadNSTreatments() TaskScheduler.shared.rescheduleTask(id: .treatments, to: Date().addingTimeInterval(2 * 60)) + TaskScheduler.shared.rescheduleTask(id: .alarmCheck, to: Date()) } } From 779fe9a941784599bab03779576136f0aaf8354e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 9 Jun 2025 16:32:01 +0200 Subject: [PATCH 127/138] Delete alarm button --- LoopFollow.xcodeproj/project.pbxproj | 4 + .../Alarm/AlarmEditing/AlarmEditor.swift | 89 ++++++++++--------- .../Components/DeleteAlarmSection.swift | 32 +++++++ .../Editors/BatteryAlarmEditor.swift | 3 +- .../Editors/BatteryDropAlarmEditor.swift | 3 +- .../Editors/BuildExpireAlarmEditor.swift | 3 +- .../AlarmEditing/Editors/COBAlarmEditor.swift | 3 +- .../Editors/FastDropAlarmEditor.swift | 3 +- .../Editors/FastRiseAlarmEditor.swift | 3 +- .../Editors/HighBgAlarmEditor.swift | 3 +- .../AlarmEditing/Editors/IOBAlarmEditor.swift | 3 +- .../Editors/LowBgAlarmEditor.swift | 3 +- .../Editors/MissedBolusAlarmEditor.swift | 3 +- .../Editors/MissedReadingEditor.swift | 3 +- .../Editors/NotLoopingAlarmEditor.swift | 3 +- .../Editors/OverrideEndAlarmEditor.swift | 3 +- .../Editors/OverrideStartAlarmEditor.swift | 3 +- .../Editors/PumpChangeAlarmEditor.swift | 3 +- .../Editors/PumpVolumeAlarmEditor.swift | 3 +- .../Editors/RecBolusAlarmEditor.swift | 3 +- .../Editors/SensorAgeAlarmEditor.swift | 3 +- .../Editors/TempTargetEndAlarmEditor.swift | 3 +- .../Editors/TempTargetStartAlarmEditor.swift | 3 +- .../Editors/TemporaryAlarmEditor.swift | 3 +- LoopFollow/Alarm/AlarmListView.swift | 4 + 25 files changed, 106 insertions(+), 86 deletions(-) create mode 100644 LoopFollow/Alarm/AlarmEditing/Components/DeleteAlarmSection.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index ca5511348..27806e6e3 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -187,6 +187,7 @@ DDD10F052C529DA200D76A8E /* ObservableValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD10F042C529DA200D76A8E /* ObservableValue.swift */; }; DDD10F072C529DE800D76A8E /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD10F062C529DE800D76A8E /* Observable.swift */; }; DDD10F0B2C54192A00D76A8E /* TemporaryTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD10F0A2C54192A00D76A8E /* TemporaryTarget.swift */; }; + DDDB86F12DF7223C00AADDAC /* DeleteAlarmSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB86F02DF7223C00AADDAC /* DeleteAlarmSection.swift */; }; DDDF6F432D479A9900884336 /* LoopNightscoutRemoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDF6F422D479A9800884336 /* LoopNightscoutRemoteView.swift */; }; DDDF6F452D479AB100884336 /* LoopOverrideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDF6F442D479AB000884336 /* LoopOverrideView.swift */; }; DDDF6F472D479AD200884336 /* LoopOverrideViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDF6F462D479AD100884336 /* LoopOverrideViewModel.swift */; }; @@ -557,6 +558,7 @@ DDD10F042C529DA200D76A8E /* ObservableValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableValue.swift; sourceTree = ""; }; DDD10F062C529DE800D76A8E /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; DDD10F0A2C54192A00D76A8E /* TemporaryTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryTarget.swift; sourceTree = ""; }; + DDDB86F02DF7223C00AADDAC /* DeleteAlarmSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAlarmSection.swift; sourceTree = ""; }; DDDF6F422D479A9800884336 /* LoopNightscoutRemoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopNightscoutRemoteView.swift; sourceTree = ""; }; DDDF6F442D479AB000884336 /* LoopOverrideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopOverrideView.swift; sourceTree = ""; }; DDDF6F462D479AD100884336 /* LoopOverrideViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopOverrideViewModel.swift; sourceTree = ""; }; @@ -1028,6 +1030,7 @@ DDC7E53B2DBD8A1600EB1127 /* Components */ = { isa = PBXGroup; children = ( + DDDB86F02DF7223C00AADDAC /* DeleteAlarmSection.swift */, DD7F4BC42DD3CE0700D449E9 /* AlarmBGLimitSection.swift */, DD7F4BA02DD2193F00D449E9 /* AlarmSnoozeSection.swift */, DD7F4B9E2DD1F92700D449E9 /* AlarmActiveSection.swift */, @@ -1821,6 +1824,7 @@ DDEF503F2D32754F00999A5D /* ProfileTask.swift in Sources */, DD7F4C052DD4BBE200D449E9 /* NotLoopingCondition.swift in Sources */, DD5334212C60EBEE00062F9D /* InsulinCartridgeChange.swift in Sources */, + DDDB86F12DF7223C00AADDAC /* DeleteAlarmSection.swift in Sources */, DD7F4B9D2DD1EAE500D449E9 /* AlarmAudioSection.swift in Sources */, FC97881E2485969B00A7906C /* NightScoutViewController.swift in Sources */, DD608A0A2C23593900F91132 /* SMB.swift in Sources */, diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index dcb516af6..b72625780 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -9,13 +9,21 @@ struct AlarmEditor: View { var isNew: Bool = false var onDone: () -> Void = {} var onCancel: () -> Void = {} + var onDelete: () -> Void = {} @Environment(\.dismiss) private var dismiss var body: some View { NavigationStack { - innerEditor() - .navigationBarTitleDisplayMode(.inline) + Form { + innerEditorBody() + if !isNew { + DeleteAlarmSection { + onDelete() + dismiss() + } + } + }.navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .confirmationAction) { Button("Done") { @@ -32,55 +40,48 @@ struct AlarmEditor: View { } } } + .navigationTitle(alarm.type.rawValue) } .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) } @ViewBuilder private func innerEditor() -> some View { + Form { + innerEditorBody() + + DeleteAlarmSection { + onDelete() + dismiss() + } + } + } + + /// Break the old `switch` out into its own helper so the call above is tidy. + @ViewBuilder + private func innerEditorBody() -> some View { switch alarm.type { - case .buildExpire: - BuildExpireAlarmEditor(alarm: $alarm) - case .high: - HighBgAlarmEditor(alarm: $alarm) - case .low: - LowBgAlarmEditor(alarm: $alarm) - case .missedReading: - MissedReadingEditor(alarm: $alarm) - case .fastDrop: - FastDropAlarmEditor(alarm: $alarm) - case .notLooping: - NotLoopingAlarmEditor(alarm: $alarm) - case .overrideStart: - OverrideStartAlarmEditor(alarm: $alarm) - case .overrideEnd: - OverrideEndAlarmEditor(alarm: $alarm) - case .tempTargetStart: - TempTargetStartAlarmEditor(alarm: $alarm) - case .tempTargetEnd: - TempTargetEndAlarmEditor(alarm: $alarm) - case .recBolus: - RecBolusAlarmEditor(alarm: $alarm) - case .cob: - COBAlarmEditor(alarm: $alarm) - case .fastRise: - FastRiseAlarmEditor(alarm: $alarm) - case .temporary: - TemporaryAlarmEditor(alarm: $alarm) - case .sensorChange: - SensorAgeAlarmEditor(alarm: $alarm) - case .pumpChange: - PumpChangeAlarmEditor(alarm: $alarm) - case .pump: - PumpVolumeAlarmEditor(alarm: $alarm) - case .iob: - IOBAlarmEditor(alarm: $alarm) - case .battery: - BatteryAlarmEditor(alarm: $alarm) - case .batteryDrop: - BatteryDropAlarmEditor(alarm: $alarm) - case .missedBolus: - MissedBolusAlarmEditor(alarm: $alarm) + case .buildExpire: BuildExpireAlarmEditor(alarm: $alarm) + case .high: HighBgAlarmEditor(alarm: $alarm) + case .low: LowBgAlarmEditor(alarm: $alarm) + case .missedReading: MissedReadingEditor(alarm: $alarm) + case .fastDrop: FastDropAlarmEditor(alarm: $alarm) + case .notLooping: NotLoopingAlarmEditor(alarm: $alarm) + case .overrideStart: OverrideStartAlarmEditor(alarm: $alarm) + case .overrideEnd: OverrideEndAlarmEditor(alarm: $alarm) + case .tempTargetStart: TempTargetStartAlarmEditor(alarm: $alarm) + case .tempTargetEnd: TempTargetEndAlarmEditor(alarm: $alarm) + case .recBolus: RecBolusAlarmEditor(alarm: $alarm) + case .cob: COBAlarmEditor(alarm: $alarm) + case .fastRise: FastRiseAlarmEditor(alarm: $alarm) + case .temporary: TemporaryAlarmEditor(alarm: $alarm) + case .sensorChange: SensorAgeAlarmEditor(alarm: $alarm) + case .pumpChange: PumpChangeAlarmEditor(alarm: $alarm) + case .pump: PumpVolumeAlarmEditor(alarm: $alarm) + case .iob: IOBAlarmEditor(alarm: $alarm) + case .battery: BatteryAlarmEditor(alarm: $alarm) + case .batteryDrop: BatteryDropAlarmEditor(alarm: $alarm) + case .missedBolus: MissedBolusAlarmEditor(alarm: $alarm) } } } diff --git a/LoopFollow/Alarm/AlarmEditing/Components/DeleteAlarmSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/DeleteAlarmSection.swift new file mode 100644 index 000000000..3faedc651 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Components/DeleteAlarmSection.swift @@ -0,0 +1,32 @@ +// LoopFollow +// DeleteAlarmSection.swift +// Created by Jonas Björkert on 2025-06-09. + +// +// DeleteAlarmSection.swift +// LoopFollow +// +// Created by Jonas Björkert on 2025-06-09. +// Copyright © 2025 Jon Fawcett. All rights reserved. +// +import SwiftUI + +struct DeleteAlarmSection: View { + @State private var ask = false + let delete: () -> Void + + var body: some View { + Section { + Button(role: .destructive) { + ask = true + } label: { + Label("Delete Alarm", systemImage: "trash") + .frame(maxWidth: .infinity, alignment: .center) + } + } + .alert("Delete this alarm?", isPresented: $ask) { + Button("Delete", role: .destructive, action: delete) + Button("Cancel", role: .cancel) {} + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift index 71eac5b69..d7b45703d 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift @@ -8,7 +8,7 @@ struct BatteryAlarmEditor: View { @Binding var alarm: Alarm var body: some View { - Form { + Group { InfoBanner( text: "This warns you when the phone’s battery gets low, based on the percentage you choose.", alarmType: alarm.type @@ -30,6 +30,5 @@ struct BatteryAlarmEditor: View { AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection(alarm: $alarm) } - .navigationTitle(alarm.type.rawValue) } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift index 2aa2e3fad..f224d7d58 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift @@ -8,7 +8,7 @@ struct BatteryDropAlarmEditor: View { @Binding var alarm: Alarm var body: some View { - Form { + Group { InfoBanner( text: "This warns you if your phone’s battery drops quickly, based on the percentage and time you set.", alarmType: alarm.type @@ -40,6 +40,5 @@ struct BatteryDropAlarmEditor: View { AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection(alarm: $alarm) } - .navigationTitle(alarm.type.rawValue) } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift index 8fe05955b..a1da6ef80 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift @@ -8,7 +8,7 @@ struct BuildExpireAlarmEditor: View { @Binding var alarm: Alarm var body: some View { - Form { + Group { InfoBanner( text: "Sends a reminder before the looping-app build you’re following reaches its " + "TestFlight or Xcode expiry date. Works with Trio 0.4 and later." @@ -29,6 +29,5 @@ struct BuildExpireAlarmEditor: View { AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection(alarm: $alarm) } - .navigationTitle(alarm.type.rawValue) } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift index e6ebd07c9..e3cd3e32a 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift @@ -8,7 +8,7 @@ struct COBAlarmEditor: View { @Binding var alarm: Alarm var body: some View { - Form { + Group { InfoBanner( text: "Alerts when Carbs-on-Board exceeds the amount you set below.", alarmType: alarm.type @@ -31,6 +31,5 @@ struct COBAlarmEditor: View { AlarmSnoozeSection(alarm: $alarm) } - .navigationTitle(alarm.type.rawValue) } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift index 88bcf732b..4e72c3f0a 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift @@ -10,7 +10,7 @@ struct FastDropAlarmEditor: View { @State private var useLimit: Bool = false var body: some View { - Form { + Group { InfoBanner( text: "Alerts when glucose readings drop rapidly. For example, three straight readings each falling by at least the amount you set. Optionally limit alerts to only fire below a certain BG level." ) @@ -47,6 +47,5 @@ struct FastDropAlarmEditor: View { AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection(alarm: $alarm) } - .navigationTitle(alarm.type.rawValue) } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift index a907212a0..9c18581c8 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift @@ -8,7 +8,7 @@ struct FastRiseAlarmEditor: View { @Binding var alarm: Alarm var body: some View { - Form { + Group { InfoBanner( text: "Alerts when glucose readings rise rapidly. For example, " + "three straight readings each climbing by at least the amount " @@ -51,6 +51,5 @@ struct FastRiseAlarmEditor: View { AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection(alarm: $alarm) } - .navigationTitle(alarm.type.rawValue) } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift index bb02ff6e5..558dab005 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift @@ -8,7 +8,7 @@ struct HighBgAlarmEditor: View { @Binding var alarm: Alarm var body: some View { - Form { + Group { InfoBanner( text: "Alerts when glucose stays above the limit " + "you set below. Use Persistent if you want to ignore brief spikes." @@ -39,6 +39,5 @@ struct HighBgAlarmEditor: View { AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection(alarm: $alarm) } - .navigationTitle(alarm.type.rawValue) } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift index 6f9593898..1ddd2cb7d 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift @@ -8,7 +8,7 @@ struct IOBAlarmEditor: View { @Binding var alarm: Alarm var body: some View { - Form { + Group { InfoBanner( text: "Alerts when insulin-on-board is high, or when several " + "boluses in quick succession exceed the limits you set.", @@ -61,6 +61,5 @@ struct IOBAlarmEditor: View { AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection(alarm: $alarm) } - .navigationTitle(alarm.type.rawValue) } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift index 69b7a1f81..a70e5c59f 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift @@ -8,7 +8,7 @@ struct LowBgAlarmEditor: View { @Binding var alarm: Alarm var body: some View { - Form { + Group { InfoBanner(text: "This warns you if the glucose is too low now or might be soon, based on predictions. Note: predictions is currently not available for Trio.") AlarmGeneralSection(alarm: $alarm) @@ -48,6 +48,5 @@ struct LowBgAlarmEditor: View { AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection(alarm: $alarm) } - .navigationTitle(alarm.type.rawValue) } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift index e03b52e60..54a8533f9 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift @@ -8,7 +8,7 @@ struct MissedBolusAlarmEditor: View { @Binding var alarm: Alarm var body: some View { - Form { + Group { InfoBanner( text: "Alerts when carbs are logged but no bolus is delivered " + "within the delay below. Allows small-carb / treatment " + @@ -74,6 +74,5 @@ struct MissedBolusAlarmEditor: View { AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection(alarm: $alarm) } - .navigationTitle(alarm.type.rawValue) } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift index 4a6430efe..79484e4d6 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift @@ -8,7 +8,7 @@ struct MissedReadingEditor: View { @Binding var alarm: Alarm var body: some View { - Form { + Group { InfoBanner(text: "This warns you if the glucose monitor stops sending readings for too long..", alarmType: alarm.type) AlarmGeneralSection(alarm: $alarm) @@ -27,6 +27,5 @@ struct MissedReadingEditor: View { AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection(alarm: $alarm) } - .navigationTitle(alarm.type.rawValue) } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift index 649dad7df..78bb5e2e4 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift @@ -10,7 +10,7 @@ struct NotLoopingAlarmEditor: View { private let bgRange: ClosedRange = 40 ... 300 var body: some View { - Form { + Group { InfoBanner( text: "Alerts when no successful loop has occurred for the time " + "you set below.", alarmType: alarm.type @@ -53,6 +53,5 @@ struct NotLoopingAlarmEditor: View { AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection(alarm: $alarm) } - .navigationTitle(alarm.type.rawValue) } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift index 9882c845a..f24de8373 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift @@ -8,7 +8,7 @@ struct OverrideEndAlarmEditor: View { @Binding var alarm: Alarm var body: some View { - Form { + Group { InfoBanner(text: "Alerts when an override ends.", alarmType: alarm.type) AlarmGeneralSection(alarm: $alarm) @@ -17,6 +17,5 @@ struct OverrideEndAlarmEditor: View { AlarmAudioSection(alarm: $alarm, hideRepeat: true) AlarmSnoozeSection(alarm: $alarm) } - .navigationTitle(alarm.type.rawValue) } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift index f63c54782..d7a27f287 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift @@ -8,7 +8,7 @@ struct OverrideStartAlarmEditor: View { @Binding var alarm: Alarm var body: some View { - Form { + Group { InfoBanner( text: "Alerts when an override begins.", alarmType: alarm.type @@ -20,6 +20,5 @@ struct OverrideStartAlarmEditor: View { AlarmAudioSection(alarm: $alarm, hideRepeat: true) AlarmSnoozeSection(alarm: $alarm) } - .navigationTitle(alarm.type.rawValue) } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift index 090b38062..6211fd01a 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift @@ -8,7 +8,7 @@ struct PumpChangeAlarmEditor: View { @Binding var alarm: Alarm var body: some View { - Form { + Group { InfoBanner( text: "Alerts when the pump / cannula is within the time " + "window you choose below (relative to the 3-day change " @@ -32,6 +32,5 @@ struct PumpChangeAlarmEditor: View { AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection(alarm: $alarm) } - .navigationTitle(alarm.type.rawValue) } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift index a2e375b46..32c200b65 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift @@ -8,7 +8,7 @@ struct PumpVolumeAlarmEditor: View { @Binding var alarm: Alarm var body: some View { - Form { + Group { InfoBanner( text: "This warns you when the insulin pump is running low on insulin.", alarmType: alarm.type @@ -31,6 +31,5 @@ struct PumpVolumeAlarmEditor: View { AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection(alarm: $alarm) } - .navigationTitle(alarm.type.rawValue) } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift index 61785b42a..7c504c3dd 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift @@ -8,7 +8,7 @@ struct RecBolusAlarmEditor: View { @Binding var alarm: Alarm var body: some View { - Form { + Group { InfoBanner( text: "Alerts when the recommended bolus equals or exceeds the " + "threshold you set below.", @@ -31,6 +31,5 @@ struct RecBolusAlarmEditor: View { AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection(alarm: $alarm) } - .navigationTitle(alarm.type.rawValue) } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift index c0d23e75b..76d488cc9 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift @@ -8,7 +8,7 @@ struct SensorAgeAlarmEditor: View { @Binding var alarm: Alarm var body: some View { - Form { + Group { InfoBanner( text: "Warn me this many hours before the sensor’s 10-day change-over.", alarmType: alarm.type @@ -31,6 +31,5 @@ struct SensorAgeAlarmEditor: View { AlarmAudioSection(alarm: $alarm) AlarmSnoozeSection(alarm: $alarm) } - .navigationTitle(alarm.type.rawValue) } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift index d72021a2a..5e4b08e87 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift @@ -8,7 +8,7 @@ struct TempTargetEndAlarmEditor: View { @Binding var alarm: Alarm var body: some View { - Form { + Group { InfoBanner(text: "Alerts when a temp target ends.", alarmType: alarm.type) AlarmGeneralSection(alarm: $alarm) @@ -17,6 +17,5 @@ struct TempTargetEndAlarmEditor: View { AlarmAudioSection(alarm: $alarm, hideRepeat: true) AlarmSnoozeSection(alarm: $alarm) } - .navigationTitle(alarm.type.rawValue) } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift index 445c46636..1e7a0da95 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift @@ -8,7 +8,7 @@ struct TempTargetStartAlarmEditor: View { @Binding var alarm: Alarm var body: some View { - Form { + Group { InfoBanner(text: "Alerts when a temp target starts.", alarmType: alarm.type) AlarmGeneralSection(alarm: $alarm) @@ -17,6 +17,5 @@ struct TempTargetStartAlarmEditor: View { AlarmAudioSection(alarm: $alarm, hideRepeat: true) AlarmSnoozeSection(alarm: $alarm) } - .navigationTitle(alarm.type.rawValue) } } diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift index d7dc5722f..97348c682 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift @@ -11,7 +11,7 @@ struct TemporaryAlarmEditor: View { private let bgRange: ClosedRange = 40 ... 300 var body: some View { - Form { + Group { InfoBanner( text: "This alert fires once when glucose crosses either of the limits you set below, and then disables itself.", alarmType: alarm.type @@ -45,6 +45,5 @@ struct TemporaryAlarmEditor: View { AlarmActiveSection(alarm: $alarm) AlarmAudioSection(alarm: $alarm) } - .navigationTitle(alarm.type.rawValue) } } diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index e6c1990f8..2c2623297 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -163,6 +163,10 @@ struct AlarmListView: View { onCancel: { if isNew { deleteAfterDismiss = id } sheetInfo = nil + }, + onDelete: { + deleteAfterDismiss = id + sheetInfo = nil } ) } else { From 2367c2e96bf7d56dff152f422979c288eadf8af8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Thu, 12 Jun 2025 19:18:54 +0200 Subject: [PATCH 128/138] Persistent Notification --- LoopFollow/Alarm/Alarm.swift | 21 +------------ LoopFollow/Alarm/AlarmManager.swift | 30 +++++++++++++++++++ .../Settings/ContactSettingsViewModel.swift | 2 +- LoopFollow/Settings/DexcomSettingsView.swift | 2 +- LoopFollow/Settings/GeneralSettingsView.swift | 2 ++ LoopFollow/Storage/Storage+Migrate.swift | 2 ++ LoopFollow/Storage/Storage.swift | 3 ++ .../ViewControllers/MainViewController.swift | 13 -------- 8 files changed, 40 insertions(+), 35 deletions(-) diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index d986b3bf3..fea76901d 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -177,26 +177,7 @@ struct Alarm: Identifiable, Codable, Equatable { } }() - UNUserNotificationCenter.current().removeAllPendingNotificationRequests() - - let content = UNMutableNotificationContent() - content.title = type.rawValue - content.subtitle += Observable.shared.bgText.value + " " - content.subtitle += Observable.shared.directionText.value + " " - content.subtitle += Observable.shared.deltaText.value - content.categoryIdentifier = "category" - // This is needed to trigger vibrate on watch and phone - // See if we can use .Critcal - // See if we should use this method instead of direct sound player - content.sound = .default - - let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) - let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger) - UNUserNotificationCenter.current().add(request, withCompletionHandler: nil) - - let action = UNNotificationAction(identifier: "snooze", title: snoozeDuration == 0 ? "Acknowledge" : "Snooze", options: []) - let category = UNNotificationCategory(identifier: "category", actions: [action], intentIdentifiers: [], options: []) - UNUserNotificationCenter.current().setNotificationCategories([category]) + AlarmManager.shared.sendNotification(title: type.rawValue, actionTitle: snoozeDuration == 0 ? "Acknowledge" : "Snooze") if playSound { AlarmSound.setSoundFile(str: soundFile.rawValue) diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index f2c26fb73..93830e876 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -42,6 +42,7 @@ class AlarmManager { func checkAlarms(data: AlarmData) { let now = Date() + var alarmTriggered = false let alarms = Storage.shared.alarms.value let sorted = alarms.sorted { lhs, rhs in @@ -149,8 +150,15 @@ class AlarmManager { Storage.shared.alarms.value = list } } + + alarmTriggered = true break } + + if isLatestReadingRecent, Storage.shared.persistentNotification.value, !alarmTriggered, let latestDate = data.bgReadings.last?.date, latestDate > Storage.shared.persistentNotificationLastBGTime.value { + sendNotification(title: "Latest BG") + Storage.shared.persistentNotificationLastBGTime.value = now + } } func performSnooze(_ snoozeUnits: Int? = nil) { @@ -173,4 +181,26 @@ class AlarmManager { Observable.shared.currentAlarm.value = nil UNUserNotificationCenter.current().removeAllPendingNotificationRequests() } + + func sendNotification(title: String, actionTitle: String? = nil) { + UNUserNotificationCenter.current().removeAllPendingNotificationRequests() + + let content = UNMutableNotificationContent() + content.title = title + content.subtitle += Observable.shared.bgText.value + " " + content.subtitle += Observable.shared.directionText.value + " " + content.subtitle += Observable.shared.deltaText.value + content.categoryIdentifier = "category" + content.sound = .default + + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) + let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger) + UNUserNotificationCenter.current().add(request, withCompletionHandler: nil) + + if let actionTitle = actionTitle { + let action = UNNotificationAction(identifier: "snooze", title: actionTitle, options: []) + let category = UNNotificationCategory(identifier: "category", actions: [action], intentIdentifiers: [], options: []) + UNUserNotificationCenter.current().setNotificationCategories([category]) + } + } } diff --git a/LoopFollow/Settings/ContactSettingsViewModel.swift b/LoopFollow/Settings/ContactSettingsViewModel.swift index f2af24c9a..a9ee26a52 100644 --- a/LoopFollow/Settings/ContactSettingsViewModel.swift +++ b/LoopFollow/Settings/ContactSettingsViewModel.swift @@ -1,6 +1,6 @@ // LoopFollow // ContactSettingsViewModel.swift -// Created by Jonas Björkert on 2025-05-23. +// Created by Jonas Björkert on 2024-12-10. import Combine import Foundation diff --git a/LoopFollow/Settings/DexcomSettingsView.swift b/LoopFollow/Settings/DexcomSettingsView.swift index 29d6fb909..d566b796f 100644 --- a/LoopFollow/Settings/DexcomSettingsView.swift +++ b/LoopFollow/Settings/DexcomSettingsView.swift @@ -1,6 +1,6 @@ // LoopFollow // DexcomSettingsView.swift -// Created by Jonas Björkert on 2025-05-23. +// Created by Jonas Björkert on 2025-01-18. import SwiftUI diff --git a/LoopFollow/Settings/GeneralSettingsView.swift b/LoopFollow/Settings/GeneralSettingsView.swift index 56910c714..d4477bb1f 100644 --- a/LoopFollow/Settings/GeneralSettingsView.swift +++ b/LoopFollow/Settings/GeneralSettingsView.swift @@ -14,6 +14,7 @@ struct GeneralSettingsView: View { @ObservedObject var screenlockSwitchState = Storage.shared.screenlockSwitchState @ObservedObject var showDisplayName = Storage.shared.showDisplayName @ObservedObject var snoozerEmoji = Storage.shared.snoozerEmoji + @ObservedObject var persistentNotification = Storage.shared.persistentNotification // Speak-BG settings @ObservedObject var speakBG = Storage.shared.speakBG @@ -31,6 +32,7 @@ struct GeneralSettingsView: View { Form { Section("App Settings") { Toggle("Display App Badge", isOn: $appBadge.value) + Toggle("Persistent Notification", isOn: $persistentNotification.value) } Section("Display") { diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index 78478937c..19dbce44c 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -136,6 +136,8 @@ extension Storage { legacySpeakLanguage.setNil(key: "speakLanguage") } + move(UserDefaultsValue(key: "persistentNotification", default: true), into: Storage.shared.persistentNotification) + // ── General (done earlier, but safe to repeat) ── move(UserDefaultsValue(key: "colorBGText", default: true), into: Storage.shared.colorBGText) move(UserDefaultsValue(key: "appBadge", default: true), into: appBadge) diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index 80a50cdcc..a29f3fdca 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -156,6 +156,9 @@ class Storage { var migrationStep = StorageValue(key: "migrationStep", defaultValue: 0) + var persistentNotification = StorageValue(key: "persistentNotification", defaultValue: false) + var persistentNotificationLastBGTime = StorageValue(key: "persistentNotificationLastBGTime", defaultValue: .distantPast) + static let shared = Storage() private init() {} } diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 801564a10..9acfa2ba6 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -648,19 +648,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } } - func sendGeneralNotification(_: Any, title: String, subtitle: String, body: String, timer: TimeInterval) { - let content = UNMutableNotificationContent() - content.title = title - content.subtitle = subtitle - content.body = body - content.categoryIdentifier = "noAction" - content.sound = .default - - let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timer, repeats: false) - let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger) - UNUserNotificationCenter.current().add(request, withCompletionHandler: nil) - } - func userNotificationCenter(_: UNUserNotificationCenter, didReceive _: UNNotificationResponse, withCompletionHandler _: @escaping () -> Void) {} // User has scrolled the chart From 392e08c18dfea89d63421da19a0ec696d8ae35af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Thu, 12 Jun 2025 19:29:39 +0200 Subject: [PATCH 129/138] Alarm Sort Order --- LoopFollow.xcodeproj/project.pbxproj | 4 +++ .../Alarm/Alarm+byPriorityThenSpec.swift | 31 +++++++++++++++++++ LoopFollow/Alarm/AlarmListView.swift | 2 +- LoopFollow/Alarm/AlarmManager.swift | 25 +-------------- 4 files changed, 37 insertions(+), 25 deletions(-) create mode 100644 LoopFollow/Alarm/Alarm+byPriorityThenSpec.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 27806e6e3..867efa313 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -150,6 +150,7 @@ DDB9FC7B2DDB573F00EFAA76 /* IOBCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB9FC7A2DDB573F00EFAA76 /* IOBCondition.swift */; }; DDB9FC7D2DDB575300EFAA76 /* IOBAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB9FC7C2DDB575300EFAA76 /* IOBAlarmEditor.swift */; }; DDB9FC7F2DDB584500EFAA76 /* BolusEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB9FC7E2DDB584500EFAA76 /* BolusEntry.swift */; }; + DDBD19962DFB44B0005C2D69 /* Alarm+byPriorityThenSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDBD19952DFB44B0005C2D69 /* Alarm+byPriorityThenSpec.swift */; }; DDBE3ABD2CB5A961006B37DC /* OverrideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDBE3ABC2CB5A961006B37DC /* OverrideView.swift */; }; DDC6CA3D2DD7C6090060EE25 /* TemporaryCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA3C2DD7C6090060EE25 /* TemporaryCondition.swift */; }; DDC6CA3F2DD7C6340060EE25 /* TemporaryAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC6CA3E2DD7C6340060EE25 /* TemporaryAlarmEditor.swift */; }; @@ -519,6 +520,7 @@ DDB9FC7A2DDB573F00EFAA76 /* IOBCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOBCondition.swift; sourceTree = ""; }; DDB9FC7C2DDB575300EFAA76 /* IOBAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOBAlarmEditor.swift; sourceTree = ""; }; DDB9FC7E2DDB584500EFAA76 /* BolusEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusEntry.swift; sourceTree = ""; }; + DDBD19952DFB44B0005C2D69 /* Alarm+byPriorityThenSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Alarm+byPriorityThenSpec.swift"; sourceTree = ""; }; DDBE3ABC2CB5A961006B37DC /* OverrideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideView.swift; sourceTree = ""; }; DDC6CA3C2DD7C6090060EE25 /* TemporaryCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryCondition.swift; sourceTree = ""; }; DDC6CA3E2DD7C6340060EE25 /* TemporaryAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryAlarmEditor.swift; sourceTree = ""; }; @@ -1117,6 +1119,7 @@ DDCF9A8B2D86005E004DF4DD /* AlarmManager.swift */, DDCF9A872D85FD33004DF4DD /* AlarmData.swift */, DDCF9A7F2D85FD09004DF4DD /* Alarm.swift */, + DDBD19952DFB44B0005C2D69 /* Alarm+byPriorityThenSpec.swift */, ); path = Alarm; sourceTree = ""; @@ -1815,6 +1818,7 @@ DD0247712DB4337700FCADF6 /* BuildExpireCondition.swift in Sources */, DDF6999B2C5AA32E0058A8D9 /* TempTargetPreset.swift in Sources */, DD7F4C0F2DD51EC200D449E9 /* TempTargetStartCondition.swift in Sources */, + DDBD19962DFB44B0005C2D69 /* Alarm+byPriorityThenSpec.swift in Sources */, DDC6CA3D2DD7C6090060EE25 /* TemporaryCondition.swift in Sources */, DD9ACA0E2D340BFF00415D8A /* AlarmTask.swift in Sources */, DDCF9A822D85FD15004DF4DD /* AlarmType.swift in Sources */, diff --git a/LoopFollow/Alarm/Alarm+byPriorityThenSpec.swift b/LoopFollow/Alarm/Alarm+byPriorityThenSpec.swift new file mode 100644 index 000000000..32c62bde3 --- /dev/null +++ b/LoopFollow/Alarm/Alarm+byPriorityThenSpec.swift @@ -0,0 +1,31 @@ +// LoopFollow +// Alarm+byPriorityThenSpec.swift +// Created by Jonas Björkert on 2025-06-12. + +import Foundation + +extension Alarm { + /// Sorts by `AlarmType.priority`, then the per-type `sortSpec` if one exists. + static let byPriorityThenSpec: (Alarm, Alarm) -> Bool = { lhs, rhs in + // 1) type-level priority + if lhs.type.priority != rhs.type.priority { + return lhs.type.priority < rhs.type.priority + } + + // 2) per-type “main value” ordering + if lhs.type == rhs.type, + let spec = lhs.type.sortSpec + { + let lv = spec.key(lhs) + let rv = spec.key(rhs) + + switch spec.direction { + case .ascending: return (lv ?? .infinity) < (rv ?? .infinity) + case .descending: return (lv ?? -.infinity) > (rv ?? -.infinity) + } + } + + // 3) fallback – keep original insertion order + return false + } +} diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index 2c2623297..dbc6ff5f8 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -95,7 +95,7 @@ struct AlarmListView: View { var body: some View { List { - ForEach(store.value) { alarm in + ForEach(store.value.sorted(by: Alarm.byPriorityThenSpec)) { alarm in Button { selectedAlarm = alarm sheetInfo = .editor(id: alarm.id, isNew: false) diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index 93830e876..e25a6d01a 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -45,30 +45,7 @@ class AlarmManager { var alarmTriggered = false let alarms = Storage.shared.alarms.value - let sorted = alarms.sorted { lhs, rhs in - // 1) type-level priority (hard-coded table in AlarmType) - if lhs.type.priority != rhs.type.priority { - return lhs.type.priority < rhs.type.priority - } - - // 2) per-type “main value” ordering - if lhs.type == rhs.type, // only makes sense within the same type - let spec = lhs.type.sortSpec - { // (direction, key extractor) - let lv = spec.key(lhs) - let rv = spec.key(rhs) - - switch spec.direction { - case .ascending: // smaller ⇒ more urgent - return (lv ?? Double.infinity) < (rv ?? Double.infinity) - case .descending: // bigger ⇒ more urgent - return (lv ?? -Double.infinity) > (rv ?? -Double.infinity) - } - } - - // 3) fallback – keep original insertion order - return false - } + let sorted = alarms.sorted(by: Alarm.byPriorityThenSpec) var skipType: AlarmType? let isLatestReadingRecent: Bool = { From 9d3e52104b4c71ed820da169736aac473ceb9151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Thu, 12 Jun 2025 20:17:44 +0200 Subject: [PATCH 130/138] Build error fix --- LoopFollow/Alarm/AlarmConfiguration.swift | 2 +- .../Extensions/ShareClientExtension.swift | 38 +++++++++---------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/LoopFollow/Alarm/AlarmConfiguration.swift b/LoopFollow/Alarm/AlarmConfiguration.swift index 993539ff3..b8c240228 100644 --- a/LoopFollow/Alarm/AlarmConfiguration.swift +++ b/LoopFollow/Alarm/AlarmConfiguration.swift @@ -28,6 +28,6 @@ struct AlarmConfiguration: Codable, Equatable { forcedOutputVolume: 0.5, audioDuringCalls: true, ignoreZeroBG: true, - autoSnoozeCGMStart: false, + autoSnoozeCGMStart: false ) } diff --git a/LoopFollow/Extensions/ShareClientExtension.swift b/LoopFollow/Extensions/ShareClientExtension.swift index 7741b972c..bcce0c6d7 100644 --- a/LoopFollow/Extensions/ShareClientExtension.swift +++ b/LoopFollow/Extensions/ShareClientExtension.swift @@ -9,7 +9,7 @@ public struct ShareGlucoseData: Decodable { var sgv: Int var date: TimeInterval var direction: String? - + enum CodingKeys: String, CodingKey { case sgv // Sensor Blood Glucose case mbg // Manual Blood Glucose @@ -17,7 +17,7 @@ public struct ShareGlucoseData: Decodable { case date case direction } - + // Decoder initializer for handling JSON data public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) @@ -31,12 +31,12 @@ public struct ShareGlucoseData: Decodable { } else { throw DecodingError.dataCorruptedError(forKey: .sgv, in: container, debugDescription: "Expected to decode Double for sgv, mbg or glucose.") } - + // Decode the date and optional direction date = try container.decode(TimeInterval.self, forKey: .date) direction = try container.decodeIfPresent(String.self, forKey: .direction) } - + public init(sgv: Int, date: TimeInterval, direction: String?) { self.sgv = sgv self.date = date @@ -45,24 +45,22 @@ public struct ShareGlucoseData: Decodable { } private var TrendTable: [String] = [ - "NONE", // 0 - "DoubleUp", // 1 - "SingleUp", // 2 - "FortyFiveUp", // 3 - "Flat", // 4 - "FortyFiveDown", // 5 - "SingleDown", // 6 - "DoubleDown", // 7 - "NOT COMPUTABLE", // 8 - "RATE OUT OF RANGE" // 9 + "NONE", // 0 + "DoubleUp", // 1 + "SingleUp", // 2 + "FortyFiveUp", // 3 + "Flat", // 4 + "FortyFiveDown", // 5 + "SingleDown", // 6 + "DoubleDown", // 7 + "NOT COMPUTABLE", // 8 + "RATE OUT OF RANGE", // 9 ] // TODO: probably better to make this an inherited class rather than an extension -extension ShareClient { - - public func fetchData(_ entries: Int, callback: @escaping (ShareError?, [ShareGlucoseData]?) -> Void) { - - self.fetchLast(entries) { (error, result) -> () in +public extension ShareClient { + func fetchData(_ entries: Int, callback: @escaping (ShareError?, [ShareGlucoseData]?) -> Void) { + fetchLast(entries) { error, result in guard error == nil, let result = result else { return callback(error ?? .fetchError, nil) } @@ -74,7 +72,7 @@ extension ShareClient { if trend < 0 || trend >= TrendTable.count { trend = 0 } - + let newShareData = ShareGlucoseData( sgv: Int(item.glucose), date: item.timestamp.timeIntervalSince1970, From 94dcdd59fccfe7649ec4c4ea79f2b840137821ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 13 Jun 2025 09:43:54 +0200 Subject: [PATCH 131/138] Fix for contact update lag --- .../Controllers/Nightscout/BGData.swift | 20 +---------- LoopFollow/Task/MinAgoTask.swift | 36 +++++++++---------- 2 files changed, 18 insertions(+), 38 deletions(-) diff --git a/LoopFollow/Controllers/Nightscout/BGData.swift b/LoopFollow/Controllers/Nightscout/BGData.swift index a2788fecb..a49687050 100644 --- a/LoopFollow/Controllers/Nightscout/BGData.swift +++ b/LoopFollow/Controllers/Nightscout/BGData.swift @@ -233,7 +233,6 @@ extension MainViewController { let deltaBG = latestBG - priorBG let lastBGTime = entries[latestEntryIndex].date - let deltaTime = (TimeInterval(Date().timeIntervalSince1970) - lastBGTime) / 60 self.updateServerText(with: sourceName) // Set BGText with the latest BG value @@ -256,28 +255,11 @@ extension MainViewController { Observable.shared.deltaText.value = "+" + Localizer.toDisplayUnits(String(deltaBG)) } - // Stale - Observable.shared.bgStale.value = deltaTime >= 12 - - // Apply strikethrough to BGText based on the staleness of the data - // Also clear badge if bgvalue is stale - let bgTextStr = self.BGText.text ?? "" - let attributeString = NSMutableAttributedString(string: bgTextStr) - attributeString.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: NSRange(location: 0, length: attributeString.length)) - if Observable.shared.bgStale.value { // Data is stale - attributeString.addAttribute(.strikethroughColor, value: UIColor.systemRed, range: NSRange(location: 0, length: attributeString.length)) - self.updateBadge(val: 0) - } else { // Data is fresh - attributeString.addAttribute(.strikethroughColor, value: UIColor.clear, range: NSRange(location: 0, length: attributeString.length)) - self.updateBadge(val: latestBG) - } - self.BGText.attributedText = attributeString - // Update contact if Storage.shared.contactEnabled.value { self.contactImageUpdater .updateContactImage( - bgValue: bgTextStr, + bgValue: Observable.shared.bgText.value, trend: Observable.shared.directionText.value, delta: Observable.shared.deltaText.value, stale: Observable.shared.bgStale.value diff --git a/LoopFollow/Task/MinAgoTask.swift b/LoopFollow/Task/MinAgoTask.swift index f5faab487..2980cae01 100644 --- a/LoopFollow/Task/MinAgoTask.swift +++ b/LoopFollow/Task/MinAgoTask.swift @@ -21,11 +21,6 @@ extension MainViewController { self.MinAgoText.text = "" Observable.shared.minAgoText.value = "" Observable.shared.bgText.value = "" - /* TODO: - if let snoozer = self.tabBarController?.viewControllers?[2] as? SnoozeViewController { - snoozer.BGLabel.attributedText = NSAttributedString(string: "") - } - */ } TaskScheduler.shared.rescheduleTask(id: .minAgoUpdate, to: Date().addingTimeInterval(1)) return @@ -56,23 +51,26 @@ extension MainViewController { guard let self = self else { return } self.MinAgoText.text = minAgoDisplayText Observable.shared.minAgoText.value = minAgoDisplayText - - /* TODO: - if let snoozer = self.tabBarController?.viewControllers?[2] as? SnoozeViewController { - let bgLabelText = snoozer.BGLabel.text ?? "" - let attributeString = NSMutableAttributedString(string: bgLabelText) - attributeString.addAttribute(.strikethroughStyle, - value: NSUnderlineStyle.single.rawValue, - range: NSRange(location: 0, length: attributeString.length)) - attributeString.addAttribute(.strikethroughColor, - value: secondsAgo >= 720 ? UIColor.systemRed : UIColor.clear, - range: NSRange(location: 0, length: attributeString.length)) - snoozer.BGLabel.attributedText = attributeString - } - */ } } + let deltaTime = secondsAgo / 60 + Observable.shared.bgStale.value = deltaTime >= 12 + + // Apply strikethrough to BGText based on the staleness of the data + // Also clear badge if bgvalue is stale + let bgTextStr = BGText.text ?? "" + let attributeString = NSMutableAttributedString(string: bgTextStr) + attributeString.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: NSRange(location: 0, length: attributeString.length)) + if Observable.shared.bgStale.value { // Data is stale + attributeString.addAttribute(.strikethroughColor, value: UIColor.systemRed, range: NSRange(location: 0, length: attributeString.length)) + updateBadge(val: 0) + } else { // Data is fresh + attributeString.addAttribute(.strikethroughColor, value: UIColor.clear, range: NSRange(location: 0, length: attributeString.length)) + updateBadge(val: Observable.shared.bg.value ?? 0) + } + BGText.attributedText = attributeString + // Determine the next run interval based on the current state let nextUpdateInterval: TimeInterval if shouldDisplaySeconds { From 5f5505b53807b400c6a34ec0e88c485a712ab1aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 27 Jun 2025 21:30:24 +0200 Subject: [PATCH 132/138] Delete alarm bugfix --- LoopFollow/Alarm/AlarmListView.swift | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index dbc6ff5f8..26f50acb2 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -93,9 +93,13 @@ struct AlarmListView: View { @State private var deleteAfterDismiss: UUID? @State private var selectedAlarm: Alarm? + private var sortedAlarms: [Alarm] { + store.value.sorted(by: Alarm.byPriorityThenSpec) + } + var body: some View { List { - ForEach(store.value.sorted(by: Alarm.byPriorityThenSpec)) { alarm in + ForEach(sortedAlarms) { alarm in Button { selectedAlarm = alarm sheetInfo = .editor(id: alarm.id, isNew: false) @@ -121,7 +125,7 @@ struct AlarmListView: View { } } } - .onDelete { store.value.remove(atOffsets: $0) } + .onDelete(perform: deleteItems) } .sheet(item: $sheetInfo, onDismiss: handleSheetDismiss) { info in sheetContent(for: info) @@ -135,6 +139,14 @@ struct AlarmListView: View { .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) } + private func deleteItems(at offsets: IndexSet) { + let alarmsToDelete = offsets.map { sortedAlarms[$0] } + + let idsToDelete = alarmsToDelete.map { $0.id } + + store.value.removeAll { idsToDelete.contains($0.id) } + } + private func handleSheetDismiss() { if let id = deleteAfterDismiss, let idx = store.value.firstIndex(where: { $0.id == id }) From a2095d15ae214cb27ec5635bb24cd8a45a3e1644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Fri, 27 Jun 2025 22:07:44 +0200 Subject: [PATCH 133/138] Force portrait mode --- LoopFollow/Application/AppDelegate.swift | 10 ++++++++++ LoopFollow/Settings/GeneralSettingsView.swift | 12 ++++++++++++ LoopFollow/Storage/Storage.swift | 1 + 3 files changed, 23 insertions(+) diff --git a/LoopFollow/Application/AppDelegate.swift b/LoopFollow/Application/AppDelegate.swift index 832111e36..05454bf46 100644 --- a/LoopFollow/Application/AppDelegate.swift +++ b/LoopFollow/Application/AppDelegate.swift @@ -126,6 +126,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate { completionHandler() } + + func application(_: UIApplication, supportedInterfaceOrientationsFor _: UIWindow?) -> UIInterfaceOrientationMask { + let forcePortrait = Storage.shared.forcePortraitMode.value + + if forcePortrait { + return .portrait + } else { + return .all + } + } } extension AppDelegate: UNUserNotificationCenterDelegate { diff --git a/LoopFollow/Settings/GeneralSettingsView.swift b/LoopFollow/Settings/GeneralSettingsView.swift index d4477bb1f..e7b3a1e2a 100644 --- a/LoopFollow/Settings/GeneralSettingsView.swift +++ b/LoopFollow/Settings/GeneralSettingsView.swift @@ -14,6 +14,7 @@ struct GeneralSettingsView: View { @ObservedObject var screenlockSwitchState = Storage.shared.screenlockSwitchState @ObservedObject var showDisplayName = Storage.shared.showDisplayName @ObservedObject var snoozerEmoji = Storage.shared.snoozerEmoji + @ObservedObject var forcePortraitMode = Storage.shared.forcePortraitMode @ObservedObject var persistentNotification = Storage.shared.persistentNotification // Speak-BG settings @@ -44,6 +45,17 @@ struct GeneralSettingsView: View { Toggle("Keep Screen Active", isOn: $screenlockSwitchState.value) Toggle("Show Display Name", isOn: $showDisplayName.value) Toggle("Snoozer emoji", isOn: $snoozerEmoji.value) + Toggle("Force portrait mode", isOn: $forcePortraitMode.value) + .onChange(of: forcePortraitMode.value) { _ in + if #available(iOS 16.0, *) { + let window = UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + .flatMap { $0.windows } + .first + + window?.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations() + } + } } Section("Speak BG") { diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index a29f3fdca..6de18afdb 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -68,6 +68,7 @@ class Storage { var screenlockSwitchState = StorageValue(key: "screenlockSwitchState", defaultValue: true) var showDisplayName = StorageValue(key: "showDisplayName", defaultValue: false) var snoozerEmoji = StorageValue(key: "snoozerEmoji", defaultValue: false) + var forcePortraitMode = StorageValue(key: "forcePortraitMode", defaultValue: false) var speakBG = StorageValue(key: "speakBG", defaultValue: false) var speakBGAlways = StorageValue(key: "speakBGAlways", defaultValue: true) From 52b43d4e02a99dc3f307b8060d597f76a38c671e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 30 Jun 2025 19:39:48 +0200 Subject: [PATCH 134/138] Removed date from header --- LoopFollow/Alarm/Alarm+byPriorityThenSpec.swift | 2 +- LoopFollow/Alarm/Alarm.swift | 2 +- LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/BatteryDropCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/BuildExpireCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/COBCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/FastRiseCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/IOBCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/MissedBolusCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/MissedReadingCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/NotLoopingCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/OverrideEndCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/PumpChangeCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/PumpVolumeCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/RecBolusCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/SensorAgeCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/TempTargetEndCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/TempTargetStartCondition.swift | 2 +- LoopFollow/Alarm/AlarmCondition/TemporaryCondition.swift | 2 +- LoopFollow/Alarm/AlarmConfiguration.swift | 2 +- LoopFollow/Alarm/AlarmData.swift | 2 +- LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift | 2 +- .../Alarm/AlarmEditing/Components/AlarmActiveSection.swift | 2 +- .../Alarm/AlarmEditing/Components/AlarmAudioSection.swift | 2 +- .../Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift | 2 +- LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift | 2 +- .../Alarm/AlarmEditing/Components/AlarmGeneralSection.swift | 2 +- .../Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift | 2 +- .../Alarm/AlarmEditing/Components/AlarmStepperSection.swift | 2 +- .../Alarm/AlarmEditing/Components/DeleteAlarmSection.swift | 2 +- LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift | 2 +- LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift | 2 +- LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift | 2 +- .../Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift | 2 +- .../Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift | 2 +- LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift | 2 +- LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift | 2 +- LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift | 2 +- LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift | 2 +- LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift | 2 +- LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift | 2 +- .../Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift | 2 +- LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift | 2 +- .../Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift | 2 +- .../Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift | 2 +- .../Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift | 2 +- .../Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift | 2 +- .../Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift | 2 +- LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift | 2 +- .../Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift | 2 +- .../Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift | 2 +- .../Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift | 2 +- .../Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift | 2 +- LoopFollow/Alarm/AlarmListView.swift | 2 +- LoopFollow/Alarm/AlarmManager.swift | 2 +- LoopFollow/Alarm/AlarmSettingsView.swift | 2 +- LoopFollow/Alarm/AlarmType/AlarmType+Snooze.swift | 2 +- LoopFollow/Alarm/AlarmType/AlarmType+SortDirection.swift | 2 +- LoopFollow/Alarm/AlarmType/AlarmType+canAcknowledge.swift | 2 +- LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift | 2 +- LoopFollow/Alarm/AlarmType/AlarmType.swift | 2 +- LoopFollow/Alarm/DataStructs/BolusEntry.swift | 2 +- LoopFollow/Alarm/DataStructs/CarbSample.swift | 2 +- LoopFollow/Alarm/DataStructs/GlucoseValue.swift | 2 +- LoopFollow/Alarm/SnoozeState.swift | 2 +- LoopFollow/Application/AppDelegate.swift | 2 +- LoopFollow/Application/SceneDelegate.swift | 2 +- LoopFollow/BackgroundRefresh/BT/BLEDevice.swift | 2 +- LoopFollow/BackgroundRefresh/BT/BLEDeviceSelectionView.swift | 2 +- LoopFollow/BackgroundRefresh/BT/BLEManager.swift | 2 +- LoopFollow/BackgroundRefresh/BT/BluetoothDevice.swift | 2 +- LoopFollow/BackgroundRefresh/BT/BluetoothDeviceDelegate.swift | 2 +- .../BT/Devices/DexcomHeartbeatBluetoothDevice.swift | 2 +- .../BT/Devices/OmnipodDashHeartbeatBluetoothTransmitter.swift | 2 +- .../BT/Devices/RileyLinkHeartbeatBluetoothDevice.swift | 2 +- LoopFollow/BackgroundRefresh/BT/DexcomG7HeartBeat.swift | 2 +- .../BackgroundRefresh/BackgroundRefreshSettingsView.swift | 2 +- .../BackgroundRefresh/BackgroundRefreshSettingsViewModel.swift | 2 +- LoopFollow/BackgroundRefresh/BackgroundRefreshType.swift | 2 +- LoopFollow/Contact/ContactColorOption.swift | 2 +- LoopFollow/Contact/ContactImageUpdater.swift | 2 +- LoopFollow/Contact/ContactIncludeOption.swift | 2 +- LoopFollow/Contact/ContactType.swift | 2 +- LoopFollow/Controllers/AlarmSound.swift | 2 +- LoopFollow/Controllers/BackgroundAlertManager.swift | 2 +- LoopFollow/Controllers/Graphs.swift | 2 +- LoopFollow/Controllers/MainViewController+updateStats.swift | 2 +- LoopFollow/Controllers/NightScout.swift | 2 +- LoopFollow/Controllers/Nightscout/BGData.swift | 2 +- LoopFollow/Controllers/Nightscout/CAge.swift | 2 +- LoopFollow/Controllers/Nightscout/DeviceStatus.swift | 2 +- LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift | 2 +- LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift | 2 +- LoopFollow/Controllers/Nightscout/IAge.swift | 2 +- LoopFollow/Controllers/Nightscout/NSProfile.swift | 2 +- LoopFollow/Controllers/Nightscout/Profile.swift | 2 +- LoopFollow/Controllers/Nightscout/ProfileManager.swift | 2 +- LoopFollow/Controllers/Nightscout/SAge.swift | 2 +- LoopFollow/Controllers/Nightscout/Treatments.swift | 2 +- LoopFollow/Controllers/Nightscout/Treatments/BGCheck.swift | 2 +- LoopFollow/Controllers/Nightscout/Treatments/Basals.swift | 2 +- LoopFollow/Controllers/Nightscout/Treatments/Bolus.swift | 2 +- LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift | 2 +- .../Nightscout/Treatments/InsulinCartridgeChange.swift | 2 +- LoopFollow/Controllers/Nightscout/Treatments/Notes.swift | 2 +- LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift | 2 +- LoopFollow/Controllers/Nightscout/Treatments/ResumePump.swift | 2 +- LoopFollow/Controllers/Nightscout/Treatments/SMB.swift | 2 +- LoopFollow/Controllers/Nightscout/Treatments/SensorStart.swift | 2 +- LoopFollow/Controllers/Nightscout/Treatments/SiteChange.swift | 2 +- LoopFollow/Controllers/Nightscout/Treatments/SuspendPump.swift | 2 +- .../Controllers/Nightscout/Treatments/TemporaryTarget.swift | 2 +- LoopFollow/Controllers/SpeakBG.swift | 2 +- LoopFollow/Controllers/Stats.swift | 2 +- LoopFollow/Controllers/Timers.swift | 2 +- LoopFollow/Extensions/Binding+Optional.swift | 2 +- LoopFollow/Extensions/EKEventStore+Extensions.swift | 2 +- LoopFollow/Extensions/HKQuantity+AnyConvertible.swift | 2 +- LoopFollow/Extensions/HKUnit+Extensions.swift | 2 +- LoopFollow/Extensions/ShareClientExtension.swift | 2 +- LoopFollow/Extensions/UIViewExtension.swift | 2 +- LoopFollow/Extensions/UUID+Identifiable.swift | 2 +- LoopFollow/Helpers/AnyConvertible.swift | 2 +- LoopFollow/Helpers/AppConstants.swift | 2 +- LoopFollow/Helpers/AppVersionManager.swift | 2 +- LoopFollow/Helpers/BackgroundTaskAudio.swift | 2 +- LoopFollow/Helpers/BinaryFloatingPoint+localized.swift | 2 +- LoopFollow/Helpers/BuildDetails.swift | 2 +- LoopFollow/Helpers/Chart.swift | 2 +- LoopFollow/Helpers/CycleHelper.swift | 2 +- LoopFollow/Helpers/DataStructs.swift | 2 +- LoopFollow/Helpers/DateTime.swift | 2 +- LoopFollow/Helpers/DictionaryKeyPath.swift | 2 +- LoopFollow/Helpers/GitHubService.swift | 2 +- LoopFollow/Helpers/Globals.swift | 2 +- LoopFollow/Helpers/GlucoseConversion.swift | 2 +- LoopFollow/Helpers/Localizer.swift | 2 +- LoopFollow/Helpers/Mobileprovision.swift | 2 +- LoopFollow/Helpers/NightscoutUtils.swift | 2 +- LoopFollow/Helpers/ObservationToken.swift | 2 +- LoopFollow/Helpers/TextFieldWithToolBar.swift | 2 +- LoopFollow/Helpers/TimeOfDay.swift | 2 +- LoopFollow/Helpers/Views/ActionRow.swift | 2 +- LoopFollow/Helpers/Views/BGPicker.swift | 2 +- LoopFollow/Helpers/Views/ErrorMessageView.swift | 2 +- LoopFollow/Helpers/Views/Glyph.swift | 2 +- LoopFollow/Helpers/Views/HKQuantityInputView.swift | 2 +- LoopFollow/Helpers/Views/LinkRow.swift | 2 +- LoopFollow/Helpers/Views/LoadingButtonView.swift | 2 +- LoopFollow/Helpers/Views/NavigationRow.swift | 2 +- LoopFollow/Helpers/Views/SettingsStepperRow.swift | 2 +- LoopFollow/Helpers/Views/TogglableSecureInput.swift | 2 +- LoopFollow/Helpers/carbBolusArrays.swift | 2 +- LoopFollow/Helpers/isOnPhoneCall.swift | 2 +- LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift | 2 +- .../InfoDisplaySettings/InfoDisplaySettingsViewModel.swift | 2 +- LoopFollow/InfoTable/InfoData.swift | 2 +- LoopFollow/InfoTable/InfoDataSeparator.swift | 2 +- LoopFollow/InfoTable/InfoManager.swift | 2 +- LoopFollow/InfoTable/InfoType.swift | 2 +- LoopFollow/Log/LogEntry.swift | 2 +- LoopFollow/Log/LogManager.swift | 2 +- LoopFollow/Log/LogView.swift | 2 +- LoopFollow/Log/LogViewModel.swift | 2 +- LoopFollow/Log/SearchBar.swift | 2 +- LoopFollow/Metric/CarbMetric.swift | 2 +- LoopFollow/Metric/InsulinMetric.swift | 2 +- LoopFollow/Metric/Metric.swift | 2 +- LoopFollow/Nightscout/NightscoutSettingsView.swift | 2 +- LoopFollow/Nightscout/NightscoutSettingsViewModel.swift | 2 +- LoopFollow/Remote/Loop/LoopNightscoutRemoteView.swift | 2 +- LoopFollow/Remote/Loop/LoopOverrideView.swift | 2 +- LoopFollow/Remote/Loop/LoopOverrideViewModel.swift | 2 +- LoopFollow/Remote/Nightscout/TrioNightscoutRemoteView.swift | 2 +- LoopFollow/Remote/NoRemoteView.swift | 2 +- LoopFollow/Remote/RemoteType.swift | 2 +- LoopFollow/Remote/RemoteViewController.swift | 2 +- LoopFollow/Remote/Settings/RemoteSettingsView.swift | 2 +- LoopFollow/Remote/Settings/RemoteSettingsViewModel.swift | 2 +- LoopFollow/Remote/TRC/BolusView.swift | 2 +- LoopFollow/Remote/TRC/MealView.swift | 2 +- LoopFollow/Remote/TRC/OverrideView.swift | 2 +- LoopFollow/Remote/TRC/PushMessage.swift | 2 +- LoopFollow/Remote/TRC/PushNotificationManager.swift | 2 +- LoopFollow/Remote/TRC/TRCCommandType.swift | 2 +- LoopFollow/Remote/TRC/TempTargetView.swift | 2 +- LoopFollow/Remote/TRC/TreatmentResponse.swift | 2 +- LoopFollow/Remote/TRC/TrioNightscoutRemoteController.swift | 2 +- LoopFollow/Remote/TRC/TrioRemoteControlView.swift | 2 +- LoopFollow/Remote/TRC/TrioRemoteControlViewModel.swift | 2 +- LoopFollow/Remote/TempTargetPreset/TempTargetPreset.swift | 2 +- .../Remote/TempTargetPreset/TempTargetPresetManager.swift | 2 +- LoopFollow/Settings/AdvancedSettingsView.swift | 2 +- LoopFollow/Settings/AdvancedSettingsViewModel.swift | 2 +- LoopFollow/Settings/CalendarSettingsView.swift | 2 +- LoopFollow/Settings/ContactSettingsView.swift | 2 +- LoopFollow/Settings/ContactSettingsViewModel.swift | 2 +- LoopFollow/Settings/DexcomSettingsView.swift | 2 +- LoopFollow/Settings/DexcomSettingsViewModel.swift | 2 +- LoopFollow/Settings/GeneralSettingsView.swift | 2 +- LoopFollow/Settings/GraphSettingsView.swift | 2 +- LoopFollow/Settings/SettingsMenuView.swift | 2 +- LoopFollow/Snoozer/SnoozerView.swift | 2 +- LoopFollow/Snoozer/SnoozerViewController.swift | 2 +- LoopFollow/Snoozer/SnoozerViewModel.swift | 2 +- LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift | 2 +- LoopFollow/Storage/Framework/ObservableValue.swift | 2 +- LoopFollow/Storage/Framework/SecureStorageValue.swift | 2 +- LoopFollow/Storage/Framework/StorageValue.swift | 2 +- LoopFollow/Storage/Framework/UserDefaultsValue.swift | 2 +- LoopFollow/Storage/Framework/UserDefaultsValueGroups.swift | 2 +- LoopFollow/Storage/Observable.swift | 2 +- LoopFollow/Storage/ObservableUserDefaults.swift | 2 +- LoopFollow/Storage/Storage+Migrate.swift | 2 +- LoopFollow/Storage/Storage.swift | 2 +- LoopFollow/Task/AlarmTask.swift | 2 +- LoopFollow/Task/BGTask.swift | 2 +- LoopFollow/Task/CalendarTask.swift | 2 +- LoopFollow/Task/DeviceStatusTask.swift | 2 +- LoopFollow/Task/MinAgoTask.swift | 2 +- LoopFollow/Task/ProfileTask.swift | 2 +- LoopFollow/Task/Task.swift | 2 +- LoopFollow/Task/TaskScheduler.swift | 2 +- LoopFollow/Task/TreatmentsTask.swift | 2 +- LoopFollow/ViewControllers/AppStateViewController.swift | 2 +- LoopFollow/ViewControllers/MainViewController.swift | 2 +- LoopFollow/ViewControllers/NightScoutViewController.swift | 2 +- LoopFollow/ViewControllers/SettingsViewController.swift | 2 +- Scripts/swiftformat.sh | 2 +- Tests/AlarmConditions/BatteryConditionTests.swift | 2 +- Tests/AlarmConditions/Helpers.swift | 2 +- Tests/Tests.swift | 2 +- 237 files changed, 237 insertions(+), 237 deletions(-) diff --git a/LoopFollow/Alarm/Alarm+byPriorityThenSpec.swift b/LoopFollow/Alarm/Alarm+byPriorityThenSpec.swift index 32c62bde3..252f9ede5 100644 --- a/LoopFollow/Alarm/Alarm+byPriorityThenSpec.swift +++ b/LoopFollow/Alarm/Alarm+byPriorityThenSpec.swift @@ -1,6 +1,6 @@ // LoopFollow // Alarm+byPriorityThenSpec.swift -// Created by Jonas Björkert on 2025-06-12. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/Alarm.swift b/LoopFollow/Alarm/Alarm.swift index fea76901d..36f6cd7ec 100644 --- a/LoopFollow/Alarm/Alarm.swift +++ b/LoopFollow/Alarm/Alarm.swift @@ -1,6 +1,6 @@ // LoopFollow // Alarm.swift -// Created by Jonas Björkert on 2025-04-26. +// Created by Jonas Björkert. import Foundation import HealthKit diff --git a/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift b/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift index f9ef020de..9ce99fff6 100644 --- a/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift @@ -1,6 +1,6 @@ // LoopFollow // AlarmCondition.swift -// Created by Jonas Björkert on 2025-04-26. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift b/LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift index 9aba1e792..bb1300958 100644 --- a/LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift @@ -1,6 +1,6 @@ // LoopFollow // BatteryCondition.swift -// Created by Jonas Björkert on 2025-05-20. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/BatteryDropCondition.swift b/LoopFollow/Alarm/AlarmCondition/BatteryDropCondition.swift index 7c30985cf..db378422c 100644 --- a/LoopFollow/Alarm/AlarmCondition/BatteryDropCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/BatteryDropCondition.swift @@ -1,6 +1,6 @@ // LoopFollow // BatteryDropCondition.swift -// Created by Jonas Björkert on 2025-05-20. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/BuildExpireCondition.swift b/LoopFollow/Alarm/AlarmCondition/BuildExpireCondition.swift index 81e3d2e28..6946ed524 100644 --- a/LoopFollow/Alarm/AlarmCondition/BuildExpireCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/BuildExpireCondition.swift @@ -1,6 +1,6 @@ // LoopFollow // BuildExpireCondition.swift -// Created by Jonas Björkert on 2025-04-26. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/COBCondition.swift b/LoopFollow/Alarm/AlarmCondition/COBCondition.swift index 08decae0d..c256ca019 100644 --- a/LoopFollow/Alarm/AlarmCondition/COBCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/COBCondition.swift @@ -1,6 +1,6 @@ // LoopFollow // COBCondition.swift -// Created by Jonas Björkert on 2025-05-15. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift b/LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift index 6b748ec1e..ac3e5d33d 100644 --- a/LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift @@ -1,6 +1,6 @@ // LoopFollow // FastDropCondition.swift -// Created by Jonas Björkert on 2025-05-14. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/FastRiseCondition.swift b/LoopFollow/Alarm/AlarmCondition/FastRiseCondition.swift index 40d81ddff..f1d18a4a1 100644 --- a/LoopFollow/Alarm/AlarmCondition/FastRiseCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/FastRiseCondition.swift @@ -1,6 +1,6 @@ // LoopFollow // FastRiseCondition.swift -// Created by Jonas Björkert on 2025-05-15. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift b/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift index 148413b2b..dacf1b669 100644 --- a/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift @@ -1,6 +1,6 @@ // LoopFollow // HighBGCondition.swift -// Created by Jonas Björkert on 2025-05-09. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/IOBCondition.swift b/LoopFollow/Alarm/AlarmCondition/IOBCondition.swift index eaf429dca..f4e7f0cd0 100644 --- a/LoopFollow/Alarm/AlarmCondition/IOBCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/IOBCondition.swift @@ -1,6 +1,6 @@ // LoopFollow // IOBCondition.swift -// Created by Jonas Björkert on 2025-05-19. +// Created by Jonas Björkert. // // IOBCondition.swift diff --git a/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift b/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift index 88443e06a..974d5016f 100644 --- a/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift @@ -1,6 +1,6 @@ // LoopFollow // LowBGCondition.swift -// Created by Jonas Björkert on 2025-05-09. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/MissedBolusCondition.swift b/LoopFollow/Alarm/AlarmCondition/MissedBolusCondition.swift index 7908a3d88..66846dc1d 100644 --- a/LoopFollow/Alarm/AlarmCondition/MissedBolusCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/MissedBolusCondition.swift @@ -1,6 +1,6 @@ // LoopFollow // MissedBolusCondition.swift -// Created by Jonas Björkert on 2025-05-20. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/MissedReadingCondition.swift b/LoopFollow/Alarm/AlarmCondition/MissedReadingCondition.swift index 35f1368e1..d75ecca33 100644 --- a/LoopFollow/Alarm/AlarmCondition/MissedReadingCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/MissedReadingCondition.swift @@ -1,6 +1,6 @@ // LoopFollow // MissedReadingCondition.swift -// Created by Jonas Björkert on 2025-05-10. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/NotLoopingCondition.swift b/LoopFollow/Alarm/AlarmCondition/NotLoopingCondition.swift index 757a6e217..dcea898df 100644 --- a/LoopFollow/Alarm/AlarmCondition/NotLoopingCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/NotLoopingCondition.swift @@ -1,6 +1,6 @@ // LoopFollow // NotLoopingCondition.swift -// Created by Jonas Björkert on 2025-05-14. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/OverrideEndCondition.swift b/LoopFollow/Alarm/AlarmCondition/OverrideEndCondition.swift index ebc463c32..faf7c6337 100644 --- a/LoopFollow/Alarm/AlarmCondition/OverrideEndCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/OverrideEndCondition.swift @@ -1,6 +1,6 @@ // LoopFollow // OverrideEndCondition.swift -// Created by Jonas Björkert on 2025-05-14. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift b/LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift index fd8e4dd24..5a7fcb7b2 100644 --- a/LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift @@ -1,6 +1,6 @@ // LoopFollow // OverrideStartCondition.swift -// Created by Jonas Björkert on 2025-05-14. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/PumpChangeCondition.swift b/LoopFollow/Alarm/AlarmCondition/PumpChangeCondition.swift index 2858a984c..26db74e38 100644 --- a/LoopFollow/Alarm/AlarmCondition/PumpChangeCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/PumpChangeCondition.swift @@ -1,6 +1,6 @@ // LoopFollow // PumpChangeCondition.swift -// Created by Jonas Björkert on 2025-05-17. +// Created by Jonas Björkert. // // PumpChangeCondition.swift diff --git a/LoopFollow/Alarm/AlarmCondition/PumpVolumeCondition.swift b/LoopFollow/Alarm/AlarmCondition/PumpVolumeCondition.swift index e01f4d3a0..8150cb23e 100644 --- a/LoopFollow/Alarm/AlarmCondition/PumpVolumeCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/PumpVolumeCondition.swift @@ -1,6 +1,6 @@ // LoopFollow // PumpVolumeCondition.swift -// Created by Jonas Björkert on 2025-05-17. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/RecBolusCondition.swift b/LoopFollow/Alarm/AlarmCondition/RecBolusCondition.swift index 3ee5d5987..db0f58d3e 100644 --- a/LoopFollow/Alarm/AlarmCondition/RecBolusCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/RecBolusCondition.swift @@ -1,6 +1,6 @@ // LoopFollow // RecBolusCondition.swift -// Created by Jonas Björkert on 2025-05-15. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/SensorAgeCondition.swift b/LoopFollow/Alarm/AlarmCondition/SensorAgeCondition.swift index 2da4d8898..abb312f2e 100644 --- a/LoopFollow/Alarm/AlarmCondition/SensorAgeCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/SensorAgeCondition.swift @@ -1,6 +1,6 @@ // LoopFollow // SensorAgeCondition.swift -// Created by Jonas Björkert on 2025-05-17. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/TempTargetEndCondition.swift b/LoopFollow/Alarm/AlarmCondition/TempTargetEndCondition.swift index 6ec434483..722b9c5a3 100644 --- a/LoopFollow/Alarm/AlarmCondition/TempTargetEndCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/TempTargetEndCondition.swift @@ -1,6 +1,6 @@ // LoopFollow // TempTargetEndCondition.swift -// Created by Jonas Björkert on 2025-05-14. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/TempTargetStartCondition.swift b/LoopFollow/Alarm/AlarmCondition/TempTargetStartCondition.swift index 153190225..538abef93 100644 --- a/LoopFollow/Alarm/AlarmCondition/TempTargetStartCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/TempTargetStartCondition.swift @@ -1,6 +1,6 @@ // LoopFollow // TempTargetStartCondition.swift -// Created by Jonas Björkert on 2025-05-14. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmCondition/TemporaryCondition.swift b/LoopFollow/Alarm/AlarmCondition/TemporaryCondition.swift index e1990951a..c9f9c951a 100644 --- a/LoopFollow/Alarm/AlarmCondition/TemporaryCondition.swift +++ b/LoopFollow/Alarm/AlarmCondition/TemporaryCondition.swift @@ -1,6 +1,6 @@ // LoopFollow // TemporaryCondition.swift -// Created by Jonas Björkert on 2025-05-16. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmConfiguration.swift b/LoopFollow/Alarm/AlarmConfiguration.swift index b8c240228..aaf9f372e 100644 --- a/LoopFollow/Alarm/AlarmConfiguration.swift +++ b/LoopFollow/Alarm/AlarmConfiguration.swift @@ -1,6 +1,6 @@ // LoopFollow // AlarmConfiguration.swift -// Created by Jonas Björkert on 2025-04-26. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmData.swift b/LoopFollow/Alarm/AlarmData.swift index b10fa64ce..96ec8fed2 100644 --- a/LoopFollow/Alarm/AlarmData.swift +++ b/LoopFollow/Alarm/AlarmData.swift @@ -1,6 +1,6 @@ // LoopFollow // AlarmData.swift -// Created by Jonas Björkert on 2025-04-26. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift index b72625780..4c374b892 100644 --- a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -1,6 +1,6 @@ // LoopFollow // AlarmEditor.swift -// Created by Jonas Björkert on 2025-04-26. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmActiveSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmActiveSection.swift index 66b594083..50aefd2e3 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmActiveSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmActiveSection.swift @@ -1,6 +1,6 @@ // LoopFollow // AlarmActiveSection.swift -// Created by Jonas Björkert on 2025-05-12. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift index ce34800be..5aa0b2bd3 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift @@ -1,6 +1,6 @@ // LoopFollow // AlarmAudioSection.swift -// Created by Jonas Björkert on 2025-05-12. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift index 2ed32e6a6..abbfb071b 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift @@ -1,6 +1,6 @@ // LoopFollow // AlarmBGLimitSection.swift -// Created by Jonas Björkert on 2025-05-14. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift index bd5ffb705..db1192d08 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift @@ -1,6 +1,6 @@ // LoopFollow // AlarmBGSection.swift -// Created by Jonas Björkert on 2025-05-06. +// Created by Jonas Björkert. import HealthKit import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmGeneralSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmGeneralSection.swift index 95e19fc05..ca0307248 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmGeneralSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmGeneralSection.swift @@ -1,6 +1,6 @@ // LoopFollow // AlarmGeneralSection.swift -// Created by Jonas Björkert on 2025-05-12. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift index 6fda8e6cf..f8ed3bad1 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift @@ -1,6 +1,6 @@ // LoopFollow // AlarmSnoozeSection.swift -// Created by Jonas Björkert on 2025-05-12. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift index 0c222bd81..db6660d17 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift @@ -1,6 +1,6 @@ // LoopFollow // AlarmStepperSection.swift -// Created by Jonas Björkert on 2025-05-10. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Components/DeleteAlarmSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/DeleteAlarmSection.swift index 3faedc651..753f7f2e8 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/DeleteAlarmSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/DeleteAlarmSection.swift @@ -1,6 +1,6 @@ // LoopFollow // DeleteAlarmSection.swift -// Created by Jonas Björkert on 2025-06-09. +// Created by Jonas Björkert. // // DeleteAlarmSection.swift diff --git a/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift b/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift index 00fffcd7e..837472fe5 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift @@ -1,6 +1,6 @@ // LoopFollow // InfoBanner.swift -// Created by Jonas Björkert on 2025-05-10. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift b/LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift index 62c8e96d0..48580750a 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift @@ -1,6 +1,6 @@ // LoopFollow // SoundFile.swift -// Created by Jonas Björkert on 2025-04-26. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift index d7b45703d..cc84769d0 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift @@ -1,6 +1,6 @@ // LoopFollow // BatteryAlarmEditor.swift -// Created by Jonas Björkert on 2025-05-17. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift index f224d7d58..2c8e8a841 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift @@ -1,6 +1,6 @@ // LoopFollow // BatteryDropAlarmEditor.swift -// Created by Jonas Björkert on 2025-05-20. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift index a1da6ef80..70d9e62db 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift @@ -1,6 +1,6 @@ // LoopFollow // BuildExpireAlarmEditor.swift -// Created by Jonas Björkert on 2025-04-26. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift index e3cd3e32a..903218ff7 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift @@ -1,6 +1,6 @@ // LoopFollow // COBAlarmEditor.swift -// Created by Jonas Björkert on 2025-05-15. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift index 4e72c3f0a..2b7e8c5df 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift @@ -1,6 +1,6 @@ // LoopFollow // FastDropAlarmEditor.swift -// Created by Jonas Björkert on 2025-05-11. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift index 9c18581c8..458b2f826 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift @@ -1,6 +1,6 @@ // LoopFollow // FastRiseAlarmEditor.swift -// Created by Jonas Björkert on 2025-05-15. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift index 558dab005..42219f971 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift @@ -1,6 +1,6 @@ // LoopFollow // HighBgAlarmEditor.swift -// Created by Jonas Björkert on 2025-04-26. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift index 1ddd2cb7d..883b189c9 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift @@ -1,6 +1,6 @@ // LoopFollow // IOBAlarmEditor.swift -// Created by Jonas Björkert on 2025-05-19. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift index a70e5c59f..9a50e4ff5 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift @@ -1,6 +1,6 @@ // LoopFollow // LowBgAlarmEditor.swift -// Created by Jonas Björkert on 2025-04-26. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift index 54a8533f9..dbd827327 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift @@ -1,6 +1,6 @@ // LoopFollow // MissedBolusAlarmEditor.swift -// Created by Jonas Björkert on 2025-05-20. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift index 79484e4d6..44c1f24c4 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift @@ -1,6 +1,6 @@ // LoopFollow // MissedReadingEditor.swift -// Created by Jonas Björkert on 2025-04-26. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift index 78bb5e2e4..f1edb3a9a 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift @@ -1,6 +1,6 @@ // LoopFollow // NotLoopingAlarmEditor.swift -// Created by Jonas Björkert on 2025-04-26. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift index f24de8373..47a55c1db 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift @@ -1,6 +1,6 @@ // LoopFollow // OverrideEndAlarmEditor.swift -// Created by Jonas Björkert on 2025-05-14. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift index d7a27f287..dbcc0c25c 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift @@ -1,6 +1,6 @@ // LoopFollow // OverrideStartAlarmEditor.swift -// Created by Jonas Björkert on 2025-05-14. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift index 6211fd01a..9ee78773a 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift @@ -1,6 +1,6 @@ // LoopFollow // PumpChangeAlarmEditor.swift -// Created by Jonas Björkert on 2025-05-17. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift index 32c200b65..96075af14 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift @@ -1,6 +1,6 @@ // LoopFollow // PumpVolumeAlarmEditor.swift -// Created by Jonas Björkert on 2025-05-17. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift index 7c504c3dd..392c4f421 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift @@ -1,6 +1,6 @@ // LoopFollow // RecBolusAlarmEditor.swift -// Created by Jonas Björkert on 2025-05-15. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift index 76d488cc9..ddaa8a24b 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift @@ -1,6 +1,6 @@ // LoopFollow // SensorAgeAlarmEditor.swift -// Created by Jonas Björkert on 2025-05-17. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift index 5e4b08e87..7460c6d94 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift @@ -1,6 +1,6 @@ // LoopFollow // TempTargetEndAlarmEditor.swift -// Created by Jonas Björkert on 2025-05-14. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift index 1e7a0da95..44c5dacac 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift @@ -1,6 +1,6 @@ // LoopFollow // TempTargetStartAlarmEditor.swift -// Created by Jonas Björkert on 2025-05-14. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift index 97348c682..6c8523133 100644 --- a/LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift +++ b/LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift @@ -1,6 +1,6 @@ // LoopFollow // TemporaryAlarmEditor.swift -// Created by Jonas Björkert on 2025-05-16. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index 26f50acb2..ccfeb530b 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -1,6 +1,6 @@ // LoopFollow // AlarmListView.swift -// Created by Jonas Björkert on 2025-04-26. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift index e25a6d01a..b70c067b7 100644 --- a/LoopFollow/Alarm/AlarmManager.swift +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -1,6 +1,6 @@ // LoopFollow // AlarmManager.swift -// Created by Jonas Björkert on 2025-04-26. +// Created by Jonas Björkert. import Foundation import UserNotifications diff --git a/LoopFollow/Alarm/AlarmSettingsView.swift b/LoopFollow/Alarm/AlarmSettingsView.swift index 90bb66754..8f0d31c59 100644 --- a/LoopFollow/Alarm/AlarmSettingsView.swift +++ b/LoopFollow/Alarm/AlarmSettingsView.swift @@ -1,6 +1,6 @@ // LoopFollow // AlarmSettingsView.swift -// Created by Jonas Björkert on 2025-04-26. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Alarm/AlarmType/AlarmType+Snooze.swift b/LoopFollow/Alarm/AlarmType/AlarmType+Snooze.swift index e3e15bede..6768f3473 100644 --- a/LoopFollow/Alarm/AlarmType/AlarmType+Snooze.swift +++ b/LoopFollow/Alarm/AlarmType/AlarmType+Snooze.swift @@ -1,6 +1,6 @@ // LoopFollow // AlarmType+Snooze.swift -// Created by Jonas Björkert on 2025-05-24. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmType/AlarmType+SortDirection.swift b/LoopFollow/Alarm/AlarmType/AlarmType+SortDirection.swift index 288bf1fa2..15cd6b4eb 100644 --- a/LoopFollow/Alarm/AlarmType/AlarmType+SortDirection.swift +++ b/LoopFollow/Alarm/AlarmType/AlarmType+SortDirection.swift @@ -1,6 +1,6 @@ // LoopFollow // AlarmType+SortDirection.swift -// Created by Jonas Björkert on 2025-05-16. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmType/AlarmType+canAcknowledge.swift b/LoopFollow/Alarm/AlarmType/AlarmType+canAcknowledge.swift index efab6d2a1..817a192f1 100644 --- a/LoopFollow/Alarm/AlarmType/AlarmType+canAcknowledge.swift +++ b/LoopFollow/Alarm/AlarmType/AlarmType+canAcknowledge.swift @@ -1,6 +1,6 @@ // LoopFollow // AlarmType+canAcknowledge.swift -// Created by Jonas Björkert on 2025-05-24. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift b/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift index 19ae85a93..25a77351c 100644 --- a/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift +++ b/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift @@ -1,6 +1,6 @@ // LoopFollow // AlarmType+timeUnit.swift -// Created by Jonas Björkert on 2025-05-16. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/AlarmType/AlarmType.swift b/LoopFollow/Alarm/AlarmType/AlarmType.swift index 2fd952a22..94c7ff9cd 100644 --- a/LoopFollow/Alarm/AlarmType/AlarmType.swift +++ b/LoopFollow/Alarm/AlarmType/AlarmType.swift @@ -1,6 +1,6 @@ // LoopFollow // AlarmType.swift -// Created by Jonas Björkert on 2025-04-26. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/DataStructs/BolusEntry.swift b/LoopFollow/Alarm/DataStructs/BolusEntry.swift index 141029ecf..a0c537458 100644 --- a/LoopFollow/Alarm/DataStructs/BolusEntry.swift +++ b/LoopFollow/Alarm/DataStructs/BolusEntry.swift @@ -1,6 +1,6 @@ // LoopFollow // BolusEntry.swift -// Created by Jonas Björkert on 2025-05-19. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/DataStructs/CarbSample.swift b/LoopFollow/Alarm/DataStructs/CarbSample.swift index 383d4801b..79a283598 100644 --- a/LoopFollow/Alarm/DataStructs/CarbSample.swift +++ b/LoopFollow/Alarm/DataStructs/CarbSample.swift @@ -1,6 +1,6 @@ // LoopFollow // CarbSample.swift -// Created by Jonas Björkert on 2025-05-20. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/DataStructs/GlucoseValue.swift b/LoopFollow/Alarm/DataStructs/GlucoseValue.swift index bd80a0ad6..c473deac7 100644 --- a/LoopFollow/Alarm/DataStructs/GlucoseValue.swift +++ b/LoopFollow/Alarm/DataStructs/GlucoseValue.swift @@ -1,6 +1,6 @@ // LoopFollow // GlucoseValue.swift -// Created by Jonas Björkert on 2025-05-06. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Alarm/SnoozeState.swift b/LoopFollow/Alarm/SnoozeState.swift index 8a3a10bc5..1d720c9ad 100644 --- a/LoopFollow/Alarm/SnoozeState.swift +++ b/LoopFollow/Alarm/SnoozeState.swift @@ -1,6 +1,6 @@ // LoopFollow // SnoozeState.swift -// Created by Jonas Björkert on 2025-04-26. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Application/AppDelegate.swift b/LoopFollow/Application/AppDelegate.swift index 05454bf46..32db917a7 100644 --- a/LoopFollow/Application/AppDelegate.swift +++ b/LoopFollow/Application/AppDelegate.swift @@ -1,6 +1,6 @@ // LoopFollow // AppDelegate.swift -// Created by Jon Fawcett on 2020-06-01. +// Created by Jon Fawcett. import CoreData import EventKit diff --git a/LoopFollow/Application/SceneDelegate.swift b/LoopFollow/Application/SceneDelegate.swift index aaa016503..6f4151a2a 100644 --- a/LoopFollow/Application/SceneDelegate.swift +++ b/LoopFollow/Application/SceneDelegate.swift @@ -1,6 +1,6 @@ // LoopFollow // SceneDelegate.swift -// Created by Jon Fawcett on 2020-06-01. +// Created by Jon Fawcett. import AVFoundation import UIKit diff --git a/LoopFollow/BackgroundRefresh/BT/BLEDevice.swift b/LoopFollow/BackgroundRefresh/BT/BLEDevice.swift index bac066e72..f30266ea9 100644 --- a/LoopFollow/BackgroundRefresh/BT/BLEDevice.swift +++ b/LoopFollow/BackgroundRefresh/BT/BLEDevice.swift @@ -1,6 +1,6 @@ // LoopFollow // BLEDevice.swift -// Created by Jonas Björkert on 2025-01-13. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/BackgroundRefresh/BT/BLEDeviceSelectionView.swift b/LoopFollow/BackgroundRefresh/BT/BLEDeviceSelectionView.swift index ada5167c8..f6bd64b98 100644 --- a/LoopFollow/BackgroundRefresh/BT/BLEDeviceSelectionView.swift +++ b/LoopFollow/BackgroundRefresh/BT/BLEDeviceSelectionView.swift @@ -1,6 +1,6 @@ // LoopFollow // BLEDeviceSelectionView.swift -// Created by Jonas Björkert on 2025-01-13. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/BackgroundRefresh/BT/BLEManager.swift b/LoopFollow/BackgroundRefresh/BT/BLEManager.swift index 598dca29a..5e47f9301 100644 --- a/LoopFollow/BackgroundRefresh/BT/BLEManager.swift +++ b/LoopFollow/BackgroundRefresh/BT/BLEManager.swift @@ -1,6 +1,6 @@ // LoopFollow // BLEManager.swift -// Created by Jonas Björkert on 2025-01-13. +// Created by Jonas Björkert. import Combine import CoreBluetooth diff --git a/LoopFollow/BackgroundRefresh/BT/BluetoothDevice.swift b/LoopFollow/BackgroundRefresh/BT/BluetoothDevice.swift index aed1f0845..1eb37a0fb 100644 --- a/LoopFollow/BackgroundRefresh/BT/BluetoothDevice.swift +++ b/LoopFollow/BackgroundRefresh/BT/BluetoothDevice.swift @@ -1,6 +1,6 @@ // LoopFollow // BluetoothDevice.swift -// Created by Jonas Björkert on 2025-01-13. +// Created by Jonas Björkert. import CoreBluetooth import Foundation diff --git a/LoopFollow/BackgroundRefresh/BT/BluetoothDeviceDelegate.swift b/LoopFollow/BackgroundRefresh/BT/BluetoothDeviceDelegate.swift index b9c946adc..5035a949a 100644 --- a/LoopFollow/BackgroundRefresh/BT/BluetoothDeviceDelegate.swift +++ b/LoopFollow/BackgroundRefresh/BT/BluetoothDeviceDelegate.swift @@ -1,6 +1,6 @@ // LoopFollow // BluetoothDeviceDelegate.swift -// Created by Jonas Björkert on 2025-01-13. +// Created by Jonas Björkert. import CoreBluetooth import Foundation diff --git a/LoopFollow/BackgroundRefresh/BT/Devices/DexcomHeartbeatBluetoothDevice.swift b/LoopFollow/BackgroundRefresh/BT/Devices/DexcomHeartbeatBluetoothDevice.swift index fa2347779..7ae5d2e91 100644 --- a/LoopFollow/BackgroundRefresh/BT/Devices/DexcomHeartbeatBluetoothDevice.swift +++ b/LoopFollow/BackgroundRefresh/BT/Devices/DexcomHeartbeatBluetoothDevice.swift @@ -1,6 +1,6 @@ // LoopFollow // DexcomHeartbeatBluetoothDevice.swift -// Created by Jonas Björkert on 2025-01-13. +// Created by Jonas Björkert. import AVFoundation import CoreBluetooth diff --git a/LoopFollow/BackgroundRefresh/BT/Devices/OmnipodDashHeartbeatBluetoothTransmitter.swift b/LoopFollow/BackgroundRefresh/BT/Devices/OmnipodDashHeartbeatBluetoothTransmitter.swift index 2ae9a07af..1a1ae79e9 100644 --- a/LoopFollow/BackgroundRefresh/BT/Devices/OmnipodDashHeartbeatBluetoothTransmitter.swift +++ b/LoopFollow/BackgroundRefresh/BT/Devices/OmnipodDashHeartbeatBluetoothTransmitter.swift @@ -1,6 +1,6 @@ // LoopFollow // OmnipodDashHeartbeatBluetoothTransmitter.swift -// Created by Jonas Björkert on 2025-01-13. +// Created by Jonas Björkert. import CoreBluetooth import Foundation diff --git a/LoopFollow/BackgroundRefresh/BT/Devices/RileyLinkHeartbeatBluetoothDevice.swift b/LoopFollow/BackgroundRefresh/BT/Devices/RileyLinkHeartbeatBluetoothDevice.swift index c00f42625..99141731c 100644 --- a/LoopFollow/BackgroundRefresh/BT/Devices/RileyLinkHeartbeatBluetoothDevice.swift +++ b/LoopFollow/BackgroundRefresh/BT/Devices/RileyLinkHeartbeatBluetoothDevice.swift @@ -1,6 +1,6 @@ // LoopFollow // RileyLinkHeartbeatBluetoothDevice.swift -// Created by Jonas Björkert on 2025-01-13. +// Created by Jonas Björkert. import CoreBluetooth import Foundation diff --git a/LoopFollow/BackgroundRefresh/BT/DexcomG7HeartBeat.swift b/LoopFollow/BackgroundRefresh/BT/DexcomG7HeartBeat.swift index 5a9639c1a..14b427208 100644 --- a/LoopFollow/BackgroundRefresh/BT/DexcomG7HeartBeat.swift +++ b/LoopFollow/BackgroundRefresh/BT/DexcomG7HeartBeat.swift @@ -1,6 +1,6 @@ // LoopFollow // DexcomG7HeartBeat.swift -// Created by Jonas Björkert on 2025-01-13. +// Created by Jonas Björkert. // Denna behövs diff --git a/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift b/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift index 18b66b2d3..bdde1a896 100644 --- a/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift +++ b/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift @@ -1,6 +1,6 @@ // LoopFollow // BackgroundRefreshSettingsView.swift -// Created by Jonas Björkert on 2025-01-13. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsViewModel.swift b/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsViewModel.swift index 746721640..cf1f22b03 100644 --- a/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsViewModel.swift +++ b/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsViewModel.swift @@ -1,6 +1,6 @@ // LoopFollow // BackgroundRefreshSettingsViewModel.swift -// Created by Jonas Björkert on 2025-01-13. +// Created by Jonas Björkert. import Combine import Foundation diff --git a/LoopFollow/BackgroundRefresh/BackgroundRefreshType.swift b/LoopFollow/BackgroundRefresh/BackgroundRefreshType.swift index ae38ccf17..da452922e 100644 --- a/LoopFollow/BackgroundRefresh/BackgroundRefreshType.swift +++ b/LoopFollow/BackgroundRefresh/BackgroundRefreshType.swift @@ -1,6 +1,6 @@ // LoopFollow // BackgroundRefreshType.swift -// Created by Jonas Björkert on 2025-01-13. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Contact/ContactColorOption.swift b/LoopFollow/Contact/ContactColorOption.swift index 0641fb524..ec472dac7 100644 --- a/LoopFollow/Contact/ContactColorOption.swift +++ b/LoopFollow/Contact/ContactColorOption.swift @@ -1,6 +1,6 @@ // LoopFollow // ContactColorOption.swift -// Created by Jonas Björkert on 2025-02-23. +// Created by Jonas Björkert. import UIKit diff --git a/LoopFollow/Contact/ContactImageUpdater.swift b/LoopFollow/Contact/ContactImageUpdater.swift index dbeaee0b4..86c06cbac 100644 --- a/LoopFollow/Contact/ContactImageUpdater.swift +++ b/LoopFollow/Contact/ContactImageUpdater.swift @@ -1,6 +1,6 @@ // LoopFollow // ContactImageUpdater.swift -// Created by Jonas Björkert on 2024-12-10. +// Created by Jonas Björkert. import Contacts import Foundation diff --git a/LoopFollow/Contact/ContactIncludeOption.swift b/LoopFollow/Contact/ContactIncludeOption.swift index b41d5bc95..adce2a431 100644 --- a/LoopFollow/Contact/ContactIncludeOption.swift +++ b/LoopFollow/Contact/ContactIncludeOption.swift @@ -1,6 +1,6 @@ // LoopFollow // ContactIncludeOption.swift -// Created by Jonas Björkert on 2025-02-23. +// Created by Jonas Björkert. enum ContactIncludeOption: String, Codable, Equatable, CaseIterable { case off = "Off" diff --git a/LoopFollow/Contact/ContactType.swift b/LoopFollow/Contact/ContactType.swift index 6181dd360..0a8438bcf 100644 --- a/LoopFollow/Contact/ContactType.swift +++ b/LoopFollow/Contact/ContactType.swift @@ -1,6 +1,6 @@ // LoopFollow // ContactType.swift -// Created by Jonas Björkert on 2025-02-23. +// Created by Jonas Björkert. enum ContactType: String, CaseIterable { case BG diff --git a/LoopFollow/Controllers/AlarmSound.swift b/LoopFollow/Controllers/AlarmSound.swift index 5907355b2..e2575a5de 100644 --- a/LoopFollow/Controllers/AlarmSound.swift +++ b/LoopFollow/Controllers/AlarmSound.swift @@ -1,6 +1,6 @@ // LoopFollow // AlarmSound.swift -// Created by Jon Fawcett on 2020-06-07. +// Created by Jon Fawcett. import AVFoundation import Foundation diff --git a/LoopFollow/Controllers/BackgroundAlertManager.swift b/LoopFollow/Controllers/BackgroundAlertManager.swift index 1aba069d6..960879f98 100644 --- a/LoopFollow/Controllers/BackgroundAlertManager.swift +++ b/LoopFollow/Controllers/BackgroundAlertManager.swift @@ -1,6 +1,6 @@ // LoopFollow // BackgroundAlertManager.swift -// Created by Jonas Björkert on 2024-06-22. +// Created by Jonas Björkert. import Foundation import UserNotifications diff --git a/LoopFollow/Controllers/Graphs.swift b/LoopFollow/Controllers/Graphs.swift index 9671e43aa..1261183a7 100644 --- a/LoopFollow/Controllers/Graphs.swift +++ b/LoopFollow/Controllers/Graphs.swift @@ -1,6 +1,6 @@ // LoopFollow // Graphs.swift -// Created by Jon Fawcett on 2020-06-17. +// Created by Jon Fawcett. import Charts import Foundation diff --git a/LoopFollow/Controllers/MainViewController+updateStats.swift b/LoopFollow/Controllers/MainViewController+updateStats.swift index ca0a1eac2..d9cfd30ae 100644 --- a/LoopFollow/Controllers/MainViewController+updateStats.swift +++ b/LoopFollow/Controllers/MainViewController+updateStats.swift @@ -1,6 +1,6 @@ // LoopFollow // MainViewController+updateStats.swift -// Created by Jon Fawcett on 2020-06-23. +// Created by Jon Fawcett. import Charts import Foundation diff --git a/LoopFollow/Controllers/NightScout.swift b/LoopFollow/Controllers/NightScout.swift index dfb8b14bd..c69dc64f1 100644 --- a/LoopFollow/Controllers/NightScout.swift +++ b/LoopFollow/Controllers/NightScout.swift @@ -1,6 +1,6 @@ // LoopFollow // NightScout.swift -// Created by Jon Fawcett on 2020-06-17. +// Created by Jon Fawcett. import Foundation import UIKit diff --git a/LoopFollow/Controllers/Nightscout/BGData.swift b/LoopFollow/Controllers/Nightscout/BGData.swift index 8f98aaad1..a616f4e52 100644 --- a/LoopFollow/Controllers/Nightscout/BGData.swift +++ b/LoopFollow/Controllers/Nightscout/BGData.swift @@ -1,6 +1,6 @@ // LoopFollow // BGData.swift -// Created by Jonas Björkert on 2023-10-05. +// Created by Jonas Björkert. import Foundation import UIKit diff --git a/LoopFollow/Controllers/Nightscout/CAge.swift b/LoopFollow/Controllers/Nightscout/CAge.swift index d6c986133..5a0b97151 100644 --- a/LoopFollow/Controllers/Nightscout/CAge.swift +++ b/LoopFollow/Controllers/Nightscout/CAge.swift @@ -1,6 +1,6 @@ // LoopFollow // CAge.swift -// Created by Jonas Björkert on 2023-10-05. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift index ff54135be..6374bdec8 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift @@ -1,6 +1,6 @@ // LoopFollow // DeviceStatus.swift -// Created by Jonas Björkert on 2023-10-05. +// Created by Jonas Björkert. import Charts import Foundation diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift index 386850013..ce9e841dc 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift @@ -1,6 +1,6 @@ // LoopFollow // DeviceStatusLoop.swift -// Created by Jonas Björkert on 2024-06-16. +// Created by Jonas Björkert. import Charts import Foundation diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift index 94a55d1f8..77a735fe5 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift @@ -1,6 +1,6 @@ // LoopFollow // DeviceStatusOpenAPS.swift -// Created by Jonas Björkert on 2024-05-31. +// Created by Jonas Björkert. import Foundation import HealthKit diff --git a/LoopFollow/Controllers/Nightscout/IAge.swift b/LoopFollow/Controllers/Nightscout/IAge.swift index d7daa42ce..f47fbe8c3 100644 --- a/LoopFollow/Controllers/Nightscout/IAge.swift +++ b/LoopFollow/Controllers/Nightscout/IAge.swift @@ -1,6 +1,6 @@ // LoopFollow // IAge.swift -// Created by Jonas Björkert on 2024-08-05. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/NSProfile.swift b/LoopFollow/Controllers/Nightscout/NSProfile.swift index 3a879e87f..d804b88a7 100644 --- a/LoopFollow/Controllers/Nightscout/NSProfile.swift +++ b/LoopFollow/Controllers/Nightscout/NSProfile.swift @@ -1,6 +1,6 @@ // LoopFollow // NSProfile.swift -// Created by Jonas Björkert on 2024-07-15. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/Profile.swift b/LoopFollow/Controllers/Nightscout/Profile.swift index e019a7104..662160eb2 100644 --- a/LoopFollow/Controllers/Nightscout/Profile.swift +++ b/LoopFollow/Controllers/Nightscout/Profile.swift @@ -1,6 +1,6 @@ // LoopFollow // Profile.swift -// Created by Jonas Björkert on 2023-10-05. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/ProfileManager.swift b/LoopFollow/Controllers/Nightscout/ProfileManager.swift index 7b786bfbe..be1ac3e28 100644 --- a/LoopFollow/Controllers/Nightscout/ProfileManager.swift +++ b/LoopFollow/Controllers/Nightscout/ProfileManager.swift @@ -1,6 +1,6 @@ // LoopFollow // ProfileManager.swift -// Created by Jonas Björkert on 2024-07-15. +// Created by Jonas Björkert. import Foundation import HealthKit diff --git a/LoopFollow/Controllers/Nightscout/SAge.swift b/LoopFollow/Controllers/Nightscout/SAge.swift index c45ea81a6..6c546187f 100644 --- a/LoopFollow/Controllers/Nightscout/SAge.swift +++ b/LoopFollow/Controllers/Nightscout/SAge.swift @@ -1,6 +1,6 @@ // LoopFollow // SAge.swift -// Created by Jonas Björkert on 2023-10-05. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/Treatments.swift b/LoopFollow/Controllers/Nightscout/Treatments.swift index f33f6c760..4ee4d939f 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments.swift @@ -1,6 +1,6 @@ // LoopFollow // Treatments.swift -// Created by Jonas Björkert on 2023-10-05. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/Treatments/BGCheck.swift b/LoopFollow/Controllers/Nightscout/Treatments/BGCheck.swift index 00ec21c39..e08f43eef 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/BGCheck.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/BGCheck.swift @@ -1,6 +1,6 @@ // LoopFollow // BGCheck.swift -// Created by Jonas Björkert on 2023-10-05. +// Created by Jonas Björkert. import Foundation import UIKit diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Basals.swift b/LoopFollow/Controllers/Nightscout/Treatments/Basals.swift index bfaf8c9ed..65aaf3dde 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Basals.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Basals.swift @@ -1,6 +1,6 @@ // LoopFollow // Basals.swift -// Created by Jonas Björkert on 2023-10-05. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Bolus.swift b/LoopFollow/Controllers/Nightscout/Treatments/Bolus.swift index 6ea898ce9..9b6c51ca8 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Bolus.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Bolus.swift @@ -1,6 +1,6 @@ // LoopFollow // Bolus.swift -// Created by Jonas Björkert on 2023-10-05. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift b/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift index 05cc044a1..8b3f238d2 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift @@ -1,6 +1,6 @@ // LoopFollow // Carbs.swift -// Created by Jonas Björkert on 2023-10-05. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/Treatments/InsulinCartridgeChange.swift b/LoopFollow/Controllers/Nightscout/Treatments/InsulinCartridgeChange.swift index 3a3aa0e20..2962fee9b 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/InsulinCartridgeChange.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/InsulinCartridgeChange.swift @@ -1,6 +1,6 @@ // LoopFollow // InsulinCartridgeChange.swift -// Created by Jonas Björkert on 2024-08-05. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Notes.swift b/LoopFollow/Controllers/Nightscout/Treatments/Notes.swift index 2e009644f..4019c0f1a 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Notes.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Notes.swift @@ -1,6 +1,6 @@ // LoopFollow // Notes.swift -// Created by Jonas Björkert on 2023-10-05. +// Created by Jonas Björkert. import Foundation import UIKit diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift b/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift index 3a081d1ef..0730fc41a 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift @@ -1,6 +1,6 @@ // LoopFollow // Overrides.swift -// Created by Jonas Björkert on 2023-10-05. +// Created by Jonas Björkert. import Foundation import UIKit diff --git a/LoopFollow/Controllers/Nightscout/Treatments/ResumePump.swift b/LoopFollow/Controllers/Nightscout/Treatments/ResumePump.swift index 8eee01618..5f42d5c74 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/ResumePump.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/ResumePump.swift @@ -1,6 +1,6 @@ // LoopFollow // ResumePump.swift -// Created by Jonas Björkert on 2023-10-05. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/Treatments/SMB.swift b/LoopFollow/Controllers/Nightscout/Treatments/SMB.swift index 7ab3b7a48..259e9da16 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/SMB.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/SMB.swift @@ -1,6 +1,6 @@ // LoopFollow // SMB.swift -// Created by Jonas Björkert on 2023-10-05. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/Treatments/SensorStart.swift b/LoopFollow/Controllers/Nightscout/Treatments/SensorStart.swift index aa0646421..3be934076 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/SensorStart.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/SensorStart.swift @@ -1,6 +1,6 @@ // LoopFollow // SensorStart.swift -// Created by Jonas Björkert on 2023-10-05. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/Treatments/SiteChange.swift b/LoopFollow/Controllers/Nightscout/Treatments/SiteChange.swift index 7a39cd7de..faa5b2491 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/SiteChange.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/SiteChange.swift @@ -1,6 +1,6 @@ // LoopFollow // SiteChange.swift -// Created by Jonas Björkert on 2023-10-06. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/Treatments/SuspendPump.swift b/LoopFollow/Controllers/Nightscout/Treatments/SuspendPump.swift index 2e1137930..758c0b71e 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/SuspendPump.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/SuspendPump.swift @@ -1,6 +1,6 @@ // LoopFollow // SuspendPump.swift -// Created by Jonas Björkert on 2023-10-05. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Controllers/Nightscout/Treatments/TemporaryTarget.swift b/LoopFollow/Controllers/Nightscout/Treatments/TemporaryTarget.swift index 529b0de8a..483e00e1d 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/TemporaryTarget.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/TemporaryTarget.swift @@ -1,6 +1,6 @@ // LoopFollow // TemporaryTarget.swift -// Created by Jonas Björkert on 2024-07-28. +// Created by Jonas Björkert. import Foundation import HealthKit diff --git a/LoopFollow/Controllers/SpeakBG.swift b/LoopFollow/Controllers/SpeakBG.swift index 05db62a2d..1d340753e 100644 --- a/LoopFollow/Controllers/SpeakBG.swift +++ b/LoopFollow/Controllers/SpeakBG.swift @@ -1,6 +1,6 @@ // LoopFollow // SpeakBG.swift -// Created by Jonas Björkert on 2025-05-03. +// Created by Jonas Björkert. import AVFoundation import CallKit diff --git a/LoopFollow/Controllers/Stats.swift b/LoopFollow/Controllers/Stats.swift index 4fb31906a..8405ee714 100644 --- a/LoopFollow/Controllers/Stats.swift +++ b/LoopFollow/Controllers/Stats.swift @@ -1,6 +1,6 @@ // LoopFollow // Stats.swift -// Created by Jon Fawcett on 2020-06-23. +// Created by Jon Fawcett. import Foundation diff --git a/LoopFollow/Controllers/Timers.swift b/LoopFollow/Controllers/Timers.swift index ced520c4c..698592f41 100644 --- a/LoopFollow/Controllers/Timers.swift +++ b/LoopFollow/Controllers/Timers.swift @@ -1,6 +1,6 @@ // LoopFollow // Timers.swift -// Created by Jon Fawcett on 2020-09-03. +// Created by Jon Fawcett. import Foundation import UIKit diff --git a/LoopFollow/Extensions/Binding+Optional.swift b/LoopFollow/Extensions/Binding+Optional.swift index 092ac2a28..d7efc1efb 100644 --- a/LoopFollow/Extensions/Binding+Optional.swift +++ b/LoopFollow/Extensions/Binding+Optional.swift @@ -1,6 +1,6 @@ // LoopFollow // Binding+Optional.swift -// Created by Jonas Björkert on 2025-04-26. +// Created by Jonas Björkert. import Foundation import SwiftUI diff --git a/LoopFollow/Extensions/EKEventStore+Extensions.swift b/LoopFollow/Extensions/EKEventStore+Extensions.swift index d218e83c3..005a4b945 100644 --- a/LoopFollow/Extensions/EKEventStore+Extensions.swift +++ b/LoopFollow/Extensions/EKEventStore+Extensions.swift @@ -1,6 +1,6 @@ // LoopFollow // EKEventStore+Extensions.swift -// Created by Jonas Björkert on 2023-07-27. +// Created by Jonas Björkert. import EventKit import Foundation diff --git a/LoopFollow/Extensions/HKQuantity+AnyConvertible.swift b/LoopFollow/Extensions/HKQuantity+AnyConvertible.swift index 332502d8f..d22f6d9da 100644 --- a/LoopFollow/Extensions/HKQuantity+AnyConvertible.swift +++ b/LoopFollow/Extensions/HKQuantity+AnyConvertible.swift @@ -1,6 +1,6 @@ // LoopFollow // HKQuantity+AnyConvertible.swift -// Created by Jonas Björkert on 2024-07-28. +// Created by Jonas Björkert. import HealthKit diff --git a/LoopFollow/Extensions/HKUnit+Extensions.swift b/LoopFollow/Extensions/HKUnit+Extensions.swift index c4d920b54..606162fc0 100644 --- a/LoopFollow/Extensions/HKUnit+Extensions.swift +++ b/LoopFollow/Extensions/HKUnit+Extensions.swift @@ -1,6 +1,6 @@ // LoopFollow // HKUnit+Extensions.swift -// Created by Jonas Björkert on 2024-07-16. +// Created by Jonas Björkert. import Foundation import HealthKit diff --git a/LoopFollow/Extensions/ShareClientExtension.swift b/LoopFollow/Extensions/ShareClientExtension.swift index bcce0c6d7..beb2fb5e3 100644 --- a/LoopFollow/Extensions/ShareClientExtension.swift +++ b/LoopFollow/Extensions/ShareClientExtension.swift @@ -1,6 +1,6 @@ // LoopFollow // ShareClientExtension.swift -// Created by Jose Paredes on 2020-07-14. +// Created by Jose Paredes. import Foundation import ShareClient diff --git a/LoopFollow/Extensions/UIViewExtension.swift b/LoopFollow/Extensions/UIViewExtension.swift index db6cf28d5..7d9b8ba10 100644 --- a/LoopFollow/Extensions/UIViewExtension.swift +++ b/LoopFollow/Extensions/UIViewExtension.swift @@ -1,6 +1,6 @@ // LoopFollow // UIViewExtension.swift -// Created by Jose Paredes on 2020-07-17. +// Created by Jose Paredes. import Foundation import UIKit diff --git a/LoopFollow/Extensions/UUID+Identifiable.swift b/LoopFollow/Extensions/UUID+Identifiable.swift index c82cff480..d8be93ef8 100644 --- a/LoopFollow/Extensions/UUID+Identifiable.swift +++ b/LoopFollow/Extensions/UUID+Identifiable.swift @@ -1,6 +1,6 @@ // LoopFollow // UUID+Identifiable.swift -// Created by Jonas Björkert on 2025-04-26. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Helpers/AnyConvertible.swift b/LoopFollow/Helpers/AnyConvertible.swift index 5735ea1b3..c8e6f6973 100644 --- a/LoopFollow/Helpers/AnyConvertible.swift +++ b/LoopFollow/Helpers/AnyConvertible.swift @@ -1,6 +1,6 @@ // LoopFollow // AnyConvertible.swift -// Created by Jon Fawcett on 2020-06-05. +// Created by Jon Fawcett. import Foundation diff --git a/LoopFollow/Helpers/AppConstants.swift b/LoopFollow/Helpers/AppConstants.swift index c8b446d43..5c1dccdaa 100644 --- a/LoopFollow/Helpers/AppConstants.swift +++ b/LoopFollow/Helpers/AppConstants.swift @@ -1,6 +1,6 @@ // LoopFollow // AppConstants.swift -// Created by Jon Fawcett on 2020-06-05. +// Created by Jon Fawcett. import Foundation diff --git a/LoopFollow/Helpers/AppVersionManager.swift b/LoopFollow/Helpers/AppVersionManager.swift index 077789fb6..5ec5df960 100644 --- a/LoopFollow/Helpers/AppVersionManager.swift +++ b/LoopFollow/Helpers/AppVersionManager.swift @@ -1,6 +1,6 @@ // LoopFollow // AppVersionManager.swift -// Created by Jonas Björkert on 2024-05-11. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Helpers/BackgroundTaskAudio.swift b/LoopFollow/Helpers/BackgroundTaskAudio.swift index 6bed5f430..df39d89c5 100755 --- a/LoopFollow/Helpers/BackgroundTaskAudio.swift +++ b/LoopFollow/Helpers/BackgroundTaskAudio.swift @@ -1,6 +1,6 @@ // LoopFollow // BackgroundTaskAudio.swift -// Created by Jon Fawcett on 2020-06-05. +// Created by Jon Fawcett. import AVFoundation diff --git a/LoopFollow/Helpers/BinaryFloatingPoint+localized.swift b/LoopFollow/Helpers/BinaryFloatingPoint+localized.swift index f0d58cb14..008fafdff 100644 --- a/LoopFollow/Helpers/BinaryFloatingPoint+localized.swift +++ b/LoopFollow/Helpers/BinaryFloatingPoint+localized.swift @@ -1,6 +1,6 @@ // LoopFollow // BinaryFloatingPoint+localized.swift -// Created by Jonas Björkert on 2025-05-26. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Helpers/BuildDetails.swift b/LoopFollow/Helpers/BuildDetails.swift index 561181359..473819218 100644 --- a/LoopFollow/Helpers/BuildDetails.swift +++ b/LoopFollow/Helpers/BuildDetails.swift @@ -1,6 +1,6 @@ // LoopFollow // BuildDetails.swift -// Created by Jonas Björkert on 2024-03-25. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Helpers/Chart.swift b/LoopFollow/Helpers/Chart.swift index 06e59d9c4..8fecbf525 100644 --- a/LoopFollow/Helpers/Chart.swift +++ b/LoopFollow/Helpers/Chart.swift @@ -1,6 +1,6 @@ // LoopFollow // Chart.swift -// Created by Jon Fawcett on 2020-06-05. +// Created by Jon Fawcett. import Charts import Foundation diff --git a/LoopFollow/Helpers/CycleHelper.swift b/LoopFollow/Helpers/CycleHelper.swift index dc3240d79..391466920 100644 --- a/LoopFollow/Helpers/CycleHelper.swift +++ b/LoopFollow/Helpers/CycleHelper.swift @@ -1,6 +1,6 @@ // LoopFollow // CycleHelper.swift -// Created by Jonas Björkert on 2025-03-01. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Helpers/DataStructs.swift b/LoopFollow/Helpers/DataStructs.swift index 983d49752..e50ea48f7 100644 --- a/LoopFollow/Helpers/DataStructs.swift +++ b/LoopFollow/Helpers/DataStructs.swift @@ -1,6 +1,6 @@ // LoopFollow // DataStructs.swift -// Created by Jon Fawcett on 2020-06-23. +// Created by Jon Fawcett. import Foundation diff --git a/LoopFollow/Helpers/DateTime.swift b/LoopFollow/Helpers/DateTime.swift index b0bc53041..3f82f856d 100644 --- a/LoopFollow/Helpers/DateTime.swift +++ b/LoopFollow/Helpers/DateTime.swift @@ -1,6 +1,6 @@ // LoopFollow // DateTime.swift -// Created by Jon Fawcett on 2020-06-17. +// Created by Jon Fawcett. import Foundation diff --git a/LoopFollow/Helpers/DictionaryKeyPath.swift b/LoopFollow/Helpers/DictionaryKeyPath.swift index e080d8fcb..f3043fa34 100644 --- a/LoopFollow/Helpers/DictionaryKeyPath.swift +++ b/LoopFollow/Helpers/DictionaryKeyPath.swift @@ -1,6 +1,6 @@ // LoopFollow // DictionaryKeyPath.swift -// Created by Jon Fawcett on 2020-06-11. +// Created by Jon Fawcett. // For details, see // http://stackoverflow.com/questions/40261857/remove-nested-key-from-dictionary diff --git a/LoopFollow/Helpers/GitHubService.swift b/LoopFollow/Helpers/GitHubService.swift index 61b00c26f..808b545ae 100644 --- a/LoopFollow/Helpers/GitHubService.swift +++ b/LoopFollow/Helpers/GitHubService.swift @@ -1,6 +1,6 @@ // LoopFollow // GitHubService.swift -// Created by Jonas Björkert on 2024-05-11. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Helpers/Globals.swift b/LoopFollow/Helpers/Globals.swift index b1b160e11..a7175ee19 100644 --- a/LoopFollow/Helpers/Globals.swift +++ b/LoopFollow/Helpers/Globals.swift @@ -1,6 +1,6 @@ // LoopFollow // Globals.swift -// Created by Jon Fawcett on 2020-07-23. +// Created by Jon Fawcett. import Foundation diff --git a/LoopFollow/Helpers/GlucoseConversion.swift b/LoopFollow/Helpers/GlucoseConversion.swift index 400f7449f..61478cc37 100644 --- a/LoopFollow/Helpers/GlucoseConversion.swift +++ b/LoopFollow/Helpers/GlucoseConversion.swift @@ -1,6 +1,6 @@ // LoopFollow // GlucoseConversion.swift -// Created by Jonas Björkert on 2024-04-28. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Helpers/Localizer.swift b/LoopFollow/Helpers/Localizer.swift index a3c2dbf45..34b45e76d 100644 --- a/LoopFollow/Helpers/Localizer.swift +++ b/LoopFollow/Helpers/Localizer.swift @@ -1,6 +1,6 @@ // LoopFollow // Localizer.swift -// Created by Jon Fawcett on 2020-06-22. +// Created by Jon Fawcett. import Foundation import HealthKit diff --git a/LoopFollow/Helpers/Mobileprovision.swift b/LoopFollow/Helpers/Mobileprovision.swift index b5d2a47f1..26aed1891 100644 --- a/LoopFollow/Helpers/Mobileprovision.swift +++ b/LoopFollow/Helpers/Mobileprovision.swift @@ -1,6 +1,6 @@ // LoopFollow // Mobileprovision.swift -// Created by Jon Fawcett on 2020-10-05. +// Created by Jon Fawcett. // // MobileProvision.swift diff --git a/LoopFollow/Helpers/NightscoutUtils.swift b/LoopFollow/Helpers/NightscoutUtils.swift index 435ba57e2..0bd21bf2b 100644 --- a/LoopFollow/Helpers/NightscoutUtils.swift +++ b/LoopFollow/Helpers/NightscoutUtils.swift @@ -1,6 +1,6 @@ // LoopFollow // NightscoutUtils.swift -// Created by bjorkert on 2023-04-09. +// Created by bjorkert. import Foundation diff --git a/LoopFollow/Helpers/ObservationToken.swift b/LoopFollow/Helpers/ObservationToken.swift index 7c3d7bc43..e2028c530 100644 --- a/LoopFollow/Helpers/ObservationToken.swift +++ b/LoopFollow/Helpers/ObservationToken.swift @@ -1,6 +1,6 @@ // LoopFollow // ObservationToken.swift -// Created by Jon Fawcett on 2020-06-05. +// Created by Jon Fawcett. import Foundation diff --git a/LoopFollow/Helpers/TextFieldWithToolBar.swift b/LoopFollow/Helpers/TextFieldWithToolBar.swift index 52aa182b8..c3119e6ee 100644 --- a/LoopFollow/Helpers/TextFieldWithToolBar.swift +++ b/LoopFollow/Helpers/TextFieldWithToolBar.swift @@ -1,6 +1,6 @@ // LoopFollow // TextFieldWithToolBar.swift -// Created by Jonas Björkert on 2024-07-28. +// Created by Jonas Björkert. import HealthKit import SwiftUI diff --git a/LoopFollow/Helpers/TimeOfDay.swift b/LoopFollow/Helpers/TimeOfDay.swift index 4c6b408ea..0076e4795 100644 --- a/LoopFollow/Helpers/TimeOfDay.swift +++ b/LoopFollow/Helpers/TimeOfDay.swift @@ -1,6 +1,6 @@ // LoopFollow // TimeOfDay.swift -// Created by Jonas Björkert on 2025-04-26. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Helpers/Views/ActionRow.swift b/LoopFollow/Helpers/Views/ActionRow.swift index 86d28cb05..077caeb87 100644 --- a/LoopFollow/Helpers/Views/ActionRow.swift +++ b/LoopFollow/Helpers/Views/ActionRow.swift @@ -1,6 +1,6 @@ // LoopFollow // ActionRow.swift -// Created by Jonas Björkert on 2025-05-27. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Helpers/Views/BGPicker.swift b/LoopFollow/Helpers/Views/BGPicker.swift index 09bd06adf..db4ae1d72 100644 --- a/LoopFollow/Helpers/Views/BGPicker.swift +++ b/LoopFollow/Helpers/Views/BGPicker.swift @@ -1,6 +1,6 @@ // LoopFollow // BGPicker.swift -// Created by Jonas Björkert on 2025-05-14. +// Created by Jonas Björkert. import HealthKit import SwiftUI diff --git a/LoopFollow/Helpers/Views/ErrorMessageView.swift b/LoopFollow/Helpers/Views/ErrorMessageView.swift index 7ba254af9..632ad4aaa 100644 --- a/LoopFollow/Helpers/Views/ErrorMessageView.swift +++ b/LoopFollow/Helpers/Views/ErrorMessageView.swift @@ -1,6 +1,6 @@ // LoopFollow // ErrorMessageView.swift -// Created by Jonas Björkert on 2024-07-31. +// Created by Jonas Björkert. import Foundation import SwiftUI diff --git a/LoopFollow/Helpers/Views/Glyph.swift b/LoopFollow/Helpers/Views/Glyph.swift index a5c166a54..61faa4474 100644 --- a/LoopFollow/Helpers/Views/Glyph.swift +++ b/LoopFollow/Helpers/Views/Glyph.swift @@ -1,6 +1,6 @@ // LoopFollow // Glyph.swift -// Created by Jonas Björkert on 2025-05-27. +// Created by Jonas Björkert. import SwiftUICore diff --git a/LoopFollow/Helpers/Views/HKQuantityInputView.swift b/LoopFollow/Helpers/Views/HKQuantityInputView.swift index 681ae488e..7417b076f 100644 --- a/LoopFollow/Helpers/Views/HKQuantityInputView.swift +++ b/LoopFollow/Helpers/Views/HKQuantityInputView.swift @@ -1,6 +1,6 @@ // LoopFollow // HKQuantityInputView.swift -// Created by Jonas Björkert on 2024-09-17. +// Created by Jonas Björkert. import Foundation import HealthKit diff --git a/LoopFollow/Helpers/Views/LinkRow.swift b/LoopFollow/Helpers/Views/LinkRow.swift index 6470ba4fe..bbee1da27 100644 --- a/LoopFollow/Helpers/Views/LinkRow.swift +++ b/LoopFollow/Helpers/Views/LinkRow.swift @@ -1,6 +1,6 @@ // LoopFollow // LinkRow.swift -// Created by Jonas Björkert on 2025-05-27. +// Created by Jonas Björkert. import Foundation import SwiftUI diff --git a/LoopFollow/Helpers/Views/LoadingButtonView.swift b/LoopFollow/Helpers/Views/LoadingButtonView.swift index 9b9585ce6..9b09df524 100644 --- a/LoopFollow/Helpers/Views/LoadingButtonView.swift +++ b/LoopFollow/Helpers/Views/LoadingButtonView.swift @@ -1,6 +1,6 @@ // LoopFollow // LoadingButtonView.swift -// Created by Jonas Björkert on 2024-09-17. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Helpers/Views/NavigationRow.swift b/LoopFollow/Helpers/Views/NavigationRow.swift index 8e137b5d9..1cfdf3999 100644 --- a/LoopFollow/Helpers/Views/NavigationRow.swift +++ b/LoopFollow/Helpers/Views/NavigationRow.swift @@ -1,6 +1,6 @@ // LoopFollow // NavigationRow.swift -// Created by Jonas Björkert on 2025-05-27. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Helpers/Views/SettingsStepperRow.swift b/LoopFollow/Helpers/Views/SettingsStepperRow.swift index 5f84d7d0c..84b63c2e3 100644 --- a/LoopFollow/Helpers/Views/SettingsStepperRow.swift +++ b/LoopFollow/Helpers/Views/SettingsStepperRow.swift @@ -1,6 +1,6 @@ // LoopFollow // SettingsStepperRow.swift -// Created by Jonas Björkert on 2025-05-26. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Helpers/Views/TogglableSecureInput.swift b/LoopFollow/Helpers/Views/TogglableSecureInput.swift index f41ea4b77..10c37a9f9 100644 --- a/LoopFollow/Helpers/Views/TogglableSecureInput.swift +++ b/LoopFollow/Helpers/Views/TogglableSecureInput.swift @@ -1,6 +1,6 @@ // LoopFollow // TogglableSecureInput.swift -// Created by Jonas Björkert on 2025-05-28. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Helpers/carbBolusArrays.swift b/LoopFollow/Helpers/carbBolusArrays.swift index 298087b3d..a7146fe42 100644 --- a/LoopFollow/Helpers/carbBolusArrays.swift +++ b/LoopFollow/Helpers/carbBolusArrays.swift @@ -1,6 +1,6 @@ // LoopFollow // carbBolusArrays.swift -// Created by Jon Fawcett on 2020-06-17. +// Created by Jon Fawcett. import Foundation diff --git a/LoopFollow/Helpers/isOnPhoneCall.swift b/LoopFollow/Helpers/isOnPhoneCall.swift index 33f780cae..fa8f7e36a 100644 --- a/LoopFollow/Helpers/isOnPhoneCall.swift +++ b/LoopFollow/Helpers/isOnPhoneCall.swift @@ -1,6 +1,6 @@ // LoopFollow // isOnPhoneCall.swift -// Created by Jonas Björkert on 2025-05-03. +// Created by Jonas Björkert. import CallKit import Foundation diff --git a/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift index 33cbc5580..23e322120 100644 --- a/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift +++ b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift @@ -1,6 +1,6 @@ // LoopFollow // InfoDisplaySettingsView.swift -// Created by Jonas Björkert on 2024-08-05. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsViewModel.swift b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsViewModel.swift index b1ef78e74..9056c11ca 100644 --- a/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsViewModel.swift +++ b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsViewModel.swift @@ -1,6 +1,6 @@ // LoopFollow // InfoDisplaySettingsViewModel.swift -// Created by Jonas Björkert on 2024-08-05. +// Created by Jonas Björkert. import Foundation import SwiftUI diff --git a/LoopFollow/InfoTable/InfoData.swift b/LoopFollow/InfoTable/InfoData.swift index 57a3939f9..6d676b641 100644 --- a/LoopFollow/InfoTable/InfoData.swift +++ b/LoopFollow/InfoTable/InfoData.swift @@ -1,6 +1,6 @@ // LoopFollow // InfoData.swift -// Created by Jonas Björkert on 2024-07-11. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/InfoTable/InfoDataSeparator.swift b/LoopFollow/InfoTable/InfoDataSeparator.swift index 095351a94..4f86a89df 100644 --- a/LoopFollow/InfoTable/InfoDataSeparator.swift +++ b/LoopFollow/InfoTable/InfoDataSeparator.swift @@ -1,6 +1,6 @@ // LoopFollow // InfoDataSeparator.swift -// Created by Jonas Björkert on 2024-07-18. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/InfoTable/InfoManager.swift b/LoopFollow/InfoTable/InfoManager.swift index 4a285294e..1343b8f8f 100644 --- a/LoopFollow/InfoTable/InfoManager.swift +++ b/LoopFollow/InfoTable/InfoManager.swift @@ -1,6 +1,6 @@ // LoopFollow // InfoManager.swift -// Created by Jonas Björkert on 2024-07-11. +// Created by Jonas Björkert. import Foundation import HealthKit diff --git a/LoopFollow/InfoTable/InfoType.swift b/LoopFollow/InfoTable/InfoType.swift index 1d9769554..ae7d9d3fa 100644 --- a/LoopFollow/InfoTable/InfoType.swift +++ b/LoopFollow/InfoTable/InfoType.swift @@ -1,6 +1,6 @@ // LoopFollow // InfoType.swift -// Created by Jonas Björkert on 2024-07-11. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Log/LogEntry.swift b/LoopFollow/Log/LogEntry.swift index 98eb7ae80..d000a9276 100644 --- a/LoopFollow/Log/LogEntry.swift +++ b/LoopFollow/Log/LogEntry.swift @@ -1,6 +1,6 @@ // LoopFollow // LogEntry.swift -// Created by Jonas Björkert on 2025-01-13. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Log/LogManager.swift b/LoopFollow/Log/LogManager.swift index 88c25bca1..7608cd4c8 100644 --- a/LoopFollow/Log/LogManager.swift +++ b/LoopFollow/Log/LogManager.swift @@ -1,6 +1,6 @@ // LoopFollow // LogManager.swift -// Created by Jonas Björkert on 2025-01-13. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Log/LogView.swift b/LoopFollow/Log/LogView.swift index 63e5a42f3..84b8cc305 100644 --- a/LoopFollow/Log/LogView.swift +++ b/LoopFollow/Log/LogView.swift @@ -1,6 +1,6 @@ // LoopFollow // LogView.swift -// Created by Jonas Björkert on 2025-01-13. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Log/LogViewModel.swift b/LoopFollow/Log/LogViewModel.swift index afc69e90c..2a556255d 100644 --- a/LoopFollow/Log/LogViewModel.swift +++ b/LoopFollow/Log/LogViewModel.swift @@ -1,6 +1,6 @@ // LoopFollow // LogViewModel.swift -// Created by Jonas Björkert on 2025-01-13. +// Created by Jonas Björkert. import Combine import Foundation diff --git a/LoopFollow/Log/SearchBar.swift b/LoopFollow/Log/SearchBar.swift index c78f860e3..e60856c93 100644 --- a/LoopFollow/Log/SearchBar.swift +++ b/LoopFollow/Log/SearchBar.swift @@ -1,6 +1,6 @@ // LoopFollow // SearchBar.swift -// Created by Jonas Björkert on 2025-01-13. +// Created by Jonas Björkert. import SwiftUI import UIKit diff --git a/LoopFollow/Metric/CarbMetric.swift b/LoopFollow/Metric/CarbMetric.swift index c8e7cda3b..3db9f9f65 100644 --- a/LoopFollow/Metric/CarbMetric.swift +++ b/LoopFollow/Metric/CarbMetric.swift @@ -1,6 +1,6 @@ // LoopFollow // CarbMetric.swift -// Created by Jonas Björkert on 2024-07-18. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Metric/InsulinMetric.swift b/LoopFollow/Metric/InsulinMetric.swift index f9a379311..130ff1cff 100644 --- a/LoopFollow/Metric/InsulinMetric.swift +++ b/LoopFollow/Metric/InsulinMetric.swift @@ -1,6 +1,6 @@ // LoopFollow // InsulinMetric.swift -// Created by Jonas Björkert on 2024-07-18. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Metric/Metric.swift b/LoopFollow/Metric/Metric.swift index babecc9fe..09c8b08f9 100644 --- a/LoopFollow/Metric/Metric.swift +++ b/LoopFollow/Metric/Metric.swift @@ -1,6 +1,6 @@ // LoopFollow // Metric.swift -// Created by Jonas Björkert on 2024-07-18. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Nightscout/NightscoutSettingsView.swift b/LoopFollow/Nightscout/NightscoutSettingsView.swift index 61f0b180c..eeaba3080 100644 --- a/LoopFollow/Nightscout/NightscoutSettingsView.swift +++ b/LoopFollow/Nightscout/NightscoutSettingsView.swift @@ -1,6 +1,6 @@ // LoopFollow // NightscoutSettingsView.swift -// Created by Jonas Björkert on 2025-01-18. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift index 76884310e..451494f56 100644 --- a/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift +++ b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift @@ -1,6 +1,6 @@ // LoopFollow // NightscoutSettingsViewModel.swift -// Created by Jonas Björkert on 2025-01-18. +// Created by Jonas Björkert. import Combine import Foundation diff --git a/LoopFollow/Remote/Loop/LoopNightscoutRemoteView.swift b/LoopFollow/Remote/Loop/LoopNightscoutRemoteView.swift index 8b44fff2a..dffe494d5 100644 --- a/LoopFollow/Remote/Loop/LoopNightscoutRemoteView.swift +++ b/LoopFollow/Remote/Loop/LoopNightscoutRemoteView.swift @@ -1,6 +1,6 @@ // LoopFollow // LoopNightscoutRemoteView.swift -// Created by Jonas Björkert on 2025-01-27. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Remote/Loop/LoopOverrideView.swift b/LoopFollow/Remote/Loop/LoopOverrideView.swift index 2fcedb002..f184cafa8 100644 --- a/LoopFollow/Remote/Loop/LoopOverrideView.swift +++ b/LoopFollow/Remote/Loop/LoopOverrideView.swift @@ -1,6 +1,6 @@ // LoopFollow // LoopOverrideView.swift -// Created by Jonas Björkert on 2024-10-09. +// Created by Jonas Björkert. import HealthKit import SwiftUI diff --git a/LoopFollow/Remote/Loop/LoopOverrideViewModel.swift b/LoopFollow/Remote/Loop/LoopOverrideViewModel.swift index 208524686..dd4a232ac 100644 --- a/LoopFollow/Remote/Loop/LoopOverrideViewModel.swift +++ b/LoopFollow/Remote/Loop/LoopOverrideViewModel.swift @@ -1,6 +1,6 @@ // LoopFollow // LoopOverrideViewModel.swift -// Created by Jonas Björkert on 2025-01-27. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Remote/Nightscout/TrioNightscoutRemoteView.swift b/LoopFollow/Remote/Nightscout/TrioNightscoutRemoteView.swift index 2be504fcb..d3767e0d6 100644 --- a/LoopFollow/Remote/Nightscout/TrioNightscoutRemoteView.swift +++ b/LoopFollow/Remote/Nightscout/TrioNightscoutRemoteView.swift @@ -1,6 +1,6 @@ // LoopFollow // TrioNightscoutRemoteView.swift -// Created by Jonas Björkert on 2024-07-19. +// Created by Jonas Björkert. import HealthKit import SwiftUI diff --git a/LoopFollow/Remote/NoRemoteView.swift b/LoopFollow/Remote/NoRemoteView.swift index fa6e8d8e7..e52636e7d 100644 --- a/LoopFollow/Remote/NoRemoteView.swift +++ b/LoopFollow/Remote/NoRemoteView.swift @@ -1,6 +1,6 @@ // LoopFollow // NoRemoteView.swift -// Created by Jonas Björkert on 2025-01-27. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Remote/RemoteType.swift b/LoopFollow/Remote/RemoteType.swift index 291263b83..3cfd0a11d 100644 --- a/LoopFollow/Remote/RemoteType.swift +++ b/LoopFollow/Remote/RemoteType.swift @@ -1,6 +1,6 @@ // LoopFollow // RemoteType.swift -// Created by Jonas Björkert on 2024-09-17. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Remote/RemoteViewController.swift b/LoopFollow/Remote/RemoteViewController.swift index b1b8f1b58..30b24e3bb 100644 --- a/LoopFollow/Remote/RemoteViewController.swift +++ b/LoopFollow/Remote/RemoteViewController.swift @@ -1,6 +1,6 @@ // LoopFollow // RemoteViewController.swift -// Created by Jonas Björkert on 2024-07-19. +// Created by Jonas Björkert. import Combine import Foundation diff --git a/LoopFollow/Remote/Settings/RemoteSettingsView.swift b/LoopFollow/Remote/Settings/RemoteSettingsView.swift index e1d9adfde..881ee665e 100644 --- a/LoopFollow/Remote/Settings/RemoteSettingsView.swift +++ b/LoopFollow/Remote/Settings/RemoteSettingsView.swift @@ -1,6 +1,6 @@ // LoopFollow // RemoteSettingsView.swift -// Created by Jonas Björkert on 2024-09-17. +// Created by Jonas Björkert. import HealthKit import SwiftUI diff --git a/LoopFollow/Remote/Settings/RemoteSettingsViewModel.swift b/LoopFollow/Remote/Settings/RemoteSettingsViewModel.swift index 5eac3ca4e..d3fd8054e 100644 --- a/LoopFollow/Remote/Settings/RemoteSettingsViewModel.swift +++ b/LoopFollow/Remote/Settings/RemoteSettingsViewModel.swift @@ -1,6 +1,6 @@ // LoopFollow // RemoteSettingsViewModel.swift -// Created by Jonas Björkert on 2024-09-17. +// Created by Jonas Björkert. import Combine import Foundation diff --git a/LoopFollow/Remote/TRC/BolusView.swift b/LoopFollow/Remote/TRC/BolusView.swift index 0c1d64401..50b06a478 100644 --- a/LoopFollow/Remote/TRC/BolusView.swift +++ b/LoopFollow/Remote/TRC/BolusView.swift @@ -1,6 +1,6 @@ // LoopFollow // BolusView.swift -// Created by Jonas Björkert on 2024-09-17. +// Created by Jonas Björkert. import HealthKit import LocalAuthentication diff --git a/LoopFollow/Remote/TRC/MealView.swift b/LoopFollow/Remote/TRC/MealView.swift index c3dd52298..ae11c61bb 100644 --- a/LoopFollow/Remote/TRC/MealView.swift +++ b/LoopFollow/Remote/TRC/MealView.swift @@ -1,6 +1,6 @@ // LoopFollow // MealView.swift -// Created by Jonas Björkert on 2024-09-17. +// Created by Jonas Björkert. import HealthKit import LocalAuthentication diff --git a/LoopFollow/Remote/TRC/OverrideView.swift b/LoopFollow/Remote/TRC/OverrideView.swift index 8ccff7620..ae32f42f9 100644 --- a/LoopFollow/Remote/TRC/OverrideView.swift +++ b/LoopFollow/Remote/TRC/OverrideView.swift @@ -1,6 +1,6 @@ // LoopFollow // OverrideView.swift -// Created by Jonas Björkert on 2024-10-09. +// Created by Jonas Björkert. import HealthKit import SwiftUI diff --git a/LoopFollow/Remote/TRC/PushMessage.swift b/LoopFollow/Remote/TRC/PushMessage.swift index 8c110c7a7..8470a5b92 100644 --- a/LoopFollow/Remote/TRC/PushMessage.swift +++ b/LoopFollow/Remote/TRC/PushMessage.swift @@ -1,6 +1,6 @@ // LoopFollow // PushMessage.swift -// Created by Jonas Björkert on 2024-09-17. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Remote/TRC/PushNotificationManager.swift b/LoopFollow/Remote/TRC/PushNotificationManager.swift index 8238d18a2..832e86573 100644 --- a/LoopFollow/Remote/TRC/PushNotificationManager.swift +++ b/LoopFollow/Remote/TRC/PushNotificationManager.swift @@ -1,6 +1,6 @@ // LoopFollow // PushNotificationManager.swift -// Created by Jonas Björkert on 2024-09-17. +// Created by Jonas Björkert. import Foundation import HealthKit diff --git a/LoopFollow/Remote/TRC/TRCCommandType.swift b/LoopFollow/Remote/TRC/TRCCommandType.swift index d391fb5c7..4e32d1d5c 100644 --- a/LoopFollow/Remote/TRC/TRCCommandType.swift +++ b/LoopFollow/Remote/TRC/TRCCommandType.swift @@ -1,6 +1,6 @@ // LoopFollow // TRCCommandType.swift -// Created by Jonas Björkert on 2024-10-05. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Remote/TRC/TempTargetView.swift b/LoopFollow/Remote/TRC/TempTargetView.swift index 19302076e..63c06f34c 100644 --- a/LoopFollow/Remote/TRC/TempTargetView.swift +++ b/LoopFollow/Remote/TRC/TempTargetView.swift @@ -1,6 +1,6 @@ // LoopFollow // TempTargetView.swift -// Created by Jonas Björkert on 2024-07-19. +// Created by Jonas Björkert. import HealthKit import SwiftUI diff --git a/LoopFollow/Remote/TRC/TreatmentResponse.swift b/LoopFollow/Remote/TRC/TreatmentResponse.swift index a12f538e8..ff8dad053 100644 --- a/LoopFollow/Remote/TRC/TreatmentResponse.swift +++ b/LoopFollow/Remote/TRC/TreatmentResponse.swift @@ -1,6 +1,6 @@ // LoopFollow // TreatmentResponse.swift -// Created by Jonas Björkert on 2024-07-28. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Remote/TRC/TrioNightscoutRemoteController.swift b/LoopFollow/Remote/TRC/TrioNightscoutRemoteController.swift index 944427d17..69ec05f88 100644 --- a/LoopFollow/Remote/TRC/TrioNightscoutRemoteController.swift +++ b/LoopFollow/Remote/TRC/TrioNightscoutRemoteController.swift @@ -1,6 +1,6 @@ // LoopFollow // TrioNightscoutRemoteController.swift -// Created by Jonas Björkert on 2024-07-19. +// Created by Jonas Björkert. import Foundation import HealthKit diff --git a/LoopFollow/Remote/TRC/TrioRemoteControlView.swift b/LoopFollow/Remote/TRC/TrioRemoteControlView.swift index 155201470..934130b1a 100644 --- a/LoopFollow/Remote/TRC/TrioRemoteControlView.swift +++ b/LoopFollow/Remote/TRC/TrioRemoteControlView.swift @@ -1,6 +1,6 @@ // LoopFollow // TrioRemoteControlView.swift -// Created by Jonas Björkert on 2024-09-17. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Remote/TRC/TrioRemoteControlViewModel.swift b/LoopFollow/Remote/TRC/TrioRemoteControlViewModel.swift index fa4b7a59d..dddf31336 100644 --- a/LoopFollow/Remote/TRC/TrioRemoteControlViewModel.swift +++ b/LoopFollow/Remote/TRC/TrioRemoteControlViewModel.swift @@ -1,6 +1,6 @@ // LoopFollow // TrioRemoteControlViewModel.swift -// Created by Jonas Björkert on 2024-09-17. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Remote/TempTargetPreset/TempTargetPreset.swift b/LoopFollow/Remote/TempTargetPreset/TempTargetPreset.swift index 7392eb505..646a68e99 100644 --- a/LoopFollow/Remote/TempTargetPreset/TempTargetPreset.swift +++ b/LoopFollow/Remote/TempTargetPreset/TempTargetPreset.swift @@ -1,6 +1,6 @@ // LoopFollow // TempTargetPreset.swift -// Created by Jonas Björkert on 2024-07-31. +// Created by Jonas Björkert. import Foundation import HealthKit diff --git a/LoopFollow/Remote/TempTargetPreset/TempTargetPresetManager.swift b/LoopFollow/Remote/TempTargetPreset/TempTargetPresetManager.swift index 54e068fbf..3fab29f91 100644 --- a/LoopFollow/Remote/TempTargetPreset/TempTargetPresetManager.swift +++ b/LoopFollow/Remote/TempTargetPreset/TempTargetPresetManager.swift @@ -1,6 +1,6 @@ // LoopFollow // TempTargetPresetManager.swift -// Created by Jonas Björkert on 2024-07-31. +// Created by Jonas Björkert. import Combine import Foundation diff --git a/LoopFollow/Settings/AdvancedSettingsView.swift b/LoopFollow/Settings/AdvancedSettingsView.swift index 30dd43336..8610f549f 100644 --- a/LoopFollow/Settings/AdvancedSettingsView.swift +++ b/LoopFollow/Settings/AdvancedSettingsView.swift @@ -1,6 +1,6 @@ // LoopFollow // AdvancedSettingsView.swift -// Created by Jonas Björkert on 2025-01-24. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Settings/AdvancedSettingsViewModel.swift b/LoopFollow/Settings/AdvancedSettingsViewModel.swift index 0766c5a05..f03492c9c 100644 --- a/LoopFollow/Settings/AdvancedSettingsViewModel.swift +++ b/LoopFollow/Settings/AdvancedSettingsViewModel.swift @@ -1,6 +1,6 @@ // LoopFollow // AdvancedSettingsViewModel.swift -// Created by Jonas Björkert on 2025-01-24. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Settings/CalendarSettingsView.swift b/LoopFollow/Settings/CalendarSettingsView.swift index 739576a24..cdab8e466 100644 --- a/LoopFollow/Settings/CalendarSettingsView.swift +++ b/LoopFollow/Settings/CalendarSettingsView.swift @@ -1,6 +1,6 @@ // LoopFollow // CalendarSettingsView.swift -// Created by Jonas Björkert on 2025-05-26. +// Created by Jonas Björkert. import EventKit import SwiftUI diff --git a/LoopFollow/Settings/ContactSettingsView.swift b/LoopFollow/Settings/ContactSettingsView.swift index c2b34c7e6..3fb10a9f8 100644 --- a/LoopFollow/Settings/ContactSettingsView.swift +++ b/LoopFollow/Settings/ContactSettingsView.swift @@ -1,6 +1,6 @@ // LoopFollow // ContactSettingsView.swift -// Created by Jonas Björkert on 2024-12-10. +// Created by Jonas Björkert. import Contacts import SwiftUI diff --git a/LoopFollow/Settings/ContactSettingsViewModel.swift b/LoopFollow/Settings/ContactSettingsViewModel.swift index a9ee26a52..a36ba9e14 100644 --- a/LoopFollow/Settings/ContactSettingsViewModel.swift +++ b/LoopFollow/Settings/ContactSettingsViewModel.swift @@ -1,6 +1,6 @@ // LoopFollow // ContactSettingsViewModel.swift -// Created by Jonas Björkert on 2024-12-10. +// Created by Jonas Björkert. import Combine import Foundation diff --git a/LoopFollow/Settings/DexcomSettingsView.swift b/LoopFollow/Settings/DexcomSettingsView.swift index d566b796f..d5ba8d074 100644 --- a/LoopFollow/Settings/DexcomSettingsView.swift +++ b/LoopFollow/Settings/DexcomSettingsView.swift @@ -1,6 +1,6 @@ // LoopFollow // DexcomSettingsView.swift -// Created by Jonas Björkert on 2025-01-18. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Settings/DexcomSettingsViewModel.swift b/LoopFollow/Settings/DexcomSettingsViewModel.swift index e6fbb64df..ae173ce80 100644 --- a/LoopFollow/Settings/DexcomSettingsViewModel.swift +++ b/LoopFollow/Settings/DexcomSettingsViewModel.swift @@ -1,6 +1,6 @@ // LoopFollow // DexcomSettingsViewModel.swift -// Created by Jonas Björkert on 2025-01-18. +// Created by Jonas Björkert. import Combine import Foundation diff --git a/LoopFollow/Settings/GeneralSettingsView.swift b/LoopFollow/Settings/GeneralSettingsView.swift index e7b3a1e2a..6713e1951 100644 --- a/LoopFollow/Settings/GeneralSettingsView.swift +++ b/LoopFollow/Settings/GeneralSettingsView.swift @@ -1,6 +1,6 @@ // LoopFollow // GeneralSettingsView.swift -// Created by Jonas Björkert on 2025-05-26. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Settings/GraphSettingsView.swift b/LoopFollow/Settings/GraphSettingsView.swift index 835acb379..01915f055 100644 --- a/LoopFollow/Settings/GraphSettingsView.swift +++ b/LoopFollow/Settings/GraphSettingsView.swift @@ -1,6 +1,6 @@ // LoopFollow // GraphSettingsView.swift -// Created by Jonas Björkert on 2025-05-26. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Settings/SettingsMenuView.swift b/LoopFollow/Settings/SettingsMenuView.swift index 678f7e611..8dab5c68b 100644 --- a/LoopFollow/Settings/SettingsMenuView.swift +++ b/LoopFollow/Settings/SettingsMenuView.swift @@ -1,6 +1,6 @@ // LoopFollow // SettingsMenuView.swift -// Created by Jonas Björkert on 2025-05-26. +// Created by Jonas Björkert. import SwiftUI import UIKit diff --git a/LoopFollow/Snoozer/SnoozerView.swift b/LoopFollow/Snoozer/SnoozerView.swift index 8d2717e8b..ffdedbc35 100644 --- a/LoopFollow/Snoozer/SnoozerView.swift +++ b/LoopFollow/Snoozer/SnoozerView.swift @@ -1,6 +1,6 @@ // LoopFollow // SnoozerView.swift -// Created by Jonas Björkert on 2025-04-26. +// Created by Jonas Björkert. import SwiftUI diff --git a/LoopFollow/Snoozer/SnoozerViewController.swift b/LoopFollow/Snoozer/SnoozerViewController.swift index 09cc35a00..0e1c5a30b 100644 --- a/LoopFollow/Snoozer/SnoozerViewController.swift +++ b/LoopFollow/Snoozer/SnoozerViewController.swift @@ -1,6 +1,6 @@ // LoopFollow // SnoozerViewController.swift -// Created by Jonas Björkert on 2025-04-26. +// Created by Jonas Björkert. import Combine import SwiftUI diff --git a/LoopFollow/Snoozer/SnoozerViewModel.swift b/LoopFollow/Snoozer/SnoozerViewModel.swift index 798b9b480..34bdccd73 100644 --- a/LoopFollow/Snoozer/SnoozerViewModel.swift +++ b/LoopFollow/Snoozer/SnoozerViewModel.swift @@ -1,6 +1,6 @@ // LoopFollow // SnoozerViewModel.swift -// Created by Jonas Björkert on 2025-05-04. +// Created by Jonas Björkert. import Combine import Foundation diff --git a/LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift b/LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift index fc5708cd1..e33841641 100644 --- a/LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift +++ b/LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift @@ -1,6 +1,6 @@ // LoopFollow // ObservableUserDefaultsValue.swift -// Created by Jonas Björkert on 2024-07-28. +// Created by Jonas Björkert. import Combine import Foundation diff --git a/LoopFollow/Storage/Framework/ObservableValue.swift b/LoopFollow/Storage/Framework/ObservableValue.swift index 15a529b4e..080be29fe 100644 --- a/LoopFollow/Storage/Framework/ObservableValue.swift +++ b/LoopFollow/Storage/Framework/ObservableValue.swift @@ -1,6 +1,6 @@ // LoopFollow // ObservableValue.swift -// Created by Jonas Björkert on 2024-07-28. +// Created by Jonas Björkert. import Combine import Foundation diff --git a/LoopFollow/Storage/Framework/SecureStorageValue.swift b/LoopFollow/Storage/Framework/SecureStorageValue.swift index 29ecd0482..f9b97479b 100644 --- a/LoopFollow/Storage/Framework/SecureStorageValue.swift +++ b/LoopFollow/Storage/Framework/SecureStorageValue.swift @@ -1,6 +1,6 @@ // LoopFollow // SecureStorageValue.swift -// Created by Jonas Björkert on 2024-09-17. +// Created by Jonas Björkert. import Combine import Foundation diff --git a/LoopFollow/Storage/Framework/StorageValue.swift b/LoopFollow/Storage/Framework/StorageValue.swift index db04ff8c7..a2938e078 100644 --- a/LoopFollow/Storage/Framework/StorageValue.swift +++ b/LoopFollow/Storage/Framework/StorageValue.swift @@ -1,6 +1,6 @@ // LoopFollow // StorageValue.swift -// Created by Jonas Björkert on 2024-09-17. +// Created by Jonas Björkert. import Combine import Foundation diff --git a/LoopFollow/Storage/Framework/UserDefaultsValue.swift b/LoopFollow/Storage/Framework/UserDefaultsValue.swift index 715e5ed1b..c552be34a 100644 --- a/LoopFollow/Storage/Framework/UserDefaultsValue.swift +++ b/LoopFollow/Storage/Framework/UserDefaultsValue.swift @@ -1,6 +1,6 @@ // LoopFollow // UserDefaultsValue.swift -// Created by Jon Fawcett on 2020-06-05. +// Created by Jon Fawcett. import Foundation diff --git a/LoopFollow/Storage/Framework/UserDefaultsValueGroups.swift b/LoopFollow/Storage/Framework/UserDefaultsValueGroups.swift index e8444bc3f..f83baeda8 100644 --- a/LoopFollow/Storage/Framework/UserDefaultsValueGroups.swift +++ b/LoopFollow/Storage/Framework/UserDefaultsValueGroups.swift @@ -1,6 +1,6 @@ // LoopFollow // UserDefaultsValueGroups.swift -// Created by Jon Fawcett on 2020-06-05. +// Created by Jon Fawcett. import Foundation diff --git a/LoopFollow/Storage/Observable.swift b/LoopFollow/Storage/Observable.swift index 6787a1461..841e1763d 100644 --- a/LoopFollow/Storage/Observable.swift +++ b/LoopFollow/Storage/Observable.swift @@ -1,6 +1,6 @@ // LoopFollow // Observable.swift -// Created by Jonas Björkert on 2024-07-28. +// Created by Jonas Björkert. import Foundation import HealthKit diff --git a/LoopFollow/Storage/ObservableUserDefaults.swift b/LoopFollow/Storage/ObservableUserDefaults.swift index ea4e7d353..26e1d1f17 100644 --- a/LoopFollow/Storage/ObservableUserDefaults.swift +++ b/LoopFollow/Storage/ObservableUserDefaults.swift @@ -1,6 +1,6 @@ // LoopFollow // ObservableUserDefaults.swift -// Created by Jonas Björkert on 2024-07-28. +// Created by Jonas Björkert. import Combine import Foundation diff --git a/LoopFollow/Storage/Storage+Migrate.swift b/LoopFollow/Storage/Storage+Migrate.swift index 19dbce44c..5af3eb8ac 100644 --- a/LoopFollow/Storage/Storage+Migrate.swift +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -1,6 +1,6 @@ // LoopFollow // Storage+Migrate.swift -// Created by Jonas Björkert on 2025-05-26. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Storage/Storage.swift b/LoopFollow/Storage/Storage.swift index 6de18afdb..3d0a814b5 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -1,6 +1,6 @@ // LoopFollow // Storage.swift -// Created by Jonas Björkert on 2024-09-17. +// Created by Jonas Björkert. import Foundation import HealthKit diff --git a/LoopFollow/Task/AlarmTask.swift b/LoopFollow/Task/AlarmTask.swift index db2c35170..2aa6cd382 100644 --- a/LoopFollow/Task/AlarmTask.swift +++ b/LoopFollow/Task/AlarmTask.swift @@ -1,6 +1,6 @@ // LoopFollow // AlarmTask.swift -// Created by Jonas Björkert on 2025-01-13. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Task/BGTask.swift b/LoopFollow/Task/BGTask.swift index f12d71049..231310cfb 100644 --- a/LoopFollow/Task/BGTask.swift +++ b/LoopFollow/Task/BGTask.swift @@ -1,6 +1,6 @@ // LoopFollow // BGTask.swift -// Created by Jonas Björkert on 2025-01-13. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Task/CalendarTask.swift b/LoopFollow/Task/CalendarTask.swift index 6758f9507..0d097f530 100644 --- a/LoopFollow/Task/CalendarTask.swift +++ b/LoopFollow/Task/CalendarTask.swift @@ -1,6 +1,6 @@ // LoopFollow // CalendarTask.swift -// Created by Jonas Björkert on 2025-01-13. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Task/DeviceStatusTask.swift b/LoopFollow/Task/DeviceStatusTask.swift index 0e25175da..010db7f7b 100644 --- a/LoopFollow/Task/DeviceStatusTask.swift +++ b/LoopFollow/Task/DeviceStatusTask.swift @@ -1,6 +1,6 @@ // LoopFollow // DeviceStatusTask.swift -// Created by Jonas Björkert on 2025-01-13. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Task/MinAgoTask.swift b/LoopFollow/Task/MinAgoTask.swift index 2980cae01..c44dc33ac 100644 --- a/LoopFollow/Task/MinAgoTask.swift +++ b/LoopFollow/Task/MinAgoTask.swift @@ -1,6 +1,6 @@ // LoopFollow // MinAgoTask.swift -// Created by Jonas Björkert on 2025-01-13. +// Created by Jonas Björkert. import Foundation import UIKit diff --git a/LoopFollow/Task/ProfileTask.swift b/LoopFollow/Task/ProfileTask.swift index c39083daa..52cf4803c 100644 --- a/LoopFollow/Task/ProfileTask.swift +++ b/LoopFollow/Task/ProfileTask.swift @@ -1,6 +1,6 @@ // LoopFollow // ProfileTask.swift -// Created by Jonas Björkert on 2025-01-13. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Task/Task.swift b/LoopFollow/Task/Task.swift index 471f15361..833e7d89f 100644 --- a/LoopFollow/Task/Task.swift +++ b/LoopFollow/Task/Task.swift @@ -1,6 +1,6 @@ // LoopFollow // Task.swift -// Created by Jonas Björkert on 2025-01-13. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/Task/TaskScheduler.swift b/LoopFollow/Task/TaskScheduler.swift index 86d11e52b..2425b991a 100644 --- a/LoopFollow/Task/TaskScheduler.swift +++ b/LoopFollow/Task/TaskScheduler.swift @@ -1,6 +1,6 @@ // LoopFollow // TaskScheduler.swift -// Created by Jonas Björkert on 2025-01-13. +// Created by Jonas Björkert. import Foundation import UIKit diff --git a/LoopFollow/Task/TreatmentsTask.swift b/LoopFollow/Task/TreatmentsTask.swift index 40c3954cd..19d10f6a6 100644 --- a/LoopFollow/Task/TreatmentsTask.swift +++ b/LoopFollow/Task/TreatmentsTask.swift @@ -1,6 +1,6 @@ // LoopFollow // TreatmentsTask.swift -// Created by Jonas Björkert on 2025-01-13. +// Created by Jonas Björkert. import Foundation diff --git a/LoopFollow/ViewControllers/AppStateViewController.swift b/LoopFollow/ViewControllers/AppStateViewController.swift index e5778a56f..5ff41be9a 100644 --- a/LoopFollow/ViewControllers/AppStateViewController.swift +++ b/LoopFollow/ViewControllers/AppStateViewController.swift @@ -1,6 +1,6 @@ // LoopFollow // AppStateViewController.swift -// Created by Jose Paredes on 2020-07-18. +// Created by Jose Paredes. import Foundation diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 9acfa2ba6..79b202f46 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -1,6 +1,6 @@ // LoopFollow // MainViewController.swift -// Created by Jon Fawcett on 2020-06-17. +// Created by Jon Fawcett. import AVFAudio import Charts diff --git a/LoopFollow/ViewControllers/NightScoutViewController.swift b/LoopFollow/ViewControllers/NightScoutViewController.swift index ad12bcb1d..f4853af1a 100644 --- a/LoopFollow/ViewControllers/NightScoutViewController.swift +++ b/LoopFollow/ViewControllers/NightScoutViewController.swift @@ -1,6 +1,6 @@ // LoopFollow // NightScoutViewController.swift -// Created by Jon Fawcett on 2020-06-05. +// Created by Jon Fawcett. import UIKit import WebKit diff --git a/LoopFollow/ViewControllers/SettingsViewController.swift b/LoopFollow/ViewControllers/SettingsViewController.swift index 101af0ac1..aaffff033 100644 --- a/LoopFollow/ViewControllers/SettingsViewController.swift +++ b/LoopFollow/ViewControllers/SettingsViewController.swift @@ -1,6 +1,6 @@ // LoopFollow // SettingsViewController.swift -// Created by Jon Fawcett on 2020-06-05. +// Created by Jon Fawcett. import SwiftUI import UIKit diff --git a/Scripts/swiftformat.sh b/Scripts/swiftformat.sh index 2538a4e3b..16c5f5e07 100755 --- a/Scripts/swiftformat.sh +++ b/Scripts/swiftformat.sh @@ -12,5 +12,5 @@ assertEnvironment "${SRCROOT}" "Please set SRCROOT to project root folder" unset SDKROOT swift run -c release --package-path BuildTools swiftformat "${SRCROOT}" \ ---header "LoopFollow\n{file}\nCreated by {author.name} on {created}." \ +--header "LoopFollow\n{file}\nCreated by {author.name}." \ --exclude Pods,Generated,R.generated.swift,fastlane/swift,Dependencies,dexcom-share-client-swift diff --git a/Tests/AlarmConditions/BatteryConditionTests.swift b/Tests/AlarmConditions/BatteryConditionTests.swift index 1b754e037..e1ad2b6f5 100644 --- a/Tests/AlarmConditions/BatteryConditionTests.swift +++ b/Tests/AlarmConditions/BatteryConditionTests.swift @@ -1,6 +1,6 @@ // LoopFollow // BatteryConditionTests.swift -// Created by Jonas Björkert on 2025-05-23. +// Created by Jonas Björkert. @testable import LoopFollow import Testing diff --git a/Tests/AlarmConditions/Helpers.swift b/Tests/AlarmConditions/Helpers.swift index 39df2d6a6..59fe5fecb 100644 --- a/Tests/AlarmConditions/Helpers.swift +++ b/Tests/AlarmConditions/Helpers.swift @@ -1,6 +1,6 @@ // LoopFollow // Helpers.swift -// Created by Jonas Björkert on 2025-05-23. +// Created by Jonas Björkert. // Tests/AlarmConditions/Helpers.swift import Foundation diff --git a/Tests/Tests.swift b/Tests/Tests.swift index 4526460ec..8d8c4f458 100644 --- a/Tests/Tests.swift +++ b/Tests/Tests.swift @@ -1,6 +1,6 @@ // LoopFollow // Tests.swift -// Created by Jonas Björkert on 2025-05-23. +// Created by Jonas Björkert. import Testing From 68d610456f6a97287626076c922e76d8ac00e945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 30 Jun 2025 19:42:54 +0200 Subject: [PATCH 135/138] suppress duplicate @Published emissions --- LoopFollow/Remote/RemoteViewController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LoopFollow/Remote/RemoteViewController.swift b/LoopFollow/Remote/RemoteViewController.swift index 30b24e3bb..bd51d24ad 100644 --- a/LoopFollow/Remote/RemoteViewController.swift +++ b/LoopFollow/Remote/RemoteViewController.swift @@ -16,8 +16,8 @@ class RemoteViewController: UIViewController { super.viewDidLoad() cancellable = Publishers.CombineLatest( - Storage.shared.remoteType.$value, - Storage.shared.device.$value + Storage.shared.remoteType.$value.removeDuplicates(), + Storage.shared.device.$value.removeDuplicates() ) .sink { [weak self] _, _ in DispatchQueue.main.async { From 4b2a6938bbc4d1da5481f3376028bd1305dfa25f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 1 Jul 2025 10:10:18 +0200 Subject: [PATCH 136/138] Categorize alarms by status --- LoopFollow.xcodeproj/project.pbxproj | 16 ++ LoopFollow/Alarm/AddAlarm/AddAlarmSheet.swift | 46 +++++ LoopFollow/Alarm/AddAlarm/AlarmTile.swift | 33 +++ LoopFollow/Alarm/AlarmListView.swift | 194 ++++++++---------- 4 files changed, 185 insertions(+), 104 deletions(-) create mode 100644 LoopFollow/Alarm/AddAlarm/AddAlarmSheet.swift create mode 100644 LoopFollow/Alarm/AddAlarm/AlarmTile.swift diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 867efa313..93733a164 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -189,6 +189,8 @@ DDD10F072C529DE800D76A8E /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD10F062C529DE800D76A8E /* Observable.swift */; }; DDD10F0B2C54192A00D76A8E /* TemporaryTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD10F0A2C54192A00D76A8E /* TemporaryTarget.swift */; }; DDDB86F12DF7223C00AADDAC /* DeleteAlarmSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB86F02DF7223C00AADDAC /* DeleteAlarmSection.swift */; }; + DDDC31CC2E13A7DF009EA0F3 /* AddAlarmSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDC31CB2E13A7DF009EA0F3 /* AddAlarmSheet.swift */; }; + DDDC31CE2E13A811009EA0F3 /* AlarmTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDC31CD2E13A811009EA0F3 /* AlarmTile.swift */; }; DDDF6F432D479A9900884336 /* LoopNightscoutRemoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDF6F422D479A9800884336 /* LoopNightscoutRemoteView.swift */; }; DDDF6F452D479AB100884336 /* LoopOverrideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDF6F442D479AB000884336 /* LoopOverrideView.swift */; }; DDDF6F472D479AD200884336 /* LoopOverrideViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDF6F462D479AD100884336 /* LoopOverrideViewModel.swift */; }; @@ -561,6 +563,8 @@ DDD10F062C529DE800D76A8E /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; DDD10F0A2C54192A00D76A8E /* TemporaryTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryTarget.swift; sourceTree = ""; }; DDDB86F02DF7223C00AADDAC /* DeleteAlarmSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAlarmSection.swift; sourceTree = ""; }; + DDDC31CB2E13A7DF009EA0F3 /* AddAlarmSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAlarmSheet.swift; sourceTree = ""; }; + DDDC31CD2E13A811009EA0F3 /* AlarmTile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmTile.swift; sourceTree = ""; }; DDDF6F422D479A9800884336 /* LoopNightscoutRemoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopNightscoutRemoteView.swift; sourceTree = ""; }; DDDF6F442D479AB000884336 /* LoopOverrideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopOverrideView.swift; sourceTree = ""; }; DDDF6F462D479AD100884336 /* LoopOverrideViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopOverrideViewModel.swift; sourceTree = ""; }; @@ -1109,6 +1113,7 @@ DDCF9A7E2D85FCE6004DF4DD /* Alarm */ = { isa = PBXGroup; children = ( + DDDC31CA2E13A7D2009EA0F3 /* AddAlarm */, DDCC3A502DDC5BD4006F1C10 /* DataStructs */, DDC6CA3B2DD7B9050060EE25 /* AlarmType */, DD4AFB602DB68BBC00BB593F /* AlarmListView.swift */, @@ -1124,6 +1129,15 @@ path = Alarm; sourceTree = ""; }; + DDDC31CA2E13A7D2009EA0F3 /* AddAlarm */ = { + isa = PBXGroup; + children = ( + DDDC31CD2E13A811009EA0F3 /* AlarmTile.swift */, + DDDC31CB2E13A7DF009EA0F3 /* AddAlarmSheet.swift */, + ); + path = AddAlarm; + sourceTree = ""; + }; DDDF6F412D479A8E00884336 /* Loop */ = { isa = PBXGroup; children = ( @@ -1894,12 +1908,14 @@ DDE75D292DE5E56C007C1FC1 /* LinkRow.swift in Sources */, DD4AFB612DB68BBC00BB593F /* AlarmListView.swift in Sources */, DD7B0D442D730A3B0063DCB6 /* CycleHelper.swift in Sources */, + DDDC31CC2E13A7DF009EA0F3 /* AddAlarmSheet.swift in Sources */, DD7F4BA12DD2193F00D449E9 /* AlarmSnoozeSection.swift in Sources */, DD9ACA0C2D33BB8600415D8A /* CalendarTask.swift in Sources */, DD13BC792C3FE63A0062313B /* InfoManager.swift in Sources */, DD0C0C702C4AFFE800DBADDF /* RemoteViewController.swift in Sources */, DD48780A2C7B30D40048F05C /* RemoteSettingsViewModel.swift in Sources */, FCFEECA02488157B00402A7F /* Chart.swift in Sources */, + DDDC31CE2E13A811009EA0F3 /* AlarmTile.swift in Sources */, DDCF979424C0D380002C9752 /* UIViewExtension.swift in Sources */, DD0C0C6D2C48606200DBADDF /* CarbMetric.swift in Sources */, DDC7E5422DBD8A1600EB1127 /* AlarmGeneralSection.swift in Sources */, diff --git a/LoopFollow/Alarm/AddAlarm/AddAlarmSheet.swift b/LoopFollow/Alarm/AddAlarm/AddAlarmSheet.swift new file mode 100644 index 000000000..6bbe18ab1 --- /dev/null +++ b/LoopFollow/Alarm/AddAlarm/AddAlarmSheet.swift @@ -0,0 +1,46 @@ +// LoopFollow +// AddAlarmSheet.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct AddAlarmSheet: View { + let onSelect: (AlarmType) -> Void + @Environment(\.dismiss) private var dismiss + + private let columns = [ + GridItem(.adaptive(minimum: 110), spacing: 16), + ] + + var body: some View { + NavigationStack { + ScrollView { + LazyVGrid(columns: columns, spacing: 16) { + ForEach(AlarmType.Group.allCases, id: \.self) { group in + if AlarmType.allCases.contains(where: { $0.group == group }) { + Section(header: Text(group.rawValue) + .font(.headline) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 4) + ) { + ForEach(AlarmType.allCases.filter { $0.group == group }, id: \.self) { type in + AlarmTile(type: type) { + onSelect(type) + } + } + } + } + } + } + .padding() + } + .navigationTitle("Add Alarm") + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { dismiss() } + } + } + } + .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + } +} diff --git a/LoopFollow/Alarm/AddAlarm/AlarmTile.swift b/LoopFollow/Alarm/AddAlarm/AlarmTile.swift new file mode 100644 index 000000000..685fd21ec --- /dev/null +++ b/LoopFollow/Alarm/AddAlarm/AlarmTile.swift @@ -0,0 +1,33 @@ +// LoopFollow +// AlarmTile.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct AlarmTile: View { + let type: AlarmType + let action: () -> Void + + var body: some View { + Button(action: action) { + VStack(spacing: 8) { + Image(systemName: type.icon) + .font(.title2) + .foregroundColor(.accentColor) + Text(type.rawValue) + .font(.subheadline) + .multilineTextAlignment(.center) + .lineLimit(2) + Text(type.blurb) + .font(.caption2) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .lineLimit(2) + } + .padding() + .frame(maxWidth: .infinity, minHeight: 110) + .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12)) + } + .buttonStyle(.plain) + } +} diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index ccfeb530b..ad3f60251 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -4,75 +4,6 @@ import SwiftUI -struct AddAlarmSheet: View { - let onSelect: (AlarmType) -> Void - @Environment(\.dismiss) private var dismiss - - private let columns = [ - GridItem(.adaptive(minimum: 110), spacing: 16), - ] - - var body: some View { - NavigationStack { - ScrollView { - LazyVGrid(columns: columns, spacing: 16) { - ForEach(AlarmType.Group.allCases, id: \.self) { group in - if AlarmType.allCases.contains(where: { $0.group == group }) { - Section(header: Text(group.rawValue) - .font(.headline) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 4) - ) { - ForEach(AlarmType.allCases.filter { $0.group == group }, id: \.self) { type in - AlarmTile(type: type) { - onSelect(type) - } - } - } - } - } - } - .padding() - } - .navigationTitle("Add Alarm") - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Cancel") { dismiss() } - } - } - } - .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) - } -} - -private struct AlarmTile: View { - let type: AlarmType - let action: () -> Void - - var body: some View { - Button(action: action) { - VStack(spacing: 8) { - Image(systemName: type.icon) - .font(.title2) - .foregroundColor(.accentColor) - Text(type.rawValue) - .font(.subheadline) - .multilineTextAlignment(.center) - .lineLimit(2) - Text(type.blurb) - .font(.caption2) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - .lineLimit(2) - } - .padding() - .frame(maxWidth: .infinity, minHeight: 110) - .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 12)) - } - .buttonStyle(.plain) - } -} - private enum SheetInfo: Identifiable { case picker case editor(id: UUID, isNew: Bool) @@ -93,39 +24,63 @@ struct AlarmListView: View { @State private var deleteAfterDismiss: UUID? @State private var selectedAlarm: Alarm? - private var sortedAlarms: [Alarm] { - store.value.sorted(by: Alarm.byPriorityThenSpec) + // MARK: - Categorized Alarms + + private var snoozedAlarms: [Alarm] { + store.value.filter { $0.snoozedUntil ?? .distantPast > Date() && $0.isEnabled } + .sorted(by: Alarm.byPriorityThenSpec) + } + + private var activeAlarms: [Alarm] { + store.value.filter { $0.isEnabled && ($0.snoozedUntil ?? .distantPast <= Date()) } + .sorted(by: Alarm.byPriorityThenSpec) + } + + private var inactiveAlarms: [Alarm] { + store.value.filter { !$0.isEnabled } + .sorted(by: Alarm.byPriorityThenSpec) } + // MARK: - Formatters + + private var timeFormatter: DateFormatter { + let formatter = DateFormatter() + formatter.dateStyle = .none + formatter.timeStyle = .short + return formatter + } + + // MARK: - Body + var body: some View { List { - ForEach(sortedAlarms) { alarm in - Button { - selectedAlarm = alarm - sheetInfo = .editor(id: alarm.id, isNew: false) - } label: { - HStack(spacing: 12) { - Glyph( - symbol: alarm.type.icon, - tint: alarm.isEnabled ? .white : Color(uiColor: .darkGray) - ) - .overlay { - if let until = alarm.snoozedUntil, until > Date() { - Image(systemName: "zzz") - .font(.caption.bold()) - .foregroundColor(.secondary) - .shadow(color: .black, radius: 2) - .offset(x: 8, y: 8) - } - } + // --- SNOOZED ALARMS SECTION --- + if !snoozedAlarms.isEmpty { + Section(header: Text("Snoozed")) { + ForEach(snoozedAlarms) { alarm in + alarmRow(for: alarm) + } + } + } + + // --- ACTIVE ALARMS SECTION --- + if !activeAlarms.isEmpty { + Section(header: Text("Active Alarms")) { + ForEach(activeAlarms) { alarm in + alarmRow(for: alarm) + } + } + } - Text(alarm.name) - .frame(maxWidth: .infinity, alignment: .leading) - .foregroundColor(.primary) + // --- INACTIVE ALARMS SECTION --- + if !inactiveAlarms.isEmpty { + Section(header: Text("Inactive Alarms")) { + ForEach(inactiveAlarms) { alarm in + alarmRow(for: alarm) + .opacity(0.6) } } } - .onDelete(perform: deleteItems) } .sheet(item: $sheetInfo, onDismiss: handleSheetDismiss) { info in sheetContent(for: info) @@ -139,14 +94,51 @@ struct AlarmListView: View { .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) } - private func deleteItems(at offsets: IndexSet) { - let alarmsToDelete = offsets.map { sortedAlarms[$0] } + // MARK: - Views + + @ViewBuilder + private func alarmRow(for alarm: Alarm) -> some View { + Button { + selectedAlarm = alarm + sheetInfo = .editor(id: alarm.id, isNew: false) + } label: { + HStack(spacing: 12) { + Glyph( + symbol: alarm.type.icon, + tint: .primary + ) - let idsToDelete = alarmsToDelete.map { $0.id } + VStack(alignment: .leading, spacing: 2) { + Text(alarm.name) + .foregroundColor(.primary) - store.value.removeAll { idsToDelete.contains($0.id) } + if let until = alarm.snoozedUntil, until > Date() { + HStack(spacing: 4) { + Image(systemName: "zzz") + .font(.caption2) + Text("Snoozed until \(until, formatter: timeFormatter)") + .font(.caption) + } + .foregroundColor(.secondary) + } + } + Spacer() + Image(systemName: "chevron.right") + .font(.footnote.weight(.semibold)) + .foregroundColor(.secondary) + } + } + .swipeActions { + Button(role: .destructive) { + store.value.removeAll { $0.id == alarm.id } + } label: { + Label("Delete", systemImage: "trash.fill") + } + } } + // MARK: - Sheet Management + private func handleSheetDismiss() { if let id = deleteAfterDismiss, let idx = store.value.firstIndex(where: { $0.id == id }) @@ -186,10 +178,4 @@ struct AlarmListView: View { } } } - - private func iconOpacity(for alarm: Alarm) -> Double { - if !alarm.isEnabled { return 0.35 } - if let until = alarm.snoozedUntil, until > Date() { return 0.55 } - return 1.0 - } } From 4656c545ed88d04f30f1ab6c6c9bb2963b559ed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 1 Jul 2025 13:10:30 +0200 Subject: [PATCH 137/138] Fix displayed decimals for AlarmStepperSection --- .../Components/AlarmStepperSection.swift | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift index db6660d17..b95ae3ea1 100644 --- a/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift @@ -19,7 +19,7 @@ struct AlarmStepperSection: View { @Binding private var value: Double? - // MARK: – designated initialiser (Double?) + // MARK: – designated initialiser (Double?) init( header: String? = nil, @@ -39,7 +39,7 @@ struct AlarmStepperSection: View { _value = value } - // MARK: – convenience initialiser (Int?) + // MARK: – convenience initialiser (Int?) /// Same API but for **`Binding`** — it bridges to Double internally. init( @@ -74,6 +74,19 @@ struct AlarmStepperSection: View { ) } + private var decimalPlaces: Int { + // If step is a whole number (e.g., 1.0, 5.0), we want 0 decimal places. + if step.truncatingRemainder(dividingBy: 1) == 0 { + return 0 + } + // Otherwise, count characters after the decimal in the step value. + let stepString = String(step) + if let decimalIndex = stepString.firstIndex(of: ".") { + return stepString.distance(from: stepString.index(after: decimalIndex), to: stepString.endIndex) + } + return 1 // Fallback to 1 + } + // MARK: – view var body: some View { @@ -86,7 +99,7 @@ struct AlarmStepperSection: View { Text(title) Spacer() Text( - "\(Int(nonOptional.wrappedValue))" + + String(format: "%.\(decimalPlaces)f", nonOptional.wrappedValue) + (unitLabel.map { " \($0)" } ?? "") ) .foregroundColor(.secondary) From f81a2256d6354ffe2e7d44bd67828e9523664784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 1 Jul 2025 13:15:10 +0200 Subject: [PATCH 138/138] Align wordings --- LoopFollow/Alarm/AlarmListView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift index ad3f60251..2674b8606 100644 --- a/LoopFollow/Alarm/AlarmListView.swift +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -65,7 +65,7 @@ struct AlarmListView: View { // --- ACTIVE ALARMS SECTION --- if !activeAlarms.isEmpty { - Section(header: Text("Active Alarms")) { + Section(header: Text("Active")) { ForEach(activeAlarms) { alarm in alarmRow(for: alarm) } @@ -74,7 +74,7 @@ struct AlarmListView: View { // --- INACTIVE ALARMS SECTION --- if !inactiveAlarms.isEmpty { - Section(header: Text("Inactive Alarms")) { + Section(header: Text("Inactive")) { ForEach(inactiveAlarms) { alarm in alarmRow(for: alarm) .opacity(0.6)