diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index b162633e0..fd434483a 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -7,10 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - B500000000000000000000A2 /* RemoteBolusHistoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000A1 /* RemoteBolusHistoryEntry.swift */; }; - B500000000000000000000A4 /* QuickPickBolusesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000A3 /* QuickPickBolusesManager.swift */; }; - B500000000000000000000B2 /* RemoteMealHistoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000B1 /* RemoteMealHistoryEntry.swift */; }; - B500000000000000000000B4 /* QuickPickMealsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000B3 /* QuickPickMealsManager.swift */; }; 2D8068C66833EEAED7B4BEB8 /* FutureCarbsCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EBAB9EECE7095238A558060 /* FutureCarbsCondition.swift */; }; 374A77992F5BD8B200E96858 /* APNSClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374A77982F5BD8AB00E96858 /* APNSClient.swift */; }; 374A77A52F5BE17000E96858 /* AppGroupID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374A779F2F5BE17000E96858 /* AppGroupID.swift */; }; @@ -69,7 +65,23 @@ 65E153C32E4BB69100693A4F /* URLTokenValidationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */; }; 65E8A2862E44B0300065037B /* VolumeButtonHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E8A2852E44B0300065037B /* VolumeButtonHandler.swift */; }; 66E3D12E66AA4534A144A54B /* BackgroundRefreshManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8CA8BE0B3D247408FE088B4 /* BackgroundRefreshManager.swift */; }; + 87F526CE28FD073330D2F516 /* BannerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 098BA6FC4279B166734A0E13 /* BannerManager.swift */; }; + 8897B18D53EA2F1963B8B4E0 /* AppBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8000BC181BCE5AA8553FB3B6 /* AppBannerView.swift */; }; + 9C7FB98C98BE4FF98F4815EE /* Telemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFBE69CEF18416D84959974 /* Telemetry.swift */; }; + A1A1A10001000000A0CFEED1 /* APNsCredentialValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1A1A10001000000A0CFEED2 /* APNsCredentialValidator.swift */; }; + A1A1A10002000000A0CFEED1 /* LogRedactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1A1A10002000000A0CFEED2 /* LogRedactor.swift */; }; + A1B2C3D4E5F6A7B8C9D0E1F3 /* StatsDisplayModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F6A7B8C9D0E1F2 /* StatsDisplayModel.swift */; }; + A1B2C3D4E5F6A7B8C9D0E1F5 /* StatsDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F6A7B8C9D0E1F4 /* StatsDisplayView.swift */; }; + AA1B2C3D4E5F6A7B8C9D0E2F /* LoopFollowApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA1B2C3D4E5F6A7B8C9D0E1F /* LoopFollowApp.swift */; }; + AB1CD0012C7B30D40048F05C /* RemoteDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1CD0022C7B30D40048F05C /* RemoteDiagnostics.swift */; }; ACE7F6DE0D065BEB52CDC0DB /* FutureCarbsAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7D2A4EFD18B7B7748B6669E /* FutureCarbsAlarmEditor.swift */; }; + B500000000000000000000A2 /* RemoteBolusHistoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000A1 /* RemoteBolusHistoryEntry.swift */; }; + B500000000000000000000A4 /* QuickPickBolusesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000A3 /* QuickPickBolusesManager.swift */; }; + B500000000000000000000B2 /* RemoteMealHistoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000B1 /* RemoteMealHistoryEntry.swift */; }; + B500000000000000000000B4 /* QuickPickMealsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000B3 /* QuickPickMealsManager.swift */; }; + B500000000000000000000C2 /* QuickPickSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000C1 /* QuickPickSectionHeader.swift */; }; + BB2C3D4E5F6A7B8C9D0E2F2A /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB2C3D4E5F6A7B8C9D0E1F2A /* MainTabView.swift */; }; + CC3D4E5F6A7B8C9D0E2F2A3B /* MoreMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3D4E5F6A7B8C9D0E1F2A3B /* MoreMenuView.swift */; }; DD0247592DB2E89600FCADF6 /* AlarmCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */; }; DD0247712DB4337700FCADF6 /* BuildExpireCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */; }; DD026E592EA2C8A200A39CB5 /* InsulinPrecisionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD026E582EA2C8A200A39CB5 /* InsulinPrecisionManager.swift */; }; @@ -93,9 +105,6 @@ DD0C0C682C48529400DBADDF /* Metric.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C672C48529400DBADDF /* Metric.swift */; }; DD0C0C6B2C48562000DBADDF /* InsulinMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C6A2C48562000DBADDF /* InsulinMetric.swift */; }; DD0C0C6D2C48606200DBADDF /* CarbMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C6C2C48606200DBADDF /* CarbMetric.swift */; }; - DD4E5F6A7B8C9D0E2F2A3B4C /* RemoteContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4E5F6A7B8C9D0E1F2A3B4C /* RemoteContentView.swift */; }; - AA1B2C3D4E5F6A7B8C9D0E2F /* LoopFollowApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA1B2C3D4E5F6A7B8C9D0E1F /* LoopFollowApp.swift */; }; - BB2C3D4E5F6A7B8C9D0E2F2A /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB2C3D4E5F6A7B8C9D0E1F2A /* MainTabView.swift */; }; DD12D4872E1705E6004E0112 /* AlarmsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD12D4862E1705E6004E0112 /* AlarmsContainerView.swift */; }; DD13BC752C3FD6210062313B /* InfoType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13BC742C3FD6200062313B /* InfoType.swift */; }; DD13BC772C3FD64E0062313B /* InfoData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13BC762C3FD64E0062313B /* InfoData.swift */; }; @@ -104,13 +113,7 @@ DD16AF0D2C98485400FB655A /* SecureStorageValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD16AF0C2C98485400FB655A /* SecureStorageValue.swift */; }; DD16AF0F2C99592F00FB655A /* HKQuantityInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD16AF0E2C99592F00FB655A /* HKQuantityInputView.swift */; }; DD16AF112C997B4600FB655A /* LoadingButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD16AF102C997B4600FB655A /* LoadingButtonView.swift */; }; - B500000000000000000000C2 /* QuickPickSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B500000000000000000000C1 /* QuickPickSectionHeader.swift */; }; DD1D52B92E1EB5DC00432050 /* TabPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1D52B82E1EB5DC00432050 /* TabPosition.swift */; }; - CC3D4E5F6A7B8C9D0E2F2A3B /* MoreMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC3D4E5F6A7B8C9D0E1F2A3B /* MoreMenuView.swift */; }; - DD7A3B5D2F1E8D9A00B4C6E1 /* BGDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7A3B5C2F1E8D9A00B4C6E1 /* BGDisplayView.swift */; }; - DD7A3B5F2F1E8DA000B4C6E1 /* LineChartWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7A3B5E2F1E8DA000B4C6E1 /* LineChartWrapper.swift */; }; - DD7A3B612F1E8DA600B4C6E1 /* MainHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7A3B602F1E8DA600B4C6E1 /* MainHomeView.swift */; }; - EE5F6A7B8C9D0E2F2A3B4C5D /* NightscoutContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE5F6A7B8C9D0E1F2A3B4C5D /* NightscoutContentView.swift */; }; DD1D52C02E4C100000000001 /* AppearanceMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1D52BF2E4C100000000001 /* AppearanceMode.swift */; }; DD1D52C22E4C100000000002 /* PredictionDisplayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1D52C12E4C100000000002 /* PredictionDisplayType.swift */; }; DD2C2E4F2D3B8AF1006413A5 /* NightscoutSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2C2E4E2D3B8AEC006413A5 /* NightscoutSettingsView.swift */; }; @@ -120,7 +123,6 @@ DD4878052C7B2C970048F05C /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4878042C7B2C970048F05C /* Storage.swift */; }; DD4878082C7B30BF0048F05C /* RemoteSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4878072C7B30BF0048F05C /* RemoteSettingsView.swift */; }; DD48780A2C7B30D40048F05C /* RemoteSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4878092C7B30D40048F05C /* RemoteSettingsViewModel.swift */; }; - AB1CD0012C7B30D40048F05C /* RemoteDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB1CD0022C7B30D40048F05C /* RemoteDiagnostics.swift */; }; DD48780E2C7B74A40048F05C /* TrioRemoteControlViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD48780D2C7B74A40048F05C /* TrioRemoteControlViewModel.swift */; }; DD4878102C7B74BF0048F05C /* TrioRemoteControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD48780F2C7B74BF0048F05C /* TrioRemoteControlView.swift */; }; DD4878132C7B750D0048F05C /* TempTargetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4878122C7B750D0048F05C /* TempTargetView.swift */; }; @@ -146,6 +148,10 @@ 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 */; }; + DD4E5F6A7B8C9D0E2F2A3B4C /* RemoteContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4E5F6A7B8C9D0E1F2A3B4C /* RemoteContentView.swift */; }; + DD50C10A2F60A00000000001 /* SocketIO in Frameworks */ = {isa = PBXBuildFile; productRef = DD50C10A2F60A00000000003 /* SocketIO */; }; + DD50C10A2F60B00000000002 /* NightscoutSocketManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD50C10A2F60B00000000001 /* NightscoutSocketManager.swift */; }; + DD50C10A2F60B00000000004 /* NightscoutSocketDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD50C10A2F60B00000000003 /* NightscoutSocketDataHandler.swift */; }; DD50C7552D0862770057AE6F /* ContactImageUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD50C7542D0862770057AE6F /* ContactImageUpdater.swift */; }; DD5334212C60EBEE00062F9D /* InsulinCartridgeChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5334202C60EBEE00062F9D /* InsulinCartridgeChange.swift */; }; DD5334232C60ED3600062F9D /* IAge.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5334222C60ED3600062F9D /* IAge.swift */; }; @@ -161,6 +167,9 @@ DD608A0A2C23593900F91132 /* SMB.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD608A092C23593900F91132 /* SMB.swift */; }; DD608A0C2C27415C00F91132 /* BackgroundAlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD608A0B2C27415C00F91132 /* BackgroundAlertManager.swift */; }; DD6A935E2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6A935D2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift */; }; + DD7A3B5D2F1E8D9A00B4C6E1 /* BGDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7A3B5C2F1E8D9A00B4C6E1 /* BGDisplayView.swift */; }; + DD7A3B5F2F1E8DA000B4C6E1 /* LineChartWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7A3B5E2F1E8DA000B4C6E1 /* LineChartWrapper.swift */; }; + DD7A3B612F1E8DA600B4C6E1 /* MainHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7A3B602F1E8DA600B4C6E1 /* MainHomeView.swift */; }; DD7B0D442D730A3B0063DCB6 /* CycleHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7B0D432D730A320063DCB6 /* CycleHelper.swift */; }; DD7E19842ACDA50C00DBD158 /* Overrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E19832ACDA50C00DBD158 /* Overrides.swift */; }; DD7E19862ACDA59700DBD158 /* BGCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E19852ACDA59700DBD158 /* BGCheck.swift */; }; @@ -259,7 +268,6 @@ DDD10F0B2C54192A00D76A8E /* TemporaryTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD10F0A2C54192A00D76A8E /* TemporaryTarget.swift */; }; DDDB86F12DF7223C00AADDAC /* DeleteAlarmSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB86F02DF7223C00AADDAC /* DeleteAlarmSection.swift */; }; DDDC01DD2E244B3100D9975C /* JWTManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDC01DC2E244B3100D9975C /* JWTManager.swift */; }; - A1A1A10002000000A0CFEED1 /* LogRedactor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1A1A10002000000A0CFEED2 /* LogRedactor.swift */; }; DDDC31CC2E13A7DF009EA0F3 /* AddAlarmSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDC31CB2E13A7DF009EA0F3 /* AddAlarmSheet.swift */; }; DDDC31CE2E13A811009EA0F3 /* AlarmTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDC31CD2E13A811009EA0F3 /* AlarmTile.swift */; }; DDE69ED22C7256260013EAEC /* RemoteType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE69ED12C7256260013EAEC /* RemoteType.swift */; }; @@ -288,6 +296,7 @@ DDFF3D852D14279B00BF9D9E /* BackgroundRefreshSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFF3D842D14279B00BF9D9E /* BackgroundRefreshSettingsView.swift */; }; DDFF3D872D14280500BF9D9E /* BackgroundRefreshSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFF3D862D14280500BF9D9E /* BackgroundRefreshSettingsViewModel.swift */; }; DDFF3D892D1429AB00BF9D9E /* BackgroundRefreshType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFF3D882D1429AB00BF9D9E /* BackgroundRefreshType.swift */; }; + EE5F6A7B8C9D0E2F2A3B4C5D /* NightscoutContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE5F6A7B8C9D0E1F2A3B4C5D /* NightscoutContentView.swift */; }; F19449721F3B792730A0F4FD /* PendingFutureCarb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9BEC26E4E48EF9B811A372 /* PendingFutureCarb.swift */; }; FC16A97A24996673003D6245 /* NightScout.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC16A97924996673003D6245 /* NightScout.swift */; }; FC16A97B249966A3003D6245 /* AlarmSound.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC7CE589248ABEA3001F83B8 /* AlarmSound.swift */; }; @@ -296,12 +305,9 @@ FC16A98124996C07003D6245 /* DateTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC16A98024996C07003D6245 /* DateTime.swift */; }; FC1BDD2B24A22650001B652C /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC1BDD2A24A22650001B652C /* Stats.swift */; }; FC1BDD2D24A23204001B652C /* MainViewController+updateStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC1BDD2C24A23204001B652C /* MainViewController+updateStats.swift */; }; - A1B2C3D4E5F6A7B8C9D0E1F3 /* StatsDisplayModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F6A7B8C9D0E1F2 /* StatsDisplayModel.swift */; }; - A1B2C3D4E5F6A7B8C9D0E1F5 /* StatsDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F6A7B8C9D0E1F4 /* StatsDisplayView.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 */; }; - A1A1A10001000000A0CFEED1 /* APNsCredentialValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1A1A10001000000A0CFEED2 /* APNsCredentialValidator.swift */; }; FC5A5C3D2497B229009C550E /* Config.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = FC5A5C3C2497B229009C550E /* Config.xcconfig */; }; FC7CE518248ABE37001F83B8 /* Siri_Alert_Calibration_Needed.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4A9248ABE2B001F83B8 /* Siri_Alert_Calibration_Needed.caf */; }; FC7CE519248ABE37001F83B8 /* Rise_And_Shine.caf in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE4AA248ABE2B001F83B8 /* Rise_And_Shine.caf */; }; @@ -432,15 +438,11 @@ 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 */; }; - 9C7FB98C98BE4FF98F4815EE /* Telemetry.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFBE69CEF18416D84959974 /* Telemetry.swift */; }; FCD2A27D24C9D044009F7B7B /* Globals.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCD2A27C24C9D044009F7B7B /* Globals.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 */; }; FCFEECA02488157B00402A7F /* Chart.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCFEEC9F2488157B00402A7F /* Chart.swift */; }; - DD50C10A2F60A00000000001 /* SocketIO in Frameworks */ = {isa = PBXBuildFile; productRef = DD50C10A2F60A00000000003 /* SocketIO */; }; - DD50C10A2F60B00000000002 /* NightscoutSocketManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD50C10A2F60B00000000001 /* NightscoutSocketManager.swift */; }; - DD50C10A2F60B00000000004 /* NightscoutSocketDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD50C10A2F60B00000000003 /* NightscoutSocketDataHandler.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -475,11 +477,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - B500000000000000000000A1 /* RemoteBolusHistoryEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteBolusHistoryEntry.swift; sourceTree = ""; }; - B500000000000000000000A3 /* QuickPickBolusesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickPickBolusesManager.swift; sourceTree = ""; }; - B500000000000000000000B1 /* RemoteMealHistoryEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMealHistoryEntry.swift; sourceTree = ""; }; - B500000000000000000000B3 /* QuickPickMealsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickPickMealsManager.swift; sourceTree = ""; }; 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 = ""; }; + 098BA6FC4279B166734A0E13 /* BannerManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BannerManager.swift; sourceTree = ""; }; 2B9BEC26E4E48EF9B811A372 /* PendingFutureCarb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PendingFutureCarb.swift; sourceTree = ""; }; 2EBAB9EECE7095238A558060 /* FutureCarbsCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FutureCarbsCondition.swift; sourceTree = ""; }; 374A77982F5BD8AB00E96858 /* APNSClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNSClient.swift; sourceTree = ""; }; @@ -532,9 +531,24 @@ 65A100022F5AA00000AA1002 /* UnitsConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitsConfigurationView.swift; sourceTree = ""; }; 65E153C22E4BB69100693A4F /* URLTokenValidationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLTokenValidationView.swift; sourceTree = ""; }; 65E8A2852E44B0300065037B /* VolumeButtonHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeButtonHandler.swift; sourceTree = ""; }; + 8000BC181BCE5AA8553FB3B6 /* AppBannerView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AppBannerView.swift; sourceTree = ""; }; + A1A1A10001000000A0CFEED2 /* APNsCredentialValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNsCredentialValidator.swift; sourceTree = ""; }; + A1A1A10002000000A0CFEED2 /* LogRedactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogRedactor.swift; sourceTree = ""; }; + A1B2C3D4E5F6A7B8C9D0E1F2 /* StatsDisplayModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsDisplayModel.swift; sourceTree = ""; }; + A1B2C3D4E5F6A7B8C9D0E1F4 /* StatsDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsDisplayView.swift; sourceTree = ""; }; A7D55B42A22051DAD69E89D0 /* Pods_LoopFollow.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_LoopFollow.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A8CA8BE0B3D247408FE088B4 /* BackgroundRefreshManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundRefreshManager.swift; sourceTree = ""; }; + AA1B2C3D4E5F6A7B8C9D0E1F /* LoopFollowApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopFollowApp.swift; sourceTree = ""; }; + AB1CD0022C7B30D40048F05C /* RemoteDiagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteDiagnostics.swift; sourceTree = ""; }; + B500000000000000000000A1 /* RemoteBolusHistoryEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteBolusHistoryEntry.swift; sourceTree = ""; }; + B500000000000000000000A3 /* QuickPickBolusesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickPickBolusesManager.swift; sourceTree = ""; }; + B500000000000000000000B1 /* RemoteMealHistoryEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMealHistoryEntry.swift; sourceTree = ""; }; + B500000000000000000000B3 /* QuickPickMealsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickPickMealsManager.swift; sourceTree = ""; }; + B500000000000000000000C1 /* QuickPickSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickPickSectionHeader.swift; sourceTree = ""; }; B7D2A4EFD18B7B7748B6669E /* FutureCarbsAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FutureCarbsAlarmEditor.swift; sourceTree = ""; }; + BB2C3D4E5F6A7B8C9D0E1F2A /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = ""; }; + BDFBE69CEF18416D84959974 /* Telemetry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Telemetry.swift; sourceTree = ""; }; + CC3D4E5F6A7B8C9D0E1F2A3B /* MoreMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreMenuView.swift; sourceTree = ""; }; DD0247582DB2E89600FCADF6 /* AlarmCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmCondition.swift; sourceTree = ""; }; DD02475B2DB2E8FB00FCADF6 /* BuildExpireCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildExpireCondition.swift; sourceTree = ""; }; DD026E582EA2C8A200A39CB5 /* InsulinPrecisionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsulinPrecisionManager.swift; sourceTree = ""; }; @@ -558,9 +572,6 @@ DD0C0C672C48529400DBADDF /* Metric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metric.swift; sourceTree = ""; }; DD0C0C6A2C48562000DBADDF /* InsulinMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsulinMetric.swift; sourceTree = ""; }; DD0C0C6C2C48606200DBADDF /* CarbMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbMetric.swift; sourceTree = ""; }; - DD4E5F6A7B8C9D0E1F2A3B4C /* RemoteContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteContentView.swift; sourceTree = ""; }; - AA1B2C3D4E5F6A7B8C9D0E1F /* LoopFollowApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopFollowApp.swift; sourceTree = ""; }; - BB2C3D4E5F6A7B8C9D0E1F2A /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = ""; }; DD12D4862E1705E6004E0112 /* AlarmsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmsContainerView.swift; sourceTree = ""; }; DD13BC742C3FD6200062313B /* InfoType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoType.swift; sourceTree = ""; }; DD13BC762C3FD64E0062313B /* InfoData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoData.swift; sourceTree = ""; }; @@ -569,13 +580,7 @@ DD16AF0C2C98485400FB655A /* SecureStorageValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureStorageValue.swift; sourceTree = ""; }; DD16AF0E2C99592F00FB655A /* HKQuantityInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKQuantityInputView.swift; sourceTree = ""; }; DD16AF102C997B4600FB655A /* LoadingButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingButtonView.swift; sourceTree = ""; }; - B500000000000000000000C1 /* QuickPickSectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickPickSectionHeader.swift; sourceTree = ""; }; DD1D52B82E1EB5DC00432050 /* TabPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPosition.swift; sourceTree = ""; }; - CC3D4E5F6A7B8C9D0E1F2A3B /* MoreMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreMenuView.swift; sourceTree = ""; }; - DD7A3B5C2F1E8D9A00B4C6E1 /* BGDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGDisplayView.swift; sourceTree = ""; }; - DD7A3B5E2F1E8DA000B4C6E1 /* LineChartWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineChartWrapper.swift; sourceTree = ""; }; - DD7A3B602F1E8DA600B4C6E1 /* MainHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeView.swift; sourceTree = ""; }; - EE5F6A7B8C9D0E1F2A3B4C5D /* NightscoutContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutContentView.swift; sourceTree = ""; }; DD1D52BF2E4C100000000001 /* AppearanceMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceMode.swift; sourceTree = ""; }; DD1D52C12E4C100000000002 /* PredictionDisplayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PredictionDisplayType.swift; sourceTree = ""; }; DD2C2E4E2D3B8AEC006413A5 /* NightscoutSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSettingsView.swift; sourceTree = ""; }; @@ -585,7 +590,6 @@ DD4878042C7B2C970048F05C /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = ""; }; DD4878072C7B30BF0048F05C /* RemoteSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSettingsView.swift; sourceTree = ""; }; DD4878092C7B30D40048F05C /* RemoteSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSettingsViewModel.swift; sourceTree = ""; }; - AB1CD0022C7B30D40048F05C /* RemoteDiagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteDiagnostics.swift; sourceTree = ""; }; DD48780D2C7B74A40048F05C /* TrioRemoteControlViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioRemoteControlViewModel.swift; sourceTree = ""; }; DD48780F2C7B74BF0048F05C /* TrioRemoteControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioRemoteControlView.swift; sourceTree = ""; }; DD4878122C7B750D0048F05C /* TempTargetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetView.swift; sourceTree = ""; }; @@ -604,8 +608,6 @@ 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 = ""; }; - DD50C10A2F60B00000000001 /* NightscoutSocketManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSocketManager.swift; sourceTree = ""; }; - DD50C10A2F60B00000000003 /* NightscoutSocketDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSocketDataHandler.swift; sourceTree = ""; }; DD4A407D2E6AFEE6007B318B /* AuthService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthService.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 = ""; }; @@ -613,6 +615,9 @@ 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 = ""; }; + DD4E5F6A7B8C9D0E1F2A3B4C /* RemoteContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteContentView.swift; sourceTree = ""; }; + DD50C10A2F60B00000000001 /* NightscoutSocketManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSocketManager.swift; sourceTree = ""; }; + DD50C10A2F60B00000000003 /* NightscoutSocketDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSocketDataHandler.swift; sourceTree = ""; }; DD50C7542D0862770057AE6F /* ContactImageUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactImageUpdater.swift; sourceTree = ""; }; DD5334202C60EBEE00062F9D /* InsulinCartridgeChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsulinCartridgeChange.swift; sourceTree = ""; }; DD5334222C60ED3600062F9D /* IAge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAge.swift; sourceTree = ""; }; @@ -628,6 +633,9 @@ DD608A092C23593900F91132 /* SMB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMB.swift; sourceTree = ""; }; DD608A0B2C27415C00F91132 /* BackgroundAlertManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundAlertManager.swift; sourceTree = ""; }; DD6A935D2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatusOpenAPS.swift; sourceTree = ""; }; + DD7A3B5C2F1E8D9A00B4C6E1 /* BGDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGDisplayView.swift; sourceTree = ""; }; + DD7A3B5E2F1E8DA000B4C6E1 /* LineChartWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineChartWrapper.swift; sourceTree = ""; }; + DD7A3B602F1E8DA600B4C6E1 /* MainHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeView.swift; sourceTree = ""; }; DD7B0D432D730A320063DCB6 /* CycleHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CycleHelper.swift; sourceTree = ""; }; DD7E19832ACDA50C00DBD158 /* Overrides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Overrides.swift; sourceTree = ""; }; DD7E19852ACDA59700DBD158 /* BGCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGCheck.swift; sourceTree = ""; }; @@ -728,7 +736,6 @@ DDD10F0A2C54192A00D76A8E /* TemporaryTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemporaryTarget.swift; sourceTree = ""; }; DDDB86F02DF7223C00AADDAC /* DeleteAlarmSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAlarmSection.swift; sourceTree = ""; }; DDDC01DC2E244B3100D9975C /* JWTManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JWTManager.swift; sourceTree = ""; }; - A1A1A10002000000A0CFEED2 /* LogRedactor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogRedactor.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 = ""; }; DDE69ED12C7256260013EAEC /* RemoteType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteType.swift; sourceTree = ""; }; @@ -759,14 +766,13 @@ DDFF3D882D1429AB00BF9D9E /* BackgroundRefreshType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundRefreshType.swift; sourceTree = ""; }; E7C2676561D686C6459CAA2D /* APNSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNSettingsView.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 = ""; }; + EE5F6A7B8C9D0E1F2A3B4C5D /* NightscoutContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutContentView.swift; sourceTree = ""; }; FC16A97924996673003D6245 /* NightScout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightScout.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 /* MainViewController+updateStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainViewController+updateStats.swift"; sourceTree = ""; }; - A1B2C3D4E5F6A7B8C9D0E1F2 /* StatsDisplayModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsDisplayModel.swift; sourceTree = ""; }; - A1B2C3D4E5F6A7B8C9D0E1F4 /* StatsDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsDisplayView.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 = ""; }; @@ -896,7 +902,6 @@ FCA2DDE52501095000254A8C /* Timers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timers.swift; sourceTree = ""; }; FCC0FAC124922A22003E610E /* DictionaryKeyPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryKeyPath.swift; sourceTree = ""; }; FCC688592489554800A0279D /* BackgroundTaskAudio.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundTaskAudio.swift; sourceTree = ""; }; - A1A1A10001000000A0CFEED2 /* APNsCredentialValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNsCredentialValidator.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 = ""; }; FCC6886624898F8000A0279D /* UserDefaultsValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsValue.swift; sourceTree = ""; }; @@ -904,7 +909,6 @@ FCC6886A24898FD800A0279D /* ObservationToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservationToken.swift; sourceTree = ""; }; FCC6886C2489909D00A0279D /* AnyConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyConvertible.swift; sourceTree = ""; }; FCC6886E2489A53800A0279D /* AppConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppConstants.swift; sourceTree = ""; }; - BDFBE69CEF18416D84959974 /* Telemetry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Telemetry.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 = ""; }; FCE537BB249A4D7D00F80BF8 /* carbBolusArrays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = carbBolusArrays.swift; sourceTree = ""; }; @@ -915,10 +919,50 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - 37A4BDDE2F5B6B4A00EEB289 /* LoopFollowLAExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = LoopFollowLAExtension; sourceTree = ""; }; - 65AC25F52ECFD5E800421360 /* Stats */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Stats; sourceTree = ""; }; - 65AC26702ED245DF00421360 /* Treatments */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Treatments; sourceTree = ""; }; - DDCC3AD72DDE1790006F1C10 /* Tests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Tests; sourceTree = ""; }; + 37A4BDDE2F5B6B4A00EEB289 /* LoopFollowLAExtension */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = LoopFollowLAExtension; + sourceTree = ""; + }; + 65AC25F52ECFD5E800421360 /* Stats */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = Stats; + sourceTree = ""; + }; + 65AC26702ED245DF00421360 /* Treatments */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = Treatments; + sourceTree = ""; + }; + DDCC3AD72DDE1790006F1C10 /* Tests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = Tests; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1006,6 +1050,16 @@ path = Settings; sourceTree = ""; }; + 6A3463E271ADFF0E844E3E61 /* Banner */ = { + isa = PBXGroup; + children = ( + 098BA6FC4279B166734A0E13 /* BannerManager.swift */, + 8000BC181BCE5AA8553FB3B6 /* AppBannerView.swift */, + ); + name = Banner; + path = Banner; + sourceTree = ""; + }; 6A5880E0B811AF443B05AB02 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -1028,6 +1082,24 @@ path = Pods; sourceTree = ""; }; + B500000000000000000000A5 /* QuickPickBoluses */ = { + isa = PBXGroup; + children = ( + B500000000000000000000A1 /* RemoteBolusHistoryEntry.swift */, + B500000000000000000000A3 /* QuickPickBolusesManager.swift */, + ); + path = QuickPickBoluses; + sourceTree = ""; + }; + B500000000000000000000B5 /* QuickPickMeals */ = { + isa = PBXGroup; + children = ( + B500000000000000000000B1 /* RemoteMealHistoryEntry.swift */, + B500000000000000000000B3 /* QuickPickMealsManager.swift */, + ); + path = QuickPickMeals; + sourceTree = ""; + }; DD02475A2DB2E8CE00FCADF6 /* AlarmCondition */ = { isa = PBXGroup; children = ( @@ -1069,24 +1141,6 @@ path = Metric; sourceTree = ""; }; - B500000000000000000000B5 /* QuickPickMeals */ = { - isa = PBXGroup; - children = ( - B500000000000000000000B1 /* RemoteMealHistoryEntry.swift */, - B500000000000000000000B3 /* QuickPickMealsManager.swift */, - ); - path = QuickPickMeals; - sourceTree = ""; - }; - B500000000000000000000A5 /* QuickPickBoluses */ = { - isa = PBXGroup; - children = ( - B500000000000000000000A1 /* RemoteBolusHistoryEntry.swift */, - B500000000000000000000A3 /* QuickPickBolusesManager.swift */, - ); - path = QuickPickBoluses; - sourceTree = ""; - }; DD0C0C6E2C4AFFB800DBADDF /* Remote */ = { isa = PBXGroup; children = ( @@ -1661,6 +1715,7 @@ DDC7E5CD2DC6637800EB1127 /* Storage */, DDEF503D2D32753A00999A5D /* Task */, FCC68871248A736700A0279D /* ViewControllers */, + 6A3463E271ADFF0E844E3E61 /* Banner */, ); path = LoopFollow; sourceTree = ""; @@ -1785,8 +1840,6 @@ 37A4BDDE2F5B6B4A00EEB289 /* LoopFollowLAExtension */, ); name = LoopFollowLAExtensionExtension; - packageProductDependencies = ( - ); productName = LoopFollowLAExtensionExtension; productReference = 37A4BDD92F5B6B4A00EEB289 /* LoopFollowLAExtensionExtension.appex */; productType = "com.apple.product-type.app-extension"; @@ -1808,8 +1861,6 @@ DDCC3AD72DDE1790006F1C10 /* Tests */, ); name = Tests; - packageProductDependencies = ( - ); productName = Tests; productReference = DDCC3AD62DDE1790006F1C10 /* Tests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; @@ -2443,6 +2494,8 @@ DDEF50422D479BAA00884336 /* LoopAPNSCarbsView.swift in Sources */, DDEF50432D479BBA00884336 /* LoopAPNSBolusView.swift in Sources */, DDEF50452D479BDA00884336 /* LoopAPNSRemoteView.swift in Sources */, + 87F526CE28FD073330D2F516 /* BannerManager.swift in Sources */, + 8897B18D53EA2F1963B8B4E0 /* AppBannerView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2853,20 +2906,6 @@ }; /* End XCConfigurationList section */ -/* Begin XCVersionGroup section */ - FC3AE7B3249E8E0E00AAE1E0 /* LoopFollow.xcdatamodeld */ = { - isa = XCVersionGroup; - children = ( - FC3AE7B4249E8E0E00AAE1E0 /* LoopFollow.xcdatamodel */, - ); - currentVersion = FC3AE7B4249E8E0E00AAE1E0 /* LoopFollow.xcdatamodel */; - name = LoopFollow.xcdatamodeld; - path = LoopFollow/LoopFollow.xcdatamodeld; - sourceTree = ""; - versionGroupType = wrapper.xcdatamodel; - }; -/* End XCVersionGroup section */ - /* Begin XCRemoteSwiftPackageReference section */ DD50C10A2F60A00000000002 /* XCRemoteSwiftPackageReference "socket.io-client-swift" */ = { isa = XCRemoteSwiftPackageReference; @@ -2885,6 +2924,20 @@ productName = SocketIO; }; /* End XCSwiftPackageProductDependency section */ + +/* Begin XCVersionGroup section */ + FC3AE7B3249E8E0E00AAE1E0 /* LoopFollow.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + FC3AE7B4249E8E0E00AAE1E0 /* LoopFollow.xcdatamodel */, + ); + currentVersion = FC3AE7B4249E8E0E00AAE1E0 /* LoopFollow.xcdatamodel */; + name = LoopFollow.xcdatamodeld; + path = LoopFollow/LoopFollow.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ }; rootObject = FC97880C2485969B00A7906C /* Project object */; } diff --git a/LoopFollow/Application/MainTabView.swift b/LoopFollow/Application/MainTabView.swift index 140204358..1755f2fe8 100644 --- a/LoopFollow/Application/MainTabView.swift +++ b/LoopFollow/Application/MainTabView.swift @@ -13,6 +13,7 @@ struct MainTabView: View { @ObservedObject private var snoozerPosition = Storage.shared.snoozerPosition @ObservedObject private var statisticsPosition = Storage.shared.statisticsPosition @ObservedObject private var treatmentsPosition = Storage.shared.treatmentsPosition + @ObservedObject private var activeBanner = Observable.shared.activeBanner @State private var showTelemetryConsent = false @@ -21,6 +22,21 @@ struct MainTabView: View { } var body: some View { + // The banner sits in a VStack above the TabView (rather than a + // .safeAreaInset) so the UIKit-hosted Home content is physically + // pushed down too — safe-area changes don't propagate into + // UIViewControllerRepresentable. + VStack(spacing: 0) { + if let message = activeBanner.value { + AppBannerView(message: message) + .transition(.move(edge: .top).combined(with: .opacity)) + } + tabView + } + .animation(.easeInOut(duration: 0.25), value: activeBanner.value) + } + + private var tabView: some View { TabView(selection: $selectedTab.value) { ForEach(Array(orderedItems.prefix(4).enumerated()), id: \.element) { index, item in tabContent(for: item) diff --git a/LoopFollow/BackgroundRefresh/BT/BLEManager.swift b/LoopFollow/BackgroundRefresh/BT/BLEManager.swift index c4a39874c..3513a68da 100644 --- a/LoopFollow/BackgroundRefresh/BT/BLEManager.swift +++ b/LoopFollow/BackgroundRefresh/BT/BLEManager.swift @@ -13,6 +13,21 @@ class BLEManager: NSObject, ObservableObject { private var centralManager: CBCentralManager! private var activeDevice: BluetoothDevice? + /// Arrival times of recent heartbeat dropouts. Main-queue-confined + /// (centralManager delivers on .main) and deliberately not persisted: + /// stale history must not resurrect the banner after a relaunch. + private var heartbeatDropoutTimes: [Date] = [] + + /// A beat this much later than expected counts as one dropout event. + /// Stricter than the 15% logging margin - normal jitter must not count. + private let dropoutFactor = 1.5 + + /// Sliding window over which dropout events are counted. + private let dropoutWindow: TimeInterval = 60 * 60 + + /// Dropout events within the window needed to raise the banner. + private let dropoutTriggerCount = 5 + override private init() { super.init() @@ -49,6 +64,8 @@ class BLEManager: NSObject, ObservableObject { activeDevice = nil device.lastHeartbeatTime = nil } + heartbeatDropoutTimes.removeAll() + BannerManager.shared.clear(.heartbeat) Storage.shared.selectedBLEDevice.value = nil } @@ -208,6 +225,7 @@ extension BLEManager: BluetoothDeviceDelegate { let delay = elapsedTime - expectedInterval LogManager.shared.log(category: .bluetooth, message: "Heartbeat triggered (Delayed by \(String(format: "%.1f", delay)) seconds)") } + recordHeartbeatOutcome(elapsed: elapsedTime, expectedInterval: expectedInterval, now: now) } else { LogManager.shared.log(category: .bluetooth, message: "Heartbeat triggered (First heartbeat)") } @@ -216,6 +234,55 @@ extension BLEManager: BluetoothDeviceDelegate { TaskScheduler.shared.checkTasksNow() } + + /// Counts late heartbeats over a sliding window and raises a banner when + /// dropouts become frequent - the typical symptom of a dying transmitter + /// battery. Detection is arrival-based on purpose: a struggling battery + /// still delivers (late) beats, whereas total silence usually means + /// out-of-range or a dead device and is already surfaced by the + /// connection status in Background Refresh settings and by BG alarms. + private func recordHeartbeatOutcome(elapsed: TimeInterval, expectedInterval: TimeInterval, now: Date) { + heartbeatDropoutTimes.removeAll { now.timeIntervalSince($0) > dropoutWindow } + + // A gap this large means out-of-range, Bluetooth off or a suspended + // app - not a struggling battery. Discard the history collected + // before the blind spot instead of counting it. + let resetGap = max(6 * expectedInterval, 30 * 60) + if elapsed > resetGap { + if !heartbeatDropoutTimes.isEmpty { + heartbeatDropoutTimes.removeAll() + LogManager.shared.log(category: .bluetooth, message: "Heartbeat gap of \(Int(elapsed)) seconds, resetting dropout history") + } + } else if elapsed > expectedInterval * dropoutFactor { + heartbeatDropoutTimes.append(now) + LogManager.shared.log(category: .bluetooth, message: "Heartbeat dropout recorded (\(heartbeatDropoutTimes.count) in the last hour)") + } + + // Hysteresis: raise at the trigger count, clear only once the window + // is completely clean, so the banner doesn't flap at the boundary. + if heartbeatDropoutTimes.count >= dropoutTriggerCount { + BannerManager.shared.report(source: .heartbeat, severity: .warning, text: heartbeatDropoutBannerText()) + } else if heartbeatDropoutTimes.isEmpty { + BannerManager.shared.clear(.heartbeat) + } + } + + /// The text must stay stable while the condition persists (no counts or + /// durations) so BannerManager's dedupe keeps the banner from + /// re-animating on every beat and user dismissal keeps working. + private func heartbeatDropoutBannerText() -> String { + let name = activeDevice?.deviceName ?? "heartbeat device" + let cause: String + switch Storage.shared.backgroundRefreshType.value { + case .rileyLink: + cause = "a low RileyLink battery" + case .omnipodDash: + cause = "a low pod battery" + default: + cause = "a low transmitter battery" + } + return "Heartbeat: repeated Bluetooth dropouts from \(name). This can be a sign of \(cause) or a weak Bluetooth connection." + } } extension BLEManager { diff --git a/LoopFollow/Banner/AppBannerView.swift b/LoopFollow/Banner/AppBannerView.swift new file mode 100644 index 000000000..56d196909 --- /dev/null +++ b/LoopFollow/Banner/AppBannerView.swift @@ -0,0 +1,75 @@ +// LoopFollow +// AppBannerView.swift + +import SwiftUI + +/// Dismissable banner shown at the top of the app, above the tab content. +struct AppBannerView: View { + let message: BannerMessage + + var body: some View { + HStack(alignment: .top, spacing: 12) { + Image(systemName: iconName) + .font(.title3) + .foregroundColor(iconColor) + + Text(message.text) + .font(.callout) + .lineLimit(3) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity, alignment: .leading) + + Button { + BannerManager.shared.dismissCurrent() + } label: { + Image(systemName: "xmark.circle.fill") + .font(.title3) + .foregroundColor(.secondary) + } + .accessibilityLabel("Dismiss") + } + .padding() + .background( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(tint) + ) + .overlay( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .stroke(border, lineWidth: 1) + ) + .padding(.horizontal, 10) + .padding(.vertical, 4) + } + + private var iconName: String { + switch message.severity { + case .info: return "info.circle.fill" + case .warning: return "exclamationmark.triangle.fill" + case .error: return "xmark.octagon.fill" + } + } + + private var iconColor: Color { + switch message.severity { + case .info: return .blue + case .warning: return .orange + case .error: return .red + } + } + + private var tint: Color { + switch message.severity { + case .info: return Color.blue.opacity(0.20) + case .warning: return Color.orange.opacity(0.20) + case .error: return Color.red.opacity(0.20) + } + } + + private var border: Color { + switch message.severity { + case .info: return Color.blue.opacity(0.40) + case .warning: return Color.orange.opacity(0.40) + case .error: return Color.red.opacity(0.40) + } + } +} diff --git a/LoopFollow/Banner/BannerManager.swift b/LoopFollow/Banner/BannerManager.swift new file mode 100644 index 000000000..bfefb0e2c --- /dev/null +++ b/LoopFollow/Banner/BannerManager.swift @@ -0,0 +1,172 @@ +// LoopFollow +// BannerManager.swift + +import Foundation +import ShareClient + +/// Identifies who produced a banner message. Each source owns at most one +/// message at a time; reporting a new message for a source replaces its old one. +enum BannerSource: Hashable { + case nightscout + case dexcom + case heartbeat + case custom(String) +} + +enum BannerSeverity: Int, Comparable { + case info + case warning + case error + + static func < (lhs: BannerSeverity, rhs: BannerSeverity) -> Bool { + lhs.rawValue < rhs.rawValue + } +} + +struct BannerMessage: Equatable, Identifiable { + let id: UUID + let source: BannerSource + let severity: BannerSeverity + let text: String + let timestamp: Date +} + +/// App-wide banner state. Producers call `report`/`clear`; the view layer +/// observes `Observable.shared.activeBanner` and calls `dismissCurrent()`. +final class BannerManager { + static let shared = BannerManager() + + /// How long a user dismissal suppresses a still-occurring error before it re-appears. + static let dismissCooldown: TimeInterval = 30 * 60 + + /// Minimum time between Nightscout diagnostic probes (status.json calls). + private static let diagnosticInterval: TimeInterval = 5 * 60 + + // All state is read and mutated on the main queue only. + private var messages: [BannerSource: BannerMessage] = [:] + private var dismissals: [BannerSource: (until: Date, text: String)] = [:] + private var lastNightscoutDiagnostic: Date? + + private init() {} + + func report(source: BannerSource, severity: BannerSeverity = .error, text: String) { + DispatchQueue.main.async { + if let existing = self.messages[source], existing.text == text, existing.severity == severity { + // Same problem re-reported (fetches retry every 10-60 s): keep the + // message untouched so the banner doesn't re-animate, but publish in + // case a dismissal cooldown has expired since the last report. + } else { + // A different problem: show it even if the previous one was dismissed. + self.dismissals[source] = nil + self.messages[source] = BannerMessage( + id: UUID(), + source: source, + severity: severity, + text: text, + timestamp: Date() + ) + } + self.publish() + } + } + + func clear(_ source: BannerSource) { + DispatchQueue.main.async { + guard self.messages[source] != nil || self.dismissals[source] != nil else { return } + self.messages[source] = nil + self.dismissals[source] = nil + self.publish() + } + } + + func dismissCurrent() { + DispatchQueue.main.async { + guard let current = Observable.shared.activeBanner.value else { return } + self.dismissals[current.source] = (Date().addingTimeInterval(Self.dismissCooldown), current.text) + self.publish() + } + } + + /// Classifies a failed Nightscout fetch by probing status.json, so the banner + /// can say *why* it failed (invalid token, site not found, no network, ...) + /// instead of showing a generic decode/transport error. + func reportNightscoutFetchFailure(_ underlying: Error) { + DispatchQueue.main.async { + if let last = self.lastNightscoutDiagnostic, Date().timeIntervalSince(last) < Self.diagnosticInterval { + return + } + self.lastNightscoutDiagnostic = Date() + + NightscoutUtils.verifyURLAndToken { error, _, _, _ in + if let error = error { + if case .emptyAddress = error { + // URL was removed while a fetch was in flight — not an error state. + self.clear(.nightscout) + return + } + self.report( + source: .nightscout, + severity: .error, + text: "Nightscout: \(error.localizedDescription)" + ) + } else { + // Server reachable and token accepted, yet the data fetch failed. + LogManager.shared.log( + category: .nightscout, + message: "Nightscout diagnostic OK but data fetch failed: \(underlying)", + limitIdentifier: "Nightscout diagnostic OK but data fetch failed" + ) + self.report( + source: .nightscout, + severity: .warning, + text: "Nightscout: data download failed, but the server is reachable. Retrying automatically." + ) + } + } + } + } + + func reportDexcomFailure(_ error: ShareError, nightscoutFallback: Bool) { + var text: String + switch error { + case let .loginError(errorCode): + // Dexcom has returned both legacy "SSO_Authenticate…" codes and newer + // ones like "AccountPasswordInvalid" — match on the common substrings. + if errorCode.contains("AccountNotFound") { + text = "Dexcom Share: account not found — check your username." + } else if errorCode.contains("PasswordInvalid") { + text = "Dexcom Share: incorrect username or password." + } else if errorCode.contains("MaxAttempts") { + text = "Dexcom Share: too many failed login attempts — temporarily locked out." + } else { + text = "Dexcom Share: login failed (\(errorCode))." + } + case .httpError: + text = "Dexcom Share: network error while downloading." + case .fetchError, .dataError, .dateError: + text = "Dexcom Share: could not download readings." + } + + if nightscoutFallback { + text += " Using Nightscout as backup." + } + report(source: .dexcom, severity: nightscoutFallback ? .warning : .error, text: text) + } + + /// Pushes the highest-priority non-dismissed message to the UI. + private func publish() { + let now = Date() + let candidate = messages.values + .filter { message in + guard let dismissal = dismissals[message.source] else { return true } + return now >= dismissal.until || dismissal.text != message.text + } + .max { lhs, rhs in + (lhs.severity, lhs.timestamp) < (rhs.severity, rhs.timestamp) + } + + if Observable.shared.activeBanner.value != candidate { + Observable.shared.activeBanner.set(candidate) + } + } +} diff --git a/LoopFollow/Controllers/Nightscout/BGData.swift b/LoopFollow/Controllers/Nightscout/BGData.swift index 2d6c73a77..d2622f25a 100644 --- a/LoopFollow/Controllers/Nightscout/BGData.swift +++ b/LoopFollow/Controllers/Nightscout/BGData.swift @@ -22,6 +22,7 @@ extension MainViewController { 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") + BannerManager.shared.reportDexcomFailure(error, nightscoutFallback: IsNightscoutEnabled()) self.webLoadNSBGData() return } @@ -32,6 +33,8 @@ extension MainViewController { return } + BannerManager.shared.clear(.dexcom) + // If Dex data is old, load from NS instead let latestDate = data[0].date let now = dateTimeUtils.getNowTimeIntervalUTC() @@ -60,6 +63,7 @@ extension MainViewController { func webLoadNSBGData(dexData: [ShareGlucoseData] = []) { // This kicks it out in the instance where dexcom fails but they aren't using NS && if !IsNightscoutEnabled() { + BannerManager.shared.clear(.nightscout) Storage.shared.lastBGChecked.value = Date() return } @@ -75,6 +79,7 @@ extension MainViewController { NightscoutUtils.executeRequest(eventType: .sgv, parameters: parameters) { (result: Result<[ShareGlucoseData], Error>) in switch result { case let .success(entriesResponse): + BannerManager.shared.clear(.nightscout) var nsData = entriesResponse DispatchQueue.main.async { // transform NS data to look like Dex data @@ -103,6 +108,7 @@ extension MainViewController { } case let .failure(error): LogManager.shared.log(category: .nightscout, message: "Failed to fetch bg data: \(error)", limitIdentifier: "Failed to fetch bg data") + BannerManager.shared.reportNightscoutFetchFailure(error) DispatchQueue.main.async { TaskScheduler.shared.rescheduleTask( id: .fetchBG, diff --git a/LoopFollow/Helpers/NightscoutUtils.swift b/LoopFollow/Helpers/NightscoutUtils.swift index 88b1995b7..559b24280 100644 --- a/LoopFollow/Helpers/NightscoutUtils.swift +++ b/LoopFollow/Helpers/NightscoutUtils.swift @@ -251,8 +251,14 @@ class NightscoutUtils { completion(.unknown, nil, false, false) } } else { - if let _ = error { - completion(.siteNotFound, nil, false, false) + if let urlError = error as? URLError { + switch urlError.code { + case .notConnectedToInternet, .networkConnectionLost, .timedOut, .dataNotAllowed: + completion(.networkError, nil, false, false) + default: + // cannotFindHost, cannotConnectToHost, etc. — the address is wrong or the site is down. + completion(.siteNotFound, nil, false, false) + } } else { completion(.networkError, nil, false, false) } diff --git a/LoopFollow/Storage/Observable.swift b/LoopFollow/Storage/Observable.swift index 4128918db..60906c936 100644 --- a/LoopFollow/Storage/Observable.swift +++ b/LoopFollow/Storage/Observable.swift @@ -53,5 +53,8 @@ class Observable { /// Selected tab index used by SwiftUI TabView — set from MainViewController to switch tabs var selectedTabIndex = ObservableValue(default: 0) + /// Currently visible app-wide banner (nil = hidden). Managed by BannerManager. + var activeBanner = ObservableValue(default: nil) + private init() {} } diff --git a/LoopFollow/Task/BGTask.swift b/LoopFollow/Task/BGTask.swift index 08b5703a3..b5728970c 100644 --- a/LoopFollow/Task/BGTask.swift +++ b/LoopFollow/Task/BGTask.swift @@ -19,6 +19,14 @@ extension MainViewController { to: Date().addingTimeInterval(60) ) + // A deconfigured source can no longer be in an error state. + if Storage.shared.shareUserName.value == "" || Storage.shared.sharePassword.value == "" { + BannerManager.shared.clear(.dexcom) + } + if !IsNightscoutEnabled() { + BannerManager.shared.clear(.nightscout) + } + // If no Dexcom credentials and no Nightscout, schedule a retry in 60 seconds. if Storage.shared.shareUserName.value == "", Storage.shared.sharePassword.value == "",