diff --git a/LoopFollow.xcodeproj/project.pbxproj b/LoopFollow.xcodeproj/project.pbxproj index 9a11022d6..3b12cc9dd 100644 --- a/LoopFollow.xcodeproj/project.pbxproj +++ b/LoopFollow.xcodeproj/project.pbxproj @@ -86,18 +86,24 @@ 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 */; }; - DD0C0C702C4AFFE800DBADDF /* RemoteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C6F2C4AFFE800DBADDF /* RemoteViewController.swift */; }; + DD4E5F6A7B8C9D0E2F2A3B4C /* RemoteContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4E5F6A7B8C9D0E1F2A3B4C /* RemoteContentView.swift */; }; DD0C0C722C4B000800DBADDF /* TrioNightscoutRemoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0C0C712C4B000800DBADDF /* TrioNightscoutRemoteView.swift */; }; - DD12D4852E1705D9004E0112 /* AlarmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD12D4842E1705D9004E0112 /* AlarmViewController.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 */; }; DD13BC792C3FE63A0062313B /* InfoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13BC782C3FE63A0062313B /* InfoManager.swift */; }; + DD13BC7B2C3FE64A0062313B /* InfoTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD13BC7A2C3FE64A0062313B /* InfoTableView.swift */; }; 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 */; }; DD1D52B92E1EB5DC00432050 /* TabPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1D52B82E1EB5DC00432050 /* TabPosition.swift */; }; - DD1D52BB2E1EB60B00432050 /* MoreMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1D52BA2E1EB60B00432050 /* MoreMenuViewController.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 */; }; DD2C2E4F2D3B8AF1006413A5 /* NightscoutSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2C2E4E2D3B8AEC006413A5 /* NightscoutSettingsView.swift */; }; DD2C2E512D3B8B0C006413A5 /* NightscoutSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2C2E502D3B8B0B006413A5 /* NightscoutSettingsViewModel.swift */; }; @@ -215,7 +221,6 @@ 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 */; }; @@ -233,7 +238,6 @@ DDCC3A582DDC9655006F1C10 /* MissedBolusAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A572DDC9655006F1C10 /* MissedBolusAlarmEditor.swift */; }; DDCC3A5A2DDC988F006F1C10 /* CarbSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A592DDC988F006F1C10 /* CarbSample.swift */; }; DDCC3A5B2DDE2000006F1C10 /* PumpBatteryAlarmEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCC3A5C2DDE2000006F1C10 /* PumpBatteryAlarmEditor.swift */; }; - DDCF979424C0D380002C9752 /* UIViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCF979324C0D380002C9752 /* UIViewExtension.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 */; }; @@ -284,6 +288,8 @@ 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 */; }; @@ -404,10 +410,8 @@ FC7CE59C248D33A9001F83B8 /* dragbar.png in Resources */ = {isa = PBXBuildFile; fileRef = FC7CE59B248D33A9001F83B8 /* dragbar.png */; }; 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 */; }; FC97881C2485969B00A7906C /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC97881B2485969B00A7906C /* MainViewController.swift */; }; FC97881E2485969B00A7906C /* NightScoutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC97881D2485969B00A7906C /* NightScoutViewController.swift */; }; - FC9788212485969B00A7906C /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FC97881F2485969B00A7906C /* Main.storyboard */; }; FC9788262485969C00A7906C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FC9788252485969C00A7906C /* Assets.xcassets */; }; FC9788292485969C00A7906C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FC9788272485969C00A7906C /* LaunchScreen.storyboard */; }; FCA2DDE62501095000254A8C /* Timers.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCA2DDE52501095000254A8C /* Timers.swift */; }; @@ -424,7 +428,6 @@ 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 */; }; - FCFEECA2248857A600402A7F /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCFEECA1248857A600402A7F /* SettingsViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -535,18 +538,24 @@ 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 = ""; }; - DD0C0C6F2C4AFFE800DBADDF /* RemoteViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteViewController.swift; sourceTree = ""; }; + DD4E5F6A7B8C9D0E1F2A3B4C /* RemoteContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteContentView.swift; sourceTree = ""; }; DD0C0C712C4B000800DBADDF /* TrioNightscoutRemoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioNightscoutRemoteView.swift; sourceTree = ""; }; - DD12D4842E1705D9004E0112 /* AlarmViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmViewController.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 = ""; }; DD13BC782C3FE63A0062313B /* InfoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoManager.swift; sourceTree = ""; }; + DD13BC7A2C3FE64A0062313B /* InfoTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoTableView.swift; sourceTree = ""; }; 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 = ""; }; DD1D52B82E1EB5DC00432050 /* TabPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPosition.swift; sourceTree = ""; }; - DD1D52BA2E1EB60B00432050 /* MoreMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreMenuViewController.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 = ""; }; DD2C2E4E2D3B8AEC006413A5 /* NightscoutSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSettingsView.swift; sourceTree = ""; }; DD2C2E502D3B8B0B006413A5 /* NightscoutSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSettingsViewModel.swift; sourceTree = ""; }; @@ -665,7 +674,6 @@ 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 = ""; }; @@ -684,7 +692,6 @@ DDCC3A5C2DDE2000006F1C10 /* PumpBatteryAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpBatteryAlarmEditor.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 = ""; }; 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 = ""; }; @@ -735,6 +742,8 @@ 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 = ""; }; @@ -857,10 +866,8 @@ 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; }; FC9788172485969B00A7906C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - FC9788192485969B00A7906C /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; FC97881B2485969B00A7906C /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; FC97881D2485969B00A7906C /* NightScoutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightScoutViewController.swift; sourceTree = ""; }; - FC9788202485969B00A7906C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; FC9788252485969C00A7906C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 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 = ""; }; @@ -880,7 +887,6 @@ FCEF87AA24A1417900AE6FA0 /* Localizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Localizer.swift; sourceTree = ""; }; FCFEEC9D2486E68E00402A7F /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; FCFEEC9F2488157B00402A7F /* Chart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Chart.swift; sourceTree = ""; }; - FCFEECA1248857A600402A7F /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -1043,7 +1049,7 @@ DD4878112C7B74F90048F05C /* TRC */, DD4878062C7B2E9E0048F05C /* Settings */, DDF699972C5AA2E50058A8D9 /* TempTargetPreset */, - DD0C0C6F2C4AFFE800DBADDF /* RemoteViewController.swift */, + DD4E5F6A7B8C9D0E1F2A3B4C /* RemoteContentView.swift */, DDE69ED12C7256260013EAEC /* RemoteType.swift */, ); path = Remote; @@ -1056,6 +1062,7 @@ DD13BC762C3FD64E0062313B /* InfoData.swift */, DD13BC782C3FE63A0062313B /* InfoManager.swift */, DD0C0C652C46E54C00DBADDF /* InfoDataSeparator.swift */, + DD13BC7A2C3FE64A0062313B /* InfoTableView.swift */, ); path = InfoTable; sourceTree = ""; @@ -1163,7 +1170,6 @@ isa = PBXGroup; children = ( DD98F54324BCEFEE0007425A /* ShareClientExtension.swift */, - DDCF979324C0D380002C9752 /* UIViewExtension.swift */, DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */, DD0C0C632C45A59400DBADDF /* HKUnit+Extensions.swift */, DD4AFB662DB68C5500BB593F /* UUID+Identifiable.swift */, @@ -1219,7 +1225,6 @@ children = ( DDC7E5CE2DC77C2000EB1127 /* SnoozerViewModel.swift */, DDC7E5122DBCE1B900EB1127 /* SnoozerView.swift */, - DDC7E5132DBCE1B900EB1127 /* SnoozerViewController.swift */, ); path = Snoozer; sourceTree = ""; @@ -1427,9 +1432,9 @@ FC16A97624995FEE003D6245 /* Application */ = { isa = PBXGroup; children = ( - FC97881F2485969B00A7906C /* Main.storyboard */, FC9788172485969B00A7906C /* AppDelegate.swift */, - FC9788192485969B00A7906C /* SceneDelegate.swift */, + AA1B2C3D4E5F6A7B8C9D0E1F /* LoopFollowApp.swift */, + BB2C3D4E5F6A7B8C9D0E1F2A /* MainTabView.swift */, FC9788272485969C00A7906C /* LaunchScreen.storyboard */, ); path = Application; @@ -1445,6 +1450,8 @@ FC16A97E249969E2003D6245 /* Graphs.swift */, FC1BDD2A24A22650001B652C /* Stats.swift */, FC1BDD2C24A23204001B652C /* MainViewController+updateStats.swift */, + A1B2C3D4E5F6A7B8C9D0E1F2 /* StatsDisplayModel.swift */, + A1B2C3D4E5F6A7B8C9D0E1F4 /* StatsDisplayView.swift */, FCA2DDE52501095000254A8C /* Timers.swift */, DD608A0B2C27415C00F91132 /* BackgroundAlertManager.swift */, 65E8A2852E44B0300065037B /* VolumeButtonHandler.swift */, @@ -1702,11 +1709,13 @@ FCC68871248A736700A0279D /* ViewControllers */ = { isa = PBXGroup; children = ( - DD1D52BA2E1EB60B00432050 /* MoreMenuViewController.swift */, - DD12D4842E1705D9004E0112 /* AlarmViewController.swift */, + DD7A3B5C2F1E8D9A00B4C6E1 /* BGDisplayView.swift */, + DD7A3B5E2F1E8DA000B4C6E1 /* LineChartWrapper.swift */, + DD7A3B602F1E8DA600B4C6E1 /* MainHomeView.swift */, + CC3D4E5F6A7B8C9D0E1F2A3B /* MoreMenuView.swift */, + EE5F6A7B8C9D0E1F2A3B4C5D /* NightscoutContentView.swift */, FC97881B2485969B00A7906C /* MainViewController.swift */, FC97881D2485969B00A7906C /* NightScoutViewController.swift */, - FCFEECA1248857A600402A7F /* SettingsViewController.swift */, ); path = ViewControllers; sourceTree = ""; @@ -1945,7 +1954,6 @@ FC7CE529248ABE37001F83B8 /* Laser_Shoot.caf in Resources */, FC7CE54A248ABE37001F83B8 /* Siri_Alert_Urgent_High_Glucose.caf in Resources */, FC7CE579248ABE37001F83B8 /* Good_Morning.caf in Resources */, - FC9788212485969B00A7906C /* Main.storyboard in Resources */, FC7CE538248ABE37001F83B8 /* 20ms-of-silence.caf in Resources */, FC7CE56B248ABE37001F83B8 /* Cartoon_Ascend_Then_Descend.caf in Resources */, FC7CE54E248ABE37001F83B8 /* Hell_Yeah_Somewhat_Calmer.caf in Resources */, @@ -2129,7 +2137,6 @@ DD608A0A2C23593900F91132 /* SMB.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 */, DDCC3A502DDED000006F1C10 /* PumpBatteryCondition.swift in Sources */, @@ -2191,7 +2198,6 @@ 374A77992F5BD8B200E96858 /* APNSClient.swift in Sources */, DD493AE72ACF23CF009A6922 /* DeviceStatus.swift in Sources */, DDC7E5162DBCFA7F00EB1127 /* SnoozerView.swift in Sources */, - FCFEECA2248857A600402A7F /* SettingsViewController.swift in Sources */, DD7F4C232DD7A62200D449E9 /* AlarmType+SortDirection.swift in Sources */, 65E8A2862E44B0300065037B /* VolumeButtonHandler.swift in Sources */, DD0650F72DCFDA26004D3B41 /* InfoBanner.swift in Sources */, @@ -2202,11 +2208,11 @@ DD7F4BA12DD2193F00D449E9 /* AlarmSnoozeSection.swift in Sources */, DD9ACA0C2D33BB8600415D8A /* CalendarTask.swift in Sources */, DD13BC792C3FE63A0062313B /* InfoManager.swift in Sources */, - DD0C0C702C4AFFE800DBADDF /* RemoteViewController.swift in Sources */, + DD13BC7B2C3FE64A0062313B /* InfoTableView.swift in Sources */, + DD4E5F6A7B8C9D0E2F2A3B4C /* RemoteContentView.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 */, @@ -2222,7 +2228,11 @@ FCEF87AC24A141A700AE6FA0 /* Localizer.swift in Sources */, FC1BDD3224A2585C001B652C /* DataStructs.swift in Sources */, DDF6999E2C5AAA640058A8D9 /* ErrorMessageView.swift in Sources */, - DD1D52BB2E1EB60B00432050 /* MoreMenuViewController.swift in Sources */, + CC3D4E5F6A7B8C9D0E2F2A3B /* MoreMenuView.swift in Sources */, + DD7A3B5D2F1E8D9A00B4C6E1 /* BGDisplayView.swift in Sources */, + DD7A3B5F2F1E8DA000B4C6E1 /* LineChartWrapper.swift in Sources */, + DD7A3B612F1E8DA600B4C6E1 /* MainHomeView.swift in Sources */, + EE5F6A7B8C9D0E2F2A3B4C5D /* NightscoutContentView.swift in Sources */, 654132E72E19EA7E00BDBE08 /* SimpleQRCodeScannerView.swift in Sources */, DD4878152C7B75230048F05C /* MealView.swift in Sources */, FC16A97F249969E2003D6245 /* Graphs.swift in Sources */, @@ -2292,7 +2302,8 @@ DD4AFB3B2DB55CB600BB593F /* TimeOfDay.swift in Sources */, DDEF503C2D31BE2D00999A5D /* TaskScheduler.swift in Sources */, DD0650EB2DCE8385004D3B41 /* LowBGCondition.swift in Sources */, - DD12D4852E1705D9004E0112 /* AlarmViewController.swift in Sources */, + AA1B2C3D4E5F6A7B8C9D0E2F /* LoopFollowApp.swift in Sources */, + BB2C3D4E5F6A7B8C9D0E2F2A /* MainTabView.swift in Sources */, DD0C0C6B2C48562000DBADDF /* InsulinMetric.swift in Sources */, DD493AD92ACF2171009A6922 /* Carbs.swift in Sources */, DD493AE92ACF2445009A6922 /* BGData.swift in Sources */, @@ -2318,7 +2329,6 @@ FCC6886D2489909D00A0279D /* AnyConvertible.swift in Sources */, DDC6CA432DD8CED20060EE25 /* SensorAgeCondition.swift in Sources */, DD7F4C212DD66BB200D449E9 /* FastRiseAlarmEditor.swift in Sources */, - FC97881A2485969B00A7906C /* SceneDelegate.swift in Sources */, DD0C0C662C46E54C00DBADDF /* InfoDataSeparator.swift in Sources */, 374A77A52F5BE17000E96858 /* AppGroupID.swift in Sources */, 374A77A62F5BE17000E96858 /* GlucoseSnapshot.swift in Sources */, @@ -2334,6 +2344,8 @@ DDA9ACAC2D6B317100E6F1A9 /* ContactType.swift in Sources */, DDD10F052C529DA200D76A8E /* ObservableValue.swift in Sources */, FC1BDD2D24A23204001B652C /* MainViewController+updateStats.swift in Sources */, + A1B2C3D4E5F6A7B8C9D0E1F3 /* StatsDisplayModel.swift in Sources */, + A1B2C3D4E5F6A7B8C9D0E1F5 /* StatsDisplayView.swift in Sources */, DD4878102C7B74BF0048F05C /* TrioRemoteControlView.swift in Sources */, DD7F4C152DD51FEB00D449E9 /* TempTargetEndAlarmEditor.swift in Sources */, FCE537BC249A4D7D00F80BF8 /* carbBolusArrays.swift in Sources */, @@ -2391,14 +2403,6 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ - FC97881F2485969B00A7906C /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - FC9788202485969B00A7906C /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; FC9788272485969C00A7906C /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( diff --git a/LoopFollow/Application/AppDelegate.swift b/LoopFollow/Application/AppDelegate.swift index a6fd9f2b9..38e14c013 100644 --- a/LoopFollow/Application/AppDelegate.swift +++ b/LoopFollow/Application/AppDelegate.swift @@ -1,15 +1,16 @@ // LoopFollow // AppDelegate.swift +import AVFoundation import CoreData import EventKit import UIKit import UserNotifications -@main class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? let notificationCenter = UNUserNotificationCenter.current() + private let speechSynthesizer = AVSpeechSynthesizer() func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { LogManager.shared.log(category: .general, message: "App started") @@ -111,32 +112,28 @@ class AppDelegate: UIResponder, UIApplicationDelegate { completionHandler(.newData) } - // MARK: - URL handling - - // Note: with scene-based lifecycle (iOS 13+), URLs are delivered to - // SceneDelegate.scene(_:openURLContexts:) — not here. The scene delegate - // handles ://la-tap for Live Activity tap navigation. - - // MARK: UISceneSession Lifecycle - 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 = Storage.shared.screenlockSwitchState.value - return true } - 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. - UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) - } + // MARK: - Quick Actions - 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. + func application(_: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { + guard let bundleIdentifier = Bundle.main.bundleIdentifier else { + completionHandler(false) + return + } + let expectedType = bundleIdentifier + ".toggleSpeakBG" + if shortcutItem.type == expectedType { + Storage.shared.speakBG.value.toggle() + let message = Storage.shared.speakBG.value ? "BG Speak is now on" : "BG Speak is now off" + let utterance = AVSpeechUtterance(string: message) + speechSynthesizer.speak(utterance) + completionHandler(true) + } else { + completionHandler(false) + } } // MARK: - Core Data stack @@ -186,10 +183,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func userNotificationCenter(_: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { if response.actionIdentifier == "OPEN_APP_ACTION" { - if let window { - window.rootViewController?.dismiss(animated: true, completion: nil) - window.rootViewController?.present(MainViewController(), animated: true, completion: nil) - } + // Switch to Home tab + Observable.shared.selectedTabIndex.value = 0 } if response.actionIdentifier == "snooze" { diff --git a/LoopFollow/Application/Base.lproj/Main.storyboard b/LoopFollow/Application/Base.lproj/Main.storyboard deleted file mode 100644 index 11e4b1a72..000000000 --- a/LoopFollow/Application/Base.lproj/Main.storyboard +++ /dev/null @@ -1,456 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/LoopFollow/Application/LoopFollowApp.swift b/LoopFollow/Application/LoopFollowApp.swift new file mode 100644 index 000000000..05956fcf3 --- /dev/null +++ b/LoopFollow/Application/LoopFollowApp.swift @@ -0,0 +1,21 @@ +// LoopFollow +// LoopFollowApp.swift + +import SwiftUI + +@main +struct LoopFollowApp: App { + @UIApplicationDelegateAdaptor private var appDelegate: AppDelegate + + var body: some Scene { + WindowGroup { + MainTabView() + .onOpenURL { url in + guard url.scheme == AppGroupID.urlScheme, url.host == "la-tap" else { return } + DispatchQueue.main.async { + NotificationCenter.default.post(name: .liveActivityDidForeground, object: nil) + } + } + } + } +} diff --git a/LoopFollow/Application/MainTabView.swift b/LoopFollow/Application/MainTabView.swift new file mode 100644 index 000000000..dbb2699b9 --- /dev/null +++ b/LoopFollow/Application/MainTabView.swift @@ -0,0 +1,62 @@ +// LoopFollow +// MainTabView.swift + +import SwiftUI + +struct MainTabView: View { + @ObservedObject private var selectedTab = Observable.shared.selectedTabIndex + @ObservedObject private var homePosition = Storage.shared.homePosition + @ObservedObject private var alarmsPosition = Storage.shared.alarmsPosition + @ObservedObject private var remotePosition = Storage.shared.remotePosition + @ObservedObject private var nightscoutPosition = Storage.shared.nightscoutPosition + @ObservedObject private var snoozerPosition = Storage.shared.snoozerPosition + @ObservedObject private var statisticsPosition = Storage.shared.statisticsPosition + @ObservedObject private var treatmentsPosition = Storage.shared.treatmentsPosition + + private var orderedItems: [TabItem] { + Storage.shared.orderedTabBarItems() + } + + var body: some View { + TabView(selection: $selectedTab.value) { + ForEach(Array(orderedItems.prefix(4).enumerated()), id: \.element) { index, item in + tabContent(for: item) + .tabItem { + Label(item.displayName, systemImage: item.icon) + } + .tag(index) + } + + NavigationStack { + MoreMenuView() + } + .tabItem { + Label("Menu", systemImage: "line.3.horizontal") + } + .tag(4) + } + .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme) + } + + @ViewBuilder + private func tabContent(for item: TabItem) -> some View { + switch item { + case .home: + HomeContentView() + case .alarms: + AlarmsContainerView() + case .remote: + RemoteContentView() + case .nightscout: + NightscoutContentView() + case .snoozer: + SnoozerView() + case .treatments: + TreatmentsView() + case .stats: + NavigationStack { + AggregatedStatsContentView(mainViewController: MainViewController.shared) + } + } + } +} diff --git a/LoopFollow/Application/SceneDelegate.swift b/LoopFollow/Application/SceneDelegate.swift deleted file mode 100644 index e702db267..000000000 --- a/LoopFollow/Application/SceneDelegate.swift +++ /dev/null @@ -1,83 +0,0 @@ -// LoopFollow -// SceneDelegate.swift - -import AVFoundation -import UIKit - -class SceneDelegate: UIResponder, UIWindowSceneDelegate { - var window: UIWindow? - let synthesizer = AVSpeechSynthesizer() - - 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 - else { - return - } - } - - func sceneDidDisconnect(_: UIScene) { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). - } - - func sceneDidBecomeActive(_: UIScene) { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - } - - func scene(_: UIScene, openURLContexts URLContexts: Set) { - guard URLContexts.contains(where: { $0.url.scheme == AppGroupID.urlScheme && $0.url.host == "la-tap" }) else { return } - // scene(_:openURLContexts:) fires after sceneDidBecomeActive when the app - // foregrounds from background. Post on the next run loop so the view - // hierarchy (including any presented modals) is fully settled. - DispatchQueue.main.async { - NotificationCenter.default.post(name: .liveActivityDidForeground, object: nil) - } - } - - func sceneWillResignActive(_: UIScene) { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - } - - func sceneWillEnterForeground(_: UIScene) { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - func sceneDidEnterBackground(_: UIScene) { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. - - // Save changes in the application's managed object context when the application transitions to the background. - (UIApplication.shared.delegate as? AppDelegate)?.saveContext() - } - - /// Handle the UIApplicationShortcutItem when the user taps on the Home Screen Quick Action. This function toggles the "Speak BG" setting in UserDefaultsRepository, speaks the current state (on/off) using AVSpeechSynthesizer, and updates the Quick Action appearance. - func handleShortcutItem(_ shortcutItem: UIApplicationShortcutItem) { - if let bundleIdentifier = Bundle.main.bundleIdentifier { - let expectedType = bundleIdentifier + ".toggleSpeakBG" - if shortcutItem.type == expectedType { - Storage.shared.speakBG.value.toggle() - let message = Storage.shared.speakBG.value ? "BG Speak is now on" : "BG Speak is now off" - let utterance = AVSpeechUtterance(string: message) - synthesizer.speak(utterance) - } - } - } - - /// The following method is called when the user taps on the Home Screen Quick Action - func windowScene(_: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler _: @escaping (Bool) -> Void) { - handleShortcutItem(shortcutItem) - } -} diff --git a/LoopFollow/Controllers/MainViewController+updateStats.swift b/LoopFollow/Controllers/MainViewController+updateStats.swift index 090f7925e..c1f2417d1 100644 --- a/LoopFollow/Controllers/MainViewController+updateStats.swift +++ b/LoopFollow/Controllers/MainViewController+updateStats.swift @@ -1,9 +1,7 @@ // LoopFollow // MainViewController+updateStats.swift -import Charts import Foundation -import UIKit extension MainViewController { func updateStats() { @@ -22,62 +20,20 @@ extension MainViewController { 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)) + statsDisplayModel.lowPercent = String(format: "%.1f%%", stats.percentLow) + statsDisplayModel.inRangePercent = String(format: "%.1f%%", stats.percentRange) + statsDisplayModel.highPercent = String(format: "%.1f%%", stats.percentHigh) + statsDisplayModel.avgBG = Localizer.toDisplayUnits(String(format: "%.0f", stats.avgBG)) if Storage.shared.useIFCC.value { - statsEstA1C.text = String(format: "%.0f", stats.a1C) + statsDisplayModel.estA1C = String(format: "%.0f", stats.a1C) } else { - statsEstA1C.text = String(format: "%.1f", stats.a1C) + statsDisplayModel.estA1C = String(format: "%.1f", stats.a1C) } - statsStdDev.text = String(format: "%.2f", stats.stdDev) + statsDisplayModel.stdDev = String(format: "%.2f", stats.stdDev) - createStatsPie(pieData: stats.pie) + statsDisplayModel.pieLow = Double(stats.percentLow) + statsDisplayModel.pieRange = Double(stats.percentRange) + statsDisplayModel.pieHigh = Double(stats.percentHigh) } } - - 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 ..< pieData.count { - var slice = Double(pieData[i].value) - if slice == 0 { slice = 0.1 } - let value = PieChartDataEntry(value: slice) - chartEntry.append(value) - - if pieData[i].name == "high" { - colors.append(NSUIColor.systemYellow) - } else if pieData[i].name == "low" { - colors.append(NSUIColor.systemRed) - } else { - colors.append(NSUIColor.systemGreen) - } - } - - let set = PieChartDataSet(entries: chartEntry, label: "") - - set.drawIconsEnabled = false - set.sliceSpace = 2 - set.drawValuesEnabled = false - set.valueLineWidth = 0 - set.formLineWidth = 0 - set.sliceSpace = 0 - - set.colors.removeAll() - if colors.count > 0 { - for i in 0 ..< colors.count { - set.addColor(colors[i]) - } - } - - let data = PieChartData(dataSet: set) - statsPieChart.data = data - } } diff --git a/LoopFollow/Controllers/Nightscout/BGData.swift b/LoopFollow/Controllers/Nightscout/BGData.swift index d97aba24c..8211ec5b0 100644 --- a/LoopFollow/Controllers/Nightscout/BGData.swift +++ b/LoopFollow/Controllers/Nightscout/BGData.swift @@ -212,9 +212,9 @@ extension MainViewController { func updateServerText(with serverText: String? = nil) { if Storage.shared.showDisplayName.value, let displayName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String { - self.serverText.text = displayName + Observable.shared.serverText.value = displayName } else if let serverText = serverText { - self.serverText.text = serverText + Observable.shared.serverText.value = serverText } } diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift index ff2b13a78..7a3935045 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatus.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatus.swift @@ -1,10 +1,9 @@ // LoopFollow // DeviceStatus.swift -import Charts import Foundation import HealthKit -import UIKit +import SwiftUI extension MainViewController { func webLoadNSDeviceStatus() { @@ -36,7 +35,6 @@ extension MainViewController { } func evaluateNotLooping() { - guard let statusStackView = LoopStatusLabel.superview as? UIStackView else { return } guard let lastLoopTime = Observable.shared.alertLastLoopTime.value, lastLoopTime > 0 else { return } @@ -47,15 +45,9 @@ extension MainViewController { if IsNightscoutEnabled(), (now - lastLoopTime) >= nonLoopingTimeThreshold, lastLoopTime > 0 { IsNotLooping = true Observable.shared.isNotLooping.value = true - statusStackView.distribution = .fill - PredictionLabel.isHidden = true - LoopStatusLabel.frame = CGRect(x: 0, y: 0, width: statusStackView.frame.width, height: statusStackView.frame.height) - - LoopStatusLabel.textAlignment = .center - LoopStatusLabel.text = "⚠️ Not Looping!" - LoopStatusLabel.textColor = UIColor.systemYellow - LoopStatusLabel.font = UIFont.boldSystemFont(ofSize: 18) + Observable.shared.loopStatusText.value = "⚠️ Not Looping!" + Observable.shared.loopStatusColor.value = .yellow #if !targetEnvironment(macCatalyst) LiveActivityManager.shared.refreshFromCurrentState(reason: "notLooping") #endif @@ -63,20 +55,8 @@ extension MainViewController { } else { IsNotLooping = false Observable.shared.isNotLooping.value = false - statusStackView.distribution = .fillEqually - PredictionLabel.isHidden = false - - LoopStatusLabel.textAlignment = .right - LoopStatusLabel.font = UIFont.systemFont(ofSize: 17) - - switch Storage.shared.appearanceMode.value { - case .dark: - LoopStatusLabel.textColor = UIColor.white - case .light: - LoopStatusLabel.textColor = UIColor.black - case .system: - LoopStatusLabel.textColor = UIColor.label - } + + Observable.shared.loopStatusColor.value = .primary #if !targetEnvironment(macCatalyst) LiveActivityManager.shared.refreshFromCurrentState(reason: "loopingResumed") #endif diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift index 56ebb6af0..771fdc68d 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift @@ -1,10 +1,9 @@ // LoopFollow // DeviceStatusLoop.swift -import Charts import Foundation import HealthKit -import UIKit +import SwiftUI extension MainViewController { func DeviceStatusLoop(formatter: ISO8601DateFormatter, lastLoopRecord: [String: AnyObject]) { @@ -18,7 +17,7 @@ extension MainViewController { let lastLoopTime = Observable.shared.alertLastLoopTime.value ?? 0 if lastLoopRecord["failureReason"] != nil { - LoopStatusLabel.text = "X" + Observable.shared.loopStatusText.value = "X" latestLoopStatusString = "X" } else { var wasEnacted = false @@ -67,8 +66,8 @@ extension MainViewController { if let predictdata = lastLoopRecord["predicted"] as? [String: AnyObject] { let prediction = predictdata["values"] as! [Double] - PredictionLabel.text = Localizer.toDisplayUnits(String(Int(round(prediction.last!)))) - PredictionLabel.textColor = UIColor.systemPurple + Observable.shared.predictionText.value = Localizer.toDisplayUnits(String(Int(round(prediction.last!)))) + Observable.shared.predictionColor.value = .purple if Storage.shared.downloadPrediction.value, previousLastLoopTime < lastLoopTime { predictionData.removeAll() var predictionTime = lastLoopTime @@ -113,15 +112,15 @@ extension MainViewController { lastBGTime = bgData[bgData.count - 1].date } if tempBasalTime > lastBGTime, !wasEnacted { - LoopStatusLabel.text = "⏀" + Observable.shared.loopStatusText.value = "⏀" latestLoopStatusString = "⏀" } else { - LoopStatusLabel.text = "↻" + Observable.shared.loopStatusText.value = "↻" latestLoopStatusString = "↻" } } } else { - LoopStatusLabel.text = "↻" + Observable.shared.loopStatusText.value = "↻" latestLoopStatusString = "↻" } diff --git a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift index c1d926fd1..e4c2462cd 100644 --- a/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift +++ b/LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift @@ -9,11 +9,11 @@ extension MainViewController { func DeviceStatusOpenAPS(formatter: ISO8601DateFormatter, lastDeviceStatus: [String: AnyObject]?, lastLoopRecord: [String: AnyObject]) { Storage.shared.device.value = lastDeviceStatus?["device"] as? String ?? "" if lastLoopRecord["failureReason"] != nil { - LoopStatusLabel.text = "X" + Observable.shared.loopStatusText.value = "X" latestLoopStatusString = "X" } else { guard let enactedOrSuggested = lastLoopRecord["suggested"] as? [String: AnyObject] ?? lastLoopRecord["enacted"] as? [String: AnyObject] else { - LoopStatusLabel.text = "↻" + Observable.shared.loopStatusText.value = "↻" latestLoopStatusString = "↻" return } @@ -117,7 +117,7 @@ extension MainViewController { // Eventual BG if let eventualBGValue = enactedOrSuggested["eventualBG"] as? Double { let eventualBGQuantity = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: eventualBGValue) - PredictionLabel.text = Localizer.formatQuantity(eventualBGQuantity) + Observable.shared.predictionText.value = Localizer.formatQuantity(eventualBGQuantity) Storage.shared.projectedBgMgdl.value = eventualBGValue } else { Storage.shared.projectedBgMgdl.value = nil @@ -173,8 +173,7 @@ extension MainViewController { return nil }() - let predictioncolor = UIColor.systemGray - PredictionLabel.textColor = predictioncolor + Observable.shared.predictionColor.value = .gray topPredictionBG = Storage.shared.minBGScale.value if let predbgdata = predBGsData { let predictionTypes: [(type: String, colorName: String, dataIndex: Int)] = [ @@ -233,15 +232,15 @@ extension MainViewController { lastBGTime = bgData[bgData.count - 1].date } if tempBasalTime > lastBGTime { - LoopStatusLabel.text = "⏀" + Observable.shared.loopStatusText.value = "⏀" latestLoopStatusString = "⏀" } else { - LoopStatusLabel.text = "↻" + Observable.shared.loopStatusText.value = "↻" latestLoopStatusString = "↻" } } } else { - LoopStatusLabel.text = "↻" + Observable.shared.loopStatusText.value = "↻" latestLoopStatusString = "↻" } diff --git a/LoopFollow/Controllers/StatsDisplayModel.swift b/LoopFollow/Controllers/StatsDisplayModel.swift new file mode 100644 index 000000000..1c29bccbc --- /dev/null +++ b/LoopFollow/Controllers/StatsDisplayModel.swift @@ -0,0 +1,16 @@ +// LoopFollow +// StatsDisplayModel.swift + +import Foundation + +class StatsDisplayModel: ObservableObject { + @Published var lowPercent: String = "" + @Published var inRangePercent: String = "" + @Published var highPercent: String = "" + @Published var avgBG: String = "" + @Published var estA1C: String = "" + @Published var stdDev: String = "" + @Published var pieLow: Double = 0 + @Published var pieRange: Double = 0 + @Published var pieHigh: Double = 0 +} diff --git a/LoopFollow/Controllers/StatsDisplayView.swift b/LoopFollow/Controllers/StatsDisplayView.swift new file mode 100644 index 000000000..39fea3143 --- /dev/null +++ b/LoopFollow/Controllers/StatsDisplayView.swift @@ -0,0 +1,84 @@ +// LoopFollow +// StatsDisplayView.swift + +import Charts +import SwiftUI + +struct StatsDisplayView: View { + @ObservedObject var model: StatsDisplayModel + var onTap: (() -> Void)? + + var body: some View { + HStack { + StatsPieChartView( + pieLow: model.pieLow, + pieRange: model.pieRange, + pieHigh: model.pieHigh + ) + .frame(width: 100, height: 100) + + VStack(spacing: 10) { + HStack { + statColumn(title: "Low:", value: model.lowPercent) + statColumn(title: "In Range:", value: model.inRangePercent) + statColumn(title: "High:", value: model.highPercent) + } + HStack { + statColumn(title: "Avg BG:", value: model.avgBG) + statColumn(title: "Est A1C:", value: model.estA1C) + statColumn(title: "Std Dev:", value: model.stdDev) + } + } + .frame(maxWidth: .infinity) + } + .frame(height: 100) + .background(Color(.secondarySystemBackground)) + .contentShape(Rectangle()) + .onTapGesture { onTap?() } + } + + private func statColumn(title: String, value: String) -> some View { + VStack { + Text(title) + .font(.system(size: 15)) + Text(value) + .font(.system(size: 15)) + } + .frame(maxWidth: .infinity) + } +} + +struct StatsPieChartView: UIViewRepresentable { + var pieLow: Double + var pieRange: Double + var pieHigh: Double + + func makeUIView(context _: Context) -> PieChartView { + let chart = PieChartView() + chart.legend.enabled = false + chart.drawEntryLabelsEnabled = false + chart.drawHoleEnabled = false + chart.rotationEnabled = false + chart.isUserInteractionEnabled = false + chart.backgroundColor = .clear + return chart + } + + func updateUIView(_ chart: PieChartView, context _: Context) { + let entries = [ + PieChartDataEntry(value: max(pieLow, 0.1)), + PieChartDataEntry(value: max(pieRange, 0.1)), + PieChartDataEntry(value: max(pieHigh, 0.1)), + ] + + let dataSet = PieChartDataSet(entries: entries, label: "") + dataSet.drawIconsEnabled = false + dataSet.sliceSpace = 0 + dataSet.drawValuesEnabled = false + dataSet.valueLineWidth = 0 + dataSet.formLineWidth = 0 + dataSet.colors = [.systemRed, .systemGreen, .systemYellow] + + chart.data = PieChartData(dataSet: dataSet) + } +} diff --git a/LoopFollow/Extensions/UIViewExtension.swift b/LoopFollow/Extensions/UIViewExtension.swift deleted file mode 100644 index 90fb15cb6..000000000 --- a/LoopFollow/Extensions/UIViewExtension.swift +++ /dev/null @@ -1,24 +0,0 @@ -// LoopFollow -// UIViewExtension.swift - -import Foundation -import UIKit - -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) - case .Right: border.frame = CGRect(x: frame.width - thickness, y: 0, width: thickness, height: frame.height) - case .Top: border.frame = CGRect(x: 0, y: 0, width: frame.width, height: thickness) - case .Bottom: border.frame = CGRect(x: 0, y: frame.height - thickness, width: frame.width, height: thickness) - } - layer.addSublayer(border) - } -} diff --git a/LoopFollow/Helpers/BackgroundRefreshManager.swift b/LoopFollow/Helpers/BackgroundRefreshManager.swift index a1168174d..ee34a4d3c 100644 --- a/LoopFollow/Helpers/BackgroundRefreshManager.swift +++ b/LoopFollow/Helpers/BackgroundRefreshManager.swift @@ -61,36 +61,6 @@ class BackgroundRefreshManager { } private func getMainViewController() -> MainViewController? { - guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let window = windowScene.windows.first, - let rootVC = window.rootViewController - else { - return nil - } - - if let mainVC = rootVC as? MainViewController { - return mainVC - } - - if let navVC = rootVC as? UINavigationController, - let mainVC = navVC.viewControllers.first as? MainViewController - { - return mainVC - } - - if let tabVC = rootVC as? UITabBarController { - for vc in tabVC.viewControllers ?? [] { - if let mainVC = vc as? MainViewController { - return mainVC - } - if let navVC = vc as? UINavigationController, - let mainVC = navVC.viewControllers.first as? MainViewController - { - return mainVC - } - } - } - - return nil + MainViewController.shared } } diff --git a/LoopFollow/Info.plist b/LoopFollow/Info.plist index 9e0f99340..dd64ccb3b 100644 --- a/LoopFollow/Info.plist +++ b/LoopFollow/Info.plist @@ -66,25 +66,6 @@ NSSupportsLiveActivities - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate - UISceneStoryboardFile - Main - - - - UIBackgroundModes audio @@ -97,9 +78,7 @@ UILaunchStoryboardName LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities +UIRequiredDeviceCapabilities armv7 diff --git a/LoopFollow/InfoTable/InfoData.swift b/LoopFollow/InfoTable/InfoData.swift index f98e58b80..30f5bf9e9 100644 --- a/LoopFollow/InfoTable/InfoData.swift +++ b/LoopFollow/InfoTable/InfoData.swift @@ -3,11 +3,13 @@ import Foundation -class InfoData { - var name: String +class InfoData: Identifiable { + let id: Int + let name: String var value: String - init(name: String, value: String = "") { + init(id: Int, name: String, value: String = "") { + self.id = id self.name = name self.value = value } diff --git a/LoopFollow/InfoTable/InfoManager.swift b/LoopFollow/InfoTable/InfoManager.swift index f6af82629..f3205511e 100644 --- a/LoopFollow/InfoTable/InfoManager.swift +++ b/LoopFollow/InfoTable/InfoManager.swift @@ -1,22 +1,20 @@ // LoopFollow // InfoManager.swift +import Combine import Foundation import HealthKit -import UIKit -class InfoManager { - var tableData: [InfoData] - weak var tableView: UITableView? +class InfoManager: ObservableObject { + @Published var tableData: [InfoData] - init(tableView: UITableView) { - tableData = InfoType.allCases.map { InfoData(name: $0.name) } - self.tableView = tableView + init() { + tableData = InfoType.allCases.map { InfoData(id: $0.rawValue, name: $0.name) } } func updateInfoData(type: InfoType, value: String) { tableData[type.rawValue].value = value - tableView?.reloadData() + objectWillChange.send() } func updateInfoData(type: InfoType, value: HKQuantity) { @@ -55,33 +53,22 @@ class InfoManager { func clearInfoData(type: InfoType) { tableData[type.rawValue].value = "" - tableView?.reloadData() + objectWillChange.send() } func clearInfoData(types: [InfoType]) { for type in types { tableData[type.rawValue].value = "" } - tableView?.reloadData() + objectWillChange.send() } - func numberOfRows() -> Int { - return Storage.shared.infoSort.value.filter { Storage.shared.infoVisible.value[$0] }.count - } - - func dataForIndexPath(_ indexPath: IndexPath) -> InfoData? { - let sortedAndVisibleIndexes = Storage.shared.infoSort.value.filter { Storage.shared.infoVisible.value[$0] } - - guard indexPath.row < sortedAndVisibleIndexes.count else { - return nil - } - - let infoIndex = sortedAndVisibleIndexes[indexPath.row] - - guard infoIndex < tableData.count else { - return nil - } - - return tableData[infoIndex] + var visibleRows: [InfoData] { + Storage.shared.infoSort.value + .filter { $0 < Storage.shared.infoVisible.value.count && Storage.shared.infoVisible.value[$0] } + .compactMap { index in + guard index < tableData.count else { return nil } + return tableData[index] + } } } diff --git a/LoopFollow/InfoTable/InfoTableView.swift b/LoopFollow/InfoTable/InfoTableView.swift new file mode 100644 index 000000000..218b3fe70 --- /dev/null +++ b/LoopFollow/InfoTable/InfoTableView.swift @@ -0,0 +1,34 @@ +// LoopFollow +// InfoTableView.swift + +import SwiftUI + +struct InfoTableView: View { + @ObservedObject var infoManager: InfoManager + var timeZoneOverride: String? + + var body: some View { + List { + if let tz = timeZoneOverride { + row(name: "Time Zone", value: tz) + } + ForEach(infoManager.visibleRows) { item in + row(name: item.name, value: item.value) + } + } + .listStyle(.plain) + .environment(\.defaultMinListRowHeight, 21) + } + + private func row(name: String, value: String) -> some View { + HStack { + Text(name) + Spacer() + Text(value) + .foregroundStyle(.primary) + } + .font(.system(size: 17)) + .frame(height: 21) + .listRowInsets(EdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 8)) + } +} diff --git a/LoopFollow/Remote/RemoteContentView.swift b/LoopFollow/Remote/RemoteContentView.swift new file mode 100644 index 000000000..d5694801e --- /dev/null +++ b/LoopFollow/Remote/RemoteContentView.swift @@ -0,0 +1,48 @@ +// LoopFollow +// RemoteContentView.swift + +import SwiftUI + +struct RemoteContentView: View { + @ObservedObject private var device = Storage.shared.device + @ObservedObject private var remoteType = Storage.shared.remoteType + + var body: some View { + Group { + switch remoteType.value { + case .nightscout: + if device.value == "Trio" { + TrioNightscoutRemoteView() + } else { + NoRemoteView() + } + + case .trc: + if device.value == "Trio" { + TrioRemoteControlView(viewModel: TrioRemoteControlViewModel()) + } else { + Text("Trio Remote Control is only supported for 'Trio'") + } + + case .loopAPNS: + LoopAPNSRemoteView() + + case .none: + Text("Please select a Remote Type in Settings.") + } + } + .onAppear { + verifyNightscoutAuth() + } + } + + private func verifyNightscoutAuth() { + guard remoteType.value == .nightscout, !Storage.shared.nsWriteAuth.value else { return } + NightscoutUtils.verifyURLAndToken { _, _, nsWriteAuth, nsAdminAuth in + DispatchQueue.main.async { + Storage.shared.nsWriteAuth.value = nsWriteAuth + Storage.shared.nsAdminAuth.value = nsAdminAuth + } + } + } +} diff --git a/LoopFollow/Remote/RemoteViewController.swift b/LoopFollow/Remote/RemoteViewController.swift deleted file mode 100644 index 56e317da3..000000000 --- a/LoopFollow/Remote/RemoteViewController.swift +++ /dev/null @@ -1,126 +0,0 @@ -// LoopFollow -// RemoteViewController.swift - -import Combine -import SwiftUI -import UIKit - -class RemoteViewController: UIViewController { - private var cancellables = Set() - private var hostingController: UIHostingController? - - override func viewDidLoad() { - super.viewDidLoad() - - // Apply initial appearance - overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle - - Storage.shared.device.$value - .removeDuplicates() - .sink { [weak self] _ in - DispatchQueue.main.async { - self?.updateView() - } - } - .store(in: &cancellables) - - // Listen for appearance setting changes - Storage.shared.appearanceMode.$value - .receive(on: DispatchQueue.main) - .sink { [weak self] mode in - self?.overrideUserInterfaceStyle = mode.userInterfaceStyle - self?.hostingController?.overrideUserInterfaceStyle = mode.userInterfaceStyle - } - .store(in: &cancellables) - - // Listen for system appearance changes (when in System mode) - NotificationCenter.default.publisher(for: .appearanceDidChange) - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - let style = Storage.shared.appearanceMode.value.userInterfaceStyle - self?.overrideUserInterfaceStyle = style - self?.hostingController?.overrideUserInterfaceStyle = style - } - .store(in: &cancellables) - } - - private func updateView() { - let remoteType = Storage.shared.remoteType.value - - if let existingHostingController = hostingController { - existingHostingController.willMove(toParent: nil) - existingHostingController.view.removeFromSuperview() - existingHostingController.removeFromParent() - } - - if remoteType == .nightscout { - var remoteView: AnyView - - switch Storage.shared.device.value { - case "Trio": - remoteView = AnyView(TrioNightscoutRemoteView()) - default: - remoteView = AnyView(NoRemoteView()) - } - - hostingController = UIHostingController(rootView: remoteView) - } else if remoteType == .trc { - if Storage.shared.device.value != "Trio" { - hostingController = UIHostingController( - rootView: AnyView( - Text("Trio Remote Control is only supported for 'Trio'") - ) - ) - } else { - let trioRemoteControlViewModel = TrioRemoteControlViewModel() - let trioRemoteControlView = TrioRemoteControlView(viewModel: trioRemoteControlViewModel) - hostingController = UIHostingController(rootView: AnyView(trioRemoteControlView)) - } - } else if remoteType == .loopAPNS { - hostingController = UIHostingController(rootView: AnyView(LoopAPNSRemoteView())) - } else { - hostingController = UIHostingController(rootView: AnyView(Text("Please select a Remote Type in Settings."))) - } - - if let hostingController = hostingController { - addChild(hostingController) - view.addSubview(hostingController.view) - - hostingController.view.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - 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.didMove(toParent: self) - } - - if remoteType == .nightscout, !Storage.shared.nsWriteAuth.value { - NightscoutUtils.verifyURLAndToken { _, _, nsWriteAuth, nsAdminAuth in - DispatchQueue.main.async { - Storage.shared.nsWriteAuth.value = nsWriteAuth - Storage.shared.nsAdminAuth.value = nsAdminAuth - } - } - } - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - updateView() - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - if Storage.shared.appearanceMode.value == .system, - previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle - { - let style = Storage.shared.appearanceMode.value.userInterfaceStyle - overrideUserInterfaceStyle = style - hostingController?.overrideUserInterfaceStyle = style - } - } -} diff --git a/LoopFollow/Settings/HomeContentView.swift b/LoopFollow/Settings/HomeContentView.swift index fe88cb569..0527aedd7 100644 --- a/LoopFollow/Settings/HomeContentView.swift +++ b/LoopFollow/Settings/HomeContentView.swift @@ -14,28 +14,9 @@ struct HomeContentView: UIViewControllerRepresentable { } func makeUIViewController(context _: Context) -> UIViewController { - let storyboard = UIStoryboard(name: "Main", bundle: nil) - - // Get the MainViewController from storyboard - guard let mainVC = storyboard.instantiateViewController(withIdentifier: "MainViewController") as? MainViewController else { - let fallbackVC = UIViewController() - fallbackVC.view.backgroundColor = .systemBackground - let label = UILabel() - label.text = "Unable to load Home screen" - label.textAlignment = .center - label.translatesAutoresizingMaskIntoConstraints = false - fallbackVC.view.addSubview(label) - NSLayoutConstraint.activate([ - label.centerXAnchor.constraint(equalTo: fallbackVC.view.centerXAnchor), - label.centerYAnchor.constraint(equalTo: fallbackVC.view.centerYAnchor), - ]) - return fallbackVC - } - + let mainVC = MainViewController() mainVC.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle - mainVC.isPresentedAsModal = isModal - return mainVC } diff --git a/LoopFollow/Settings/SettingsMenuView.swift b/LoopFollow/Settings/SettingsMenuView.swift index 80ae07f16..fb99a942a 100644 --- a/LoopFollow/Settings/SettingsMenuView.swift +++ b/LoopFollow/Settings/SettingsMenuView.swift @@ -222,37 +222,7 @@ struct AggregatedStatsViewWrapper: View { } private func getMainViewController() -> MainViewController? { - guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let window = windowScene.windows.first, - let rootVC = window.rootViewController - else { - return nil - } - - if let mainVC = rootVC as? MainViewController { - return mainVC - } - - if let navVC = rootVC as? UINavigationController, - let mainVC = navVC.viewControllers.first as? MainViewController - { - return mainVC - } - - if let tabVC = rootVC as? UITabBarController { - for vc in tabVC.viewControllers ?? [] { - if let mainVC = vc as? MainViewController { - return mainVC - } - if let navVC = vc as? UINavigationController, - let mainVC = navVC.viewControllers.first as? MainViewController - { - return mainVC - } - } - } - - return nil + MainViewController.shared } } diff --git a/LoopFollow/Snoozer/SnoozerViewController.swift b/LoopFollow/Snoozer/SnoozerViewController.swift deleted file mode 100644 index ea63d8e0f..000000000 --- a/LoopFollow/Snoozer/SnoozerViewController.swift +++ /dev/null @@ -1,65 +0,0 @@ -// LoopFollow -// SnoozerViewController.swift - -import Combine -import SwiftUI -import UIKit - -class SnoozerViewController: UIViewController { - private var hostingController: UIHostingController? - private var cancellables = Set() - - @State private var snoozeMinutes = 15 - - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .black - - let snoozerView = SnoozerView() - - let hosting = UIHostingController(rootView: snoozerView) - hostingController = hosting - - // Apply initial appearance - hosting.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle - - // Listen for appearance setting changes - Storage.shared.appearanceMode.$value - .receive(on: DispatchQueue.main) - .sink { [weak self] mode in - self?.hostingController?.overrideUserInterfaceStyle = mode.userInterfaceStyle - } - .store(in: &cancellables) - - // Listen for system appearance changes (when in System mode) - NotificationCenter.default.publisher(for: .appearanceDidChange) - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - self?.hostingController?.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle - } - .store(in: &cancellables) - - 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) - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - if Storage.shared.appearanceMode.value == .system, - previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle - { - hostingController?.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle - } - } -} diff --git a/LoopFollow/Storage/Observable.swift b/LoopFollow/Storage/Observable.swift index f32887523..7ea134926 100644 --- a/LoopFollow/Storage/Observable.swift +++ b/LoopFollow/Storage/Observable.swift @@ -24,6 +24,12 @@ class Observable { var deltaText = ObservableValue(default: "+0") var iobText = ObservableValue(default: "--") + var serverText = ObservableValue(default: "Server") + var loopStatusText = ObservableValue(default: "") + var loopStatusColor = ObservableValue(default: .primary) + var predictionText = ObservableValue(default: "") + var predictionColor = ObservableValue(default: .purple) + var currentAlarm = ObservableValue(default: nil) var alarmSoundPlaying = ObservableValue(default: false) @@ -46,5 +52,8 @@ class Observable { var isNotLooping = ObservableValue(default: false) + /// Selected tab index used by SwiftUI TabView — set from MainViewController to switch tabs + var selectedTabIndex = ObservableValue(default: 0) + private init() {} } diff --git a/LoopFollow/Task/MinAgoTask.swift b/LoopFollow/Task/MinAgoTask.swift index 0ea011cee..0ba964d99 100644 --- a/LoopFollow/Task/MinAgoTask.swift +++ b/LoopFollow/Task/MinAgoTask.swift @@ -2,7 +2,6 @@ // MinAgoTask.swift import Foundation -import UIKit extension MainViewController { func scheduleMinAgoTask(initialDelay: TimeInterval = 1.0) { @@ -15,9 +14,7 @@ extension MainViewController { func minAgoTaskAction() { guard bgData.count > 0, let lastBG = bgData.last else { - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - self.MinAgoText.text = "" + DispatchQueue.main.async { Observable.shared.minAgoText.value = "" Observable.shared.bgText.value = "" } @@ -46,9 +43,7 @@ extension MainViewController { // Update UI only if the display text has changed if minAgoDisplayText != Observable.shared.minAgoText.value { - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - self.MinAgoText.text = minAgoDisplayText + DispatchQueue.main.async { Observable.shared.minAgoText.value = minAgoDisplayText } } @@ -56,19 +51,12 @@ extension MainViewController { let deltaTime = secondsAgo / 60 Observable.shared.bgStale.value = deltaTime >= 12 - // Apply strikethrough to BGText based on the staleness of the data - // Also clear badge if bgvalue is stale - let bgTextStr = BGText.text ?? "" - let attributeString = NSMutableAttributedString(string: bgTextStr) - attributeString.addAttribute(.strikethroughStyle, value: NSUnderlineStyle.single.rawValue, range: NSRange(location: 0, length: attributeString.length)) - if Observable.shared.bgStale.value { // Data is stale - attributeString.addAttribute(.strikethroughColor, value: UIColor.systemRed, range: NSRange(location: 0, length: attributeString.length)) + // Update badge based on staleness + if Observable.shared.bgStale.value { updateBadge(val: 0) - } else { // Data is fresh - attributeString.addAttribute(.strikethroughColor, value: UIColor.clear, range: NSRange(location: 0, length: attributeString.length)) + } else { updateBadge(val: Observable.shared.bg.value ?? 0) } - BGText.attributedText = attributeString // Determine the next run interval based on the current state let nextUpdateInterval: TimeInterval diff --git a/LoopFollow/Treatments/TreatmentsView.swift b/LoopFollow/Treatments/TreatmentsView.swift index da6d97182..f1b1c7595 100644 --- a/LoopFollow/Treatments/TreatmentsView.swift +++ b/LoopFollow/Treatments/TreatmentsView.swift @@ -826,25 +826,7 @@ class TreatmentDetailViewModel: ObservableObject { } private func getMainViewController() -> MainViewController? { - guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let window = windowScene.windows.first, - let tabBarController = window.rootViewController as? UITabBarController - else { - return nil - } - - for vc in tabBarController.viewControllers ?? [] { - if let mainVC = vc as? MainViewController { - return mainVC - } - if let navVC = vc as? UINavigationController, - let mainVC = navVC.viewControllers.first as? MainViewController - { - return mainVC - } - } - - return nil + MainViewController.shared } } @@ -1417,26 +1399,7 @@ class TreatmentsViewModel: ObservableObject { } private func getMainViewController() -> MainViewController? { - // Try to find MainViewController in the app's window hierarchy - guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let window = windowScene.windows.first, - let tabBarController = window.rootViewController as? UITabBarController - else { - return nil - } - - for vc in tabBarController.viewControllers ?? [] { - if let mainVC = vc as? MainViewController { - return mainVC - } - if let navVC = vc as? UINavigationController, - let mainVC = navVC.viewControllers.first as? MainViewController - { - return mainVC - } - } - - return nil + MainViewController.shared } } diff --git a/LoopFollow/ViewControllers/AlarmViewController.swift b/LoopFollow/ViewControllers/AlarmViewController.swift deleted file mode 100644 index 1b3c4d60b..000000000 --- a/LoopFollow/ViewControllers/AlarmViewController.swift +++ /dev/null @@ -1,60 +0,0 @@ -// LoopFollow -// AlarmViewController.swift - -import Combine -import SwiftUI -import UIKit - -class AlarmViewController: UIViewController { - private var hostingController: UIHostingController! - private var cancellables = Set() - - override func viewDidLoad() { - super.viewDidLoad() - - let alarmsView = AlarmsContainerView() - hostingController = UIHostingController(rootView: alarmsView) - - // Apply initial appearance - hostingController.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle - - // Listen for appearance setting changes - Storage.shared.appearanceMode.$value - .receive(on: DispatchQueue.main) - .sink { [weak self] mode in - self?.hostingController.overrideUserInterfaceStyle = mode.userInterfaceStyle - } - .store(in: &cancellables) - - // Listen for system appearance changes (when in System mode) - NotificationCenter.default.publisher(for: .appearanceDidChange) - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - self?.hostingController.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle - } - .store(in: &cancellables) - - addChild(hostingController) - view.addSubview(hostingController.view) - - hostingController.view.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - 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.didMove(toParent: self) - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - if Storage.shared.appearanceMode.value == .system, - previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle - { - hostingController.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle - } - } -} diff --git a/LoopFollow/ViewControllers/BGDisplayView.swift b/LoopFollow/ViewControllers/BGDisplayView.swift new file mode 100644 index 000000000..ae626fa47 --- /dev/null +++ b/LoopFollow/ViewControllers/BGDisplayView.swift @@ -0,0 +1,74 @@ +// LoopFollow +// BGDisplayView.swift + +import SwiftUI + +struct BGDisplayView: View { + @ObservedObject var serverText = Observable.shared.serverText + @ObservedObject var bgText = Observable.shared.bgText + @ObservedObject var bgTextColor = Observable.shared.bgTextColor + @ObservedObject var bgStale = Observable.shared.bgStale + @ObservedObject var directionText = Observable.shared.directionText + @ObservedObject var deltaText = Observable.shared.deltaText + @ObservedObject var minAgoText = Observable.shared.minAgoText + @ObservedObject var loopStatusText = Observable.shared.loopStatusText + @ObservedObject var loopStatusColor = Observable.shared.loopStatusColor + @ObservedObject var predictionText = Observable.shared.predictionText + @ObservedObject var predictionColor = Observable.shared.predictionColor + @ObservedObject var isNotLooping = Observable.shared.isNotLooping + + var onRefresh: (() -> Void)? + + var body: some View { + ScrollView { + VStack(spacing: 0) { + Text(serverText.value) + .font(.system(size: 13)) + + Text(bgText.value) + .font(.system(size: 85, weight: .black)) + .foregroundColor(bgTextColor.value) + .strikethrough( + bgStale.value, + pattern: .solid, + color: bgStale.value ? .red : .clear + ) + .frame(maxWidth: .infinity) + .lineLimit(1) + .minimumScaleFactor(0.5) + + HStack { + Text(directionText.value) + .font(.system(size: 60, weight: .black)) + Text(deltaText.value) + .font(.system(size: 32)) + } + .lineLimit(1) + .minimumScaleFactor(0.5) + + Text(minAgoText.value) + .font(.system(size: 17)) + + if isNotLooping.value { + Text(loopStatusText.value) + .font(.system(size: 18, weight: .bold)) + .foregroundColor(loopStatusColor.value) + .frame(maxWidth: .infinity) + } else { + HStack { + Spacer() + Text(loopStatusText.value) + .foregroundColor(loopStatusColor.value) + Text(predictionText.value) + .foregroundColor(predictionColor.value) + Spacer() + } + .font(.system(size: 17)) + } + } + } + .refreshable { + onRefresh?() + } + } +} diff --git a/LoopFollow/ViewControllers/LineChartWrapper.swift b/LoopFollow/ViewControllers/LineChartWrapper.swift new file mode 100644 index 000000000..7eb66b5ea --- /dev/null +++ b/LoopFollow/ViewControllers/LineChartWrapper.swift @@ -0,0 +1,15 @@ +// LoopFollow +// LineChartWrapper.swift + +import Charts +import SwiftUI + +struct LineChartWrapper: UIViewRepresentable { + let chartView: LineChartView + + func makeUIView(context _: Context) -> LineChartView { + chartView + } + + func updateUIView(_: LineChartView, context _: Context) {} +} diff --git a/LoopFollow/ViewControllers/MainHomeView.swift b/LoopFollow/ViewControllers/MainHomeView.swift new file mode 100644 index 000000000..6e933bbb8 --- /dev/null +++ b/LoopFollow/ViewControllers/MainHomeView.swift @@ -0,0 +1,70 @@ +// LoopFollow +// MainHomeView.swift + +import Charts +import SwiftUI + +struct MainHomeView: View { + let bgChart: LineChartView + let bgChartFull: LineChartView + @ObservedObject var infoManager: InfoManager + @ObservedObject var statsModel: StatsDisplayModel + + @ObservedObject var showSmallGraph = Storage.shared.showSmallGraph + @ObservedObject var showStats = Storage.shared.showStats + @ObservedObject var hideInfoTable = Storage.shared.hideInfoTable + @ObservedObject var smallGraphHeight = Storage.shared.smallGraphHeight + @ObservedObject var url = Storage.shared.url + @ObservedObject var graphTimeZoneEnabled = Storage.shared.graphTimeZoneEnabled + @ObservedObject var graphTimeZoneIdentifier = Storage.shared.graphTimeZoneIdentifier + + var onRefresh: (() -> Void)? + var onStatsTap: (() -> Void)? + + private var timeZoneOverride: String? { + guard graphTimeZoneEnabled.value, + let tz = TimeZone(identifier: graphTimeZoneIdentifier.value) + else { return nil } + return tz.identifier + } + + private var isNightscoutEnabled: Bool { + !url.value.isEmpty + } + + var body: some View { + VStack(spacing: 8) { + // Top section: BG display + info table + HStack(spacing: 10) { + BGDisplayView(onRefresh: onRefresh) + + if isNightscoutEnabled && !hideInfoTable.value { + InfoTableView(infoManager: infoManager, timeZoneOverride: timeZoneOverride) + .frame(minWidth: 160, maxWidth: 250) + .overlay( + Rectangle() + .fill(Color(UIColor.darkGray)) + .frame(width: 2), + alignment: .leading + ) + } + } + .fixedSize(horizontal: false, vertical: true) + + // Main chart (fills remaining space) + LineChartWrapper(chartView: bgChart) + + // Small overview chart + if showSmallGraph.value { + LineChartWrapper(chartView: bgChartFull) + .frame(height: CGFloat(smallGraphHeight.value)) + } + + // Statistics + if showStats.value { + StatsDisplayView(model: statsModel, onTap: onStatsTap) + } + } + .padding(8) + } +} diff --git a/LoopFollow/ViewControllers/MainViewController.swift b/LoopFollow/ViewControllers/MainViewController.swift index ac1f19a24..91f314789 100644 --- a/LoopFollow/ViewControllers/MainViewController.swift +++ b/LoopFollow/ViewControllers/MainViewController.swift @@ -23,32 +23,19 @@ private struct APNSCredentialSnapshot: Equatable { let lfKeyId: String } -class MainViewController: UIViewController, UITableViewDataSource, ChartViewDelegate, UNUserNotificationCenterDelegate, UIScrollViewDelegate { +class MainViewController: UIViewController, ChartViewDelegate, UNUserNotificationCenterDelegate { + /// Singleton reference set during viewDidLoad. Used by code that needs + /// to reach MainViewController without walking the view hierarchy. + private(set) weak static var shared: MainViewController? + var isPresentedAsModal: Bool = false - @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! + var BGChart: LineChartView! + var BGChartFull: LineChartView! + var statsDisplayModel = StatsDisplayModel() + + /// The hosting controller's view — hidden during loading / first-time setup. + private var mainContentView: UIView! // Setup buttons for first-time configuration private var setupNightscoutButton: UIButton! @@ -133,7 +120,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele let contactImageUpdater = ContactImageUpdater() private var cancellables = Set() - private var isViewHierarchyReady = false // Loading state management private var loadingOverlay: UIView? @@ -145,8 +131,49 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele ] private var loadingTimeoutTimer: Timer? + // MARK: - Programmatic UI Setup + + private func setupUI() { + view.backgroundColor = .systemBackground + + BGChart = LineChartView() + BGChart.backgroundColor = .systemBackground + + BGChartFull = LineChartView() + BGChartFull.backgroundColor = .systemBackground + + infoManager = InfoManager() + + let mainView = MainHomeView( + bgChart: BGChart, + bgChartFull: BGChartFull, + infoManager: infoManager, + statsModel: statsDisplayModel, + onRefresh: { [weak self] in self?.refresh() }, + onStatsTap: { [weak self] in self?.statsViewTapped() } + ) + let hosting = UIHostingController(rootView: mainView) + hosting.view.translatesAutoresizingMaskIntoConstraints = false + hosting.view.backgroundColor = .clear + + addChild(hosting) + view.addSubview(hosting.view) + let safeArea = view.safeAreaLayoutGuide + NSLayoutConstraint.activate([ + hosting.view.topAnchor.constraint(equalTo: safeArea.topAnchor), + hosting.view.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor), + hosting.view.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor), + hosting.view.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor), + ]) + hosting.didMove(toParent: self) + mainContentView = hosting.view + } + override func viewDidLoad() { super.viewDidLoad() + MainViewController.shared = self + + setupUI() loadDebugData() @@ -156,29 +183,13 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele // Synchronize info types to ensure arrays are the correct size 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) - - 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) - // setup show/hide small graph and stats + // setup show/hide graphs (first-time setup check) updateGraphVisibility() - statsView.isHidden = !Storage.shared.showStats.value - - // Tap on stats view to open full statistics screen - let statsTap = UITapGestureRecognizer(target: self, action: #selector(statsViewTapped)) - statsView.addGestureRecognizer(statsTap) BGChart.delegate = self BGChartFull.delegate = self @@ -207,59 +218,17 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele scheduleAllTasks() - // Set up refreshScrollView for BGText - refreshScrollView = UIScrollView() - 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), - ]) - - refreshControl = UIRefreshControl() - refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged) - refreshScrollView.addSubview(refreshControl) - 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 } - .sink { [weak self] _ in - guard let self = self, - let tabBarController = self.tabBarController, - let vcs = tabBarController.viewControllers, !vcs.isEmpty, - let snoozerIndex = self.getSnoozerTabIndex(), - snoozerIndex < vcs.count else { return } - tabBarController.selectedIndex = snoozerIndex + .sink { _ in + let orderedItems = Storage.shared.orderedTabBarItems() + if let index = orderedItems.firstIndex(of: .snoozer) { + Observable.shared.selectedTabIndex.value = index + } } .store(in: &cancellables) @@ -278,13 +247,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } .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 @@ -292,13 +254,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } .store(in: &cancellables) - Storage.shared.showSmallGraph.$value - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - self?.updateGraphVisibility() - } - .store(in: &cancellables) - Storage.shared.screenlockSwitchState.$value .receive(on: DispatchQueue.main) .sink { newValue in @@ -313,20 +268,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } .store(in: &cancellables) - Storage.shared.graphTimeZoneEnabled.$value - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - self?.infoTable.reloadData() - } - .store(in: &cancellables) - - Storage.shared.graphTimeZoneIdentifier.$value - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - self?.infoTable.reloadData() - } - .store(in: &cancellables) - Storage.shared.speakBG.$value .receive(on: DispatchQueue.main) .sink { [weak self] _ in @@ -334,26 +275,9 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } .store(in: &cancellables) - // Observe all tab position changes with debouncing to handle batch updates - Publishers.MergeMany( - Storage.shared.homePosition.$value.map { _ in () }.eraseToAnyPublisher(), - Storage.shared.alarmsPosition.$value.map { _ in () }.eraseToAnyPublisher(), - Storage.shared.remotePosition.$value.map { _ in () }.eraseToAnyPublisher(), - Storage.shared.nightscoutPosition.$value.map { _ in () }.eraseToAnyPublisher(), - Storage.shared.snoozerPosition.$value.map { _ in () }.eraseToAnyPublisher(), - Storage.shared.statisticsPosition.$value.map { _ in () }.eraseToAnyPublisher(), - Storage.shared.treatmentsPosition.$value.map { _ in () }.eraseToAnyPublisher() - ) - .debounce(for: .milliseconds(100), scheduler: DispatchQueue.main) - .sink { [weak self] _ in - self?.setupTabBar() - } - .store(in: &cancellables) - Storage.shared.url.$value .receive(on: DispatchQueue.main) .sink { [weak self] _ in - self?.updateNightscoutTabState() self?.checkAndShowImportButtonIfNeeded() } .store(in: &cancellables) @@ -424,13 +348,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele updateQuickActions() - // Delay initial tab setup to ensure view hierarchy is ready - // This prevents crashes when trying to modify tabs during viewWillAppear - DispatchQueue.main.async { [weak self] in - self?.isViewHierarchyReady = true - self?.setupTabBar() - } - speechSynthesizer.delegate = self // Check configuration and show appropriate UI @@ -543,199 +460,22 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } } - private func setupTabBar() { - guard isViewHierarchyReady else { return } - - guard !isPresentedAsModal else { return } - - var tbc = tabBarController - if tbc == nil { - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let window = windowScene.windows.first, - let rootVC = window.rootViewController as? UITabBarController - { - tbc = rootVC - } - } - - guard let tabBarController = tbc else { return } - - // If settings modal is presented, skip rebuild - it will happen when settings is dismissed - if tabBarController.presentedViewController != nil { - return - } - - rebuildTabs(tabBarController: tabBarController) - } - - /// Static method to rebuild tabs from anywhere in the app - /// This is useful when the MainViewController instance may not be in the tab bar - static func rebuildTabsIfNeeded() { - guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let window = windowScene.windows.first, - let tabBarController = window.rootViewController as? UITabBarController - else { return } - - let previousSelectedIndex = tabBarController.selectedIndex - - let storyboard = UIStoryboard(name: "Main", bundle: nil) - var viewControllers: [UIViewController] = [] - - let orderedItems = Storage.shared.orderedTabBarItems() - - for (index, item) in orderedItems.prefix(4).enumerated() { - let position = TabPosition.customizablePositions[index] - if let vc = createViewControllerStatic(for: item, position: position, storyboard: storyboard) { - viewControllers.append(vc) - } - } - - // Preserve existing Menu nav controller to keep its push stack intact - let existingMenuNav = (tabBarController.viewControllers ?? []).first(where: { - $0.tabBarItem.title == "Menu" - }) - if let menuNav = existingMenuNav { - menuNav.tabBarItem = UITabBarItem(title: "Menu", image: UIImage(systemName: "line.3.horizontal"), tag: 4) - viewControllers.append(menuNav) - } else { - viewControllers.append(Self.makeMenuViewController(tag: 4)) - } - - if let presented = tabBarController.presentedViewController { - presented.dismiss(animated: false) { - tabBarController.setViewControllers(viewControllers, animated: false) - guard !viewControllers.isEmpty else { return } - let targetIndex = min(previousSelectedIndex, viewControllers.count - 1) - tabBarController.selectedIndex = targetIndex - } - } else { - tabBarController.setViewControllers(viewControllers, animated: false) - guard !viewControllers.isEmpty else { return } - let targetIndex = min(previousSelectedIndex, viewControllers.count - 1) - tabBarController.selectedIndex = targetIndex - } - } - - /// Static helper to create view controllers - private static func createViewControllerStatic(for item: TabItem, position: TabPosition, storyboard: UIStoryboard) -> UIViewController? { - let tag = position.tabIndex ?? 0 - - switch item { - case .home: - guard let mainVC = storyboard.instantiateViewController(withIdentifier: "MainViewController") as? MainViewController else { - return nil - } - mainVC.tabBarItem = UITabBarItem(title: "Home", image: UIImage(systemName: item.icon), tag: tag) - return mainVC - - case .alarms: - let vc = storyboard.instantiateViewController(withIdentifier: "AlarmViewController") - vc.tabBarItem = UITabBarItem(title: item.displayName, image: UIImage(systemName: item.icon), tag: tag) - return vc - - case .remote: - let vc = storyboard.instantiateViewController(withIdentifier: "RemoteViewController") - vc.tabBarItem = UITabBarItem(title: item.displayName, image: UIImage(systemName: item.icon), tag: tag) - return vc - - case .nightscout: - let vc = storyboard.instantiateViewController(withIdentifier: "NightscoutViewController") - vc.tabBarItem = UITabBarItem(title: item.displayName, image: UIImage(systemName: item.icon), tag: tag) - return vc - - case .snoozer: - let vc = storyboard.instantiateViewController(withIdentifier: "SnoozerViewController") - vc.tabBarItem = UITabBarItem(title: item.displayName, image: UIImage(systemName: item.icon), tag: tag) - return vc - - case .treatments: - let treatmentsVC = UIHostingController(rootView: TreatmentsView()) - treatmentsVC.tabBarItem = UITabBarItem(title: item.displayName, image: UIImage(systemName: item.icon), tag: tag) - return treatmentsVC - - case .stats: - let statsVC = UIHostingController(rootView: AggregatedStatsContentView(mainViewController: nil)) - let navController = UINavigationController(rootViewController: statsVC) - navController.tabBarItem = UITabBarItem(title: item.displayName, image: UIImage(systemName: item.icon), tag: tag) - return navController - } - } - - private func rebuildTabs(tabBarController: UITabBarController) { - let previousSelectedIndex = tabBarController.selectedIndex - - let storyboard = UIStoryboard(name: "Main", bundle: nil) - var viewControllers: [UIViewController] = [] - - let orderedItems = Storage.shared.orderedTabBarItems() - - for (index, item) in orderedItems.prefix(4).enumerated() { - let position = TabPosition.customizablePositions[index] - if let vc = createViewController(for: item, position: position, storyboard: storyboard) { - viewControllers.append(vc) - } - } - - // Preserve existing Menu nav controller to keep its push stack intact - let existingMenuNav = (tabBarController.viewControllers ?? []).first(where: { - $0.tabBarItem.title == "Menu" - }) - if let menuNav = existingMenuNav { - menuNav.tabBarItem = UITabBarItem(title: "Menu", image: UIImage(systemName: "line.3.horizontal"), tag: 4) - viewControllers.append(menuNav) - } else { - viewControllers.append(Self.makeMenuViewController(tag: 4)) - } - - tabBarController.setViewControllers(viewControllers, animated: false) - - guard !viewControllers.isEmpty else { return } - let targetIndex = min(previousSelectedIndex, viewControllers.count - 1) - tabBarController.selectedIndex = targetIndex - - updateNightscoutTabState() - } - @objc private func navigateOnLAForeground() { - guard let tabBarController = tabBarController, - let vcs = tabBarController.viewControllers, !vcs.isEmpty else { return } - - let targetIndex: Int + let orderedItems = Storage.shared.orderedTabBarItems() if Observable.shared.currentAlarm.value != nil, - let snoozerIndex = getSnoozerTabIndex(), snoozerIndex < vcs.count + let snoozerIndex = orderedItems.firstIndex(of: .snoozer) { - targetIndex = snoozerIndex - } else { - targetIndex = 0 - } - - if let presented = tabBarController.presentedViewController { - presented.dismiss(animated: false) { - tabBarController.selectedIndex = targetIndex - } + Observable.shared.selectedTabIndex.value = snoozerIndex } else { - tabBarController.selectedIndex = targetIndex + Observable.shared.selectedTabIndex.value = 0 } } - private func getSnoozerTabIndex() -> Int? { - guard let tabBarController = tabBarController, - let viewControllers = tabBarController.viewControllers else { return nil } - - for (index, vc) in viewControllers.enumerated() { - if let _ = vc as? SnoozerViewController { - return index - } - } - - return nil - } - @objc private func statsViewTapped() { #if !targetEnvironment(macCatalyst) - let position = Storage.shared.position(for: .stats).normalized - if position != .menu, let tabIndex = position.tabIndex, let tbc = tabBarController { - tbc.selectedIndex = tabIndex + let orderedItems = Storage.shared.orderedTabBarItems() + if let statsIndex = orderedItems.firstIndex(of: .stats) { + Observable.shared.selectedTabIndex.value = statsIndex return } #endif @@ -747,94 +487,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele present(hostingController, animated: true) } - private func createViewController(for item: TabItem, position: TabPosition, storyboard: UIStoryboard) -> UIViewController? { - let tag = position.tabIndex ?? 0 - - switch item { - case .home: - tabBarItem = UITabBarItem(title: "Home", image: UIImage(systemName: item.icon), tag: tag) - return self - - case .alarms: - let vc = storyboard.instantiateViewController(withIdentifier: "AlarmViewController") - vc.tabBarItem = UITabBarItem(title: item.displayName, image: UIImage(systemName: item.icon), tag: tag) - return vc - - case .remote: - let vc = storyboard.instantiateViewController(withIdentifier: "RemoteViewController") - vc.tabBarItem = UITabBarItem(title: item.displayName, image: UIImage(systemName: item.icon), tag: tag) - return vc - - case .nightscout: - let vc = storyboard.instantiateViewController(withIdentifier: "NightscoutViewController") - vc.tabBarItem = UITabBarItem(title: item.displayName, image: UIImage(systemName: item.icon), tag: tag) - return vc - - case .snoozer: - let vc = storyboard.instantiateViewController(withIdentifier: "SnoozerViewController") - vc.tabBarItem = UITabBarItem(title: item.displayName, image: UIImage(systemName: item.icon), tag: tag) - return vc - - case .treatments: - let treatmentsVC = UIHostingController(rootView: TreatmentsView()) - treatmentsVC.tabBarItem = UITabBarItem(title: item.displayName, image: UIImage(systemName: item.icon), tag: tag) - return treatmentsVC - - case .stats: - let statsVC = UIHostingController(rootView: AggregatedStatsContentView(mainViewController: self)) - let navController = UINavigationController(rootViewController: statsVC) - navController.tabBarItem = UITabBarItem(title: item.displayName, image: UIImage(systemName: item.icon), tag: tag) - return navController - } - } - - private static func makeMenuViewController(tag: Int) -> UIViewController { - let menuVC = MoreMenuViewController() - let navController = UINavigationController(rootViewController: menuVC) - navController.navigationBar.prefersLargeTitles = true - navController.tabBarItem = UITabBarItem(title: "Menu", image: UIImage(systemName: "line.3.horizontal"), tag: tag) - navController.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle - return navController - } - - private func createComingSoonViewController(title: String, icon: String) -> UIViewController { - let vc = UIViewController() - vc.view.backgroundColor = .systemBackground - - let stackView = UIStackView() - stackView.axis = .vertical - stackView.alignment = .center - stackView.spacing = 16 - stackView.translatesAutoresizingMaskIntoConstraints = false - - let imageView = UIImageView(image: UIImage(systemName: icon)) - imageView.tintColor = .secondaryLabel - imageView.contentMode = .scaleAspectFit - imageView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - imageView.widthAnchor.constraint(equalToConstant: 60), - imageView.heightAnchor.constraint(equalToConstant: 60), - ]) - - let titleLabel = UILabel() - titleLabel.text = title - titleLabel.font = .preferredFont(forTextStyle: .title1) - titleLabel.textColor = .label - - stackView.addArrangedSubview(imageView) - stackView.addArrangedSubview(titleLabel) - - vc.view.addSubview(stackView) - NSLayoutConstraint.activate([ - stackView.centerXAnchor.constraint(equalTo: vc.view.centerXAnchor), - stackView.centerYAnchor.constraint(equalTo: vc.view.centerYAnchor), - ]) - - vc.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle - - return vc - } - // 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" @@ -879,98 +531,30 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } } - MinAgoText.text = "Refreshing" Observable.shared.minAgoText.value = "Refreshing" scheduleAllTasks() currentCage = nil currentSage = nil currentIage = nil - refreshControl.endRefreshing() - } - - // Scroll down BGText when refreshing - func scrollViewDidScroll(_ scrollView: UIScrollView) { - if scrollView == refreshScrollView { - let yOffset = scrollView.contentOffset.y - if yOffset < 0 { - BGText.transform = CGAffineTransform(translationX: 0, y: -yOffset) - } else { - BGText.transform = CGAffineTransform.identity - } - } } - override func viewWillAppear(_: Bool) { + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) UIApplication.shared.isIdleTimerDisabled = Storage.shared.screenlockSwitchState.value - infoTable.reloadData() if Observable.shared.chartSettingsChanged.value { updateBGGraphSettings() - - smallGraphHeightConstraint.constant = CGFloat(Storage.shared.smallGraphHeight.value) - view.layoutIfNeeded() - Observable.shared.chartSettingsChanged.value = false } } - private var timeZoneOverrideInfoValue: String? { - guard Storage.shared.graphTimeZoneEnabled.value, - let overrideTimeZone = TimeZone(identifier: Storage.shared.graphTimeZoneIdentifier.value) - else { - return nil - } - - return overrideTimeZone.identifier - } - - // Info Table Functions - func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { - guard let infoManager = infoManager else { - return 0 - } - let overrideRowCount = timeZoneOverrideInfoValue == nil ? 0 : 1 - return infoManager.numberOfRows() + overrideRowCount - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "LabelCell", for: indexPath) - - if indexPath.row == 0, let timeZoneOverrideInfoValue { - cell.textLabel?.text = "Time Zone" - cell.detailTextLabel?.text = timeZoneOverrideInfoValue - return cell - } - - let adjustedIndexPath: IndexPath - if timeZoneOverrideInfoValue != nil { - adjustedIndexPath = IndexPath(row: indexPath.row - 1, section: indexPath.section) - } else { - adjustedIndexPath = indexPath - } - - if let values = infoManager.dataForIndexPath(adjustedIndexPath) { - cell.textLabel?.text = values.name - cell.detailTextLabel?.text = values.value - } else { - cell.textLabel?.text = "" - cell.detailTextLabel?.text = "" - } - - return cell - } - @objc func appMovedToBackground() { // Allow screen to turn off UIApplication.shared.isIdleTimerDisabled = false // We want to always come back to the home screen - if let tabBarController = tabBarController, - let vcs = tabBarController.viewControllers, !vcs.isEmpty - { - tabBarController.selectedIndex = 0 - } + Observable.shared.selectedTabIndex.value = 0 if Storage.shared.backgroundRefreshType.value == .silentTune { backgroundTask.startBackgroundTask() @@ -1139,7 +723,8 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } } - @objc override func viewDidAppear(_: Bool) { + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) showHideNSDetails() #if !targetEnvironment(macCatalyst) LiveActivityManager.shared.startFromCurrentState() @@ -1153,42 +738,8 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele return String(format: "%02d:%02d", hours, minutes) } - private func updateNightscoutTabState() { - guard let tabBarController = tabBarController, - let viewControllers = tabBarController.viewControllers else { return } - - let isNightscoutEnabled = !Storage.shared.url.value.isEmpty - - for (index, vc) in viewControllers.enumerated() { - if vc is NightscoutViewController { - tabBarController.tabBar.items?[index].isEnabled = isNightscoutEnabled - } - } - } - func showHideNSDetails() { - if isInitialLoad || !isDataSourceConfigured() { - return - } - - var isHidden = false - if !IsNightscoutEnabled() { - isHidden = true - } - - LoopStatusLabel.isHidden = isHidden - if IsNotLooping { - PredictionLabel.isHidden = true - } else { - PredictionLabel.isHidden = isHidden - } - infoTable.isHidden = isHidden - - if Storage.shared.hideInfoTable.value { - infoTable.isHidden = true - } - - updateNightscoutTabState() + // Info table visibility is handled reactively by MainHomeView. } func updateBadge(val: Int) { @@ -1203,29 +754,17 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele func updateBGTextAppearance() { if bgData.count > 0 { let latestBG = bgData[bgData.count - 1].sgv - var color = NSUIColor.label if Storage.shared.colorBGText.value { if Double(latestBG) >= Storage.shared.highLine.value { - color = NSUIColor.systemYellow 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 - - if latestBG <= globalVariables.minDisplayGlucose || latestBG >= globalVariables.maxDisplayGlucose { - BGText.font = UIFont.systemFont(ofSize: 65, weight: .black) - } else { - BGText.font = UIFont.systemFont(ofSize: 85, weight: .black) - } } } @@ -1247,25 +786,10 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele // Update this view controller overrideUserInterfaceStyle = style - // Update the tab bar controller (affects all tabs) - tabBarController?.overrideUserInterfaceStyle = style - // Update the window (affects the entire app including modals) window.overrideUserInterfaceStyle = style } - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - // When system appearance changes and we're in "System" mode, notify all observers - if Storage.shared.appearanceMode.value == .system, - previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle - { - // Post notification so other view controllers can update if needed - NotificationCenter.default.post(name: .appearanceDidChange, object: nil) - } - } - 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": "-", "": "-"] @@ -1578,15 +1102,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele present(navController, animated: true) } - private func hideGraphs() { - BGChart.isHidden = true - BGChartFull.isHidden = true - } - - private func showGraphs() { - updateGraphVisibility() - } - private func makeCloseBarButtonItem() -> UIBarButtonItem { let button = UIBarButtonItem(barButtonSystemItem: .close, target: self, action: #selector(dismissModal)) button.tintColor = .systemBlue @@ -1594,61 +1109,18 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele } private func hideAllDataUI() { - // Hide graphs - BGChart.isHidden = true - BGChartFull.isHidden = true - - // Hide BG display elements - BGText.isHidden = true - DeltaText.isHidden = true - DirectionText.isHidden = true - MinAgoText.isHidden = true - serverText.isHidden = true - - // Hide info table and stats - infoTable.isHidden = true - statsView.isHidden = true - - // Hide loop status and prediction - LoopStatusLabel.isHidden = true - PredictionLabel.isHidden = true + mainContentView?.isHidden = true } private func showAllDataUI() { - // Show BG display elements - BGText.isHidden = false - DeltaText.isHidden = false - DirectionText.isHidden = false - MinAgoText.isHidden = false - serverText.isHidden = false - - // Show graphs based on settings - updateGraphVisibility() - - // Show/hide info table and stats based on user settings - let isNightscoutEnabled = IsNightscoutEnabled() - if isNightscoutEnabled { - infoTable.isHidden = Storage.shared.hideInfoTable.value - LoopStatusLabel.isHidden = false - PredictionLabel.isHidden = IsNotLooping - } else { - infoTable.isHidden = true - LoopStatusLabel.isHidden = true - PredictionLabel.isHidden = true - } - - statsView.isHidden = !Storage.shared.showStats.value + mainContentView?.isHidden = false } private func updateGraphVisibility() { - let isFirstTimeSetup = !isDataSourceConfigured() - - if isFirstTimeSetup { - BGChart.isHidden = true - BGChartFull.isHidden = true - } else { - BGChart.isHidden = false - BGChartFull.isHidden = !Storage.shared.showSmallGraph.value + // Graph and component visibility is handled reactively by MainHomeView. + // This method now only manages the overall content visibility for first-time setup. + if !isDataSourceConfigured() { + mainContentView?.isHidden = true } } diff --git a/LoopFollow/ViewControllers/MoreMenuView.swift b/LoopFollow/ViewControllers/MoreMenuView.swift new file mode 100644 index 000000000..6321107f1 --- /dev/null +++ b/LoopFollow/ViewControllers/MoreMenuView.swift @@ -0,0 +1,214 @@ +// LoopFollow +// MoreMenuView.swift + +import SwiftUI + +struct MoreMenuView: View { + @State private var latestVersion: String? + @State private var versionTint: Color = .secondary + @State private var showShareSheet = false + @State private var shareFiles: [URL] = [] + @State private var alertTitle = "" + @State private var alertMessage = "" + @State private var showAlert = false + @State private var showSettingsView = false + @State private var showAlarmsView = false + @State private var showRemoteView = false + @State private var showNightscoutView = false + @State private var showSnoozerView = false + @State private var showTreatmentsView = false + @State private var showStatsView = false + @State private var showHomeView = false + @State private var showLogView = false + + var body: some View { + List { + // Settings + Section { + Button { showSettingsView = true } label: { + Label("Settings", systemImage: "gearshape") + .foregroundStyle(.primary) + } + } + + // Features + Section("Features") { + ForEach(TabItem.featureOrder) { item in + Button { openItem(item) } label: { + Label(item.displayName, systemImage: item.icon) + .foregroundStyle(.primary) + } + } + } + + // Logging + Section("Logging") { + Button { showLogView = true } label: { + Label("View Log", systemImage: "doc.text.magnifyingglass") + .foregroundStyle(.primary) + } + + Button { shareLogs() } label: { + Label("Share Logs", systemImage: "square.and.arrow.up") + .foregroundStyle(.primary) + } + } + + // Support & Community + Section("Support & Community") { + Link(destination: URL(string: "https://loopfollowdocs.org/")!) { + HStack { + Label("LoopFollow Docs", systemImage: "book") + Spacer() + Image(systemName: "arrow.up.right.square") + .foregroundStyle(.tertiary) + } + } + + Link(destination: URL(string: "https://discord.gg/KQgk3gzuYU")!) { + HStack { + Label("Loop and Learn Discord", systemImage: "bubble.left.and.bubble.right") + Spacer() + Image(systemName: "arrow.up.right.square") + .foregroundStyle(.tertiary) + } + } + + Link(destination: URL(string: "https://www.facebook.com/groups/loopfollowlnl")!) { + HStack { + Label("LoopFollow Facebook Group", systemImage: "person.2.fill") + Spacer() + Image(systemName: "arrow.up.right.square") + .foregroundStyle(.tertiary) + } + } + } + + // Build Information + Section("Build Information") { + buildInfoRow(title: "Version", value: AppVersionManager().version(), color: versionTint) + buildInfoRow(title: "Latest version", value: latestVersion ?? "Fetching…", color: .secondary) + + let build = BuildDetails.default + if !(build.isMacApp() || build.isSimulatorBuild()) { + buildInfoRow( + title: build.expirationHeaderString, + value: dateTimeUtils.formattedDate(from: build.calculateExpirationDate()), + color: .secondary + ) + } + + buildInfoRow(title: "Built", value: dateTimeUtils.formattedDate(from: build.buildDate()), color: .secondary) + buildInfoRow(title: "Branch", value: build.branchAndSha, color: .secondary) + } + } + .listStyle(.insetGrouped) + .navigationTitle("Menu") + .navigationBarTitleDisplayMode(.large) + .task { + await fetchVersionInfo() + } + .sheet(isPresented: $showShareSheet) { + ActivityView(activityItems: shareFiles) + } + .alert(alertTitle, isPresented: $showAlert) { + Button("OK", role: .cancel) {} + } message: { + Text(alertMessage) + } + .navigationDestination(isPresented: $showSettingsView) { + SettingsMenuView() + } + .navigationDestination(isPresented: $showAlarmsView) { + AlarmsContainerView() + } + .navigationDestination(isPresented: $showRemoteView) { + RemoteContentView() + } + .navigationDestination(isPresented: $showNightscoutView) { + NightscoutContentView() + } + .navigationDestination(isPresented: $showSnoozerView) { + SnoozerView() + } + .navigationDestination(isPresented: $showTreatmentsView) { + TreatmentsView() + } + .navigationDestination(isPresented: $showStatsView) { + AggregatedStatsContentView(mainViewController: MainViewController.shared) + } + .navigationDestination(isPresented: $showHomeView) { + HomeContentView(isModal: true) + } + .navigationDestination(isPresented: $showLogView) { + LogView() + } + } + + // MARK: - Helpers + + private func buildInfoRow(title: String, value: String, color: Color) -> some View { + HStack { + Text(title) + Spacer() + Text(value) + .foregroundStyle(color) + } + } + + private func openItem(_ item: TabItem) { + // Check if the item is in the tab bar — if so, switch to it + let orderedItems = Storage.shared.orderedTabBarItems() + if let index = orderedItems.firstIndex(of: item) { + Observable.shared.selectedTabIndex.value = index + return + } + + // Otherwise push it onto the navigation stack + switch item { + case .home: showHomeView = true + case .alarms: showAlarmsView = true + case .remote: showRemoteView = true + case .nightscout: showNightscoutView = true + case .snoozer: showSnoozerView = true + case .treatments: showTreatmentsView = true + case .stats: showStatsView = true + } + } + + private func shareLogs() { + let files = LogManager.shared.logFilesForTodayAndYesterday() + guard !files.isEmpty else { + alertTitle = "No Logs Available" + alertMessage = "There are no logs to share." + showAlert = true + return + } + shareFiles = files + showShareSheet = true + } + + private func fetchVersionInfo() 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 + } +} + +// MARK: - UIActivityViewController wrapper + +struct ActivityView: UIViewControllerRepresentable { + let activityItems: [Any] + + func makeUIViewController(context _: Context) -> UIActivityViewController { + UIActivityViewController(activityItems: activityItems, applicationActivities: nil) + } + + func updateUIViewController(_: UIActivityViewController, context _: Context) {} +} diff --git a/LoopFollow/ViewControllers/MoreMenuViewController.swift b/LoopFollow/ViewControllers/MoreMenuViewController.swift deleted file mode 100644 index 2693cde33..000000000 --- a/LoopFollow/ViewControllers/MoreMenuViewController.swift +++ /dev/null @@ -1,466 +0,0 @@ -// LoopFollow -// MoreMenuViewController.swift - -import Combine -import SwiftUI -import UIKit - -class MoreMenuViewController: UIViewController { - private var tableView: UITableView! - private var cancellables = Set() - private var fallbackMainViewController: MainViewController? - var needsTabRebuild = false - - // Build Information state - private var latestVersion: String? - private var versionTint: UIColor = .secondaryLabel - - // MARK: - Menu models - - enum MenuItemStyle { - case navigation - case action - case detail(String, UIColor) - case externalLink - } - - struct MenuItem { - let title: String - let icon: String - let style: MenuItemStyle - let action: () -> Void - - init(title: String, icon: String, style: MenuItemStyle = .navigation, action: @escaping () -> Void = {}) { - self.title = title - self.icon = icon - self.style = style - self.action = action - } - } - - struct MenuSection { - let title: String? - let items: [MenuItem] - } - - private var menuSections: [MenuSection] = [] - - // MARK: - Lifecycle - - override func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = .systemBackground - navigationItem.title = "Menu" - navigationItem.largeTitleDisplayMode = .always - navigationItem.backButtonDisplayMode = .minimal - - // Apply appearance mode - overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle - - // Listen for appearance setting changes - Storage.shared.appearanceMode.$value - .receive(on: DispatchQueue.main) - .sink { [weak self] mode in - self?.overrideUserInterfaceStyle = mode.userInterfaceStyle - } - .store(in: &cancellables) - - // Listen for system appearance changes (when in System mode) - NotificationCenter.default.publisher(for: .appearanceDidChange) - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - self?.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle - } - .store(in: &cancellables) - - setupTableView() - updateMenuItems() - - Task { [weak self] in - await self?.fetchVersionInfo() - } - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationController?.setNavigationBarHidden(false, animated: animated) - navigationController?.navigationBar.prefersLargeTitles = true - updateMenuItems() - tableView.reloadData() - Observable.shared.settingsPath.set(NavigationPath()) - - if needsTabRebuild { - needsTabRebuild = false - MainViewController.rebuildTabsIfNeeded() - } - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - if Storage.shared.appearanceMode.value == .system, - previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle - { - overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle - } - } - - // MARK: - Setup - - private func setupTableView() { - tableView = UITableView(frame: view.bounds, style: .insetGrouped) - tableView.translatesAutoresizingMaskIntoConstraints = false - tableView.delegate = self - tableView.dataSource = self - tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") - tableView.contentInsetAdjustmentBehavior = .automatic - - view.addSubview(tableView) - - NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: view.topAnchor), - tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - } - - // MARK: - Menu construction - - private func updateMenuItems() { - let build = BuildDetails.default - let ver = AppVersionManager().version() - - var sections: [MenuSection] = [ - MenuSection(title: nil, items: [ - MenuItem(title: "Settings", icon: "gearshape") { [weak self] in - self?.openSettings() - }, - ]), - ] - - sections.append( - MenuSection(title: "Features", items: TabItem.featureOrder.map { item in - MenuItem(title: item.displayName, icon: item.icon) { [weak self] in - self?.openItem(item) - } - }) - ) - - sections.append(contentsOf: [ - MenuSection(title: "Logging", items: [ - MenuItem(title: "View Log", icon: "doc.text.magnifyingglass") { [weak self] in - self?.openViewLog() - }, - MenuItem(title: "Share Logs", icon: "square.and.arrow.up", style: .action) { [weak self] in - self?.shareLogs() - }, - ]), - - // Section 3: Support & Community - MenuSection(title: "Support & Community", items: [ - MenuItem(title: "LoopFollow Docs", icon: "book", style: .externalLink) { [weak self] in - self?.openURL("https://loopfollowdocs.org/") - }, - MenuItem(title: "Loop and Learn Discord", icon: "bubble.left.and.bubble.right", style: .externalLink) { [weak self] in - self?.openURL("https://discord.gg/KQgk3gzuYU") - }, - MenuItem(title: "LoopFollow Facebook Group", icon: "person.2.fill", style: .externalLink) { [weak self] in - self?.openURL("https://www.facebook.com/groups/loopfollowlnl") - }, - ]), - - // Section 4: Build Information - MenuSection(title: "Build Information", items: { - var items: [MenuItem] = [ - MenuItem(title: "Version", icon: "", style: .detail(ver, versionTint)), - MenuItem(title: "Latest version", icon: "", style: .detail(latestVersion ?? "Fetching…", .secondaryLabel)), - ] - - if !(build.isMacApp() || build.isSimulatorBuild()) { - items.append(MenuItem( - title: build.expirationHeaderString, - icon: "", - style: .detail(dateTimeUtils.formattedDate(from: build.calculateExpirationDate()), .secondaryLabel) - )) - } - - items.append(MenuItem( - title: "Built", - icon: "", - style: .detail(dateTimeUtils.formattedDate(from: build.buildDate()), .secondaryLabel) - )) - items.append(MenuItem( - title: "Branch", - icon: "", - style: .detail(build.branchAndSha, .secondaryLabel) - )) - - return items - }()), - ]) - - menuSections = sections - } - - // MARK: - Version fetching - - private func fetchVersionInfo() async { - let mgr = AppVersionManager() - let (latest, newer, blacklisted) = await mgr.checkForNewVersionAsync() - latestVersion = latest ?? "Unknown" - - let current = mgr.version() - versionTint = blacklisted ? .systemRed - : newer ? .systemOrange - : latest == current ? .systemGreen - : .secondaryLabel - - await MainActor.run { - updateMenuItems() - tableView.reloadData() - } - } - - // MARK: - Navigation - - private func openItem(_ item: TabItem) { - // If the item is in the tab bar, switch to it - if let tabVC = tabBarController, - let index = (tabVC.viewControllers ?? []).firstIndex(where: { $0.tabBarItem.title == item.displayName }) - { - tabVC.selectedIndex = index - return - } - // Otherwise push onto navigation stack - pushItem(item) - } - - private func pushItem(_ item: TabItem) { - switch item { - case .home: - openHome() - case .alarms: - openAlarmsConfig() - case .remote: - openRemote() - case .nightscout: - openNightscout() - case .snoozer: - openSnoozer() - case .treatments: - openTreatments() - case .stats: - openAggregatedStats() - } - } - - private func openSettings() { - needsTabRebuild = true - let settingsView = SettingsMenuView(onBack: { [weak self] in - self?.navigationController?.popViewController(animated: true) - }) - let settingsVC = NavBarHidingHostingController(rootView: settingsView) - settingsVC.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle - navigationController?.pushViewController(settingsVC, animated: true) - } - - private func openAlarmsConfig() { - let alarmsView = AlarmsContainerView(onBack: { [weak self] in - self?.navigationController?.popViewController(animated: true) - }) - let alarmsVC = NavBarHidingHostingController(rootView: alarmsView) - alarmsVC.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle - navigationController?.pushViewController(alarmsVC, animated: true) - } - - private func openRemote() { - let storyboard = UIStoryboard(name: "Main", bundle: nil) - let remoteVC = storyboard.instantiateViewController(withIdentifier: "RemoteViewController") - remoteVC.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle - navigationController?.pushViewController(remoteVC, animated: true) - remoteVC.navigationItem.largeTitleDisplayMode = .never - } - - private func openNightscout() { - let storyboard = UIStoryboard(name: "Main", bundle: nil) - let nightscoutVC = storyboard.instantiateViewController(withIdentifier: "NightscoutViewController") - nightscoutVC.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle - navigationController?.pushViewController(nightscoutVC, animated: true) - nightscoutVC.navigationItem.largeTitleDisplayMode = .never - } - - private func openSnoozer() { - let storyboard = UIStoryboard(name: "Main", bundle: nil) - let snoozerVC = storyboard.instantiateViewController(withIdentifier: "SnoozerViewController") - snoozerVC.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle - navigationController?.pushViewController(snoozerVC, animated: true) - snoozerVC.navigationItem.largeTitleDisplayMode = .never - } - - private func openTreatments() { - let treatmentsView = TreatmentsView(onBack: { [weak self] in - self?.navigationController?.popViewController(animated: true) - }) - let treatmentsVC = NavBarHidingHostingController(rootView: treatmentsView) - treatmentsVC.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle - navigationController?.pushViewController(treatmentsVC, animated: true) - } - - private func openAggregatedStats() { - guard let mainVC = getMainViewController() else { - presentSimpleAlert(title: "Error", message: "Unable to access data") - return - } - - let statsView = AggregatedStatsContentView(mainViewController: mainVC) - let statsVC = UIHostingController(rootView: statsView) - statsVC.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle - navigationController?.pushViewController(statsVC, animated: true) - } - - private func openHome() { - let storyboard = UIStoryboard(name: "Main", bundle: nil) - guard let mainVC = storyboard.instantiateViewController(withIdentifier: "MainViewController") as? MainViewController else { return } - mainVC.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle - mainVC.navigationItem.largeTitleDisplayMode = .never - navigationController?.pushViewController(mainVC, animated: true) - } - - private func openViewLog() { - let logView = LogView(onBack: { [weak self] in - self?.navigationController?.popViewController(animated: true) - }) - let logVC = NavBarHidingHostingController(rootView: logView) - logVC.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle - navigationController?.pushViewController(logVC, animated: true) - } - - private func shareLogs() { - let files = LogManager.shared.logFilesForTodayAndYesterday() - guard !files.isEmpty else { - presentSimpleAlert(title: "No Logs Available", message: "There are no logs to share.") - return - } - let avc = UIActivityViewController(activityItems: files, applicationActivities: nil) - present(avc, animated: true) - } - - private func openURL(_ urlString: String) { - if let url = URL(string: urlString) { - UIApplication.shared.open(url) - } - } - - // MARK: - Helpers - - private func getMainViewController() -> MainViewController? { - guard let tabBarController = tabBarController else { return nil } - - for vc in tabBarController.viewControllers ?? [] { - if let mainVC = vc as? MainViewController { - return mainVC - } - if let navVC = vc as? UINavigationController, - let mainVC = navVC.viewControllers.first as? MainViewController - { - return mainVC - } - } - - if let fallbackMainViewController { - return fallbackMainViewController - } - - let storyboard = UIStoryboard(name: "Main", bundle: nil) - guard let mainVC = storyboard.instantiateViewController(withIdentifier: "MainViewController") as? MainViewController else { - return nil - } - - mainVC.isPresentedAsModal = true - fallbackMainViewController = mainVC - return mainVC - } -} - -// MARK: - NavBarHidingHostingController - -/// A UIHostingController subclass that hides the UIKit navigation bar. -/// Used for SwiftUI views that have their own NavigationStack/NavigationView -/// to prevent double navigation bars when pushed onto a UINavigationController. -private class NavBarHidingHostingController: UIHostingController { - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - navigationController?.setNavigationBarHidden(true, animated: animated) - } -} - -// MARK: - UITableViewDataSource & UITableViewDelegate - -extension MoreMenuViewController: UITableViewDataSource, UITableViewDelegate { - func numberOfSections(in _: UITableView) -> Int { - return menuSections.count - } - - func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int { - return menuSections[section].items.count - } - - func tableView(_: UITableView, titleForHeaderInSection section: Int) -> String? { - return menuSections[section].title - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) - let item = menuSections[indexPath.section].items[indexPath.row] - - switch item.style { - case let .detail(value, color): - var config = UIListContentConfiguration.valueCell() - config.text = item.title - config.secondaryText = value - config.secondaryTextProperties.color = color - cell.contentConfiguration = config - cell.accessoryType = .none - cell.selectionStyle = .none - - case .externalLink: - var config = cell.defaultContentConfiguration() - config.text = item.title - config.image = UIImage(systemName: item.icon) - cell.contentConfiguration = config - let linkImage = UIImageView(image: UIImage(systemName: "arrow.up.right.square")) - linkImage.tintColor = .tertiaryLabel - cell.accessoryView = linkImage - cell.selectionStyle = .default - - case .navigation: - var config = cell.defaultContentConfiguration() - config.text = item.title - config.image = UIImage(systemName: item.icon) - cell.contentConfiguration = config - cell.accessoryView = nil - cell.accessoryType = .disclosureIndicator - cell.selectionStyle = .default - - case .action: - var config = cell.defaultContentConfiguration() - config.text = item.title - config.image = UIImage(systemName: item.icon) - cell.contentConfiguration = config - cell.accessoryView = nil - cell.accessoryType = .none - cell.selectionStyle = .default - } - - return cell - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - let item = menuSections[indexPath.section].items[indexPath.row] - if case .detail = item.style { return } - item.action() - } -} diff --git a/LoopFollow/ViewControllers/NightScoutViewController.swift b/LoopFollow/ViewControllers/NightScoutViewController.swift index cecb29c68..f4b71f074 100644 --- a/LoopFollow/ViewControllers/NightScoutViewController.swift +++ b/LoopFollow/ViewControllers/NightScoutViewController.swift @@ -6,13 +6,29 @@ import UIKit import WebKit class NightscoutViewController: UIViewController { - @IBOutlet var webView: WKWebView! + var webView: WKWebView! private var cancellables = Set() override func viewDidLoad() { super.viewDidLoad() + view.backgroundColor = .systemBackground overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle + // Create WKWebView programmatically + let webConfiguration = WKWebViewConfiguration() + webConfiguration.mediaTypesRequiringUserActionForPlayback = [] + webView = WKWebView(frame: .zero, configuration: webConfiguration) + webView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(webView) + + let safeArea = view.safeAreaLayoutGuide + NSLayoutConstraint.activate([ + webView.topAnchor.constraint(equalTo: safeArea.topAnchor), + webView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + webView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + webView.bottomAnchor.constraint(equalTo: safeArea.bottomAnchor), + ]) + // Listen for appearance setting changes Storage.shared.appearanceMode.$value .receive(on: DispatchQueue.main) @@ -21,14 +37,6 @@ class NightscoutViewController: UIViewController { } .store(in: &cancellables) - // Listen for system appearance changes (when in System mode) - NotificationCenter.default.publisher(for: .appearanceDidChange) - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - self?.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle - } - .store(in: &cancellables) - var url = Storage.shared.url.value let token = Storage.shared.token.value @@ -57,16 +65,6 @@ class NightscoutViewController: UIViewController { sender.endRefreshing() } - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - if Storage.shared.appearanceMode.value == .system, - previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle - { - overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle - } - } - func clearWebCache() { let dataStore = WKWebsiteDataStore.default() let cacheTypes = Set([WKWebsiteDataTypeDiskCache, WKWebsiteDataTypeMemoryCache]) diff --git a/LoopFollow/ViewControllers/NightscoutContentView.swift b/LoopFollow/ViewControllers/NightscoutContentView.swift new file mode 100644 index 000000000..8f78f4769 --- /dev/null +++ b/LoopFollow/ViewControllers/NightscoutContentView.swift @@ -0,0 +1,14 @@ +// LoopFollow +// NightscoutContentView.swift + +import SwiftUI + +struct NightscoutContentView: UIViewControllerRepresentable { + func makeUIViewController(context _: Context) -> NightscoutViewController { + NightscoutViewController() + } + + func updateUIViewController(_ uiViewController: NightscoutViewController, context _: Context) { + uiViewController.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle + } +} diff --git a/LoopFollow/ViewControllers/SettingsViewController.swift b/LoopFollow/ViewControllers/SettingsViewController.swift deleted file mode 100644 index b27e32171..000000000 --- a/LoopFollow/ViewControllers/SettingsViewController.swift +++ /dev/null @@ -1,73 +0,0 @@ -// LoopFollow -// SettingsViewController.swift - -import Combine -import SwiftUI -import UIKit - -final class SettingsViewController: UIViewController { - // MARK: Stored properties - - private var host: UIHostingController! - private var cancellables = Set() - - // MARK: Life-cycle - - override func viewDidLoad() { - super.viewDidLoad() - - // Build SwiftUI menu - host = UIHostingController(rootView: SettingsMenuView()) - - // Appearance mode override - host.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle - - // Listen for appearance setting changes - Storage.shared.appearanceMode.$value - .receive(on: DispatchQueue.main) - .sink { [weak self] mode in - self?.updateAppearance(mode) - } - .store(in: &cancellables) - - // Listen for system appearance changes (when in System mode) - NotificationCenter.default.publisher(for: .appearanceDidChange) - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - self?.updateAppearance(Storage.shared.appearanceMode.value) - } - .store(in: &cancellables) - - // 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) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - Observable.shared.settingsPath.set(NavigationPath()) - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - if Storage.shared.appearanceMode.value == .system, - previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle - { - updateAppearance(.system) - } - } - - private func updateAppearance(_ mode: AppearanceMode) { - host.overrideUserInterfaceStyle = mode.userInterfaceStyle - } -}