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/BuildTools/Package.swift b/BuildTools/Package.swift new file mode 100644 index 000000000..afff8edaf --- /dev/null +++ b/BuildTools/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version:5.3 +import PackageDescription + +let package = Package( + name: "BuildTools", + platforms: [ + .macOS(.v10_11), + ], + dependencies: [ + .package( + url: "https://github.com/nicklockwood/SwiftFormat.git", + from: "0.56.1" + ), + ], + targets: [ + .target( + name: "BuildTools", + path: "" + ), + ] +) diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 5b25e5ffe..93733a164 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -3,12 +3,25 @@ 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 */; }; + 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 */; }; + 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 */; }; + 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 */; }; @@ -54,6 +67,12 @@ 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 */; }; + 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 */; }; @@ -66,6 +85,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 */; }; @@ -75,7 +95,38 @@ 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 */; }; + DD7F4BA12DD2193F00D449E9 /* AlarmSnoozeSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7F4BA02DD2193F00D449E9 /* AlarmSnoozeSection.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 */; }; + 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 */; }; + 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 */; }; + 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 */; }; + 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 */; }; + 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 */; }; + 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 */; }; DD98F54424BCEFEE0007425A /* ShareClientExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD98F54324BCEFEE0007425A /* ShareClientExtension.swift */; }; @@ -96,12 +147,40 @@ 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 */; }; + 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 */; }; + 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 */; }; + 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 */; }; + 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 */; }; + 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 */; }; + 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 */; }; - 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 */; }; @@ -109,18 +188,25 @@ 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 */; }; + 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 */; }; 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 */; }; + 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 */; }; 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 */; }; @@ -133,11 +219,11 @@ 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 */; }; - 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 */; }; @@ -256,7 +342,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 */; }; @@ -267,17 +352,14 @@ 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 */; }; 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 */; }; 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 */; }; @@ -285,10 +367,33 @@ FCFEECA2248857A600402A7F /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCFEECA1248857A600402A7F /* SettingsViewController.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + DDCC3ADA2DDE1790006F1C10 /* 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; @@ -333,6 +438,12 @@ 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 = ""; }; + 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 = ""; }; @@ -345,6 +456,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 = ""; }; @@ -354,7 +466,38 @@ 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 = ""; }; + DD7F4BA02DD2193F00D449E9 /* AlarmSnoozeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmSnoozeSection.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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; DD98F54324BCEFEE0007425A /* ShareClientExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareClientExtension.swift; sourceTree = ""; }; @@ -376,12 +519,42 @@ 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; @@ -389,18 +562,25 @@ 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 = ""; }; + 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 = ""; }; 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 = ""; }; + 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 = ""; }; 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 = ""; }; @@ -413,11 +593,11 @@ 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 = ""; }; - 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 = ""; }; @@ -536,7 +716,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; }; @@ -549,11 +728,9 @@ 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 = ""; }; - 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 = ""; }; @@ -561,7 +738,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 = ""; }; @@ -570,7 +746,18 @@ 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 */ + DDCC3AD32DDE1790006F1C10 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; FC9788112485969B00A7906C /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -587,6 +774,7 @@ 6A5880E0B811AF443B05AB02 /* Frameworks */ = { isa = PBXGroup; children = ( + DDCC3ABF2DDE10B0006F1C10 /* Testing.framework */, FCFEEC9D2486E68E00402A7F /* WebKit.framework */, A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */, FCE537C2249AAB2600F80BF8 /* NotificationCenter.framework */, @@ -603,6 +791,35 @@ path = Pods; sourceTree = ""; }; + DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = { + isa = PBXGroup; + children = ( + DDCC3A552DDC9617006F1C10 /* MissedBolusCondition.swift */, + DDCC3A4E2DDC5B54006F1C10 /* BatteryDropCondition.swift */, + DDCC3A4A2DDBB5E4006F1C10 /* BatteryCondition.swift */, + DDB9FC7A2DDB573F00EFAA76 /* IOBCondition.swift */, + DDC6CA482DD8E47A0060EE25 /* PumpVolumeCondition.swift */, + DDC6CA442DD8D8E60060EE25 /* PumpChangeCondition.swift */, + DDC6CA422DD8CED20060EE25 /* SensorAgeCondition.swift */, + DDC6CA3C2DD7C6090060EE25 /* TemporaryCondition.swift */, + DD7F4C1E2DD6648B00D449E9 /* FastRiseCondition.swift */, + DD7F4C1A2DD6501D00D449E9 /* COBCondition.swift */, + DD7F4C162DD63FA700D449E9 /* RecBolusCondition.swift */, + DD7F4C122DD51FD500D449E9 /* TempTargetEndCondition.swift */, + DD7F4C0E2DD51EC200D449E9 /* TempTargetStartCondition.swift */, + DD7F4C0A2DD51C5500D449E9 /* OverrideEndCondition.swift */, + DD7F4C042DD4BBE200D449E9 /* NotLoopingCondition.swift */, + DD7F4C082DD504A700D449E9 /* OverrideStartCondition.swift */, + DD7F4BC62DD473A600D449E9 /* FastDropCondition.swift */, + DD0650EA2DCE8385004D3B41 /* LowBGCondition.swift */, + DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */, + DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */, + DD0650EE2DCE96FF004D3B41 /* HighBGCondition.swift */, + DD0650F02DCE9A9E004D3B41 /* MissedReadingCondition.swift */, + ); + path = AlarmCondition; + sourceTree = ""; + }; DD0C0C692C4852A100DBADDF /* Metric */ = { isa = PBXGroup; children = ( @@ -642,8 +859,16 @@ DD1A97122D429495000DDC11 /* Settings */ = { isa = PBXGroup; children = ( + DD83164F2DE4E635004467AA /* SettingsMenuView.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 = ""; @@ -657,15 +882,6 @@ path = Nightscout; sourceTree = ""; }; - DD2C2E522D3C36A8006413A5 /* Dexcom */ = { - isa = PBXGroup; - children = ( - DD2C2E552D3C3913006413A5 /* DexcomSettingsView.swift */, - DD2C2E532D3C37D7006413A5 /* DexcomSettingsViewModel.swift */, - ); - path = Dexcom; - sourceTree = ""; - }; DD4878062C7B2E9E0048F05C /* Settings */ = { isa = PBXGroup; children = ( @@ -719,21 +935,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 = ( @@ -769,6 +975,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 +1011,133 @@ path = Scripts; sourceTree = ""; }; + DDC6CA3B2DD7B9050060EE25 /* AlarmType */ = { + isa = PBXGroup; + children = ( + DDCF9A812D85FD14004DF4DD /* AlarmType.swift */, + DD7F4C222DD7A62200D449E9 /* AlarmType+SortDirection.swift */, + DD7F4C242DD7B20700D449E9 /* AlarmType+timeUnit.swift */, + DD0B9D552DE1EC8A0090C337 /* AlarmType+Snooze.swift */, + DD0B9D572DE1F3B20090C337 /* AlarmType+canAcknowledge.swift */, + ); + path = AlarmType; + sourceTree = ""; + }; + DDC7E5142DBCE1B900EB1127 /* Snoozer */ = { + isa = PBXGroup; + children = ( + DDC7E5CE2DC77C2000EB1127 /* SnoozerViewModel.swift */, + DDC7E5122DBCE1B900EB1127 /* SnoozerView.swift */, + DDC7E5132DBCE1B900EB1127 /* SnoozerViewController.swift */, + ); + path = Snoozer; + sourceTree = ""; + }; + DDC7E53B2DBD8A1600EB1127 /* Components */ = { + isa = PBXGroup; + children = ( + DDDB86F02DF7223C00AADDAC /* DeleteAlarmSection.swift */, + DD7F4BC42DD3CE0700D449E9 /* AlarmBGLimitSection.swift */, + DD7F4BA02DD2193F00D449E9 /* AlarmSnoozeSection.swift */, + DD7F4B9E2DD1F92700D449E9 /* AlarmActiveSection.swift */, + DD7F4B9C2DD1EAE500D449E9 /* AlarmAudioSection.swift */, + DD0650F62DCFDA26004D3B41 /* InfoBanner.swift */, + DD0650F42DCF303F004D3B41 /* AlarmStepperSection.swift */, + DD0650A82DCA8A10004D3B41 /* AlarmBGSection.swift */, + DDC7E5392DBD8A1600EB1127 /* AlarmGeneralSection.swift */, + DDC7E53A2DBD8A1600EB1127 /* SoundFile.swift */, + ); + path = Components; + sourceTree = ""; + }; + DDC7E53F2DBD8A1600EB1127 /* Editors */ = { + isa = PBXGroup; + children = ( + DDCC3A572DDC9655006F1C10 /* MissedBolusAlarmEditor.swift */, + DDCC3A532DDC5D62006F1C10 /* BatteryDropAlarmEditor.swift */, + DDCC3A4C2DDBB77C006F1C10 /* BatteryAlarmEditor.swift */, + DDB9FC7C2DDB575300EFAA76 /* IOBAlarmEditor.swift */, + DDC6CA4A2DD8E4960060EE25 /* PumpVolumeAlarmEditor.swift */, + DDC6CA462DD8D9010060EE25 /* PumpChangeAlarmEditor.swift */, + DDC6CA402DD8CCCE0060EE25 /* SensorAgeAlarmEditor.swift */, + DDC6CA3E2DD7C6340060EE25 /* TemporaryAlarmEditor.swift */, + DD7F4C202DD66BB100D449E9 /* FastRiseAlarmEditor.swift */, + DD7F4C1C2DD650D500D449E9 /* COBAlarmEditor.swift */, + DD7F4C182DD63FD500D449E9 /* RecBolusAlarmEditor.swift */, + DD7F4C142DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift */, + DD7F4C102DD51ED900D449E9 /* TempTargetStartAlarmEditor.swift */, + DD7F4C0C2DD51C8100D449E9 /* OverrideEndAlarmEditor.swift */, + DD7F4C062DD5042F00D449E9 /* OverrideStartAlarmEditor.swift */, + DD7F4C022DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift */, + DD0650EC2DCE9371004D3B41 /* HighBgAlarmEditor.swift */, + DDC7E53C2DBD8A1600EB1127 /* BuildExpireAlarmEditor.swift */, + DDC7E53E2DBD8A1600EB1127 /* LowBgAlarmEditor.swift */, + DD0650F22DCE9B3D004D3B41 /* MissedReadingEditor.swift */, + DD0650F82DCFE7BE004D3B41 /* FastDropAlarmEditor.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 */, + DDD10F002C510C6B00D76A8E /* ObservableUserDefaults.swift */, + DDD10F062C529DE800D76A8E /* Observable.swift */, + DD4878042C7B2C970048F05C /* Storage.swift */, + DD8316472DE49EE5004467AA /* Storage+Migrate.swift */, + ); + path = Storage; + sourceTree = ""; + }; + DDCC3A502DDC5BD4006F1C10 /* DataStructs */ = { + isa = PBXGroup; + children = ( + DDCC3A592DDC988F006F1C10 /* CarbSample.swift */, + DDB9FC7E2DDB584500EFAA76 /* BolusEntry.swift */, + DD5DA27B2DC930D6003D44FC /* GlucoseValue.swift */, + ); + path = DataStructs; + sourceTree = ""; + }; + DDCF9A7E2D85FCE6004DF4DD /* Alarm */ = { + isa = PBXGroup; + children = ( + DDDC31CA2E13A7D2009EA0F3 /* AddAlarm */, + DDCC3A502DDC5BD4006F1C10 /* DataStructs */, + DDC6CA3B2DD7B9050060EE25 /* AlarmType */, + DD4AFB602DB68BBC00BB593F /* AlarmListView.swift */, + DDC7E5412DBD8A1600EB1127 /* AlarmEditing */, + DD4AFB482DB576C200BB593F /* AlarmSettingsView.swift */, + DD4AFB3C2DB55D2900BB593F /* AlarmConfiguration.swift */, + DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */, + DDCF9A8B2D86005E004DF4DD /* AlarmManager.swift */, + DDCF9A872D85FD33004DF4DD /* AlarmData.swift */, + DDCF9A7F2D85FD09004DF4DD /* Alarm.swift */, + DDBD19952DFB44B0005C2D69 /* Alarm+byPriorityThenSpec.swift */, + ); + path = Alarm; + sourceTree = ""; + }; + DDDC31CA2E13A7D2009EA0F3 /* AddAlarm */ = { + isa = PBXGroup; + children = ( + DDDC31CD2E13A811009EA0F3 /* AlarmTile.swift */, + DDDC31CB2E13A7DF009EA0F3 /* AddAlarmSheet.swift */, + ); + path = AddAlarm; + sourceTree = ""; + }; DDDF6F412D479A8E00884336 /* Loop */ = { isa = PBXGroup; children = ( @@ -849,9 +1184,16 @@ DDF6999C2C5AAA4C0058A8D9 /* Views */ = { isa = PBXGroup; children = ( + DDE75D2C2DE71401007C1FC1 /* TogglableSecureInput.swift */, + 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 = ""; @@ -895,16 +1237,14 @@ isa = PBXGroup; children = ( DD7E19802ACDA0EA00DBD158 /* Nightscout */, - DDCF979D24C2382A002C9752 /* AppStateController.swift */, FC7CE589248ABEA3001F83B8 /* AlarmSound.swift */, FC16A97924996673003D6245 /* NightScout.swift */, - FC16A97C24996747003D6245 /* Alarms.swift */, + FC16A97C24996747003D6245 /* SpeakBG.swift */, FC16A97E249969E2003D6245 /* Graphs.swift */, FC1BDD2A24A22650001B652C /* Stats.swift */, - FC1BDD2C24A23204001B652C /* StatsView.swift */, + FC1BDD2C24A23204001B652C /* MainViewController+updateStats.swift */, FCA2DDE52501095000254A8C /* Timers.swift */, DD608A0B2C27415C00F91132 /* BackgroundAlertManager.swift */, - DDF699932C555B310058A8D9 /* ViewControllerManager.swift */, ); path = Controllers; sourceTree = ""; @@ -1044,8 +1384,9 @@ FC8DEEE32485D1680075863F /* LoopFollow */ = { isa = PBXGroup; children = ( + DDC7E5142DBCE1B900EB1127 /* Snoozer */, + DDCF9A7E2D85FCE6004DF4DD /* Alarm */, DD1A97122D429495000DDC11 /* Settings */, - DD2C2E522D3C36A8006413A5 /* Dexcom */, DD2C2E4D2D3B8ACF006413A5 /* Nightscout */, DD9ED0C62D355225000D2A63 /* Log */, DDEF503D2D32753A00999A5D /* Task */, @@ -1062,7 +1403,7 @@ DD98F54224BCEF190007425A /* Extensions */, FC16A9782499657E003D6245 /* Controllers */, FCC688542489367300A0279D /* Helpers */, - FCC688512489363F00A0279D /* Storage */, + DDC7E5CD2DC6637800EB1127 /* Storage */, FC16A97624995FEE003D6245 /* Application */, ); path = LoopFollow; @@ -1078,6 +1419,7 @@ FC3AE7B3249E8E0E00AAE1E0 /* LoopFollow.xcdatamodeld */, FC5A5C3C2497B229009C550E /* Config.xcconfig */, FC8DEEE32485D1680075863F /* LoopFollow */, + DDCC3AD72DDE1790006F1C10 /* Tests */, FC9788152485969B00A7906C /* Products */, 8E32230C453C93FDCE59C2B9 /* Pods */, 6A5880E0B811AF443B05AB02 /* Frameworks */, @@ -1088,30 +1430,29 @@ isa = PBXGroup; children = ( FC9788142485969B00A7906C /* Loop Follow.app */, + DDCC3AD62DDE1790006F1C10 /* Tests.xctest */, ); 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 */ = { isa = PBXGroup; children = ( + DD83164B2DE4DB3A004467AA /* BinaryFloatingPoint+localized.swift */, + DD4AFB3A2DB55CB600BB593F /* TimeOfDay.swift */, DD7B0D432D730A320063DCB6 /* CycleHelper.swift */, DDF6999C2C5AAA4C0058A8D9 /* Views */, FCC6886E2489A53800A0279D /* AppConstants.swift */, @@ -1132,6 +1473,7 @@ DDF2C00F2BEFA991007A20E6 /* GitHubService.swift */, DDF2C0112BEFB733007A20E6 /* AppVersionManager.swift */, DDF699952C5582290058A8D9 /* TextFieldWithToolBar.swift */, + DDC7E5372DBD887400EB1127 /* isOnPhoneCall.swift */, ); path = Helpers; sourceTree = ""; @@ -1141,13 +1483,7 @@ children = ( FC97881B2485969B00A7906C /* MainViewController.swift */, FC97881D2485969B00A7906C /* NightScoutViewController.swift */, - FCD49B6B24AA536E007879DC /* DebugViewController.swift */, FCFEECA1248857A600402A7F /* SettingsViewController.swift */, - FCC6884F248935D800A0279D /* AlarmViewController.swift */, - FC7CE59E248D8D23001F83B8 /* SnoozeViewController.swift */, - DDCF979524C1443C002C9752 /* GeneralSettingsViewController.swift */, - DDCF979724C1489C002C9752 /* GraphSettingsViewController.swift */, - DDCF979924C14DB4002C9752 /* WatchSettingsViewController.swift */, ); path = ViewControllers; sourceTree = ""; @@ -1155,10 +1491,34 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + DDCC3AD52DDE1790006F1C10 /* Tests */ = { + isa = PBXNativeTarget; + buildConfigurationList = DDCC3ADC2DDE1790006F1C10 /* Build configuration list for PBXNativeTarget "Tests" */; + buildPhases = ( + DDCC3AD22DDE1790006F1C10 /* Sources */, + DDCC3AD32DDE1790006F1C10 /* Frameworks */, + DDCC3AD42DDE1790006F1C10 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + DDCC3ADB2DDE1790006F1C10 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + DDCC3AD72DDE1790006F1C10 /* Tests */, + ); + name = Tests; + packageProductDependencies = ( + ); + productName = Tests; + productReference = DDCC3AD62DDE1790006F1C10 /* Tests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; FC9788132485969B00A7906C /* LoopFollow */ = { isa = PBXNativeTarget; buildConfigurationList = FC97882D2485969C00A7906C /* Build configuration list for PBXNativeTarget "LoopFollow" */; buildPhases = ( + DD7F4BEA2DD48B9600D449E9 /* Swiftformat */, B038D39450A1F9A97D2B8BA4 /* [CP] Check Pods Manifest.lock */, FC9788102485969B00A7906C /* Sources */, FC9788112485969B00A7906C /* Frameworks */, @@ -1184,10 +1544,14 @@ FC97880C2485969B00A7906C /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1150; + LastSwiftUpdateCheck = 1630; LastUpgradeCheck = 1140; ORGANIZATIONNAME = "Jon Fawcett"; TargetAttributes = { + DDCC3AD52DDE1790006F1C10 = { + CreatedOnToolsVersion = 16.3; + TestTargetID = FC9788132485969B00A7906C; + }; FC9788132485969B00A7906C = { CreatedOnToolsVersion = 11.4.1; }; @@ -1210,11 +1574,19 @@ projectRoot = ""; targets = ( FC9788132485969B00A7906C /* LoopFollow */, + DDCC3AD52DDE1790006F1C10 /* Tests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + DDCC3AD42DDE1790006F1C10 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; FC9788122485969B00A7906C /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1356,10 +1728,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"; @@ -1387,6 +1763,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; @@ -1408,131 +1803,210 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + DDCC3AD22DDE1790006F1C10 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; FC9788102485969B00A7906C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( DD5334292C6166A500062F9D /* InfoDisplaySettingsView.swift in Sources */, - FCC68850248935D800A0279D /* AlarmViewController.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 */, + 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 */, + DD7F4C032DD4B92E00D449E9 /* NotLoopingAlarmEditor.swift in Sources */, + DD7F4C1B2DD6501D00D449E9 /* COBCondition.swift in Sources */, + DDE75D232DE5E505007C1FC1 /* Glyph.swift in Sources */, DD4878202C7DAF890048F05C /* PushMessage.swift in Sources */, - FC7CE59F248D8D23001F83B8 /* SnoozeViewController.swift in Sources */, + 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 */, DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */, FCC6886724898F8000A0279D /* UserDefaultsValue.swift in Sources */, - DDF699942C555B310058A8D9 /* ViewControllerManager.swift in Sources */, - DDCF979E24C2382A002C9752 /* AppStateController.swift in Sources */, + DD7F4C092DD504A700D449E9 /* OverrideStartCondition.swift in Sources */, 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 */, - 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 */, + DDE75D2D2DE71401007C1FC1 /* TogglableSecureInput.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 */, + 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 */, 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 */, 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 */, + 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 */, + 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 */, 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 */, + 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 */, + 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 */, + 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 */, + 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 */, + 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 */, 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 */, 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 */, + DDC6CA452DD8D8E60060EE25 /* PumpChangeCondition.swift in Sources */, 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 */, + DDC6CA492DD8E47A0060EE25 /* PumpVolumeCondition.swift in Sources */, DD1A97142D4294A5000DDC11 /* AdvancedSettingsView.swift in Sources */, 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 */, + DD7F4C192DD63FD500D449E9 /* RecBolusAlarmEditor.swift in Sources */, DD50C7502D0828800057AE6F /* ContactSettingsViewModel.swift in Sources */, DDAD162F2D2EF9830084BE10 /* RileyLinkHeartbeatBluetoothDevice.swift in Sources */, FC97881C2485969B00A7906C /* MainViewController.swift in Sources */, 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 */, 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 */, + DDE75D2B2DE5E613007C1FC1 /* NavigationRow.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 */, + DD0650A92DCA8A10004D3B41 /* AlarmBGSection.swift in Sources */, 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 */, 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 */, + 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 */, + DDC6CA432DD8CED20060EE25 /* SensorAgeCondition.swift in Sources */, + DD7F4C212DD66BB200D449E9 /* FastRiseAlarmEditor.swift in Sources */, FC97881A2485969B00A7906C /* SceneDelegate.swift in Sources */, + DD83164E2DE4E093004467AA /* CalendarSettingsView.swift in Sources */, 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 */, @@ -1540,19 +2014,28 @@ 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 */, 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 */, - 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 */, + 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 */, + DD8316502DE4E635004467AA /* SettingsMenuView.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 */, @@ -1562,11 +2045,20 @@ DD493ADD2ACF21E0009A6922 /* Basals.swift in Sources */, FC16A98124996C07003D6245 /* DateTime.swift in Sources */, FC3CAB022493B6220068A152 /* BackgroundTaskAudio.swift in Sources */, + DDCC3A582DDC9655006F1C10 /* MissedBolusAlarmEditor.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + DDCC3ADB2DDE1790006F1C10 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = FC9788132485969B00A7906C /* LoopFollow */; + targetProxy = DDCC3ADA2DDE1790006F1C10 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ FC97881F2485969B00A7906C /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -1587,6 +2079,59 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + DDCC3ADD2DDE1790006F1C10 /* 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-.Tests"; + PRODUCT_MODULE_NAME = Tests; + 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; + }; + DDCC3ADE2DDE1790006F1C10 /* 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-.Tests"; + PRODUCT_MODULE_NAME = Tests; + 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 +2167,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 +2188,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 +2230,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 +2244,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,12 +2256,13 @@ 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; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1729,12 +2279,13 @@ 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; + IPHONEOS_DEPLOYMENT_TARGET = 16.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1750,6 +2301,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + DDCC3ADC2DDE1790006F1C10 /* Build configuration list for PBXNativeTarget "Tests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DDCC3ADD2DDE1790006F1C10 /* Debug */, + DDCC3ADE2DDE1790006F1C10 /* 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..19ef472c3 100644 --- a/LoopFollow.xcworkspace/xcshareddata/xcschemes/LoopFollow.xcscheme +++ b/LoopFollow.xcworkspace/xcshareddata/xcschemes/LoopFollow.xcscheme @@ -28,6 +28,30 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES" shouldAutocreateTestPlan = "YES"> + + + + + + + + + + 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/Alarm+byPriorityThenSpec.swift b/LoopFollow/Alarm/Alarm+byPriorityThenSpec.swift new file mode 100644 index 000000000..252f9ede5 --- /dev/null +++ b/LoopFollow/Alarm/Alarm+byPriorityThenSpec.swift @@ -0,0 +1,31 @@ +// LoopFollow +// Alarm+byPriorityThenSpec.swift +// Created by Jonas Björkert. + +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/Alarm.swift b/LoopFollow/Alarm/Alarm.swift new file mode 100644 index 000000000..36f6cd7ec --- /dev/null +++ b/LoopFollow/Alarm/Alarm.swift @@ -0,0 +1,350 @@ +// LoopFollow +// Alarm.swift +// Created by Jonas Björkert. + +import Foundation +import HealthKit +import UserNotifications + +protocol DayNightDisplayable { + var displayName: String { get } +} + +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 RepeatSoundOption: String, CaseIterable, Codable, DayNightDisplayable { + case always, day, night, never +} + +enum ActiveOption: String, CaseIterable, Codable, DayNightDisplayable { + case always, day, night +} + +extension PlaySoundOption { + static func allowed(for active: ActiveOption) -> [PlaySoundOption] { + switch active { + case .always: return PlaySoundOption.allCases + case .day: return [.day, .never] + case .night: return [.night, .never] + } + } +} + +extension RepeatSoundOption { + static func allowed(for active: ActiveOption) -> [RepeatSoundOption] { + switch active { + case .always: return RepeatSoundOption.allCases + case .day: return [.day, .never] + case .night: return [.night, .never] + } + } +} + +struct Alarm: Identifiable, Codable, Equatable { + var id: UUID = .init() + var type: AlarmType + + /// Name of the alarm, defaults to alarm type + var name: String + + var isEnabled: Bool = true + + /// If the alarm is manually snoozed, we store the end time for the snooze here + var snoozedUntil: Date? + + /// 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 + 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 + var delta: Double? + + /// 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? + + var soundFile: SoundFile + + /// Snooze duration, it can be minutes, days or hours. Stepping is different per alarm type + var snoozeDuration: Int = 5 + + /// When the alarm should play it's sound + var playSoundOption: PlaySoundOption = .always + + /// When the sound should repeat + var repeatSoundOption: RepeatSoundOption = .always + + /// When is the alarm active + var activeOption: ActiveOption = .always + + // ───────────────────────────────────────────────────────────── + // Missed‑Bolus‑specific settings + // ───────────────────────────────────────────────────────────── + + /// “Prebolus Max Time” (if a bolus comes within this many minutes *before* the carbs, treat it as prebolus) + var missedBolusPrebolusWindow: Int? + + /// “Ignore Bolus <= X units” (don’t count any bolus smaller than or equal to this) + var missedBolusIgnoreSmallBolusUnits: Double? + + /// “Ignore Under Grams” (if carb entry is under this many grams, skip the alert) + var missedBolusIgnoreUnderGrams: Double? + + /// “Ignore Under BG” (if current BG is below this, skip the alert) + var missedBolusIgnoreUnderBG: Double? + + // ───────────────────────────────────────────────────────────── + // Bolus‑Count fields ─ + // ───────────────────────────────────────────────────────────── + /// trigger when N or more of those boluses occur... + var bolusCountThreshold: Int? + /// ...within this many minutes + var bolusWindowMinutes: Int? + + /// 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 = 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 + } + }() + + AlarmManager.shared.sendNotification(title: type.rawValue, actionTitle: snoozeDuration == 0 ? "Acknowledge" : "Snooze") + + if playSound { + AlarmSound.setSoundFile(str: soundFile.rawValue) + AlarmSound.play(repeating: shouldRepeat) + } + } + + init(type: AlarmType) { + self.type = type + name = type.rawValue + + switch type { + case .buildExpire: + /// Alert 7 days before the build expires + threshold = 7 + soundFile = .wrongAnswer + snoozeDuration = 1 + 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 + case .overrideEnd: + soundFile = .alertToneBusy + repeatSoundOption = .never + case .tempTargetStart: + soundFile = .endingReached + repeatSoundOption = .never + case .tempTargetEnd: + soundFile = .alertToneBusy + repeatSoundOption = .never + case .temporary: + soundFile = .indeed + snoozeDuration = 0 + aboveBG = 180 + belowBG = 70 + } + } +} + +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, .temporary: + return .glucose + case .iob, .cob, .missedBolus, .recBolus: + return .insulin + case .battery, .batteryDrop, .pump, .pumpChange, + .sensorChange, .notLooping, .buildExpire: + return .device + case .overrideStart, .overrideEnd, .tempTargetStart, .tempTargetEnd: + 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: 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" + case .temporary: return "bell" + } + } + + 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 .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 .temporary: return "One-time BG limit alert." + } + } +} diff --git a/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift b/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift new file mode 100644 index 000000000..9ce99fff6 --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/AlarmCondition.swift @@ -0,0 +1,83 @@ +// LoopFollow +// AlarmCondition.swift +// Created by Jonas Björkert. + +import Foundation + +protocol AlarmCondition { + static var type: AlarmType { get } + init() + /// pure, per-alarm logic against `AlarmData` + func evaluate(alarm: Alarm, data: AlarmData, now: Date) -> Bool +} + +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 + guard alarm.isEnabled else { return false } + // 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! + let dStart = config.dayStart.minutesSinceMidnight + let nStart = 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, now: now) + } +} diff --git a/LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift b/LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift new file mode 100644 index 000000000..bb1300958 --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/BatteryCondition.swift @@ -0,0 +1,17 @@ +// LoopFollow +// BatteryCondition.swift +// Created by Jonas Björkert. + +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/AlarmCondition/BatteryDropCondition.swift b/LoopFollow/Alarm/AlarmCondition/BatteryDropCondition.swift new file mode 100644 index 000000000..db378422c --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/BatteryDropCondition.swift @@ -0,0 +1,36 @@ +// LoopFollow +// BatteryDropCondition.swift +// Created by Jonas Björkert. + +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/AlarmCondition/BuildExpireCondition.swift b/LoopFollow/Alarm/AlarmCondition/BuildExpireCondition.swift new file mode 100644 index 000000000..6946ed524 --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/BuildExpireCondition.swift @@ -0,0 +1,20 @@ +// LoopFollow +// BuildExpireCondition.swift +// Created by Jonas Björkert. + +import Foundation + +struct BuildExpireCondition: AlarmCondition { + static let type: AlarmType = .buildExpire + init() {} + + 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 } + + let thresholdSeconds = TimeInterval(thresholdDays) * TimeUnit.day.seconds + let thresholdDate = expiry.addingTimeInterval(-thresholdSeconds) + + return Date() >= thresholdDate + } +} diff --git a/LoopFollow/Alarm/AlarmCondition/COBCondition.swift b/LoopFollow/Alarm/AlarmCondition/COBCondition.swift new file mode 100644 index 000000000..c256ca019 --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/COBCondition.swift @@ -0,0 +1,27 @@ +// LoopFollow +// COBCondition.swift +// Created by Jonas Björkert. + +import Foundation + +struct COBCondition: AlarmCondition { + static let type: AlarmType = .cob + init() {} + + 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 + 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/FastDropCondition.swift b/LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift new file mode 100644 index 000000000..ac3e5d33d --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/FastDropCondition.swift @@ -0,0 +1,34 @@ +// LoopFollow +// FastDropCondition.swift +// Created by Jonas Björkert. + +import Foundation + +struct FastDropCondition: AlarmCondition { + static let type: AlarmType = .fastDrop + init() {} + + func evaluate(alarm: Alarm, data: AlarmData, now _: Date) -> 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 } + + // ──────────────────────────────── + // 1. compute recent deltas + // (BG-limit check is now handled by passesBGLimits) + // ──────────────────────────────── + 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/AlarmCondition/FastRiseCondition.swift b/LoopFollow/Alarm/AlarmCondition/FastRiseCondition.swift new file mode 100644 index 000000000..f1d18a4a1 --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/FastRiseCondition.swift @@ -0,0 +1,27 @@ +// LoopFollow +// FastRiseCondition.swift +// Created by Jonas Björkert. + +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, now _: Date) -> Bool { + guard + let rise = alarm.delta, rise > 0, + let streak = alarm.monitoringWindow, streak > 0, + data.bgReadings.count >= streak + 1 + else { return false } + + // grab the last (streak + 1) readings, newest last + let recent = data.bgReadings.suffix(streak + 1).map(\.sgv) + + // every forward delta must hit the threshold + return zip(recent.dropFirst(), recent).allSatisfy { + Double($0 - $1) >= rise + } + } +} diff --git a/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift b/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift new file mode 100644 index 000000000..dacf1b669 --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/HighBGCondition.swift @@ -0,0 +1,41 @@ +// LoopFollow +// HighBGCondition.swift +// Created by Jonas Björkert. + +import Foundation + +/// 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, now _: Date) -> Bool { + // ──────────────────────────────── + // 0. get the limit + // ──────────────────────────────── + guard let high = alarm.aboveBG else { return false } + + func isHigh(_ g: GlucoseValue) -> Bool { + g.sgv > 0 && Double(g.sgv) >= high + } + + // 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 + { + 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 + } + } + + return persistentOK + } +} diff --git a/LoopFollow/Alarm/AlarmCondition/IOBCondition.swift b/LoopFollow/Alarm/AlarmCondition/IOBCondition.swift new file mode 100644 index 000000000..f4e7f0cd0 --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/IOBCondition.swift @@ -0,0 +1,68 @@ +// LoopFollow +// IOBCondition.swift +// Created by Jonas Björkert. + +// +// 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/AlarmCondition/LowBGCondition.swift b/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift new file mode 100644 index 000000000..974d5016f --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/LowBGCondition.swift @@ -0,0 +1,66 @@ +// LoopFollow +// LowBGCondition.swift +// Created by Jonas Björkert. + +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, now _: Date) -> Bool { + // ──────────────────────────────── + // 0. sanity checks + // ──────────────────────────────── + guard let belowBG = alarm.belowBG else { return false } + + func isLow(_ g: GlucoseValue) -> Bool { + g.sgv > 0 && Double(g.sgv) <= belowBG + } + + // ──────────────────────────────── + // 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 ..< lookAhead where isLow(data.predictionData[i]) { + predictiveTrigger = true + break + } + } + + // ──────────────────────────────── + // 2. persistent low window (ALL readings must be low) + // ──────────────────────────────── + 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(isLow) + } else { + // not enough samples to prove persistence ⇒ don’t alarm yet + persistentOK = false + } + } + + // ──────────────────────────────── + // 3. final decision + // ──────────────────────────────── + return persistentOK || predictiveTrigger + } +} diff --git a/LoopFollow/Alarm/AlarmCondition/MissedBolusCondition.swift b/LoopFollow/Alarm/AlarmCondition/MissedBolusCondition.swift new file mode 100644 index 000000000..66846dc1d --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/MissedBolusCondition.swift @@ -0,0 +1,66 @@ +// LoopFollow +// MissedBolusCondition.swift +// Created by Jonas Björkert. + +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/AlarmCondition/MissedReadingCondition.swift b/LoopFollow/Alarm/AlarmCondition/MissedReadingCondition.swift new file mode 100644 index 000000000..d75ecca33 --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/MissedReadingCondition.swift @@ -0,0 +1,24 @@ +// LoopFollow +// MissedReadingCondition.swift +// Created by Jonas Björkert. + +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, now _: Date) -> 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/AlarmCondition/NotLoopingCondition.swift b/LoopFollow/Alarm/AlarmCondition/NotLoopingCondition.swift new file mode 100644 index 000000000..dcea898df --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/NotLoopingCondition.swift @@ -0,0 +1,30 @@ +// LoopFollow +// NotLoopingCondition.swift +// Created by Jonas Björkert. + +import Foundation + +struct NotLoopingCondition: AlarmCondition { + static let type: AlarmType = .notLooping + init() {} + + func evaluate(alarm: Alarm, data: AlarmData, now _: Date) -> 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/AlarmCondition/OverrideEndCondition.swift b/LoopFollow/Alarm/AlarmCondition/OverrideEndCondition.swift new file mode 100644 index 000000000..faf7c6337 --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/OverrideEndCondition.swift @@ -0,0 +1,21 @@ +// LoopFollow +// OverrideEndCondition.swift +// Created by Jonas Björkert. + +import Foundation + +struct OverrideEndCondition: AlarmCondition { + static let type: AlarmType = .overrideEnd + init() {} + + func evaluate(alarm _: Alarm, data: AlarmData, now: Date) -> Bool { + guard let endTS = data.latestOverrideEnd, endTS > 0 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 } + + Storage.shared.lastOverrideEndNotified.value = endTS + return true + } +} diff --git a/LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift b/LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift new file mode 100644 index 000000000..5a7fcb7b2 --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/OverrideStartCondition.swift @@ -0,0 +1,23 @@ +// LoopFollow +// OverrideStartCondition.swift +// Created by Jonas Björkert. + +import Foundation + +struct OverrideStartCondition: AlarmCondition { + static let type: AlarmType = .overrideStart + init() {} + + func evaluate(alarm _: Alarm, data: AlarmData, now: Date) -> Bool { + guard let startTS = data.latestOverrideStart, startTS > 0 else { return false } + + let recent = now.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/AlarmCondition/PumpChangeCondition.swift b/LoopFollow/Alarm/AlarmCondition/PumpChangeCondition.swift new file mode 100644 index 000000000..26db74e38 --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/PumpChangeCondition.swift @@ -0,0 +1,37 @@ +// LoopFollow +// PumpChangeCondition.swift +// Created by Jonas Björkert. + +// +// 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, now _: Date) -> 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) + + return Date() >= trigger + } +} diff --git a/LoopFollow/Alarm/AlarmCondition/PumpVolumeCondition.swift b/LoopFollow/Alarm/AlarmCondition/PumpVolumeCondition.swift new file mode 100644 index 000000000..8150cb23e --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/PumpVolumeCondition.swift @@ -0,0 +1,19 @@ +// LoopFollow +// PumpVolumeCondition.swift +// Created by Jonas Björkert. + +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, now _: Date) -> 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/AlarmCondition/RecBolusCondition.swift b/LoopFollow/Alarm/AlarmCondition/RecBolusCondition.swift new file mode 100644 index 000000000..db0f58d3e --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/RecBolusCondition.swift @@ -0,0 +1,32 @@ +// LoopFollow +// RecBolusCondition.swift +// Created by Jonas Björkert. + +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, now _: Date) -> Bool { + // ──────────────────────────────── + // 0. sanity checks + // ──────────────────────────────── + guard let threshold = alarm.threshold, threshold > 0 else { return false } + guard let rec = data.recBolus, 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/AlarmCondition/SensorAgeCondition.swift b/LoopFollow/Alarm/AlarmCondition/SensorAgeCondition.swift new file mode 100644 index 000000000..abb312f2e --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/SensorAgeCondition.swift @@ -0,0 +1,30 @@ +// LoopFollow +// SensorAgeCondition.swift +// Created by Jonas Björkert. + +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, now _: Date) -> 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/AlarmCondition/TempTargetEndCondition.swift b/LoopFollow/Alarm/AlarmCondition/TempTargetEndCondition.swift new file mode 100644 index 000000000..722b9c5a3 --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/TempTargetEndCondition.swift @@ -0,0 +1,22 @@ +// LoopFollow +// TempTargetEndCondition.swift +// Created by Jonas Björkert. + +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, now: Date) -> Bool { + guard let endTS = data.latestTempTargetEnd, endTS > 0 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 } + + Storage.shared.lastTempTargetEndNotified.value = endTS + return true + } +} diff --git a/LoopFollow/Alarm/AlarmCondition/TempTargetStartCondition.swift b/LoopFollow/Alarm/AlarmCondition/TempTargetStartCondition.swift new file mode 100644 index 000000000..538abef93 --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/TempTargetStartCondition.swift @@ -0,0 +1,22 @@ +// LoopFollow +// TempTargetStartCondition.swift +// Created by Jonas Björkert. + +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, now: Date) -> Bool { + guard let startTS = data.latestTempTargetStart, startTS > 0 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 } + + Storage.shared.lastTempTargetStartNotified.value = startTS + return true + } +} diff --git a/LoopFollow/Alarm/AlarmCondition/TemporaryCondition.swift b/LoopFollow/Alarm/AlarmCondition/TemporaryCondition.swift new file mode 100644 index 000000000..c9f9c951a --- /dev/null +++ b/LoopFollow/Alarm/AlarmCondition/TemporaryCondition.swift @@ -0,0 +1,20 @@ +// LoopFollow +// TemporaryCondition.swift +// Created by Jonas Björkert. + +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, now _: Date) -> 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/AlarmConfiguration.swift b/LoopFollow/Alarm/AlarmConfiguration.swift new file mode 100644 index 000000000..aaf9f372e --- /dev/null +++ b/LoopFollow/Alarm/AlarmConfiguration.swift @@ -0,0 +1,33 @@ +// LoopFollow +// AlarmConfiguration.swift +// Created by Jonas Björkert. + +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/AlarmData.swift b/LoopFollow/Alarm/AlarmData.swift new file mode 100644 index 000000000..96ec8fed2 --- /dev/null +++ b/LoopFollow/Alarm/AlarmData.swift @@ -0,0 +1,26 @@ +// LoopFollow +// AlarmData.swift +// Created by Jonas Björkert. + +import Foundation + +struct AlarmData: Codable { + let bgReadings: [GlucoseValue] + let predictionData: [GlucoseValue] + let expireDate: Date? + let lastLoopTime: TimeInterval? + let latestOverrideStart: TimeInterval? + let latestOverrideEnd: TimeInterval? + let latestTempTargetStart: TimeInterval? + let latestTempTargetEnd: TimeInterval? + let recBolus: Double? + let COB: Double? + let sageInsertTime: TimeInterval? + let pumpInsertTime: TimeInterval? + let latestPumpVolume: Double? + let IOB: Double? + let recentBoluses: [BolusEntry] + let latestBattery: Double? + let batteryHistory: [DataStructs.batteryStruct] + let recentCarbs: [CarbSample] +} diff --git a/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift new file mode 100644 index 000000000..4c374b892 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/AlarmEditor.swift @@ -0,0 +1,87 @@ +// LoopFollow +// AlarmEditor.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct AlarmEditor: View { + @Binding var alarm: Alarm + var isNew: Bool = false + var onDone: () -> Void = {} + var onCancel: () -> Void = {} + var onDelete: () -> Void = {} + + @Environment(\.dismiss) private var dismiss + + var body: some View { + NavigationStack { + Form { + innerEditorBody() + if !isNew { + DeleteAlarmSection { + onDelete() + dismiss() + } + } + }.navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button("Done") { + onDone() + dismiss() + } + } + if isNew { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + onCancel() + dismiss() + } + } + } + } + .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) + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmActiveSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmActiveSection.swift new file mode 100644 index 000000000..50aefd2e3 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmActiveSection.swift @@ -0,0 +1,16 @@ +// LoopFollow +// AlarmActiveSection.swift +// Created by Jonas Björkert. + +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..5aa0b2bd3 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmAudioSection.swift @@ -0,0 +1,129 @@ +// LoopFollow +// AlarmAudioSection.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct AlarmAudioSection: View { + @Binding var alarm: Alarm + var hideRepeat: Bool = false + @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, + allowed: PlaySoundOption.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) { + alarm.playSoundOption = playAllowed.last! + } + + let repeatAllowed = RepeatSoundOption.allowed(for: newActive) + if !repeatAllowed.contains(alarm.repeatSoundOption) { + alarm.repeatSoundOption = repeatAllowed.last! + } + } + } +} + +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(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 { + @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/AlarmBGLimitSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift new file mode 100644 index 000000000..abbfb071b --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGLimitSection.swift @@ -0,0 +1,78 @@ +// LoopFollow +// AlarmBGLimitSection.swift +// Created by Jonas Björkert. + +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, + footer: String? = nil, + 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 + 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 = defaultOnValue } + if !on { value = nil } + } + ) + } + + private var pickerValue: Binding { + Binding( + get: { value ?? defaultOnValue }, + set: { newVal in value = newVal } + ) + } + + // MARK: - Body + + var body: some View { + Section( + header: header.map(Text.init), + footer: footer.map(Text.init) + ) { + Toggle(toggleText, isOn: isOn) + + if isOn.wrappedValue { + BGPicker( + title: pickerTitle, + range: range, + value: pickerValue + ) + } + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift new file mode 100644 index 000000000..db1192d08 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmBGSection.swift @@ -0,0 +1,59 @@ +// LoopFollow +// AlarmBGSection.swift +// Created by Jonas Björkert. + +import HealthKit +import SwiftUI + +struct AlarmBGSection: View { + // MARK: – public parameters + + let header: String? + let footer: String? + let title: String + let range: ClosedRange + + // 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 + ) { + self.header = header + self.footer = footer + self.title = title + self.range = range + _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), + footer: footer.map(Text.init) + ) { + BGPicker( + title: title, + range: range, + value: nonOptional + ) + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Components/AlarmGeneralSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmGeneralSection.swift new file mode 100644 index 000000000..ca0307248 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmGeneralSection.swift @@ -0,0 +1,25 @@ +// LoopFollow +// AlarmGeneralSection.swift +// Created by Jonas Björkert. + +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..f8ed3bad1 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmSnoozeSection.swift @@ -0,0 +1,77 @@ +// LoopFollow +// AlarmSnoozeSection.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct AlarmSnoozeSection: View { + @Binding var alarm: Alarm + + private var unitLabel: String { alarm.type.snoozeTimeUnit.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.snoozeTimeUnit.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 the default value for how long the alert stays quiet after you press Snooze. \ + "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: alarm.type.snoozeRange, + step: alarm.type.snoozeStep + ) { + 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/Components/AlarmStepperSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift new file mode 100644 index 000000000..b95ae3ea1 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Components/AlarmStepperSection.swift @@ -0,0 +1,110 @@ +// LoopFollow +// AlarmStepperSection.swift +// Created by Jonas Björkert. + +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? + + // MARK: – private binding (always Double?) + + @Binding + private var value: Double? + + // MARK: – designated initialiser (Double?) + + init( + header: String? = nil, + footer: String? = nil, + title: String, + range: ClosedRange, + step: Double, + unitLabel: String? = nil, + value: Binding + ) { + self.header = header + self.footer = footer + self.title = title + self.range = range + self.step = step + self.unitLabel = unitLabel + _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 } + ) + } + + 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 { + Section( + header: header.map(Text.init), + footer: footer.map(Text.init) + ) { + Stepper(value: nonOptional, in: range, step: step) { + HStack { + Text(title) + Spacer() + Text( + String(format: "%.\(decimalPlaces)f", nonOptional.wrappedValue) + + (unitLabel.map { " \($0)" } ?? "") + ) + .foregroundColor(.secondary) + } + } + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Components/DeleteAlarmSection.swift b/LoopFollow/Alarm/AlarmEditing/Components/DeleteAlarmSection.swift new file mode 100644 index 000000000..753f7f2e8 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Components/DeleteAlarmSection.swift @@ -0,0 +1,32 @@ +// LoopFollow +// DeleteAlarmSection.swift +// Created by Jonas Björkert. + +// +// 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/Components/InfoBanner.swift b/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift new file mode 100644 index 000000000..837472fe5 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Components/InfoBanner.swift @@ -0,0 +1,45 @@ +// LoopFollow +// InfoBanner.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct InfoBanner: View { + /// Main explanatory text + let text: String + + /// Optional alarm type whose icon you’d like to show. + /// If `nil`, we fall back to the standard “info” symbol. + var alarmType: AlarmType? = nil + + /// Colour for the leading symbol + var iconColour: Color = .accentColor + + /// 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: alarmType?.icon ?? "info.circle.fill") + .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) + ) + .overlay( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .stroke(border, lineWidth: 1) + ) + .listRowInsets(EdgeInsets()) + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift b/LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift new file mode 100644 index 000000000..48580750a --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Components/SoundFile.swift @@ -0,0 +1,125 @@ +// LoopFollow +// SoundFile.swift +// Created by Jonas Björkert. + +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/BatteryAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift new file mode 100644 index 000000000..cc84769d0 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryAlarmEditor.swift @@ -0,0 +1,34 @@ +// LoopFollow +// BatteryAlarmEditor.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct BatteryAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Group { + InfoBanner( + text: "This warns you when the phone’s battery gets low, based on the percentage you choose.", + alarmType: alarm.type + ) + + AlarmGeneralSection(alarm: $alarm) + + AlarmStepperSection( + header: "Battery Level", + footer: "This alerts you when the battery drops below this level.", + title: "Battery Below", + range: 0 ... 100, + step: 5, + unitLabel: "%", + value: $alarm.threshold + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm) + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift new file mode 100644 index 000000000..2c8e8a841 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BatteryDropAlarmEditor.swift @@ -0,0 +1,44 @@ +// LoopFollow +// BatteryDropAlarmEditor.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct BatteryDropAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Group { + InfoBanner( + 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: "Battery Drop", + footer: "This alerts you if the battery drops by this much or more.", + title: "Drop Amount", + range: 5 ... 100, + step: 5, + unitLabel: "%", + value: $alarm.delta + ) + + AlarmStepperSection( + header: "Over This Time", + footer: "How far back to look for that drop.", + title: "Time Window", + range: 5 ... 30, + step: 5, + unitLabel: "min", + value: $alarm.monitoringWindow + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm) + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift new file mode 100644 index 000000000..70d9e62db --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/BuildExpireAlarmEditor.swift @@ -0,0 +1,33 @@ +// LoopFollow +// BuildExpireAlarmEditor.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct BuildExpireAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + 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." + ) + AlarmGeneralSection(alarm: $alarm) + + AlarmStepperSection( + header: "Notice Period", + footer: "Choose how many days of notice you’d like before the build becomes unusable.", + title: "Days of notice", + range: 1 ... 14, + step: 1, + unitLabel: alarm.type.snoozeTimeUnit.label, + value: $alarm.threshold + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm) + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift new file mode 100644 index 000000000..903218ff7 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/COBAlarmEditor.swift @@ -0,0 +1,35 @@ +// LoopFollow +// COBAlarmEditor.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct COBAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Group { + InfoBanner( + text: "Alerts when Carbs-on-Board exceeds the amount you set below.", + alarmType: alarm.type + ) + + AlarmGeneralSection(alarm: $alarm) + + AlarmStepperSection( + header: "Carbs on Board Limit", + footer: "Alert when carbs-on-board is above this number.", + title: "Above", + range: 1 ... 200, + step: 1, + unitLabel: "g", + value: $alarm.threshold + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + + AlarmSnoozeSection(alarm: $alarm) + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift new file mode 100644 index 000000000..2b7e8c5df --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastDropAlarmEditor.swift @@ -0,0 +1,51 @@ +// LoopFollow +// FastDropAlarmEditor.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct FastDropAlarmEditor: View { + @Binding var alarm: Alarm + + @State private var useLimit: Bool = false + + var body: some View { + 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." + ) + AlarmGeneralSection(alarm: $alarm) + + AlarmBGSection( + header: "Rate of Fall", + footer: "This is how much the glucose must drop to be considered a fast drop.", + title: "Falls by", + range: 3 ... 54, + value: $alarm.delta + ) + + 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: $alarm.monitoringWindow + ) + + 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, + defaultOnValue: 120, + value: $alarm.belowBG + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm) + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift new file mode 100644 index 000000000..458b2f826 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/FastRiseAlarmEditor.swift @@ -0,0 +1,55 @@ +// LoopFollow +// FastRiseAlarmEditor.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct FastRiseAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Group { + 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: "This is how much the glucose must rise to be considered a fast rise.", + title: "Rises by", + range: 3 ... 54, + value: $alarm.delta + ) + + 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: $alarm.monitoringWindow + ) + + 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, + defaultOnValue: 200, + value: $alarm.aboveBG + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm) + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift new file mode 100644 index 000000000..42219f971 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/HighBgAlarmEditor.swift @@ -0,0 +1,43 @@ +// LoopFollow +// HighBgAlarmEditor.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct HighBgAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Group { + InfoBanner( + 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: "High Glucose Limit", + footer: "The alert becomes eligible once any reading is at or above this value.", + title: "BG", + range: 120 ... 350, + value: $alarm.aboveBG + ) + + 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.", + title: "Persistent for", + range: 0 ... 120, + step: 5, + unitLabel: alarm.type.snoozeTimeUnit.label, + value: $alarm.persistentMinutes + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm) + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift new file mode 100644 index 000000000..883b189c9 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/IOBAlarmEditor.swift @@ -0,0 +1,65 @@ +// LoopFollow +// IOBAlarmEditor.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct IOBAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Group { + 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) + + AlarmStepperSection( + 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: $alarm.delta + ) + + AlarmStepperSection( + header: "Bolus Count", + footer: "Number of qualifying boluses needed to trigger.", + title: "Count", + range: 1 ... 10, + step: 1, + unitLabel: "Boluses", + value: $alarm.monitoringWindow + ) + + AlarmStepperSection( + header: "Time Window", + footer: "How far back to look for those boluses.", + title: "Time", + range: 5 ... 120, + step: 5, + unitLabel: "min", + value: $alarm.predictiveMinutes + ) + + AlarmStepperSection( + header: "Insulin On Board", + footer: "Alert if current IOB or total boluses reach this.", + title: "IOB Above", + range: 1 ... 20, + step: 0.5, + unitLabel: "Units", + value: $alarm.threshold + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm) + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift new file mode 100644 index 000000000..9a50e4ff5 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/LowBgAlarmEditor.swift @@ -0,0 +1,52 @@ +// LoopFollow +// LowBgAlarmEditor.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct LowBgAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + 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) + + AlarmBGSection( + header: "Low Limit", + footer: "Alert when any reading or prediction is at or below this value.", + title: "BG", + range: 40 ... 150, + value: $alarm.belowBG + ) + + 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", + range: 0 ... 120, + step: 5, + unitLabel: alarm.type.snoozeTimeUnit.label, + value: $alarm.persistentMinutes + ) + + 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.", + title: "Predictive", + range: 0 ... 60, + step: 5, + unitLabel: alarm.type.snoozeTimeUnit.label, + value: $alarm.predictiveMinutes + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm) + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift new file mode 100644 index 000000000..dbd827327 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/MissedBolusAlarmEditor.swift @@ -0,0 +1,78 @@ +// LoopFollow +// MissedBolusAlarmEditor.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct MissedBolusAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Group { + 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: "Delay", + range: 5 ... 60, + step: 5, + unitLabel: "min", + value: $alarm.monitoringWindow + ) + + AlarmStepperSection( + header: "Pre-bolus", + footer: "Count boluses given up to this many minutes before " + + "the carb entry as valid.", + title: "Pre-Bolus Time", + range: 0 ... 45, + step: 5, + unitLabel: "min", + value: $alarm.predictiveMinutes + ) + + AlarmStepperSection( + header: "Ignore small boluses", + footer: "Boluses below this size are ignored.", + title: "Ignore below", + range: 0.05 ... 2, + step: 0.05, + unitLabel: "Units", + value: $alarm.delta + ) + + AlarmStepperSection( + header: "Ignore small carbs", + footer: "Carb entries below this amount will not trigger the alarm.", + title: "Below", + range: 0 ... 15, + step: 1, + unitLabel: "Grams", + value: $alarm.threshold + ) + + AlarmBGLimitSection( + header: "Ignore low BG", + footer: "Only alert if the current BG is above this value.", + toggleText: "Use BG Limit", + pickerTitle: "Above", + range: 40 ... 140, + defaultOnValue: 70, + value: $alarm.aboveBG + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm) + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift new file mode 100644 index 000000000..44c1f24c4 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/MissedReadingEditor.swift @@ -0,0 +1,31 @@ +// LoopFollow +// MissedReadingEditor.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct MissedReadingEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Group { + 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, + step: 5, + unitLabel: alarm.type.snoozeTimeUnit.label, + value: $alarm.threshold + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm) + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift new file mode 100644 index 000000000..f1edb3a9a --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/NotLoopingAlarmEditor.swift @@ -0,0 +1,57 @@ +// LoopFollow +// NotLoopingAlarmEditor.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct NotLoopingAlarmEditor: View { + @Binding var alarm: Alarm + + private let bgRange: ClosedRange = 40 ... 300 + + var body: some View { + Group { + 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.snoozeTimeUnit.label, + value: $alarm.threshold + ) + + AlarmBGLimitSection( + header: "Low Limit", + footer: "Alert only if BG is equal to or below this value.", + toggleText: "Enable low limit", + pickerTitle: "Below", + range: bgRange, + defaultOnValue: 100, + 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, + defaultOnValue: 160, + value: $alarm.aboveBG + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm) + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift new file mode 100644 index 000000000..47a55c1db --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideEndAlarmEditor.swift @@ -0,0 +1,21 @@ +// LoopFollow +// OverrideEndAlarmEditor.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct OverrideEndAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Group { + InfoBanner(text: "Alerts when an override ends.", alarmType: alarm.type) + + AlarmGeneralSection(alarm: $alarm) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm, hideRepeat: true) + AlarmSnoozeSection(alarm: $alarm) + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift new file mode 100644 index 000000000..dbcc0c25c --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/OverrideStartAlarmEditor.swift @@ -0,0 +1,24 @@ +// LoopFollow +// OverrideStartAlarmEditor.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct OverrideStartAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Group { + InfoBanner( + text: "Alerts when an override begins.", + alarmType: alarm.type + ) + + AlarmGeneralSection(alarm: $alarm) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm, hideRepeat: true) + AlarmSnoozeSection(alarm: $alarm) + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift new file mode 100644 index 000000000..9ee78773a --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/PumpChangeAlarmEditor.swift @@ -0,0 +1,36 @@ +// LoopFollow +// PumpChangeAlarmEditor.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct PumpChangeAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Group { + InfoBanner( + text: "Alerts when the pump / cannula is within the time " + + "window you choose below (relative to the 3-day change " + + "limit).", + alarmType: alarm.type + ) + + AlarmGeneralSection(alarm: $alarm) + + AlarmStepperSection( + header: "Advance Notice", + 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: $alarm.threshold + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm) + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift new file mode 100644 index 000000000..96075af14 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/PumpVolumeAlarmEditor.swift @@ -0,0 +1,35 @@ +// LoopFollow +// PumpVolumeAlarmEditor.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct PumpVolumeAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Group { + InfoBanner( + text: "This warns you when the insulin pump is running low on insulin.", + alarmType: alarm.type + ) + + AlarmGeneralSection(alarm: $alarm) + + AlarmStepperSection( + header: "Trigger Level", + footer: "An alert fires once the reservoir is at this value " + + "or lower.", + title: "Reservoir Below", + range: 1 ... 50, + step: 1, + unitLabel: "Units", + value: $alarm.threshold + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm) + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift new file mode 100644 index 000000000..392c4f421 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/RecBolusAlarmEditor.swift @@ -0,0 +1,35 @@ +// LoopFollow +// RecBolusAlarmEditor.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct RecBolusAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Group { + 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 is above this value.", + title: "More than", + range: 0.1 ... 50, + step: 0.1, + unitLabel: "Units", + value: $alarm.threshold + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm) + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift new file mode 100644 index 000000000..ddaa8a24b --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/SensorAgeAlarmEditor.swift @@ -0,0 +1,35 @@ +// LoopFollow +// SensorAgeAlarmEditor.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct SensorAgeAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Group { + InfoBanner( + text: "Warn me this many hours before the sensor’s 10-day change-over.", + alarmType: alarm.type + ) + + AlarmGeneralSection(alarm: $alarm) + + AlarmStepperSection( + header: "Early Reminder", + footer: "Number of hours before the 10-day mark that the alert " + + "will fire.", + title: "Reminder Time", + range: 1 ... 24, + step: 1, + unitLabel: "hours", + value: $alarm.threshold + ) + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + AlarmSnoozeSection(alarm: $alarm) + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift new file mode 100644 index 000000000..7460c6d94 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetEndAlarmEditor.swift @@ -0,0 +1,21 @@ +// LoopFollow +// TempTargetEndAlarmEditor.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct TempTargetEndAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Group { + 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) + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift new file mode 100644 index 000000000..44c5dacac --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/TempTargetStartAlarmEditor.swift @@ -0,0 +1,21 @@ +// LoopFollow +// TempTargetStartAlarmEditor.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct TempTargetStartAlarmEditor: View { + @Binding var alarm: Alarm + + var body: some View { + Group { + 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) + } + } +} diff --git a/LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift b/LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift new file mode 100644 index 000000000..6c8523133 --- /dev/null +++ b/LoopFollow/Alarm/AlarmEditing/Editors/TemporaryAlarmEditor.swift @@ -0,0 +1,49 @@ +// LoopFollow +// TemporaryAlarmEditor.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct TemporaryAlarmEditor: View { + @Binding var alarm: Alarm + + // Shared BG range + private let bgRange: ClosedRange = 40 ... 300 + + var body: some View { + Group { + 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: "Below", + 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: "Above", + range: bgRange, + value: $alarm.aboveBG + ) + + if alarm.belowBG == nil && alarm.aboveBG == nil { + Text("⚠️ Please enable at least one limit.") + .foregroundColor(.red) + } + + AlarmActiveSection(alarm: $alarm) + AlarmAudioSection(alarm: $alarm) + } + } +} diff --git a/LoopFollow/Alarm/AlarmListView.swift b/LoopFollow/Alarm/AlarmListView.swift new file mode 100644 index 000000000..2674b8606 --- /dev/null +++ b/LoopFollow/Alarm/AlarmListView.swift @@ -0,0 +1,181 @@ +// LoopFollow +// AlarmListView.swift +// Created by Jonas Björkert. + +import SwiftUI + +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 let .editor(id, _): + return id + } + } +} + +struct AlarmListView: View { + @ObservedObject private var store = Storage.shared.alarms + @State private var sheetInfo: SheetInfo? + @State private var deleteAfterDismiss: UUID? + @State private var selectedAlarm: Alarm? + + // 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 { + // --- 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")) { + ForEach(activeAlarms) { alarm in + alarmRow(for: alarm) + } + } + } + + // --- INACTIVE ALARMS SECTION --- + if !inactiveAlarms.isEmpty { + Section(header: Text("Inactive")) { + ForEach(inactiveAlarms) { alarm in + alarmRow(for: alarm) + .opacity(0.6) + } + } + } + } + .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) + } + + // 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 + ) + + VStack(alignment: .leading, spacing: 2) { + Text(alarm.name) + .foregroundColor(.primary) + + 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 }) + { + 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 let .editor(id, isNew): + if let idx = store.value.firstIndex(where: { $0.id == id }) { + AlarmEditor( + alarm: $store.value[idx], + isNew: isNew, + onDone: { sheetInfo = nil }, + onCancel: { + if isNew { deleteAfterDismiss = id } + sheetInfo = nil + }, + onDelete: { + deleteAfterDismiss = id + sheetInfo = nil + } + ) + } else { + Text("Alarm not found").padding() + } + } + } +} diff --git a/LoopFollow/Alarm/AlarmManager.swift b/LoopFollow/Alarm/AlarmManager.swift new file mode 100644 index 000000000..b70c067b7 --- /dev/null +++ b/LoopFollow/Alarm/AlarmManager.swift @@ -0,0 +1,183 @@ +// LoopFollow +// AlarmManager.swift +// Created by Jonas Björkert. + +import Foundation +import UserNotifications + +class AlarmManager { + static let shared = AlarmManager() + + private let evaluators: [AlarmType: AlarmCondition] + private var lastBGAlarmTime: Date? + + private init( + conditionTypes: [AlarmCondition.Type] = [ + BuildExpireCondition.self, + LowBGCondition.self, + HighBGCondition.self, + FastDropCondition.self, + NotLoopingCondition.self, + OverrideStartCondition.self, + OverrideEndCondition.self, + TempTargetStartCondition.self, + TempTargetEndCondition.self, + RecBolusCondition.self, + COBCondition.self, + MissedReadingCondition.self, + FastRiseCondition.self, + TemporaryCondition.self, + SensorAgeCondition.self, + PumpChangeCondition.self, + PumpVolumeCondition.self, + IOBCondition.self, + BatteryCondition.self, + BatteryDropCondition.self, + ] + ) { + var dict = [AlarmType: AlarmCondition]() + conditionTypes.forEach { dict[$0.type] = $0.init() } + evaluators = dict + } + + func checkAlarms(data: AlarmData) { + let now = Date() + var alarmTriggered = false + let alarms = Storage.shared.alarms.value + + let sorted = alarms.sorted(by: Alarm.byPriorityThenSpec) + var skipType: AlarmType? + + 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 { + skipType = alarm.type + continue + } + + // Evaluate the alarm condition. + guard let checker = evaluators[alarm.type], + checker + .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 + 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 + + 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 + } + + 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 + list[idx].snoozedUntil = nil + 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) { + 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 + if units > 0 { + let snoozeSeconds = Double(units) * alarm.type.snoozeTimeUnit.seconds + alarms[idx].snoozedUntil = Date().addingTimeInterval(snoozeSeconds) + Storage.shared.alarms.value = alarms + } + stopAlarm() + } + } + + func stopAlarm() { + AlarmSound.stop() + 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/Alarm/AlarmSettingsView.swift b/LoopFollow/Alarm/AlarmSettingsView.swift new file mode 100644 index 000000000..8f0d31c59 --- /dev/null +++ b/LoopFollow/Alarm/AlarmSettingsView.swift @@ -0,0 +1,193 @@ +// LoopFollow +// AlarmSettingsView.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct AlarmSettingsView: View { + @ObservedObject private var cfgStore = Storage.shared.alarmConfiguration + + /// 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” disables every alarm. \ + “Mute All” silences phone sounds but still vibrates \ + and shows iOS notifications. + """) + ) { + 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] + ) + .datePickerStyle(.compact) + } + + 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. " + + "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", + 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 } + ) + ) + } + } + } + .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .navigationBarTitle("Alarm Settings", displayMode: .inline) + } +} diff --git a/LoopFollow/Alarm/AlarmType/AlarmType+Snooze.swift b/LoopFollow/Alarm/AlarmType/AlarmType+Snooze.swift new file mode 100644 index 000000000..6768f3473 --- /dev/null +++ b/LoopFollow/Alarm/AlarmType/AlarmType+Snooze.swift @@ -0,0 +1,49 @@ +// LoopFollow +// AlarmType+Snooze.swift +// Created by Jonas Björkert. + +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+SortDirection.swift b/LoopFollow/Alarm/AlarmType/AlarmType+SortDirection.swift new file mode 100644 index 000000000..15cd6b4eb --- /dev/null +++ b/LoopFollow/Alarm/AlarmType/AlarmType+SortDirection.swift @@ -0,0 +1,49 @@ +// LoopFollow +// AlarmType+SortDirection.swift +// Created by Jonas Björkert. + +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 }) + + case .sensorChange: + return (direction: .ascending, + key: { $0.threshold }) + + default: + return nil + } + } +} diff --git a/LoopFollow/Alarm/AlarmType/AlarmType+canAcknowledge.swift b/LoopFollow/Alarm/AlarmType/AlarmType+canAcknowledge.swift new file mode 100644 index 000000000..817a192f1 --- /dev/null +++ b/LoopFollow/Alarm/AlarmType/AlarmType+canAcknowledge.swift @@ -0,0 +1,20 @@ +// LoopFollow +// AlarmType+canAcknowledge.swift +// Created by Jonas Björkert. + +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 new file mode 100644 index 000000000..25a77351c --- /dev/null +++ b/LoopFollow/Alarm/AlarmType/AlarmType+timeUnit.swift @@ -0,0 +1,29 @@ +// LoopFollow +// AlarmType+timeUnit.swift +// Created by Jonas Björkert. + +import Foundation + +enum TimeUnit { + case minute, hour, day, none + + /// 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 + case .none: return 0 + } + } + + /// A user-facing label + var label: String { + switch self { + case .minute: return "min" + case .hour: return "hours" + case .day: return "days" + case .none: return "none" + } + } +} diff --git a/LoopFollow/Alarm/AlarmType/AlarmType.swift b/LoopFollow/Alarm/AlarmType/AlarmType.swift new file mode 100644 index 000000000..94c7ff9cd --- /dev/null +++ b/LoopFollow/Alarm/AlarmType/AlarmType.swift @@ -0,0 +1,50 @@ +// LoopFollow +// AlarmType.swift +// Created by Jonas Björkert. + +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 cob = "COB 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" + case notLooping = "Not Looping Alert" + case missedBolus = "Missed Bolus Alert" + case sensorChange = "Sensor Change Alert" + case pumpChange = "Pump Change Alert" + case pump = "Pump 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 { + /// `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, .temporary: + return true + default: + return false + } + } +} diff --git a/LoopFollow/Alarm/DataStructs/BolusEntry.swift b/LoopFollow/Alarm/DataStructs/BolusEntry.swift new file mode 100644 index 000000000..a0c537458 --- /dev/null +++ b/LoopFollow/Alarm/DataStructs/BolusEntry.swift @@ -0,0 +1,10 @@ +// LoopFollow +// BolusEntry.swift +// Created by Jonas Björkert. + +import Foundation + +struct BolusEntry: Codable { + let units: Double + let date: Date +} diff --git a/LoopFollow/Alarm/DataStructs/CarbSample.swift b/LoopFollow/Alarm/DataStructs/CarbSample.swift new file mode 100644 index 000000000..79a283598 --- /dev/null +++ b/LoopFollow/Alarm/DataStructs/CarbSample.swift @@ -0,0 +1,10 @@ +// LoopFollow +// CarbSample.swift +// Created by Jonas Björkert. + +import Foundation + +public struct CarbSample: Codable { + public let grams: Double + public let date: Date +} diff --git a/LoopFollow/Alarm/DataStructs/GlucoseValue.swift b/LoopFollow/Alarm/DataStructs/GlucoseValue.swift new file mode 100644 index 000000000..c473deac7 --- /dev/null +++ b/LoopFollow/Alarm/DataStructs/GlucoseValue.swift @@ -0,0 +1,11 @@ +// LoopFollow +// GlucoseValue.swift +// Created by Jonas Björkert. + +import Foundation + +// Make use of this more clean glucose struct in more places +struct GlucoseValue: Codable { + let sgv: Int + let date: Date +} diff --git a/LoopFollow/Alarm/SnoozeState.swift b/LoopFollow/Alarm/SnoozeState.swift new file mode 100644 index 000000000..1d720c9ad --- /dev/null +++ b/LoopFollow/Alarm/SnoozeState.swift @@ -0,0 +1,10 @@ +// LoopFollow +// SnoozeState.swift +// Created by Jonas Björkert. + +import Foundation + +struct SnoozeState: Codable { + var isSnoozed: Bool = false + var snoozeUntil: Date? +} diff --git a/LoopFollow/Application/AppDelegate.swift b/LoopFollow/Application/AppDelegate.swift index e067d7d2e..32db917a7 100644 --- a/LoopFollow/Application/AppDelegate.swift +++ b/LoopFollow/Application/AppDelegate.swift @@ -1,36 +1,31 @@ -// -// 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. -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 @@ -43,43 +38,30 @@ class AppDelegate: UIResponder, UIApplicationDelegate { UNUserNotificationCenter.current().delegate = self - // Ensure ViewControllerManager is initialized - _ = ViewControllerManager.shared - _ = BLEManager.shared return true } - func applicationWillTerminate(_ application: UIApplication) { - if UserDefaultsRepository.alertAppInactive.value { - AlarmSound.setSoundFile(str: "Alarm_Buzzer") - AlarmSound.playTerminated() - } - } + func applicationWillTerminate(_: UIApplication) {} // MARK: UISceneSession Lifecycle - func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [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 - } - + func application(_: UIApplication, willFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // 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 } - 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. @@ -95,7 +77,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. @@ -116,7 +98,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // MARK: - Core Data Saving support - func saveContext () { + func saveContext() { let context = persistentContainer.viewContext if context.hasChanges { do { @@ -130,20 +112,35 @@ 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) window.rootViewController?.present(MainViewController(), animated: true, completion: nil) } } + + if response.actionIdentifier == "snooze" { + AlarmManager.shared.performSnooze() + } + completionHandler() } + + func application(_: UIApplication, supportedInterfaceOrientationsFor _: UIWindow?) -> UIInterfaceOrientationMask { + let forcePortrait = Storage.shared.forcePortraitMode.value + + if forcePortrait { + return .portrait + } else { + return .all + } + } } 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/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..6f4151a2a 100644 --- a/LoopFollow/Application/SceneDelegate.swift +++ b/LoopFollow/Application/SceneDelegate.swift @@ -1,87 +1,51 @@ -// -// 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. -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) } - - @objc func handleToggleSpeakBGEvent() { - updateQuickActions() - } } diff --git a/LoopFollow/BackgroundRefresh/BT/BLEDevice.swift b/LoopFollow/BackgroundRefresh/BT/BLEDevice.swift index a444390cf..f30266ea9 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. import Foundation @@ -23,7 +20,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/BLEDeviceSelectionView.swift b/LoopFollow/BackgroundRefresh/BT/BLEDeviceSelectionView.swift index 77002f366..f6bd64b98 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. import SwiftUI diff --git a/LoopFollow/BackgroundRefresh/BT/BLEManager.swift b/LoopFollow/BackgroundRefresh/BT/BLEManager.swift index 9966ddcb6..5e47f9301 100644 --- a/LoopFollow/BackgroundRefresh/BT/BLEManager.swift +++ b/LoopFollow/BackgroundRefresh/BT/BLEManager.swift @@ -1,11 +1,10 @@ -// -// BLEManager.swift -// LoopFollow -// +// LoopFollow +// BLEManager.swift +// Created by Jonas Björkert. -import Foundation -import CoreBluetooth import Combine +import CoreBluetooth +import Foundation class BLEManager: NSObject, ObservableObject { static let shared = BLEManager() @@ -15,7 +14,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 +123,7 @@ class BLEManager: NSObject, ObservableObject { } // MARK: - CBCentralManagerDelegate + extension BLEManager: CBCentralManagerDelegate { func centralManagerDidUpdateState(_ central: CBCentralManager) { switch central.state { @@ -134,10 +134,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 +199,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 @@ -247,16 +248,16 @@ extension BLEManager { return nil } - let pollingDelay: TimeInterval = Double(UserDefaultsRepository.bgUpdateDelay.value) + let pollingDelay: TimeInterval = Double(Storage.shared.bgUpdateDelay.value) let expectedOffset = sensorOffset + pollingDelay // 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..1eb37a0fb 100644 --- a/LoopFollow/BackgroundRefresh/BT/BluetoothDevice.swift +++ b/LoopFollow/BackgroundRefresh/BT/BluetoothDevice.swift @@ -1,35 +1,31 @@ -// -// 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. -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 +63,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 +91,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 +118,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 +137,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 +163,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 +181,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 +191,7 @@ class BluetoothDevice: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate } } - func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { + func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) { cancelConnectionTimer() timeStampLastStatusUpdate = Date() @@ -206,7 +201,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 +222,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 +244,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 +257,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 +290,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 +304,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..5035a949a 100644 --- a/LoopFollow/BackgroundRefresh/BT/BluetoothDeviceDelegate.swift +++ b/LoopFollow/BackgroundRefresh/BT/BluetoothDeviceDelegate.swift @@ -1,13 +1,9 @@ -// -// 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. -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..7ae5d2e91 100644 --- a/LoopFollow/BackgroundRefresh/BT/Devices/DexcomHeartbeatBluetoothDevice.swift +++ b/LoopFollow/BackgroundRefresh/BT/Devices/DexcomHeartbeatBluetoothDevice.swift @@ -1,22 +1,18 @@ -// -// 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. +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 +25,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..1a1ae79e9 100644 --- a/LoopFollow/BackgroundRefresh/BT/Devices/OmnipodDashHeartbeatBluetoothTransmitter.swift +++ b/LoopFollow/BackgroundRefresh/BT/Devices/OmnipodDashHeartbeatBluetoothTransmitter.swift @@ -1,13 +1,9 @@ -// -// 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. -import Foundation import CoreBluetooth +import Foundation class OmnipodDashHeartbeatBluetoothTransmitter: BluetoothDevice { private let CBUUID_Service: String = "1A7E4024-E3ED-4464-8B7E-751E03D0DC5F" @@ -16,7 +12,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 +30,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..99141731c 100644 --- a/LoopFollow/BackgroundRefresh/BT/Devices/RileyLinkHeartbeatBluetoothDevice.swift +++ b/LoopFollow/BackgroundRefresh/BT/Devices/RileyLinkHeartbeatBluetoothDevice.swift @@ -1,20 +1,16 @@ -// -// RileyLinkHeartbeatBluetoothTransmitter.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. -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 +32,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..14b427208 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. // Denna behövs @@ -13,7 +9,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 +22,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..bdde1a896 100644 --- a/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift +++ b/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift @@ -1,13 +1,11 @@ -// -// BackgroundRefreshSettingsView.swift -// LoopFollow -// +// LoopFollow +// BackgroundRefreshSettingsView.swift +// Created by Jonas Björkert. import SwiftUI struct BackgroundRefreshSettingsView: View { @ObservedObject var viewModel: BackgroundRefreshSettingsViewModel - @Environment(\.presentationMode) var presentationMode @State private var forceRefresh = false @State private var timer: Timer? @@ -23,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 +28,8 @@ struct BackgroundRefreshSettingsView: View { stopTimer() } } + .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .navigationBarTitle("Background Refresh Settings", displayMode: .inline) } // MARK: - Subviews / Computed Properties @@ -96,8 +88,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..cf1f22b03 100644 --- a/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsViewModel.swift +++ b/LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsViewModel.swift @@ -1,12 +1,9 @@ -// -// BackgroundRefreshSettingsViewModel.swift -// LoopFollow -// -// Created by Jonas Björkert on 2025-01-02. -// +// LoopFollow +// BackgroundRefreshSettingsViewModel.swift +// Created by Jonas Björkert. -import Foundation import Combine +import Foundation class BackgroundRefreshSettingsViewModel: ObservableObject { @Published var backgroundRefreshType: BackgroundRefreshType @@ -17,7 +14,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/BackgroundRefresh/BackgroundRefreshType.swift b/LoopFollow/BackgroundRefresh/BackgroundRefreshType.swift index 7e2da5177..da452922e 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. import Foundation diff --git a/LoopFollow/Contact/ContactColorOption.swift b/LoopFollow/Contact/ContactColorOption.swift index 8c4297f82..ec472dac7 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. import UIKit @@ -13,15 +9,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..86c06cbac 100644 --- a/LoopFollow/Contact/ContactImageUpdater.swift +++ b/LoopFollow/Contact/ContactImageUpdater.swift @@ -1,13 +1,9 @@ -// -// 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. -import Foundation import Contacts +import Foundation import UIKit class ContactImageUpdater { @@ -34,11 +30,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 } @@ -89,27 +85,27 @@ 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 { + 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 +117,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 +128,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 +138,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/ContactIncludeOption.swift b/LoopFollow/Contact/ContactIncludeOption.swift index cf2bbb1ab..adce2a431 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. enum ContactIncludeOption: String, Codable, Equatable, CaseIterable { case off = "Off" diff --git a/LoopFollow/Contact/ContactType.swift b/LoopFollow/Contact/ContactType.swift index 764e783b5..0a8438bcf 100644 --- a/LoopFollow/Contact/ContactType.swift +++ b/LoopFollow/Contact/ContactType.swift @@ -1,13 +1,9 @@ -// -// ContactSuffix.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. enum ContactType: String, CaseIterable { - case BG = "BG" - case Trend = "Trend" - case Delta = "Delta" + case BG + case Trend + case Delta } diff --git a/LoopFollow/Controllers/AlarmSound.swift b/LoopFollow/Controllers/AlarmSound.swift index bb19107c3..e2575a5de 100644 --- a/LoopFollow/Controllers/AlarmSound.swift +++ b/LoopFollow/Controllers/AlarmSound.swift @@ -1,13 +1,9 @@ -// -// AlarmSound.swift -// scoutwatch -// -// Created by Dirk Hermanns on 03.01.16. -// Copyright © 2016 private. All rights reserved. -// +// LoopFollow +// AlarmSound.swift +// Created by Jon Fawcett. -import Foundation import AVFoundation +import Foundation import MediaPlayer import UIKit @@ -15,236 +11,203 @@ 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 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) - } else { - self.audioPlayer?.volume = 1.0 - } - self.muted = false + audioPlayer?.volume = 1.0 + 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 - - // 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() { - NSLog("AlarmSound - audio player failed preparing to play") + + audioPlayer?.numberOfLoops = 0 + + if !audioPlayer!.prepareToPlay() { + LogManager.shared.log(category: .alarm, 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)") + + 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: \(audioPlayer!.rate)") } } else { - NSLog("AlarmSound - audio player failed to play") + LogManager.shared.log(category: .alarm, message: "AlarmSound - audio player failed to play") } - - - } catch let error { - NSLog("AlarmSound - unable to play sound; error: \(error)") + + } catch { + LogManager.shared.log(category: .alarm, message: "AlarmSound - unable to play sound; error: \(error)") } } - - - static func play(overrideVolume: Bool, numLoops: Int) { - guard !self.isPlaying else { + + static func play(repeating: Bool) { + 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) - - // Play endless loops - self.audioPlayer!.numberOfLoops = numLoops - + + 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() { - NSLog("AlarmSound - audio player failed preparing to play") + + if !audioPlayer!.prepareToPlay() { + LogManager.shared.log(category: .alarm, 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)") + + 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: \(audioPlayer!.rate)") } } else { - NSLog("AlarmSound - audio player failed to play") + 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 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)") + } 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() { - NSLog("Terminate AlarmSound - audio player failed preparing to play") + + if !audioPlayer!.prepareToPlay() { + LogManager.shared.log(category: .alarm, 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)") + + 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: \(audioPlayer!.rate)") } } else { - NSLog("Terminate AlarmSound - audio player failed to play") + LogManager.shared.log(category: .alarm, message: "Terminate AlarmSound - audio player failed to play") } - - + MPVolumeView.setVolume(1.0) - - - } catch let error { - NSLog("Terminate AlarmSound - unable to play sound; error: \(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 { + guard Storage.shared.alarmConfiguration.value.overrideSystemOutputVolume 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() { @@ -252,48 +215,45 @@ 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)") } } } 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) + 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: .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") } } - + /* 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) { - LogManager.shared.log(category: .general, message: "AlarmRule - audioPlayerBeginInterruption") + 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) { - LogManager.shared.log(category: .general, message: "AlarmRule - audioPlayerEndInterruption withOptions: \(flags)") + 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/Alarms.swift b/LoopFollow/Controllers/Alarms.swift deleted file mode 100644 index 6778d78f8..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: latestDirectionString ?? "", deltaVal: latestDeltaString, minAgoVal: latestMinAgoString ?? "", 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: latestDirectionString ?? "", deltaVal: latestDeltaString, minAgoVal: latestMinAgoString ?? "", 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: latestDirectionString ?? "", deltaVal: latestDeltaString, minAgoVal: latestMinAgoString ?? "", 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/AppStateController.swift b/LoopFollow/Controllers/AppStateController.swift deleted file mode 100644 index 18053ca7c..000000000 --- a/LoopFollow/Controllers/AppStateController.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// AppStateController.swift -// LoopFollow -// -// Created by Jose Paredes on 7/17/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// - -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 - -// 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 -} - -// 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 - - // 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 -} diff --git a/LoopFollow/Controllers/BackgroundAlertManager.swift b/LoopFollow/Controllers/BackgroundAlertManager.swift index 4b73b2780..960879f98 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. import Foundation import UserNotifications @@ -92,7 +88,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..1261183a7 100644 --- a/LoopFollow/Controllers/Graphs.swift +++ b/LoopFollow/Controllers/Graphs.swift @@ -1,13 +1,9 @@ -// -// 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. -import Foundation import Charts +import Foundation import UIKit import Charts @@ -63,13 +59,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 +83,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 +139,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 +165,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 +200,7 @@ class TempTargetRenderer: LineChartRenderer { } } -let ScaleXMax:Float = 150.0 +let ScaleXMax: Double = 150.0 extension MainViewController { func updateChartRenderers() { let tempTargetDataIndex = GraphDataIndex.tempTarget.rawValue @@ -222,56 +218,55 @@ 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 = Double(BGChart.scaleX) + if scale > ScaleXMax { scale = ScaleXMax } - UserDefaultsRepository.chartScaleX.value = Float(scale) + Storage.shared.chartScaleX.value = scale } - func createGraph(){ + 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: "") + 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 { + + 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 } 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,24 +274,24 @@ extension MainViewController { linePrediction.axisDependency = YAxis.AxisDependency.right 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 } 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 maxBasal = Storage.shared.minBasalScale.value + let lineBasal = LineChartDataSet(entries: chartEntry, label: "") lineBasal.setDrawHighlightIndicators(false) lineBasal.setColor(NSUIColor.systemBlue, alpha: 0.5) lineBasal.lineWidth = 0 @@ -308,10 +303,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 +318,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 Storage.shared.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,21 +344,21 @@ extension MainViewController { lineCarbs.valueTextColor = NSUIColor.label lineCarbs.fillColor = NSUIColor.systemOrange lineCarbs.fillAlpha = 0.6 - - lineCarbs.drawCirclesEnabled = true - lineCarbs.drawFilledEnabled = false - - if UserDefaultsRepository.showValues.value { + + lineCarbs.drawCirclesEnabled = true + lineCarbs.drawFilledEnabled = false + + if Storage.shared.showValues.value { lineCarbs.drawValuesEnabled = true lineCarbs.highlightEnabled = false } else { 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 +368,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 +382,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 +397,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 +412,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 +427,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 +442,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 +460,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,23 +468,23 @@ extension MainViewController { COBlinePrediction.axisDependency = YAxis.AxisDependency.right 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 } 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,23 +492,23 @@ extension MainViewController { IOBlinePrediction.axisDependency = YAxis.AxisDependency.right 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 } 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,23 +516,23 @@ extension MainViewController { UAMlinePrediction.axisDependency = YAxis.AxisDependency.right 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 } 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,13 +540,13 @@ extension MainViewController { ZTlinePrediction.axisDependency = YAxis.AxisDependency.right 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 @@ -571,11 +566,11 @@ extension MainViewController { lineSmb.axisDependency = YAxis.AxisDependency.right lineSmb.valueFormatter = ChartYDataValueFormatter() lineSmb.valueTextColor = NSUIColor.label - + lineSmb.drawCirclesEnabled = false lineSmb.drawFilledEnabled = false - - if UserDefaultsRepository.showValues.value { + + if Storage.shared.showValues.value { lineSmb.drawValuesEnabled = true lineSmb.highlightEnabled = false } else { @@ -585,7 +580,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 +593,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 +614,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.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 + + // 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) - + // 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 +652,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,43 +662,43 @@ 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 { + + if Storage.shared.show30MinLine.value { let ul2 = ChartLimitLine() ul2.limit = Double(dateTimeUtils.getNowTimeIntervalUTC().advanced(by: -30 * 60)) ul2.lineColor = NSUIColor.systemBlue.withAlphaComponent(0.5) ul2.lineWidth = 1 BGChart.xAxis.addLimitLine(ul2) } - - if UserDefaultsRepository.showDIALines.value { - for i in 1..<7 { + + if Storage.shared.showDIALines.value { + for i in 1 ..< 7 { let ul = ChartLimitLine() ul.limit = Double(dateTimeUtils.getNowTimeIntervalUTC() - Double(i * 60 * 60)) ul.lineColor = NSUIColor.systemGray.withAlphaComponent(0.3) @@ -715,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) @@ -723,12 +718,12 @@ extension MainViewController { BGChart.xAxis.addLimitLine(ul3) } } - + 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 @@ -746,58 +741,57 @@ 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 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 { 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.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 + + // 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) - + // 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 @@ -808,28 +802,27 @@ 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 - for i in 0.. topBG - maxBGOffset { - topBG = Float(entries[i].sgv) + maxBGOffset + topBG = Storage.shared.minBGScale.value + for i in 0 ..< entries.count { + 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) { - colors.append(NSUIColor.systemRed) + } else if Double(entries[i].sgv) <= Storage.shared.lowLine.value { + colors.append(NSUIColor.systemRed) } else { colors.append(NSUIColor.systemGreen) } } - // Set Colors let lineBG = BGChart.lineData!.dataSets[dataIndex] as! LineChartDataSet @@ -841,15 +834,14 @@ extension MainViewController { lineBGSmall.circleColors.removeAll() if colors.count > 0 { - for i in 0.. CGFloat(ScaleXMax) ) { + 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 } - + // 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 + let maxBGOffset: Double = 20 - topPredictionBG = UserDefaultsRepository.minBGScale.value - for i in 0.. 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)) @@ -907,18 +899,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 { + + if Storage.shared.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, Storage.shared.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 + let graphHours = 24 * Storage.shared.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 { + if Storage.shared.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,178 +1152,176 @@ 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) 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) } } - + 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() @@ -1709,8 +1690,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) } @@ -1722,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) } } @@ -1731,7 +1713,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 } @@ -1739,10 +1722,11 @@ 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 { + let smallDataSet = smallChartData.dataSets[dataIndex] as? LineChartDataSet + { smallChartDataSet = smallDataSet smallChartDataSet?.clear() } @@ -1867,14 +1851,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 +1869,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 predictionVal > topPredictionBG - maxBGOffset { + topPredictionBG = predictionVal + maxBGOffset } - + if i == 0 { - if UserDefaultsRepository.showDots.value { - colors.append((color).withAlphaComponent(0.0)) + if Storage.shared.showDots.value { + 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 +1901,7 @@ extension MainViewController { mainChart.addEntry(value) smallChart.addEntry(value) } - + smallChart.circleColors.removeAll() smallChart.colors.removeAll() mainChart.colors.removeAll() @@ -1930,7 +1914,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/StatsView.swift b/LoopFollow/Controllers/MainViewController+updateStats.swift similarity index 59% rename from LoopFollow/Controllers/StatsView.swift rename to LoopFollow/Controllers/MainViewController+updateStats.swift index b5e6c27e8..d9cfd30ae 100644 --- a/LoopFollow/Controllers/StatsView.swift +++ b/LoopFollow/Controllers/MainViewController+updateStats.swift @@ -1,98 +1,84 @@ -// -// StatsView.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/23/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// MainViewController+updateStats.swift +// Created by Jon Fawcett. -import Foundation import Charts +import Foundation import UIKit - extension MainViewController { - - func updateStats() - { + 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) 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)) - if UserDefaultsRepository.useIFCC.value { - statsEstA1C.text = String(format:"%.0f%", stats.a1C) - } - else - { - statsEstA1C.text = String(format:"%.1f%", stats.a1C) + + 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 Storage.shared.useIFCC.value { + 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]) { + + fileprivate 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.. 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() - { + + 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 4f96c3712..a616f4e52 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. import Foundation import UIKit @@ -14,10 +10,9 @@ 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 - + 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 +28,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") @@ -57,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 @@ -66,11 +61,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 +99,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( @@ -123,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.") @@ -172,10 +167,11 @@ 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) + TaskScheduler.shared.rescheduleTask(id: .alarmCheck, to: Date()) } TaskScheduler.shared.rescheduleTask(id: .fetchBG, to: Date().addingTimeInterval(delayToSchedule)) @@ -188,7 +184,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 @@ -213,7 +209,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 @@ -237,61 +233,37 @@ extension MainViewController { let deltaBG = latestBG - priorBG let lastBGTime = entries[latestEntryIndex].date - 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)) + Observable.shared.bg.value = 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 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 ?? "" - 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 - 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 - - // Snoozer Display - 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: Observable.shared.bgText.value, + trend: Observable.shared.directionText.value, + delta: Observable.shared.deltaText.value, + stale: Observable.shared.bgStale.value + ) } } } diff --git a/LoopFollow/Controllers/Nightscout/CAge.swift b/LoopFollow/Controllers/Nightscout/CAge.swift index b5546e457..5a0b97151 100644 --- a/LoopFollow/Controllers/Nightscout/CAge.swift +++ b/LoopFollow/Controllers/Nightscout/CAge.swift @@ -1,59 +1,56 @@ -// -// 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. 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 - + let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withFullDate, .withTime, .withDashSeparatorInDate, .withColonSeparatorInTime] - UserDefaultsRepository.alertCageInsertTime.value = formatter.date(from: (lastCageString))?.timeIntervalSince1970 as! TimeInterval - if let cageTime = formatter.date(from: (lastCageString))?.timeIntervalSince1970 { + 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 - //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..6374bdec8 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift @@ -1,21 +1,17 @@ -// -// 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. +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) @@ -39,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 @@ -71,7 +62,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 @@ -80,7 +71,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 +80,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 +100,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) + "%" @@ -117,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) @@ -132,20 +124,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,13 +154,13 @@ 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) } // 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) { @@ -199,6 +192,7 @@ extension MainViewController { id: .deviceStatus, to: Date().addingTimeInterval(interval) ) + TaskScheduler.shared.rescheduleTask(id: .alarmCheck, to: Date()) } } diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift index bf8177290..ce9e841dc 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift @@ -1,37 +1,31 @@ -// -// 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. -import Foundation -import UIKit import Charts +import Foundation import HealthKit +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 } - if let lastLoopTime = formatter.date(from: (lastLoopRecord["timestamp"] as! String))?.timeIntervalSince1970 { - let previousLastLoopTime = UserDefaultsRepository.alertLastLoopTime.value - UserDefaultsRepository.alertLastLoopTime.value = lastLoopTime + if let lastLoopTime = formatter.date(from: (lastLoopRecord["timestamp"] as! String))?.timeIntervalSince1970 { + let previousLastLoopTime = Observable.shared.alertLastLoopTime.value ?? 0 + Observable.shared.alertLastLoopTime.value = lastLoopTime if let failure = lastLoopRecord["failureReason"] { LoopStatusLabel.text = "X" 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,14 +62,14 @@ 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 Storage.shared.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 { @@ -89,7 +83,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)) @@ -107,15 +101,15 @@ 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 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 +121,6 @@ extension MainViewController { LoopStatusLabel.text = "↻" latestLoopStatusString = "↻" } - } } } diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift index d7aa0d63c..77a735fe5 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift @@ -1,15 +1,14 @@ -// 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. import Foundation -import UIKit import HealthKit +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" @@ -20,16 +19,17 @@ 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 storedTime = UserDefaultsRepository.alertLastLoopTime.value + let lastLoopTime = formatter.date(from: timestampString)?.timeIntervalSince1970 + { + 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) } } @@ -41,7 +41,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 +98,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] @@ -118,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 @@ -164,13 +166,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 @@ -178,13 +181,13 @@ 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), ("IOB", "Insulin", 13), ("COB", "LoopYellow", 14), - ("UAM", "UAM", 15) + ("UAM", "UAM", 15), ] var minPredBG = Double.infinity @@ -194,9 +197,9 @@ 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 { + for i in 0 ... toLoad { if i < graphdata.count { let predictionValue = graphdata[i] minPredBG = min(minPredBG, predictionValue) @@ -218,7 +221,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 +235,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..f47fbe8c3 100644 --- a/LoopFollow/Controllers/Nightscout/IAge.swift +++ b/LoopFollow/Controllers/Nightscout/IAge.swift @@ -1,12 +1,9 @@ -// -// 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. import Foundation + extension MainViewController { // NS Iage Web Call func webLoadNSIage() { @@ -17,16 +14,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 +51,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 9c31947a9..d804b88a7 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. import Foundation @@ -12,16 +11,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 @@ -46,6 +48,7 @@ struct NSProfile: Decodable { let isAPNSProduction: Bool? let deviceToken: String? let teamID: String? + let expirationDate: String? struct TrioOverrideEntry: Decodable { let name: String @@ -92,7 +95,8 @@ 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..662160eb2 100644 --- a/LoopFollow/Controllers/Nightscout/Profile.swift +++ b/LoopFollow/Controllers/Nightscout/Profile.swift @@ -1,25 +1,22 @@ -// -// 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. 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,19 +24,19 @@ 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 + + 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() @@ -55,53 +52,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) + var predictionEndTime = dateTimeUtils.getNowTimeIntervalUTC() + (3600 * Storage.shared.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,8 +109,8 @@ extension MainViewController { } } } - - if UserDefaultsRepository.graphBasal.value { + + if Storage.shared.graphBasal.value { updateBasalScheduledGraph() } } diff --git a/LoopFollow/Controllers/Nightscout/ProfileManager.swift b/LoopFollow/Controllers/Nightscout/ProfileManager.swift index 0977c40d7..be1ac3e28 100644 --- a/LoopFollow/Controllers/Nightscout/ProfileManager.swift +++ b/LoopFollow/Controllers/Nightscout/ProfileManager.swift @@ -1,16 +1,17 @@ -// 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. import Foundation 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 +24,7 @@ final class ProfileManager { var defaultProfile: String // MARK: - Nested Structures + struct TimeValue { let timeAsSeconds: Int let value: T @@ -44,39 +46,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 +92,7 @@ final class ProfileManager { ) } } else { - self.loopOverrides = [] + loopOverrides = [] } if let trioOverrides = profileData.trioOverrides { @@ -101,10 +106,15 @@ final class ProfileManager { ) } } else { - self.trioOverrides = [] + trioOverrides = [] } Storage.shared.deviceToken.value = profileData.deviceToken ?? "" + if let expirationDate = profileData.expirationDate { + Storage.shared.expirationDate.value = NightscoutUtils.parseDate(expirationDate) + } 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 ?? "" @@ -138,11 +148,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 { @@ -183,15 +193,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 4f07a0ef2..6c546187f 100644 --- a/LoopFollow/Controllers/Nightscout/SAge.swift +++ b/LoopFollow/Controllers/Nightscout/SAge.swift @@ -1,37 +1,34 @@ -// -// 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. 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,32 +38,40 @@ 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) { - 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) + Storage.shared.sageInsertTime.value = formatter.date(from: lastSageString)?.timeIntervalSince1970 as! TimeInterval + + // -- 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 = 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. + 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 { 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 3fd986948..4ee4d939f 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments.swift @@ -1,27 +1,24 @@ -// -// 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. 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) + if !Storage.shared.downloadTreatments.value { return } + + 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, - "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 +26,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 +52,7 @@ extension MainViewController { guard let eventType = entry["eventType"] as? String else { continue } - + switch eventType { case "Temp Basal": tempBasal.append(entry) @@ -104,7 +100,7 @@ extension MainViewController { print("No Match: \(String(describing: entry))") } } - + if tempBasal.count > 0 { processNSBasals(entries: tempBasal) } else { @@ -141,14 +137,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..e08f43eef 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/BGCheck.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/BGCheck.swift @@ -1,27 +1,24 @@ -// -// CarbsToday.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. import Foundation 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 +28,9 @@ extension MainViewController { bgCheckData.append(dot) } } - - 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 409646211..65aaf3dde 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Basals.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Basals.swift @@ -1,17 +1,12 @@ -// -// 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. 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 +16,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 +51,7 @@ extension MainViewController { var midGapTime: TimeInterval = 0 var midGapValue: Double = 0 - for b in 0..= basalScheduleData[b].date { scheduled = basalScheduleData[b].basalRate @@ -101,12 +97,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 +118,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 } @@ -142,12 +138,13 @@ extension MainViewController { basalData.append(endDot) } - if UserDefaultsRepository.graphBasal.value { + if Storage.shared.graphBasal.value { updateBasalGraph() } 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..9b6c51ca8 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Bolus.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Bolus.swift @@ -1,44 +1,41 @@ -// -// 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. 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 { + + if Storage.shared.graphBolus.value { updateBolusGraph() } } diff --git a/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift b/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift index 285e1f5e4..8b3f238d2 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Carbs.swift @@ -1,68 +1,64 @@ -// -// 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. 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)) { + + 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) } } - - if UserDefaultsRepository.graphCarbs.value { + + if Storage.shared.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 +67,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 +80,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..2962fee9b 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/InsulinCartridgeChange.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/InsulinCartridgeChange.swift @@ -1,19 +1,16 @@ -// -// 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. import Foundation + extension MainViewController { func processIage(entries: [iageData]) { if !entries.isEmpty { 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 1dea8e0ce..4019c0f1a 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Notes.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Notes.swift @@ -1,40 +1,36 @@ -// -// CarbsToday.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. import Foundation 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,8 +39,8 @@ extension MainViewController { print("Failed to parse date") } } - - 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 8289168de..0730fc41a 100644 --- a/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift +++ b/LoopFollow/Controllers/Nightscout/Treatments/Overrides.swift @@ -1,16 +1,11 @@ -// -// 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. import Foundation import UIKit extension MainViewController { - func processNSOverrides(entries: [[String: AnyObject]]) { overrideGraphData.removeAll() var activeOverrideNote: String? @@ -26,10 +21,10 @@ 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..= 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" + ) + } + } + } + + enum 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 + lastSpeechTime = currentTime + + let bloodGlucoseDifference = currentValue - previousValue + + let preferredLanguage = Storage.shared.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/Stats.swift b/LoopFollow/Controllers/Stats.swift index a359db236..8405ee714 100644 --- a/LoopFollow/Controllers/Stats.swift +++ b/LoopFollow/Controllers/Stats.swift @@ -1,16 +1,10 @@ -// -// 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. import Foundation - class StatsData { - var countLow: Int var percentLow: Float var percentRange: Float @@ -23,38 +17,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 + if Double(bgData[i].sgv) <= Storage.shared.lowLine.value { + countLow += 1 + } else if Double(bgData[i].sgv) >= Storage.shared.highLine.value { + 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 +56,20 @@ class StatsData { avgBG = Float(totalGlucose / bgDataCount) // compute std dev (sigma) - var partialSum: Float = 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 } + ) + } +} diff --git a/LoopFollow/Extensions/EKEventStore+Extensions.swift b/LoopFollow/Extensions/EKEventStore+Extensions.swift index 41917baa1..005a4b945 100644 --- a/LoopFollow/Extensions/EKEventStore+Extensions.swift +++ b/LoopFollow/Extensions/EKEventStore+Extensions.swift @@ -1,34 +1,30 @@ -// -// 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. -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..d22f6d9da 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. import HealthKit @@ -23,7 +20,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..606162fc0 100644 --- a/LoopFollow/Extensions/HKUnit+Extensions.swift +++ b/LoopFollow/Extensions/HKUnit+Extensions.swift @@ -1,22 +1,14 @@ -// -// 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. 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 c91c57b38..beb2fb5e3 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. import Foundation import ShareClient @@ -13,19 +9,19 @@ 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 - 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 } - + // 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,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 @@ -49,28 +45,26 @@ 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) } - + // parse data to conanical form var shareData = [ShareGlucoseData]() for item in result { @@ -78,7 +72,7 @@ extension ShareClient { if trend < 0 || trend >= TrendTable.count { trend = 0 } - + let newShareData = ShareGlucoseData( sgv: Int(item.glucose), date: item.timestamp.timeIntervalSince1970, diff --git a/LoopFollow/Extensions/UIViewExtension.swift b/LoopFollow/Extensions/UIViewExtension.swift index dc9472f7f..7d9b8ba10 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. import Foundation import UIKit @@ -13,17 +9,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/Extensions/UUID+Identifiable.swift b/LoopFollow/Extensions/UUID+Identifiable.swift new file mode 100644 index 000000000..d8be93ef8 --- /dev/null +++ b/LoopFollow/Extensions/UUID+Identifiable.swift @@ -0,0 +1,9 @@ +// LoopFollow +// UUID+Identifiable.swift +// Created by Jonas Björkert. + +import Foundation + +extension UUID: @retroactive Identifiable { + public var id: UUID { self } +} diff --git a/LoopFollow/Helpers/AnyConvertible.swift b/LoopFollow/Helpers/AnyConvertible.swift index 90cdf54d3..c8e6f6973 100644 --- a/LoopFollow/Helpers/AnyConvertible.swift +++ b/LoopFollow/Helpers/AnyConvertible.swift @@ -1,14 +1,10 @@ -// -// AnyConvertible.swift -// nightguard -// -// Created by Florian Preknya on 1/27/19. -// Copyright © 2019 private. All rights reserved. -// +// LoopFollow +// AnyConvertible.swift +// Created by Jon Fawcett. 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 +15,7 @@ extension Bool: AnyConvertible { func toAny() -> Any { return self } - + static func fromAny(_ anyValue: Any) -> Bool? { return anyValue as? Bool } @@ -29,7 +25,7 @@ extension String: AnyConvertible { func toAny() -> Any { return self } - + static func fromAny(_ anyValue: Any) -> String? { return anyValue as? String } @@ -39,7 +35,7 @@ extension Int: AnyConvertible { func toAny() -> Any { return self } - + static func fromAny(_ anyValue: Any) -> Int? { return anyValue as? Int } @@ -49,7 +45,7 @@ extension Float: AnyConvertible { func toAny() -> Any { return self } - + static func fromAny(_ anyValue: Any) -> Float? { return anyValue as? Float } @@ -59,7 +55,7 @@ extension Double: AnyConvertible { func toAny() -> Any { return self } - + static func fromAny(_ anyValue: Any) -> Double? { return anyValue as? Double } @@ -69,7 +65,7 @@ extension Date: AnyConvertible { func toAny() -> Any { return self } - + static func fromAny(_ anyValue: Any) -> Date? { return anyValue as? Date } @@ -79,7 +75,7 @@ extension Data: AnyConvertible { func toAny() -> Any { return self } - + static func fromAny(_ anyValue: Any) -> Data? { return anyValue as? Data } @@ -87,19 +83,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 +103,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 +118,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..5c1dccdaa 100644 --- a/LoopFollow/Helpers/AppConstants.swift +++ b/LoopFollow/Helpers/AppConstants.swift @@ -1,14 +1,10 @@ -// -// AppConstants.swift -// scoutwatch -// -// Created by Dirk Hermanns on 26.12.15. -// Copyright © 2015 private. All rights reserved. -// +// LoopFollow +// AppConstants.swift +// Created by Jon Fawcett. 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..5ec5df960 100644 --- a/LoopFollow/Helpers/AppVersionManager.swift +++ b/LoopFollow/Helpers/AppVersionManager.swift @@ -1,16 +1,20 @@ -// -// 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. 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 ... }` @@ -19,20 +23,21 @@ 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 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 +54,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 - + 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) } @@ -82,9 +87,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..df39d89c5 100755 --- a/LoopFollow/Helpers/BackgroundTaskAudio.swift +++ b/LoopFollow/Helpers/BackgroundTaskAudio.swift @@ -1,53 +1,51 @@ -// -// BackgroundTask.swift -// -// Created by Yaro on 8/27/16. -// Copyright © 2016 Yaro. All rights reserved. -// +// LoopFollow +// BackgroundTaskAudio.swift +// Created by Jon Fawcett. 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/BinaryFloatingPoint+localized.swift b/LoopFollow/Helpers/BinaryFloatingPoint+localized.swift new file mode 100644 index 000000000..008fafdff --- /dev/null +++ b/LoopFollow/Helpers/BinaryFloatingPoint+localized.swift @@ -0,0 +1,13 @@ +// LoopFollow +// BinaryFloatingPoint+localized.swift +// Created by Jonas Björkert. + +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/BuildDetails.swift b/LoopFollow/Helpers/BuildDetails.swift index d70e287b4..473819218 100644 --- a/LoopFollow/Helpers/BuildDetails.swift +++ b/LoopFollow/Helpers/BuildDetails.swift @@ -1,28 +1,25 @@ -// -// 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. 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 +33,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 +75,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 +90,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..8fecbf525 100644 --- a/LoopFollow/Helpers/Chart.swift +++ b/LoopFollow/Helpers/Chart.swift @@ -1,42 +1,35 @@ -// -// 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. -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 +38,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 +48,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 +58,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 +89,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 +99,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 +114,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..391466920 100644 --- a/LoopFollow/Helpers/CycleHelper.swift +++ b/LoopFollow/Helpers/CycleHelper.swift @@ -1,14 +1,10 @@ -// -// 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. 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 +32,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 +42,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..e50ea48f7 100644 --- a/LoopFollow/Helpers/DataStructs.swift +++ b/LoopFollow/Helpers/DataStructs.swift @@ -1,48 +1,43 @@ -// -// Enums.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/23/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// DataStructs.swift +// Created by Jon Fawcett. 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..3f82f856d 100644 --- a/LoopFollow/Helpers/DateTime.swift +++ b/LoopFollow/Helpers/DateTime.swift @@ -1,16 +1,10 @@ -// -// 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. import Foundation - class dateTimeUtils { - static func getTimeIntervalMidnightToday() -> TimeInterval { let now = Date() let formatter = DateFormatter() @@ -24,7 +18,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 +32,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 +50,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 +78,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..f3043fa34 100644 --- a/LoopFollow/Helpers/DictionaryKeyPath.swift +++ b/LoopFollow/Helpers/DictionaryKeyPath.swift @@ -1,3 +1,7 @@ +// LoopFollow +// DictionaryKeyPath.swift +// Created by Jon Fawcett. + // For details, see // http://stackoverflow.com/questions/40261857/remove-nested-key-from-dictionary import Foundation @@ -6,19 +10,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 +31,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 +40,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..808b545ae 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. import Foundation @@ -12,7 +8,7 @@ class GitHubService { enum GitHubDataType { case versionConfig case blacklistedVersions - + var url: String { switch self { case .versionConfig: @@ -22,14 +18,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..a7175ee19 100644 --- a/LoopFollow/Helpers/Globals.swift +++ b/LoopFollow/Helpers/Globals.swift @@ -1,17 +1,12 @@ -// -// 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. 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..61478cc37 100644 --- a/LoopFollow/Helpers/GlucoseConversion.swift +++ b/LoopFollow/Helpers/GlucoseConversion.swift @@ -1,14 +1,10 @@ -// -// 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. 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 8e15f13c0..34b45e76d 100644 --- a/LoopFollow/Helpers/Localizer.swift +++ b/LoopFollow/Helpers/Localizer.swift @@ -1,15 +1,21 @@ -// -// Units.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/22/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// Localizer.swift +// Created by Jon Fawcett. 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 @@ -22,15 +28,14 @@ class Localizer { } static func formatQuantity(_ quantity: HKQuantity) -> String { - let unitPreference = UserDefaultsRepository.units.value + let unit: HKUnit = 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 { @@ -46,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 @@ -64,27 +69,27 @@ class Localizer { static func toDisplayUnits(_ value: String) -> String { 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 numberFormatter.minimumFractionDigits = 1 // This ensures even .0 is displayed } - + 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 { - 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 } @@ -96,20 +101,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 7752a5f7f..26aed1891 100644 --- a/LoopFollow/Helpers/Mobileprovision.swift +++ b/LoopFollow/Helpers/Mobileprovision.swift @@ -1,8 +1,11 @@ +// LoopFollow +// Mobileprovision.swift +// Created by Jon Fawcett. + // // MobileProvision.swift // Fluux.io // -// Created by Mickaël Rémond on 03/11/2018. // Copyright © 2018 ProcessOne. // Distributed under Apache License v2 // @@ -10,14 +13,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 +30,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 +40,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,24 +85,23 @@ 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 { let provision = try decoder.decode(MobileProvision.self, from: plist) return provision } catch { - // TODO: log / handle error return nil } } diff --git a/LoopFollow/Helpers/NightscoutUtils.swift b/LoopFollow/Helpers/NightscoutUtils.swift index ede541bb6..0bd21bf2b 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. import Foundation @@ -67,8 +63,8 @@ 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 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 { completion(.failure(NSError(domain: "NightscoutUtils", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to construct URL"]))) @@ -78,7 +74,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,17 +86,35 @@ class NightscoutUtils { DispatchQueue.main.async { completion(.success(decodedObject)) } + } catch let decodingError as DecodingError { + print("[ERROR] Failed to decode \(T.self):") + switch decodingError { + case let .typeMismatch(type, context): + print("Type mismatch for type \(type), context: \(context.debugDescription)") + print("Coding path:", context.codingPath) + case let .valueNotFound(type, context): + print("Value not found for type \(type), context: \(context.debugDescription)") + print("Coding path:", context.codingPath) + case let .keyNotFound(key, context): + print("Key '\(key.stringValue)' not found, context: \(context.debugDescription)") + print("Coding path:", context.codingPath) + case let .dataCorrupted(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)) } } 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 + 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 { completion(.failure(NSError(domain: "NightscoutUtils", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to construct URL"]))) @@ -110,7 +124,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 @@ -172,8 +186,8 @@ class NightscoutUtils { } static func verifyURLAndToken(completion: @escaping (NightscoutError?, String?, Bool, Bool) -> Void) { - let urlUser = ObservableUserDefaults.shared.url.value - let token = UserDefaultsRepository.token.value + let urlUser = Storage.shared.url.value + let token = Storage.shared.token.value if urlUser.isEmpty { completion(.emptyAddress, nil, false, false) @@ -202,8 +216,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 @@ -245,9 +259,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) } @@ -270,15 +284,16 @@ class NightscoutUtils { } static func retrieveJWTToken() async throws -> String { - let urlUser = ObservableUserDefaults.shared.url.value - let token = UserDefaultsRepository.token.value + let urlUser = Storage.shared.url.value + let token = Storage.shared.token.value if urlUser.isEmpty { throw NightscoutError.emptyAddress } 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 } @@ -297,7 +312,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 @@ -311,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 @@ -341,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 @@ -360,7 +376,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 != "" { @@ -377,15 +393,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..e2028c530 100644 --- a/LoopFollow/Helpers/ObservationToken.swift +++ b/LoopFollow/Helpers/ObservationToken.swift @@ -1,22 +1,17 @@ -// -// ObservationToken.swift -// nightguard -// -// Created by Florian Preknya on 1/30/19. -// Copyright © 2019 private. All rights reserved. -// +// LoopFollow +// ObservationToken.swift +// Created by Jon Fawcett. 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..c3119e6ee 100644 --- a/LoopFollow/Helpers/TextFieldWithToolBar.swift +++ b/LoopFollow/Helpers/TextFieldWithToolBar.swift @@ -1,14 +1,10 @@ -// -// 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. +import HealthKit import SwiftUI import UIKit -import HealthKit public struct TextFieldWithToolBar: UIViewRepresentable { @Binding var quantity: HKQuantity @@ -247,8 +243,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 new file mode 100644 index 000000000..0076e4795 --- /dev/null +++ b/LoopFollow/Helpers/TimeOfDay.swift @@ -0,0 +1,21 @@ +// LoopFollow +// TimeOfDay.swift +// Created by Jonas Björkert. + +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/Helpers/Views/ActionRow.swift b/LoopFollow/Helpers/Views/ActionRow.swift new file mode 100644 index 000000000..077caeb87 --- /dev/null +++ b/LoopFollow/Helpers/Views/ActionRow.swift @@ -0,0 +1,25 @@ +// LoopFollow +// ActionRow.swift +// Created by Jonas Björkert. + +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/BGPicker.swift b/LoopFollow/Helpers/Views/BGPicker.swift new file mode 100644 index 000000000..db4ae1d72 --- /dev/null +++ b/LoopFollow/Helpers/Views/BGPicker.swift @@ -0,0 +1,48 @@ +// LoopFollow +// BGPicker.swift +// Created by Jonas Björkert. + +import HealthKit +import SwiftUI + +/// 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 { Localizer.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) { + ForEach(allValues, id: \.self) { v in + Text("\(Localizer.formatQuantity(v)) \(unit.localizedShortUnitString)") + .tag(v) + } + } label: { + Text(title) + } + } +} diff --git a/LoopFollow/Helpers/Views/ErrorMessageView.swift b/LoopFollow/Helpers/Views/ErrorMessageView.swift index 1603abe3c..632ad4aaa 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. import Foundation import SwiftUI diff --git a/LoopFollow/Helpers/Views/Glyph.swift b/LoopFollow/Helpers/Views/Glyph.swift new file mode 100644 index 000000000..61faa4474 --- /dev/null +++ b/LoopFollow/Helpers/Views/Glyph.swift @@ -0,0 +1,25 @@ +// LoopFollow +// Glyph.swift +// Created by Jonas Björkert. + +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/HKQuantityInputView.swift b/LoopFollow/Helpers/Views/HKQuantityInputView.swift index d1e0a614f..7417b076f 100644 --- a/LoopFollow/Helpers/Views/HKQuantityInputView.swift +++ b/LoopFollow/Helpers/Views/HKQuantityInputView.swift @@ -1,14 +1,10 @@ -// -// 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. import Foundation -import SwiftUI import HealthKit +import SwiftUI struct HKQuantityInputView: View { var label: String diff --git a/LoopFollow/Helpers/Views/LinkRow.swift b/LoopFollow/Helpers/Views/LinkRow.swift new file mode 100644 index 000000000..bbee1da27 --- /dev/null +++ b/LoopFollow/Helpers/Views/LinkRow.swift @@ -0,0 +1,18 @@ +// LoopFollow +// LinkRow.swift +// Created by Jonas Björkert. + +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/LoadingButtonView.swift b/LoopFollow/Helpers/Views/LoadingButtonView.swift index 9fa7acdf7..9b09df524 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. import SwiftUI diff --git a/LoopFollow/Helpers/Views/NavigationRow.swift b/LoopFollow/Helpers/Views/NavigationRow.swift new file mode 100644 index 000000000..1cfdf3999 --- /dev/null +++ b/LoopFollow/Helpers/Views/NavigationRow.swift @@ -0,0 +1,26 @@ +// LoopFollow +// NavigationRow.swift +// Created by Jonas Björkert. + +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/Helpers/Views/SettingsStepperRow.swift b/LoopFollow/Helpers/Views/SettingsStepperRow.swift new file mode 100644 index 000000000..84b63c2e3 --- /dev/null +++ b/LoopFollow/Helpers/Views/SettingsStepperRow.swift @@ -0,0 +1,43 @@ +// LoopFollow +// SettingsStepperRow.swift +// Created by Jonas Björkert. + +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/Helpers/Views/TogglableSecureInput.swift b/LoopFollow/Helpers/Views/TogglableSecureInput.swift new file mode 100644 index 000000000..10c37a9f9 --- /dev/null +++ b/LoopFollow/Helpers/Views/TogglableSecureInput.swift @@ -0,0 +1,67 @@ +// LoopFollow +// TogglableSecureInput.swift +// Created by Jonas Björkert. + +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/Helpers/carbBolusArrays.swift b/LoopFollow/Helpers/carbBolusArrays.swift index d8d16c597..a7146fe42 100644 --- a/LoopFollow/Helpers/carbBolusArrays.swift +++ b/LoopFollow/Helpers/carbBolusArrays.swift @@ -1,68 +1,56 @@ -// -// 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. 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 +58,7 @@ extension MainViewController { return true } } - + return false } - } diff --git a/LoopFollow/Helpers/isOnPhoneCall.swift b/LoopFollow/Helpers/isOnPhoneCall.swift new file mode 100644 index 000000000..fa8f7e36a --- /dev/null +++ b/LoopFollow/Helpers/isOnPhoneCall.swift @@ -0,0 +1,12 @@ +// LoopFollow +// isOnPhoneCall.swift +// Created by Jonas Björkert. + +import CallKit +import Foundation + +private let callObserver = CXCallObserver() + +func isOnPhoneCall() -> Bool { + return callObserver.calls.contains { !$0.hasEnded } +} 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/InfoDisplaySettings/InfoDisplaySettingsView.swift b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift index f9cea472c..23e322120 100644 --- a/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift +++ b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift @@ -1,24 +1,19 @@ -// -// 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. import SwiftUI struct InfoDisplaySettingsView: View { @ObservedObject var viewModel: InfoDisplaySettingsViewModel - @Environment(\.presentationMode) var presentationMode var body: some View { NavigationView { 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") } @@ -44,12 +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/InfoDisplaySettings/InfoDisplaySettingsViewModel.swift b/LoopFollow/InfoDisplaySettings/InfoDisplaySettingsViewModel.swift index b4f171f94..9056c11ca 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. import Foundation import SwiftUI @@ -14,18 +10,18 @@ class InfoDisplaySettingsViewModel: ObservableObject { @Published var infoVisible: [Bool] init() { - self.infoSort = UserDefaultsRepository.infoSort.value - self.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/InfoData.swift b/LoopFollow/InfoTable/InfoData.swift index d8db84c17..6d676b641 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. import Foundation diff --git a/LoopFollow/InfoTable/InfoDataSeparator.swift b/LoopFollow/InfoTable/InfoDataSeparator.swift index d34b24023..4f86a89df 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. import Foundation diff --git a/LoopFollow/InfoTable/InfoManager.swift b/LoopFollow/InfoTable/InfoManager.swift index 22d01ba0e..1343b8f8f 100644 --- a/LoopFollow/InfoTable/InfoManager.swift +++ b/LoopFollow/InfoTable/InfoManager.swift @@ -1,21 +1,17 @@ -// -// 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. 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 +53,7 @@ class InfoManager { let formattedValue = value.formattedValue() updateInfoData(type: type, value: formattedValue) } - + func clearInfoData(type: InfoType) { tableData[type.rawValue].value = "" tableView?.reloadData() @@ -71,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/InfoTable/InfoType.swift b/LoopFollow/InfoTable/InfoType.swift index 4d602896f..ae7d9d3fa 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. import Foundation @@ -45,6 +41,6 @@ enum InfoType: Int, CaseIterable { } var sortOrder: Int { - return self.rawValue + return rawValue } } diff --git a/LoopFollow/Log/LogEntry.swift b/LoopFollow/Log/LogEntry.swift index a4d459fed..d000a9276 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. import Foundation diff --git a/LoopFollow/Log/LogManager.swift b/LoopFollow/Log/LogManager.swift index bc3a4f19d..7608cd4c8 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. import Foundation @@ -63,7 +59,7 @@ class LogManager { consoleQueue.async { print(logMessage) } - + if category == .taskScheduler && isDebug { return } @@ -85,9 +81,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 +91,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 +116,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 +125,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 +137,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/LogView.swift b/LoopFollow/Log/LogView.swift index eff97efee..84b8cc305 100644 --- a/LoopFollow/Log/LogView.swift +++ b/LoopFollow/Log/LogView.swift @@ -1,16 +1,11 @@ -// -// 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. import SwiftUI struct LogView: View { @ObservedObject var viewModel = LogViewModel() - @Environment(\.presentationMode) var presentationMode var body: some View { NavigationView { @@ -38,17 +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/Log/LogViewModel.swift b/LoopFollow/Log/LogViewModel.swift index bba616634..2a556255d 100644 --- a/LoopFollow/Log/LogViewModel.swift +++ b/LoopFollow/Log/LogViewModel.swift @@ -1,13 +1,9 @@ -// -// 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. -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..e60856c93 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. import SwiftUI import UIKit @@ -19,7 +15,7 @@ struct SearchBar: UIViewRepresentable { _text = text } - func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { + func searchBar(_: UISearchBar, textDidChange searchText: String) { text = searchText } @@ -41,7 +37,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/Metric/CarbMetric.swift b/LoopFollow/Metric/CarbMetric.swift index 3a95a8821..3db9f9f65 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. import Foundation diff --git a/LoopFollow/Metric/InsulinMetric.swift b/LoopFollow/Metric/InsulinMetric.swift index a06d85787..130ff1cff 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. import Foundation diff --git a/LoopFollow/Metric/Metric.swift b/LoopFollow/Metric/Metric.swift index 64d9f09b6..09c8b08f9 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. import Foundation diff --git a/LoopFollow/Nightscout/NightscoutSettingsView.swift b/LoopFollow/Nightscout/NightscoutSettingsView.swift index 6433ccae6..eeaba3080 100644 --- a/LoopFollow/Nightscout/NightscoutSettingsView.swift +++ b/LoopFollow/Nightscout/NightscoutSettingsView.swift @@ -1,16 +1,11 @@ -// -// 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. import SwiftUI struct NightscoutSettingsView: View { @ObservedObject var viewModel: NightscoutSettingsViewModel - @Environment(\.presentationMode) var presentationMode var body: some View { NavigationView { @@ -19,18 +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/Nightscout/NightscoutSettingsViewModel.swift b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift index 28f73ade1..451494f56 100644 --- a/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift +++ b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift @@ -1,13 +1,9 @@ -// -// 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. -import Foundation import Combine +import Foundation protocol NightscoutSettingsViewModelDelegate: AnyObject { func nightscoutSettingsDidFinish() @@ -19,22 +15,24 @@ 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() } } } - @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() } } } + @Published var nightscoutStatus: String = "Checking..." private var cancellables = Set() @@ -42,8 +40,8 @@ class NightscoutSettingsViewModel: ObservableObject { private var checkStatusWorkItem: DispatchWorkItem? init() { - self.initialURL = ObservableUserDefaults.shared.url.value - self.initialToken = UserDefaultsRepository.token.value + initialURL = Storage.shared.url.value + initialToken = Storage.shared.token.value setupDebounce() checkNightscoutStatus() @@ -98,10 +96,10 @@ 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 + Storage.shared.nsWriteAuth.value = nsWriteAuth + Storage.shared.nsAdminAuth.value = nsAdminAuth self.updateStatusLabel(error: error) } @@ -128,15 +126,15 @@ 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))" - 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..dffe494d5 100644 --- a/LoopFollow/Remote/Loop/LoopNightscoutRemoteView.swift +++ b/LoopFollow/Remote/Loop/LoopNightscoutRemoteView.swift @@ -1,38 +1,35 @@ -// -// 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. 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 { 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..f184cafa8 100644 --- a/LoopFollow/Remote/Loop/LoopOverrideView.swift +++ b/LoopFollow/Remote/Loop/LoopOverrideView.swift @@ -1,20 +1,16 @@ -// -// 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. -import SwiftUI import HealthKit +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() @@ -52,7 +48,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")) { @@ -104,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) } @@ -177,6 +172,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..dd4a232ac 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. import Foundation @@ -18,7 +14,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 +41,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..d3767e0d6 100644 --- a/LoopFollow/Remote/Nightscout/TrioNightscoutRemoteView.swift +++ b/LoopFollow/Remote/Nightscout/TrioNightscoutRemoteView.swift @@ -1,20 +1,16 @@ -// -// 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. -import SwiftUI import HealthKit +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) @@ -62,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 @@ -85,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 @@ -93,7 +89,7 @@ struct TrioNightscoutRemoteView: View { } ) .focused($targetFieldIsFocused) - Text(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString).foregroundColor(.secondary) + Text(Localizer.getPreferredUnit().localizedShortUnitString).foregroundColor(.secondary) } HStack { Text("Duration") @@ -185,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() }), @@ -248,8 +244,8 @@ struct TrioNightscoutRemoteView: View { } private var isButtonDisabled: Bool { - return newHKTarget.doubleValue(for: UserDefaultsRepository.getPreferredUnit()) == 0 || - duration.doubleValue(for: HKUnit.minute()) == 0 || isLoading + return newHKTarget.doubleValue(for: Localizer.getPreferredUnit()) == 0 || + duration.doubleValue(for: HKUnit.minute()) == 0 || isLoading } private func enactTempTarget() { @@ -261,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/NoRemoteView.swift b/LoopFollow/Remote/NoRemoteView.swift index 27fbcc710..e52636e7d 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. import SwiftUI diff --git a/LoopFollow/Remote/RemoteType.swift b/LoopFollow/Remote/RemoteType.swift index c906899a3..3cfd0a11d 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. import Foundation diff --git a/LoopFollow/Remote/RemoteViewController.swift b/LoopFollow/Remote/RemoteViewController.swift index e76e12d9b..bd51d24ad 100644 --- a/LoopFollow/Remote/RemoteViewController.swift +++ b/LoopFollow/Remote/RemoteViewController.swift @@ -1,19 +1,14 @@ -// -// 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. +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? @@ -22,9 +17,9 @@ class RemoteViewController: UIViewController { cancellable = Publishers.CombineLatest( Storage.shared.remoteType.$value.removeDuplicates(), - ObservableUserDefaults.shared.device.$value.removeDuplicates() + Storage.shared.device.$value.removeDuplicates() ) - .sink { [weak self] newRemoteType, newDevice in + .sink { [weak self] _, _ in DispatchQueue.main.async { self?.updateView() } @@ -45,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": @@ -56,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'") @@ -80,17 +75,17 @@ 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 + 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/RemoteSettingsView.swift b/LoopFollow/Remote/Settings/RemoteSettingsView.swift index 0fcb71af2..881ee665e 100644 --- a/LoopFollow/Remote/Settings/RemoteSettingsView.swift +++ b/LoopFollow/Remote/Settings/RemoteSettingsView.swift @@ -1,18 +1,12 @@ -// -// 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. -import SwiftUI import HealthKit +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 @@ -26,6 +20,7 @@ struct RemoteSettingsView: View { NavigationView { Form { // MARK: - Remote Type Section (Custom Rows) + Section(header: Text("Remote Type")) { remoteTypeRow(type: .none, label: "None", isEnabled: true) @@ -43,6 +38,7 @@ struct RemoteSettingsView: View { } // MARK: - User Information Section + if viewModel.remoteType != .none { Section(header: Text("User Information")) { HStack { @@ -56,38 +52,40 @@ struct RemoteSettingsView: View { } // MARK: - Trio Remote Control Settings + if viewModel.remoteType == .trc { Section(header: Text("Trio Remote Control Settings")) { HStack { Text("Shared Secret") - TextField("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") - TextField("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) } } // MARK: - Guardrails + Section(header: Text("Guardrails")) { HStack { Text("Max Bolus") @@ -167,6 +165,7 @@ struct RemoteSettingsView: View { } // MARK: - Meal Section + Section(header: Text("Meal Settings")) { Toggle("Meal with Bolus", isOn: $viewModel.mealWithBolus) .toggleStyle(SwitchToggleStyle()) @@ -176,6 +175,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")") @@ -184,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: @@ -205,9 +197,12 @@ struct RemoteSettingsView: View { } } } + .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .navigationBarTitle("Remote Settings", displayMode: .inline) } // MARK: - Custom Row for Remote Type Selection + private func remoteTypeRow(type: RemoteType, label: String, isEnabled: Bool) -> some View { Button(action: { if isEnabled { @@ -229,6 +224,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..d3fd8054e 100644 --- a/LoopFollow/Remote/Settings/RemoteSettingsViewModel.swift +++ b/LoopFollow/Remote/Settings/RemoteSettingsViewModel.swift @@ -1,13 +1,9 @@ -// -// 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. -import Foundation import Combine +import Foundation import HealthKit class RemoteSettingsViewModel: ObservableObject { @@ -23,23 +19,23 @@ 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() 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 +64,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) @@ -89,8 +85,8 @@ class RemoteSettingsViewModel: ObservableObject { .sink { [weak self] in self?.storage.mealWithFatProtein.value = $0 } .store(in: &cancellables) - ObservableUserDefaults.shared.device.$value - .receive(on: DispatchQueue.main ) + Storage.shared.device.$value + .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..50b06a478 100644 --- a/LoopFollow/Remote/TRC/BolusView.swift +++ b/LoopFollow/Remote/TRC/BolusView.swift @@ -1,14 +1,10 @@ -// -// 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. -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..ae11c61bb 100644 --- a/LoopFollow/Remote/TRC/MealView.swift +++ b/LoopFollow/Remote/TRC/MealView.swift @@ -1,14 +1,10 @@ -// -// 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. -import SwiftUI import HealthKit import LocalAuthentication +import SwiftUI struct MealView: View { @Environment(\.presentationMode) private var presentationMode @@ -138,8 +134,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 +208,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..ae32f42f9 100644 --- a/LoopFollow/Remote/TRC/OverrideView.swift +++ b/LoopFollow/Remote/TRC/OverrideView.swift @@ -1,19 +1,15 @@ -// -// 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. -import SwiftUI import HealthKit +import SwiftUI 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 @@ -96,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/PushMessage.swift b/LoopFollow/Remote/TRC/PushMessage.swift index 1de381cce..8470a5b92 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. import Foundation diff --git a/LoopFollow/Remote/TRC/PushNotificationManager.swift b/LoopFollow/Remote/TRC/PushNotificationManager.swift index d07092add..832e86573 100644 --- a/LoopFollow/Remote/TRC/PushNotificationManager.swift +++ b/LoopFollow/Remote/TRC/PushNotificationManager.swift @@ -1,14 +1,10 @@ -// -// 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. import Foundation -import SwiftJWT import HealthKit +import SwiftJWT struct APNsJWTClaims: Claims { let iss: String @@ -26,14 +22,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 +185,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..4e32d1d5c 100644 --- a/LoopFollow/Remote/TRC/TRCCommandType.swift +++ b/LoopFollow/Remote/TRC/TRCCommandType.swift @@ -1,18 +1,14 @@ -// -// 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. 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..63c06f34c 100644 --- a/LoopFollow/Remote/TRC/TempTargetView.swift +++ b/LoopFollow/Remote/TRC/TempTargetView.swift @@ -1,19 +1,15 @@ -// -// 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. -import SwiftUI import HealthKit +import SwiftUI 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) @@ -54,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 @@ -77,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 @@ -85,7 +81,7 @@ struct TempTargetView: View { } ) .focused($targetFieldIsFocused) - Text(UserDefaultsRepository.getPreferredUnit().localizedShortUnitString).foregroundColor(.secondary) + Text(Localizer.getPreferredUnit().localizedShortUnitString).foregroundColor(.secondary) } HStack { Text("Duration") @@ -177,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() }), @@ -246,8 +242,8 @@ struct TempTargetView: View { } private var isButtonDisabled: Bool { - return newHKTarget.doubleValue(for: UserDefaultsRepository.getPreferredUnit()) == 0 || - duration.doubleValue(for: HKUnit.minute()) == 0 || isLoading + return newHKTarget.doubleValue(for: Localizer.getPreferredUnit()) == 0 || + 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..ff8dad053 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. import Foundation @@ -20,14 +16,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 +38,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..69ec05f88 100644 --- a/LoopFollow/Remote/TRC/TrioNightscoutRemoteController.swift +++ b/LoopFollow/Remote/TRC/TrioNightscoutRemoteController.swift @@ -1,16 +1,11 @@ -// -// 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. import Foundation import HealthKit class TrioNightscoutRemoteController { - func cancelExistingTarget(completion: @escaping (Bool) -> Void) { Task { let tempTargetBody: [String: Any] = [ @@ -18,7 +13,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 +35,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..934130b1a 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. import SwiftUI @@ -17,7 +13,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..dddf31336 100644 --- a/LoopFollow/Remote/TRC/TrioRemoteControlViewModel.swift +++ b/LoopFollow/Remote/TRC/TrioRemoteControlViewModel.swift @@ -1,12 +1,7 @@ -// -// 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. import Foundation -class TrioRemoteControlViewModel: ObservableObject { -} +class TrioRemoteControlViewModel: ObservableObject {} diff --git a/LoopFollow/Remote/TempTargetPreset/TempTargetPreset.swift b/LoopFollow/Remote/TempTargetPreset/TempTargetPreset.swift index 00f6c9de1..646a68e99 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. import Foundation import HealthKit diff --git a/LoopFollow/Remote/TempTargetPreset/TempTargetPresetManager.swift b/LoopFollow/Remote/TempTargetPreset/TempTargetPresetManager.swift index 89abb7e14..3fab29f91 100644 --- a/LoopFollow/Remote/TempTargetPreset/TempTargetPresetManager.swift +++ b/LoopFollow/Remote/TempTargetPreset/TempTargetPresetManager.swift @@ -1,14 +1,10 @@ -// -// 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. +import Combine import Foundation import HealthKit -import Combine class TempTargetPresetManager: ObservableObject { static let shared = TempTargetPresetManager() @@ -24,7 +20,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..8610f549f 100644 --- a/LoopFollow/Settings/AdvancedSettingsView.swift +++ b/LoopFollow/Settings/AdvancedSettingsView.swift @@ -1,16 +1,11 @@ -// -// 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. import SwiftUI struct AdvancedSettingsView: View { @ObservedObject var viewModel: AdvancedSettingsViewModel - @Environment(\.presentationMode) var presentationMode var body: some View { NavigationView { @@ -23,7 +18,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)") } } @@ -32,14 +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/AdvancedSettingsViewModel.swift b/LoopFollow/Settings/AdvancedSettingsViewModel.swift index 163263e57..f03492c9c 100644 --- a/LoopFollow/Settings/AdvancedSettingsViewModel.swift +++ b/LoopFollow/Settings/AdvancedSettingsViewModel.swift @@ -1,62 +1,66 @@ -// -// 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. 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 } } + @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 = 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/CalendarSettingsView.swift b/LoopFollow/Settings/CalendarSettingsView.swift new file mode 100644 index 000000000..cdab8e466 --- /dev/null +++ b/LoopFollow/Settings/CalendarSettingsView.swift @@ -0,0 +1,118 @@ +// LoopFollow +// CalendarSettingsView.swift +// Created by Jonas Björkert. + +import EventKit +import SwiftUI + +struct CalendarSettingsView: 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 + + @State private var calendars: [EKCalendar] = [] + @State private var accessDenied = false + + // MARK: Body + + var body: some View { + NavigationView { + 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) + } + } + } + .task { // runs once on appear + await requestCalendarAccessAndLoad() + } + } + .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .navigationBarTitle("Calendar", displayMode: .inline) + } + + // 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/Contact/Settings/ContactSettingsView.swift b/LoopFollow/Settings/ContactSettingsView.swift similarity index 90% rename from LoopFollow/Contact/Settings/ContactSettingsView.swift rename to LoopFollow/Settings/ContactSettingsView.swift index b49401002..3fb10a9f8 100644 --- a/LoopFollow/Contact/Settings/ContactSettingsView.swift +++ b/LoopFollow/Settings/ContactSettingsView.swift @@ -1,17 +1,12 @@ -// -// 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. -import SwiftUI import Contacts +import SwiftUI struct ContactSettingsView: View { @ObservedObject var viewModel: ContactSettingsViewModel - @Environment(\.presentationMode) var presentationMode @State private var showAlert: Bool = false @State private var alertTitle: String = "" @@ -81,18 +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() { @@ -102,7 +91,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/Settings/ContactSettingsViewModel.swift similarity index 80% rename from LoopFollow/Contact/Settings/ContactSettingsViewModel.swift rename to LoopFollow/Settings/ContactSettingsViewModel.swift index 7d058bc6c..a36ba9e14 100644 --- a/LoopFollow/Contact/Settings/ContactSettingsViewModel.swift +++ b/LoopFollow/Settings/ContactSettingsViewModel.swift @@ -1,13 +1,9 @@ -// -// 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. -import Foundation import Combine +import Foundation extension Bundle { var displayName: String { @@ -53,7 +49,7 @@ class ContactSettingsViewModel: ObservableObject { triggerRefresh() } } - + @Published var contactTextColor: String { didSet { Storage.shared.contactTextColor.value = contactTextColor @@ -65,11 +61,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 +78,7 @@ class ContactSettingsViewModel: ObservableObject { Storage.shared.contactBackgroundColor.$value .assign(to: &$contactBackgroundColor) - + Storage.shared.contactTextColor.$value .assign(to: &$contactTextColor) } diff --git a/LoopFollow/Dexcom/DexcomSettingsView.swift b/LoopFollow/Settings/DexcomSettingsView.swift similarity index 62% rename from LoopFollow/Dexcom/DexcomSettingsView.swift rename to LoopFollow/Settings/DexcomSettingsView.swift index 7e1ecdd8e..d5ba8d074 100644 --- a/LoopFollow/Dexcom/DexcomSettingsView.swift +++ b/LoopFollow/Settings/DexcomSettingsView.swift @@ -1,16 +1,11 @@ -// -// 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. import SwiftUI struct DexcomSettingsView: View { @ObservedObject var viewModel: DexcomSettingsViewModel - @Environment(\.presentationMode) var presentationMode var body: some View { NavigationView { @@ -31,14 +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/Settings/DexcomSettingsViewModel.swift b/LoopFollow/Settings/DexcomSettingsViewModel.swift new file mode 100644 index 000000000..ae173ce80 --- /dev/null +++ b/LoopFollow/Settings/DexcomSettingsViewModel.swift @@ -0,0 +1,34 @@ +// LoopFollow +// DexcomSettingsViewModel.swift +// Created by Jonas Björkert. + +import Combine +import Foundation + +class DexcomSettingsViewModel: ObservableObject { + @Published var userName: String = Storage.shared.shareUserName.value { + willSet { + if newValue != userName { + Storage.shared.shareUserName.value = newValue + } + } + } + + @Published var password: String = Storage.shared.sharePassword.value { + willSet { + if newValue != password { + Storage.shared.sharePassword.value = newValue + } + } + } + + @Published var server: String = Storage.shared.shareServer.value { + willSet { + if newValue != server { + Storage.shared.shareServer.value = newValue + } + } + } + + init() {} +} diff --git a/LoopFollow/Settings/GeneralSettingsView.swift b/LoopFollow/Settings/GeneralSettingsView.swift new file mode 100644 index 000000000..6713e1951 --- /dev/null +++ b/LoopFollow/Settings/GeneralSettingsView.swift @@ -0,0 +1,122 @@ +// LoopFollow +// GeneralSettingsView.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct GeneralSettingsView: View { + @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 + @ObservedObject var forcePortraitMode = Storage.shared.forcePortraitMode + @ObservedObject var persistentNotification = Storage.shared.persistentNotification + + // 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 { + NavigationView { + Form { + Section("App Settings") { + Toggle("Display App Badge", isOn: $appBadge.value) + Toggle("Persistent Notification", isOn: $persistentNotification.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) + 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") { + 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 + ) + } + } + } + } + } + } + .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .navigationBarTitle("General Settings", displayMode: .inline) + } +} diff --git a/LoopFollow/Settings/GraphSettingsView.swift b/LoopFollow/Settings/GraphSettingsView.swift new file mode 100644 index 000000000..01915f055 --- /dev/null +++ b/LoopFollow/Settings/GraphSettingsView.swift @@ -0,0 +1,144 @@ +// LoopFollow +// GraphSettingsView.swift +// Created by Jonas Björkert. + +import SwiftUI + +struct GraphSettingsView: View { + @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 { + NavigationView { + 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" } + ) + } + } + } + } + .preferredColorScheme(Storage.shared.forceDarkMode.value ? .dark : nil) + .navigationBarTitle("Graph Settings", displayMode: .inline) + } + + /// Marks the chart as needing a redraw + private func markDirty() { + Observable.shared.chartSettingsChanged.value = true + } +} diff --git a/LoopFollow/Settings/SettingsMenuView.swift b/LoopFollow/Settings/SettingsMenuView.swift new file mode 100644 index 000000000..8dab5c68b --- /dev/null +++ b/LoopFollow/Settings/SettingsMenuView.swift @@ -0,0 +1,278 @@ +// LoopFollow +// SettingsMenuView.swift +// Created by Jonas Björkert. + +import SwiftUI +import UIKit + +struct SettingsMenuView: View { + // MARK: – Call-backs + + let onNightscoutVisibilityChange: (_ enabled: Bool) -> Void + + // MARK: – Local state + + @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(path: $path) { + List { + // ───────── Data settings ───────── + dataSection + + // ───────── App settings ───────── + Section("App Settings") { + NavigationRow(title: "Background Refresh Settings", + icon: "arrow.clockwise") + { + path.append(Sheet.backgroundRefresh) + } + + NavigationRow(title: "General Settings", + icon: "gearshape") + { + path.append(Sheet.general) + } + + NavigationRow(title: "Graph Settings", + icon: "chart.xyaxis.line") + { + path.append(Sheet.graph) + } + + if IsNightscoutEnabled() { + NavigationRow(title: "Information Display Settings", + icon: "info.circle") + { + path.append(Sheet.infoDisplay) + } + + NavigationRow(title: "Remote Settings", + icon: "antenna.radiowaves.left.and.right") + { + path.append(Sheet.remote) + } + } + } + + // ───────── Alarms ───────── + Section { + NavigationRow(title: "Alarms", + icon: "bell") + { + path.append(Sheet.alarmsList) + } + + NavigationRow(title: "Alarm Settings", + icon: "bell.badge") + { + path.append(Sheet.alarmSettings) + } + } + + // ───────── Integrations ───────── + Section("Integrations") { + NavigationRow(title: "Calendar", + icon: "calendar") + { + path.append(Sheet.calendar) + } + + NavigationRow(title: "Contact", + icon: "person.circle") + { + path.append(Sheet.contact) + } + } + + // ───────── Advanced / Logs ───────── + Section("Advanced Settings") { + NavigationRow(title: "Advanced Settings", + icon: "exclamationmark.shield") + { + path.append(Sheet.advanced) + } + } + + Section("Logging") { + NavigationRow(title: "View Log", + icon: "doc.text.magnifyingglass") + { + path.append(Sheet.viewLog) + } + + ActionRow(title: "Share Logs", + icon: "square.and.arrow.up", + action: shareLogs) + } + + // ───────── Community ───────── + Section("Community") { + LinkRow(title: "LoopFollow Facebook Group", + icon: "person.2.fill", + url: URL(string: "https://www.facebook.com/groups/loopfollowlnl")!) + } + + // ───────── Build info ───────── + buildInfoSection + } + .navigationTitle("Settings") + .navigationDestination(for: Sheet.self) { $0.destination } + } + .task { await refreshVersionInfo() } + } + + // MARK: – Section builders + + @ViewBuilder + private var dataSection: some View { + Section("Data Settings") { + Picker("Units", + selection: Binding( + get: { Storage.shared.units.value }, + set: { Storage.shared.units.value = $0 } + )) { + Text("mg/dL").tag("mg/dL") + Text("mmol/L").tag("mmol/L") + } + .pickerStyle(.segmented) + + NavigationRow(title: "Nightscout Settings", + icon: "network") + { + path.append(Sheet.nightscout) + } + + NavigationRow(title: "Dexcom Settings", + icon: "sensor.tag.radiowaves.forward") + { + path.append(Sheet.dexcom) + } + } + .onAppear { + onNightscoutVisibilityChange(IsNightscoutEnabled()) + } + } + + @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: – Helpers + + private func keyValue(_ key: String, + _ value: String, + tint: Color = .secondary) -> some View + { + HStack { + Text(key) + Spacer() + Text(value).foregroundColor(tint) + } + } + + private func refreshVersionInfo() async { + let mgr = AppVersionManager() + let (latest, newer, blacklisted) = await mgr.checkForNewVersionAsync() + latestVersion = latest ?? "Unknown" + + let current = mgr.version() + versionTint = blacklisted ? .red + : newer ? .orange + : latest == current ? .green + : .secondary + } + + 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 + +private enum Sheet: Hashable, 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: Self { self } + + @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: CalendarSettingsView() + case .contact: ContactSettingsView(viewModel: .init()) + case .advanced: AdvancedSettingsView(viewModel: .init()) + case .viewLog: LogView(viewModel: .init()) + } + } +} + +// MARK: – UIKit helpers (unchanged) + +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/Snoozer/SnoozerView.swift b/LoopFollow/Snoozer/SnoozerView.swift new file mode 100644 index 000000000..ffdedbc35 --- /dev/null +++ b/LoopFollow/Snoozer/SnoozerView.swift @@ -0,0 +1,208 @@ +// LoopFollow +// SnoozerView.swift +// Created by Jonas Björkert. + +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 + @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 + 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 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(directionText.value) + .font(.system(size: 110, weight: .black)) + .minimumScaleFactor(0.5) + .foregroundColor(.white) + .frame(maxWidth: .infinity, maxHeight: 96) + + 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() + + if let alarm = vm.activeAlarm { + 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 + 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: alarm.type.snoozeRange, + step: alarm.type.snoozeStep) + .labelsHidden() + } + .padding(.horizontal, 24) + } + + Button(action: vm.snoozeTapped) { + Text(vm.snoozeUnits == 0 ? "Acknowledge" : "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 + 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 Localizer.getPreferredUnit() == .millimolesPerLiter, Localizer.removePeriodAndCommaForBadge(bgText.value) == "55" { + return "🦄" + } + + if Localizer.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 { + func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View { + clipShape(RoundedCorner(radius: radius, corners: corners)) + } +} + +private 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/SnoozerViewController.swift b/LoopFollow/Snoozer/SnoozerViewController.swift new file mode 100644 index 000000000..0e1c5a30b --- /dev/null +++ b/LoopFollow/Snoozer/SnoozerViewController.swift @@ -0,0 +1,35 @@ +// LoopFollow +// SnoozerViewController.swift +// Created by Jonas Björkert. + +import Combine +import SwiftUI +import UIKit + +class SnoozerViewController: UIViewController { + private var hostingController: UIHostingController? + + @State private var snoozeMinutes = 15 + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .black + + let snoozerView = SnoozerView() + + let hosting = UIHostingController(rootView: snoozerView) + 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) + } +} diff --git a/LoopFollow/Snoozer/SnoozerViewModel.swift b/LoopFollow/Snoozer/SnoozerViewModel.swift new file mode 100644 index 000000000..34bdccd73 --- /dev/null +++ b/LoopFollow/Snoozer/SnoozerViewModel.swift @@ -0,0 +1,38 @@ +// LoopFollow +// SnoozerViewModel.swift +// Created by Jonas Björkert. + +import Combine +import Foundation + +final class SnoozerViewModel: ObservableObject { + @Published var activeAlarm: Alarm? + @Published var snoozeUnits: Int = 5 + @Published var timeUnitLabel: String = "minutes" + + private var cancellables = Set() + + init() { + Observable.shared.currentAlarm.$value + .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 + if let a = alarm { + self?.snoozeUnits = a.snoozeDuration + self?.timeUnitLabel = a.type.snoozeTimeUnit.label + } + } + .store(in: &cancellables) + if let alarm = activeAlarm { + snoozeUnits = alarm.snoozeDuration + } + } + + func snoozeTapped() { + AlarmManager.shared.performSnooze(snoozeUnits) + } +} diff --git a/LoopFollow/Storage/ObservableUserDefaultsValue.swift b/LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift similarity index 89% rename from LoopFollow/Storage/ObservableUserDefaultsValue.swift rename to LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift index 2c4807125..e33841641 100644 --- a/LoopFollow/Storage/ObservableUserDefaultsValue.swift +++ b/LoopFollow/Storage/Framework/ObservableUserDefaultsValue.swift @@ -1,13 +1,9 @@ -// -// 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. -import Foundation import Combine +import Foundation class ObservableUserDefaultsValue: ObservableObject, UserDefaultsAnyValue { // user defaults key (UserDefaultsAnyValue protocol implementation) @@ -18,7 +14,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 +39,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 +47,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 +65,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 +87,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/ObservableValue.swift b/LoopFollow/Storage/Framework/ObservableValue.swift similarity index 64% rename from LoopFollow/Storage/ObservableValue.swift rename to LoopFollow/Storage/Framework/ObservableValue.swift index 2264e477b..080be29fe 100644 --- a/LoopFollow/Storage/ObservableValue.swift +++ b/LoopFollow/Storage/Framework/ObservableValue.swift @@ -1,13 +1,9 @@ -// -// 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. -import Foundation import Combine +import Foundation import HealthKit import SwiftUI @@ -15,7 +11,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/SecureStorageValue.swift b/LoopFollow/Storage/Framework/SecureStorageValue.swift similarity index 73% rename from LoopFollow/Storage/SecureStorageValue.swift rename to LoopFollow/Storage/Framework/SecureStorageValue.swift index aa6c07d46..f9b97479b 100644 --- a/LoopFollow/Storage/SecureStorageValue.swift +++ b/LoopFollow/Storage/Framework/SecureStorageValue.swift @@ -1,20 +1,16 @@ -// -// 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. -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 +28,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/StorageValue.swift b/LoopFollow/Storage/Framework/StorageValue.swift similarity index 73% rename from LoopFollow/Storage/StorageValue.swift rename to LoopFollow/Storage/Framework/StorageValue.swift index 3677c4cf5..a2938e078 100644 --- a/LoopFollow/Storage/StorageValue.swift +++ b/LoopFollow/Storage/Framework/StorageValue.swift @@ -1,20 +1,16 @@ -// -// 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. -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 +30,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/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 83% rename from LoopFollow/Storage/UserDefaultsValue.swift rename to LoopFollow/Storage/Framework/UserDefaultsValue.swift index 093abab6f..c552be34a 100644 --- a/LoopFollow/Storage/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. import Foundation @@ -17,20 +13,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 +32,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 +93,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/UserDefaultsValueGroups.swift b/LoopFollow/Storage/Framework/UserDefaultsValueGroups.swift similarity index 74% rename from LoopFollow/Storage/UserDefaultsValueGroups.swift rename to LoopFollow/Storage/Framework/UserDefaultsValueGroups.swift index 56bfdf9c3..f83baeda8 100644 --- a/LoopFollow/Storage/UserDefaultsValueGroups.swift +++ b/LoopFollow/Storage/Framework/UserDefaultsValueGroups.swift @@ -1,74 +1,66 @@ -// -// UserDefaultsValueGroups.swift -// nightguard -// -// Created by Florian Preknya on 1/29/19. -// Copyright © 2019 private. All rights reserved. -// +// LoopFollow +// UserDefaultsValueGroups.swift +// Created by Jon Fawcett. 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 8cc97e140..841e1763d 100644 --- a/LoopFollow/Storage/Observable.swift +++ b/LoopFollow/Storage/Observable.swift @@ -1,20 +1,38 @@ - // - // 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. import Foundation import HealthKit +import SwiftUI + +/* + Observable in memory storage + */ class Observable { static let shared = 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") + var bg = ObservableValue(default: nil) + var bgStale = ObservableValue(default: true) + var bgTextColor = ObservableValue(default: .primary) + var directionText = ObservableValue(default: "-") + var deltaText = ObservableValue(default: "+0") + + var currentAlarm = ObservableValue(default: nil) + + var debug = ObservableValue(default: false) + + 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/ObservableUserDefaults.swift b/LoopFollow/Storage/ObservableUserDefaults.swift index 7c98c6501..26e1d1f17 100644 --- a/LoopFollow/Storage/ObservableUserDefaults.swift +++ b/LoopFollow/Storage/ObservableUserDefaults.swift @@ -1,21 +1,21 @@ -// -// 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. -import Foundation import Combine +import Foundation + +/* + Legacy storage, we are moving away from this + */ 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 new file mode 100644 index 000000000..5af3eb8ac --- /dev/null +++ b/LoopFollow/Storage/Storage+Migrate.swift @@ -0,0 +1,1668 @@ +// LoopFollow +// Storage+Migrate.swift +// Created by Jonas Björkert. + +import Foundation + +extension Storage { + 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, + 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) + } + + // 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") + } + + 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) + 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] + + // ── 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) + + // 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 + } + + // ── 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() + migrateUrgentHighAlarm() + migrateFastDropAlarm() + migrateFastRiseAlarm() + migrateMissedReadingAlarm() + migrateNotLoopingAlarm() + migrateMissedBolusAlarm() + migrateSensorChangeAlarm() + migratePumpChangeAlarm() + migrateOverrideStartAlarm() + migrateOverrideEndAlarm() + migrateTempTargetStartAlarm() + migrateTempTargetEndAlarm() + migrateTemporaryBGAlarm() + migratePumpVolumeAlarm() + migrateIOBAlarm() + migrateCOBAlarm() + migrateBatteryAlarm() + migrateBatteryDropAlarm() + migrateRecBolusAlarm() + } + + // 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) + } + + // 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) + } + + // 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) + } + + // 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) - 1 // 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) + } + + // 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.threshold = 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) + } + + // 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 + } + + // 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 + } + + // ───────────────────────────────────────────────────────────────────────────── + + // 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 + } + + // ───────────────────────────────────────────────────────────────────────────── + + // 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.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.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 + } + + // 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 + } + + // 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) + } + + // ----------------------------------------------------------------------------- + + // 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) + } + + // ============================================================================= + // BATTERY-LEVEL alarm (old keys → .battery) + // ============================================================================= + private func migrateBatteryAlarm() { + guard UserDefaultsValue(key: "alertBatteryActive", + default: false).exists else { return } + + 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/Storage.swift b/LoopFollow/Storage/Storage.swift index ba760ef53..3d0a814b5 100644 --- a/LoopFollow/Storage/Storage.swift +++ b/LoopFollow/Storage/Storage.swift @@ -1,17 +1,19 @@ -// -// 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. import Foundation import HealthKit +import UIKit + +/* + Observable persistant storage + */ 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: "") @@ -42,10 +44,122 @@ 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) - static let shared = Storage() + var alarms = StorageValue<[Alarm]>(key: "alarms", defaultValue: []) + var alarmConfiguration = StorageValue(key: "alarmConfiguration", defaultValue: .default) + + 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) + var lastRecBolusNotified = StorageValue(key: "lastRecBolusNotified", defaultValue: nil) + 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 forcePortraitMode = StorageValue(key: "forcePortraitMode", 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] + + // 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] + + // 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] + + // 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) - private init() { } + // 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 }) + + 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) + + var persistentNotification = StorageValue(key: "persistentNotification", defaultValue: false) + var persistentNotificationLastBGTime = StorageValue(key: "persistentNotificationLastBGTime", defaultValue: .distantPast) + + static let shared = Storage() + private init() {} } diff --git a/LoopFollow/Storage/UserDefaults.swift b/LoopFollow/Storage/UserDefaults.swift deleted file mode 100644 index a7e306957..000000000 --- a/LoopFollow/Storage/UserDefaults.swift +++ /dev/null @@ -1,551 +0,0 @@ -// -// UserDefaults.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/4/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// -// -// -// -// - -import Foundation -import UIKit -import HealthKit - -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 = 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) - 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) - - - // 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) - 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 downloadDays = UserDefaultsValue(key: "downloadDays", default: 1) - - - // 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) - 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) - 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) - 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) - 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) - 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) - 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) - 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) - 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) - 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) - 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 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") - 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) - 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") - 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) - - //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 8fe64b1c9..2aa6cd382 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. import Foundation @@ -19,17 +15,90 @@ extension MainViewController { func alarmTaskAction() { DispatchQueue.main.async { - if self.bgData.count > 0 { - self.checkAlarms(bgs: self.bgData) - } - if self.overrideGraphData.count > 0 { - self.checkOverrideAlarms() - } - if self.tempTargetGraphData.count > 0 { - self.checkTempTargetAlarms() + 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 = Observable.shared.deviceRecBolus.value + let COB = self.latestCOB?.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 + let recentCarbs: [CarbSample] = self.carbData.map { CarbSample(grams: $0.value, date: Date(timeIntervalSince1970: $0.date)) } + + let alarmData = AlarmData( + bgReadings: self.bgData + .suffix(24) + .map { GlucoseValue(sgv: $0.sgv, date: Date(timeIntervalSince1970: $0.date)) }, /// These are oldest .. newest + 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, + lastLoopTime: Observable.shared.alertLastLoopTime.value, + latestOverrideStart: latestOverrideStart, + latestOverrideEnd: latestOverrideEnd, + latestTempTargetStart: latestTempTargetStart, + latestTempTargetEnd: latestTempTargetEnd, + recBolus: recBolus, + COB: COB, + sageInsertTime: sensorInsertedAt, + pumpInsertTime: pumpInsertTime, + latestPumpVolume: latestPumpVol, + IOB: self.latestIOB?.value, + recentBoluses: bolusEntries, + latestBattery: latestBattery, + batteryHistory: self.deviceBatteryData, + recentCarbs: recentCarbs + ) + + let finalAlarmData: AlarmData + if Observable.shared.debug.value { + self.saveLatestAlarmDataToFile(alarmData) + finalAlarmData = self.loadTestAlarmData() ?? alarmData + } else { + finalAlarmData = alarmData } - TaskScheduler.shared.rescheduleTask(id: .alarmCheck, to: Date().addingTimeInterval(30)) + 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) + + 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/BGTask.swift b/LoopFollow/Task/BGTask.swift index 526a8c943..231310cfb 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. import Foundation @@ -25,20 +21,20 @@ 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 != "" { - self.webLoadDexShare() + webLoadDexShare() } else { - self.webLoadNSBGData() + webLoadNSBGData() } } } diff --git a/LoopFollow/Task/CalendarTask.swift b/LoopFollow/Task/CalendarTask.swift index 9a21ecd8c..0d097f530 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. import Foundation @@ -18,10 +14,10 @@ extension MainViewController { } func calendarTaskAction() { - if UserDefaultsRepository.writeCalendarEvent.value, - !UserDefaultsRepository.calendarIdentifier.value.isEmpty + if Storage.shared.writeCalendarEvent.value, + !Storage.shared.calendarIdentifier.value.isEmpty { - self.writeCalendar() + writeCalendar() } TaskScheduler.shared.rescheduleTask(id: .calendarWrite, to: Date().addingTimeInterval(30)) diff --git a/LoopFollow/Task/DeviceStatusTask.swift b/LoopFollow/Task/DeviceStatusTask.swift index e023b0b9d..010db7f7b 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. import Foundation diff --git a/LoopFollow/Task/MinAgoTask.swift b/LoopFollow/Task/MinAgoTask.swift index 31c8b5653..c44dc33ac 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. import Foundation import UIKit @@ -23,12 +19,8 @@ extension MainViewController { DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.MinAgoText.text = "" - self.latestMinAgoString = "" - if let snoozer = self.tabBarController?.viewControllers?[2] as? SnoozeViewController { - snoozer.MinAgoLabel.text = "" - snoozer.BGLabel.text = "" - snoozer.BGLabel.attributedText = NSAttributedString(string: "") - } + Observable.shared.minAgoText.value = "" + Observable.shared.bgText.value = "" } TaskScheduler.shared.rescheduleTask(id: .minAgoUpdate, to: Date().addingTimeInterval(1)) return @@ -54,34 +46,37 @@ 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 + } + } - if let snoozer = self.tabBarController?.viewControllers?[2] as? SnoozeViewController { - snoozer.MinAgoLabel.text = minAgoDisplayText + let deltaTime = secondsAgo / 60 + Observable.shared.bgStale.value = deltaTime >= 12 - 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 - } - } + // 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 { // 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..52cf4803c 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. import Foundation @@ -24,7 +20,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..833e7d89f 100644 --- a/LoopFollow/Task/Task.swift +++ b/LoopFollow/Task/Task.swift @@ -1,15 +1,10 @@ -// -// 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. import Foundation extension MainViewController { - func scheduleAllTasks() { scheduleBGTask() scheduleProfileTask() diff --git a/LoopFollow/Task/TaskScheduler.swift b/LoopFollow/Task/TaskScheduler.swift index ebcc689b1..2425b991a 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. import Foundation import UIKit @@ -47,8 +43,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 +75,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 } @@ -101,15 +97,18 @@ class TaskScheduler { continue } + // 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 1 second later to retry after data updates. if taskID == .alarmCheck { let shouldSkip = tasksToSkipAlarmCheck.contains { guard let checkTask = tasks[$0] else { return false } return checkTask.nextRun <= now || checkTask.nextRun == .distantFuture } if shouldSkip { - guard var existingTask = self.tasks[taskID] else { continue } - existingTask.nextRun = Date().addingTimeInterval(5) - self.tasks[taskID] = existingTask + guard var existingTask = tasks[taskID] else { continue } + existingTask.nextRun = Date().addingTimeInterval(1) + tasks[taskID] = existingTask continue } } @@ -118,7 +117,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/Task/TreatmentsTask.swift b/LoopFollow/Task/TreatmentsTask.swift index a886bdabc..19d10f6a6 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. import Foundation @@ -19,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 } @@ -27,5 +23,6 @@ extension MainViewController { WebLoadNSTreatments() TaskScheduler.shared.rescheduleTask(id: .treatments, to: Date().addingTimeInterval(2 * 60)) + TaskScheduler.shared.rescheduleTask(id: .alarmCheck, to: Date()) } } 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/AppStateViewController.swift b/LoopFollow/ViewControllers/AppStateViewController.swift index 199bbdce5..5ff41be9a 100644 --- a/LoopFollow/ViewControllers/AppStateViewController.swift +++ b/LoopFollow/ViewControllers/AppStateViewController.swift @@ -1,13 +1,9 @@ -// -// 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. import Foundation class AppStateViewController { - var appStateController: AppStateController? + var appStateController: AppStateController? } 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() - - } - - - - -} diff --git a/LoopFollow/ViewControllers/GeneralSettingsViewController.swift b/LoopFollow/ViewControllers/GeneralSettingsViewController.swift deleted file mode 100644 index e6ca65429..000000000 --- a/LoopFollow/ViewControllers/GeneralSettingsViewController.swift +++ /dev/null @@ -1,350 +0,0 @@ -// -// GeneralSetingsViewController.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 - -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 { - 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() - } - - 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 - 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 = self.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 deleted file mode 100644 index 25942ccc0..000000000 --- a/LoopFollow/ViewControllers/GraphSettingsViewController.swift +++ /dev/null @@ -1,308 +0,0 @@ -// -// GraphSettingsViewController.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 - -class GraphSettingsViewController: FormViewController { - - var appStateController: AppStateController? - - override func viewDidLoad() { - super.viewDidLoad() - if UserDefaultsRepository.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"], {form in - return isHidden - }) - row1.evaluateHidden() - } - if let row2 = form.rowBy(tag: "smallGraphTreatments") as? SwitchRow { - row2.hidden = .function(["hide"], {form in - return isHidden - }) - row2.evaluateHidden() - } - if let row3 = form.rowBy(tag: "minBasalScale") as? StepperRow { - row3.hidden = .function(["hide"], {form in - return isHidden - }) - row3.evaluateHidden() - } - - if let row4 = form.rowBy(tag: "showValues") as? SwitchRow { - row4.hidden = .function(["hide"], {form in - return isHidden - }) - row4.evaluateHidden() - } - if let row5 = form.rowBy(tag: "showAbsorption") as? SwitchRow { - row5.hidden = .function(["hide"], {form in - return 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 - 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 - 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 - appState.chartSettingsChanges |= ChartSettingsChangeEnum.showDIALinesChanged.rawValue - } - } - <<< 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 - appState.chartSettingsChanges |= ChartSettingsChangeEnum.show30MinLineChanged.rawValue - } - } - <<< 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 - appState.chartSettingsChanges |= ChartSettingsChangeEnum.show90MinLineChanged.rawValue - } - } - <<< 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 - appState.chartSettingsChanges |= ChartSettingsChangeEnum.smallGraphHeight.rawValue - } - } - <<< 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 - 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)) - } - }.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 - 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 - appState.chartSettingsChanges |= ChartSettingsChangeEnum.showMidnightLinesChanged.rawValue - } - } - - - +++ ButtonRow() { - $0.title = "DONE" - }.onCellSelection { (row, arg) in - self.dismiss(animated:true, completion: nil) - } - } -} diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index 86ed34d9f..79b202f46 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -1,83 +1,58 @@ -// -// FirstViewController.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/1/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// MainViewController.swift +// Created by Jon Fawcett. -import UIKit +import AVFAudio import Charts +import Combine +import CoreBluetooth import EventKit import ShareClient +import SwiftUI +import UIKit import UserNotifications -import AVFAudio -import CoreBluetooth func IsNightscoutEnabled() -> Bool { - return !ObservableUserDefaults.shared.url.value.isEmpty + return !Storage.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! let speechSynthesizer = AVSpeechSynthesizer() - 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 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() - var checkAlarmInterval: TimeInterval = 60.0 + var graphNowTimer = Timer() var lastCalendarWriteAttemptTime: TimeInterval = 0 @@ -103,11 +78,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele var noteGraphData: [DataStructs.noteStruct] = [] var chartData = LineChartData() var deviceBatteryData: [DataStructs.batteryStruct] = [] - var newBGPulled = false var lastCalDate: Double = 0 - var latestDirectionString = "" - var latestMinAgoString = "" - var latestDeltaString = "" var latestLoopStatusString = "" var latestCOB: CarbMetric? var latestBasal = "" @@ -115,9 +86,9 @@ 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 + + var topBG: Double = Storage.shared.minBGScale.value + var topPredictionBG: Double = Storage.shared.minBGScale.value var lastOverrideAlarm: TimeInterval = 0 @@ -127,76 +98,63 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele // share var bgDataShare: [ShareGlucoseData] = [] - var dexShare: ShareClient?; - + var dexShare: ShareClient? + // 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? var autoScrollPauseUntil: Date? = nil - + var IsNotLooping = false let contactImageUpdater = ContactImageUpdater() + private var cancellables = Set() + override func viewDidLoad() { super.viewDidLoad() - if ObservableUserDefaults.shared.device.value != "Trio" && Storage.shared.remoteType.value == .trc { - Storage.shared.remoteType.value = .none - } - - //Migration of UserDefaultsRepository -> Storage handling - if !UserDefaultsRepository.backgroundRefresh.value { - Storage.shared.backgroundRefreshType.value = .none - UserDefaultsRepository.backgroundRefresh.value = true - } + loadDebugData() - // Ensure alertNotLooping has a minimum value of 16. - if UserDefaultsRepository.alertNotLooping.value < 16 { - UserDefaultsRepository.alertNotLooping.value = 16 + if Storage.shared.migrationStep.value < 1 { + Storage.shared.migrateStep1() + Storage.shared.migrationStep.value = 1 } // Synchronize info types to ensure arrays are the correct size - UserDefaultsRepository.synchronizeInfoTypes() + synchronizeInfoTypes() infoTable.rowHeight = 21 infoTable.dataSource = self infoTable.tableFooterView = UIView(frame: .zero) infoTable.bounces = false infoTable.addBorder(toSide: .Left, withColor: UIColor.darkGray.cgColor, andThickness: 2) - - self.infoManager = InfoManager(tableView: infoTable) - smallGraphHeightConstraint.constant = CGFloat(UserDefaultsRepository.smallGraphHeight.value) - self.view.layoutIfNeeded() + infoManager = InfoManager(tableView: infoTable) + + smallGraphHeightConstraint.constant = CGFloat(Storage.shared.smallGraphHeight.value) + view.layoutIfNeeded() + + 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) - 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 ) - // 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 - self.tabBarController?.overrideUserInterfaceStyle = .dark + tabBarController?.overrideUserInterfaceStyle = .dark } - // Load the snoozer tab - 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) @@ -207,10 +165,10 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele createGraph() createSmallBGGraph() } - + // setup display for NS vs Dex showHideNSDetails() - + scheduleAllTasks() // Set up refreshScrollView for BGText @@ -218,29 +176,123 @@ 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) + + 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) + + Observable.shared.deltaText.$value + .receive(on: DispatchQueue.main) + .sink { [weak self] newValue in + 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) + + 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 { 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") @@ -269,7 +321,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } MinAgoText.text = "Refreshing" - latestMinAgoString = "Refreshing" + Observable.shared.minAgoText.value = "Refreshing" scheduleAllTasks() currentCage = nil @@ -278,7 +330,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele lastSpeechTime = nil refreshControl.endRefreshing() } - + // Scroll down BGText when refreshing func scrollViewDidScroll(_ scrollView: UIScrollView) { if scrollView == refreshScrollView { @@ -290,77 +342,22 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } } } - - override func viewWillAppear(_ animated: Bool) { - // set screen lock - UIApplication.shared.isIdleTimerDisabled = UserDefaultsRepository.screenlockSwitchState.value; - - // check the app state - // TODO: move to a function ? - if let appState = self.appStateController { - - if appState.chartSettingsChanged { - - // can look at settings flags to be more fine tuned - self.updateBGGraphSettings() - - if ChartSettingsChangeEnum.smallGraphHeight.rawValue != 0 { - smallGraphHeightConstraint.constant = CGFloat(UserDefaultsRepository.smallGraphHeight.value) - self.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 { - - } - - // settings for textcolor changed - if appState.generalSettingsChanges & GeneralSettingsChangeEnum.colorBGTextChange.rawValue != 0 { - self.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() - } - - // reset the app state - appState.generalSettingsChanged = false - appState.generalSettingsChanges = 0 - } - if appState.infoDataSettingsChanged { - self.infoTable.reloadData() - - // reset - appState.infoDataSettingsChanged = false - } - - // add more processing of the app state + + override func viewWillAppear(_: Bool) { + UIApplication.shared.isIdleTimerDisabled = Storage.shared.screenlockSwitchState.value + + if Observable.shared.chartSettingsChanged.value { + updateBGGraphSettings() + + smallGraphHeightConstraint.constant = CGFloat(Storage.shared.smallGraphHeight.value) + view.layoutIfNeeded() + + Observable.shared.chartSettingsChanged.value = false } } // Info Table Functions - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { guard let infoManager = infoManager else { return 0 } @@ -383,11 +380,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() } @@ -396,48 +393,48 @@ 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 = Storage.shared.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 + 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 - if now.timeIntervalSince(lastVersionUpdateShown) > 1209600 { // 2 weeks + 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 } } } } - + func versionAlert(title: String = "Update Available", message: String) { DispatchQueue.main.async { let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) @@ -445,21 +442,21 @@ 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 + 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 } } } - + 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) @@ -467,18 +464,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 @@ -486,31 +483,29 @@ 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 { + + if Storage.shared.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 { + if Storage.shared.appBadge.value { let latestBG = String(val) UIApplication.shared.applicationIconBadgeNumber = Int(Localizer.removePeriodAndCommaForBadge(Localizer.toDisplayUnits(latestBG))) ?? val } else { @@ -520,34 +515,35 @@ 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 { + var color = NSUIColor.label + if Storage.shared.colorBGText.value { + if Double(latestBG) >= Storage.shared.highLine.value { color = NSUIColor.systemYellow - } else if Float(latestBG) <= UserDefaultsRepository.lowLine.value { + Observable.shared.bgTextColor.value = .yellow + } else if Double(latestBG) <= Storage.shared.lowLine.value { color = NSUIColor.systemRed + Observable.shared.bgTextColor.value = .red } else { color = NSUIColor.systemGreen + Observable.shared.bgTextColor.value = .green } + } else { + Observable.shared.bgTextColor.value = .primary } - + 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":"-", "": "-"] + + 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 @@ -557,9 +553,9 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } func processCalendarUpdates() { - if UserDefaultsRepository.calendarIdentifier.value == "" { return } + if Storage.shared.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() @@ -573,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 ?? "") + + let 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(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 @@ -612,83 +606,52 @@ 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) + 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 = 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: Storage.shared.calendarIdentifier.value) as? EKCalendar else { return } + 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 { - 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) + let 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: Storage.shared.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 } catch { - LogManager.shared.log(category: .calendar, message: "Error storing to the calendar") - } - } - - - func persistentNotification(bgTime: TimeInterval) - { - if UserDefaultsRepository.persistentNotification.value && bgTime > UserDefaultsRepository.persistentNotificationLastBGTime.value && bgData.count > 0 { - 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") + let msg = "Error storing to calendar: \(error.localizedDescription) (\(error))" + LogManager.shared.log(category: .calendar, message: msg) } } - // 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 - 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 @@ -698,6 +661,78 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } func calculateMaxBgGraphValue() -> Float { - return max(topBG, topPredictionBG) + return max(Float(topBG), Float(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 { + Storage.shared.url.value = url + } + + if let token = debugData.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 5fa6348fc..f4853af1a 100644 --- a/LoopFollow/ViewControllers/NightScoutViewController.swift +++ b/LoopFollow/ViewControllers/NightScoutViewController.swift @@ -1,123 +1,109 @@ -// -// SecondViewController.swift -// LoopFollow -// -// Created by Jon Fawcett on 6/1/20. -// Copyright © 2020 Jon Fawcett. All rights reserved. -// +// LoopFollow +// NightScoutViewController.swift +// Created by Jon Fawcett. 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 { + if Storage.shared.forceDarkMode.value { overrideUserInterfaceStyle = .dark } - - var url = ObservableUserDefaults.shared.url.value - let token = UserDefaultsRepository.token.value - + + var url = Storage.shared.url.value + let token = Storage.shared.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 de97da484..aaffff033 100644 --- a/LoopFollow/ViewControllers/SettingsViewController.swift +++ b/LoopFollow/ViewControllers/SettingsViewController.swift @@ -1,388 +1,41 @@ -// -// 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. -import UIKit -import Eureka -import EventKit -import EventKitUI import SwiftUI +import UIKit -class SettingsViewController: FormViewController, NightscoutSettingsViewModelDelegate { - var tokenRow: TextRow? - var appStateController: AppStateController? - var statusLabelRow: LabelRow! - - 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"], {form in - return isHidden - }) - row1.evaluateHidden() - } +final class SettingsViewController: UIViewController { + // MARK: Stored properties - if IsNightscoutEnabled() { - isEnabled = true - } + private var host: UIHostingController! - guard let nightscoutTab = self.tabBarController?.tabBar.items![3] else { return } - nightscoutTab.isEnabled = isEnabled - } + // MARK: Life-cycle override func viewDidLoad() { super.viewDidLoad() - if UserDefaultsRepository.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: { - 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("alarmsSettings") { - $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("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) - - } - <<< 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, row 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 UserDefaultsRepository.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 UserDefaultsRepository.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 UserDefaultsRepository.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 UserDefaultsRepository.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 UserDefaultsRepository.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 UserDefaultsRepository.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 UserDefaultsRepository.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 UserDefaultsRepository.forceDarkMode.value { - hostingController.overrideUserInterfaceStyle = .dark - } - - present(hostingController, animated: true, completion: nil) - } - - private func shareLogs() { - let logFilesToShare = LogManager.shared.logFilesForTodayAndYesterday() + // Build SwiftUI menu + host = UIHostingController( + rootView: SettingsMenuView { [weak self] nightscoutEnabled in + self?.tabBarController?.tabBar.items?[3].isEnabled = nightscoutEnabled + }) - if !logFilesToShare.isEmpty { - let activityViewController = UIActivityViewController(activityItems: logFilesToShare, applicationActivities: nil) - activityViewController.popoverPresentationController?.sourceView = self.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) - } + // Dark-mode override + if Storage.shared.forceDarkMode.value { + host.overrideUserInterfaceStyle = .dark + } + + // 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) } } diff --git a/LoopFollow/ViewControllers/SnoozeViewController.swift b/LoopFollow/ViewControllers/SnoozeViewController.swift deleted file mode 100644 index ebe48c819..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 - - -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/LoopFollow/ViewControllers/WatchSettingsViewController.swift b/LoopFollow/ViewControllers/WatchSettingsViewController.swift deleted file mode 100644 index 68f920116..000000000 --- a/LoopFollow/ViewControllers/WatchSettingsViewController.swift +++ /dev/null @@ -1,161 +0,0 @@ -// -// WatchSettingsViewController.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 - -class WatchSettingsViewController: FormViewController { - - var appStateController: AppStateController? - - override func viewDidLoad() { - super.viewDidLoad() - if UserDefaultsRepository.forceDarkMode.value { - overrideUserInterfaceStyle = .dark - } - - let eventStore = EKEventStore() - eventStore.requestCalendarAccess { [weak self] (granted, error) 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..("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 { (row, arg) in - self.dismiss(animated:true, completion: nil) - } - } - -} 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) diff --git a/Scripts/swiftformat.sh b/Scripts/swiftformat.sh new file mode 100755 index 000000000..16c5f5e07 --- /dev/null +++ b/Scripts/swiftformat.sh @@ -0,0 +1,16 @@ +#! /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}" \ +--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 new file mode 100644 index 000000000..e1ad2b6f5 --- /dev/null +++ b/Tests/AlarmConditions/BatteryConditionTests.swift @@ -0,0 +1,38 @@ +// LoopFollow +// BatteryConditionTests.swift +// Created by Jonas Björkert. + +@testable import LoopFollow +import Testing + +struct BatteryConditionTests { + let cond = BatteryCondition() + + @Test("#fires when battery ≤ threshold") + func firesBelowOrEqual() { + let alarm = Alarm.battery(threshold: 20) + let data = AlarmData.withBattery(20) + #expect(cond.evaluate(alarm: alarm, data: data, now: .init())) + } + + @Test("#does NOT fire when battery > 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..59fe5fecb --- /dev/null +++ b/Tests/AlarmConditions/Helpers.swift @@ -0,0 +1,48 @@ +// LoopFollow +// Helpers.swift +// Created by Jonas Björkert. + +// 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..8d8c4f458 --- /dev/null +++ b/Tests/Tests.swift @@ -0,0 +1,11 @@ +// LoopFollow +// Tests.swift +// Created by Jonas Björkert. + +import Testing + +struct Tests { + @Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + } +}