ソースを参照

Remove storyboard, migrate to SwiftUI layout (#608)

* Remove Main.storyboard and migrate to SwiftUI app lifecycle

Replace UIKit storyboard/SceneDelegate architecture with SwiftUI App
entry point (LoopFollowApp.swift) and TabView (MainTabView.swift).
Convert MoreMenuViewController to SwiftUI (MoreMenuView.swift). Add
SwiftUI wrappers for Remote and Nightscout tabs. Remove 6 obsolete
UIKit wrapper view controllers and ~300 lines of tab management code
from MainViewController.

* Migrate info table from UITableView to SwiftUI List

Replace UITableView with SwiftUI InfoTableView hosted in
MainViewController. Make InfoManager an ObservableObject so data
updates trigger SwiftUI rebuilds automatically. Remove
UITableViewDataSource conformance and table delegate methods.
No changes needed to the 10 Nightscout controller files that
populate the table data.

* Migrate statistics and pie chart from UIKit to SwiftUI

Replace 7 UILabel properties and DGCharts PieChartView with a
StatsDisplayModel ObservableObject and hosted StatsDisplayView.
The pie chart uses a UIViewRepresentable wrapper for DGCharts
since the Charts pod name shadows Swift Charts. Remove ~60 lines
of UIKit stack layout code from MainViewController setupUI().

* Migrate BG display area from UIKit labels to SwiftUI

Replace BGText, DirectionText, DeltaText, MinAgoText, serverText,
LoopStatusLabel, and PredictionLabel with a SwiftUI BGDisplayView.
Add pull-to-refresh via .refreshable modifier. Move loop status and
prediction text updates to Observable values across DeviceStatus,
DeviceStatusLoop, DeviceStatusOpenAPS, and BGData. Remove
UIScrollView overlay and UIScrollViewDelegate conformance.

* Migrate main layout to SwiftUI with UIKit charts embedded

Replace UIStackView layout with MainHomeView SwiftUI view that composes
BGDisplayView, InfoTableView, LineChartWrapper (UIViewRepresentable for
DGCharts), and StatsDisplayView. MainViewController now hosts a single
UIHostingController instead of managing individual UIView containers.

Visibility of info table, small graph, and stats is now reactive via
Storage observables in SwiftUI, removing several Combine subscriptions.
BG text uses lineLimit + minimumScaleFactor instead of manual font sizing.

* Clean up migration artifacts and fix post-migration bugs

- Fix AVSpeechSynthesizer temporary in AppDelegate that would be
  deallocated before speech completes; use stored property instead
- Fix appMovedToBackground tab switching to use Observable instead of
  dead UIKit tabBarController reference
- Remove dead code: rebuildTabsIfNeeded(), updateNightscoutTabState(),
  traitCollectionDidChange notification relay, UIViewExtension.addBorder
- Remove unused imports (Charts, UIKit, Combine) from migrated files
- Remove unused synthesizer from LoopFollowApp
- Remove redundant .appearanceDidChange subscription from NightscoutVC
- Add missing super calls in viewWillAppear/viewDidAppear

* Replace view hierarchy walking with MainViewController.shared

The getMainViewController() methods in TreatmentsView, SettingsMenuView,
and BackgroundRefreshManager tried to find MainViewController by casting
rootViewController as UITabBarController, which always fails with the
SwiftUI lifecycle. Add a weak static shared reference set during
viewDidLoad and use it everywhere instead.

* Fix MainViewController.shared references for stats and treatments

Pass MainViewController.shared instead of nil when creating
AggregatedStatsContentView in MainTabView and MoreMenuView. Replace
view-hierarchy-walking getMainViewController() in TreatmentsViewModel
with MainViewController.shared.

* Fix info table font size to match storyboard

The storyboard used system 17pt for both title and detail labels.
The SwiftUI migration used .subheadline (~15pt) making text smaller.

* Fix Share Logs sheet rendering blank

Present UIActivityViewController via UIApplication.topMost instead of
wrapping it in a SwiftUI .sheet, which rendered an empty view.

* Fix back navigation from Settings sub-pages

SettingsMenuView declared its own NavigationStack(path:) while already
being pushed onto the outer NavigationStack from MainTabView, so sub-page
back buttons popped the outer stack and jumped past Settings to Menu.

Drop the nested NavigationStack and route Settings entries through the
ambient stack: a single SettingsRoute enum drives a navigationDestination
attached at the MoreMenuView root. The Settings entry itself becomes a
NavigationLink(value:) so it doesn't compete with a navigationDestination
(isPresented:) modifier, which was re-asserting Settings as the top of
stack whenever a sub-page was pushed.

* Harden post-storyboard migration

* MainViewController is now a strong static singleton bootstrapped from
  LoopFollowApp.init(). Lifecycle work in viewDidLoad (Combine sinks,
  observers, scheduleAllTasks, migrations) runs at launch regardless of
  whether the Home tab is rendered, and HomeContentView reuses the
  singleton instead of instantiating a fresh VC each time.

* MoreMenuView's eight .navigationDestination(isPresented:) modifiers are
  collapsed to a single MenuRoute enum routed through one
  .navigationDestination(for:), preventing the same destination-slot
  contention that previously caused Settings → Graph back navigation to
  jump past Settings.

* MainTabView observes Storage.shared.appearanceMode so theme changes
  propagate; the orphaned .appearanceDidChange notification name is
  removed.

* OPEN_APP_ACTION notification taps now dismiss any presented modal
  before switching to Home, matching prior SceneDelegate behavior.

* Drop the unused Core Data stack (NSPersistentCloudKitContainer,
  saveContext) from AppDelegate, the dead AppDelegate.window property,
  and the legacy UIRequiredDeviceCapabilities=armv7 /
  UIStatusBarTintParameters keys from Info.plist. Switch
  AlarmSound's keyWindow access to the connected-scenes API and
  generalize UIApplication.topMost likewise so it works on Mac
  Catalyst.

* Strip redundant inner NavigationView wrappers from settings sub-views
  pushed onto the outer NavigationStack: Graph, General, Advanced,
  Calendar, Contact, Dexcom, Nightscout, BackgroundRefresh,
  InfoDisplay, ImportExport. Drop unused onBack parameters from
  AlarmsContainerView and SettingsMenuView, the unused
  isPresentedAsModal flag from MainViewController, and the leftover
  debug print in ObservableValue.set.

* LineChartWrapper.updateUIView now flushes the chart on SwiftUI
  re-render. MainViewController.deinit removes all observers, not
  just the custom "refresh" one. MoreMenuView caches the app version
  in @State instead of constructing AppVersionManager on every body
  re-render. HomeModalView uses NavigationStack (not deprecated
  NavigationView).

* MoreMenuView: render tab-switch buttons in primary color

Buttons in a List inherit the accent tint, so the Features rows that switch tabs appeared blue while the NavigationLink rows that push appeared white. Use .buttonStyle(.plain) to suppress the tint and drop the now-redundant .foregroundStyle(.primary) calls.

* Revert MainViewController singleton bootstrap

Constructing MainViewController.shared from LoopFollowApp.init() — and
reusing the same VC across HomeContentView re-creations — caused tapping
the BG chart to crash with `-[__NSArrayM insertObject:atIndex:]: object
cannot be nil`. Bisected to the singleton+bootstrap piece of the
post-storyboard hardening; the rest of that commit (programmatic UI,
MoreMenuView routing, NavigationView strip-out) is retained.

Restore the prior behavior: shared is a weak static set in viewDidLoad,
HomeContentView constructs a fresh MainViewController each time, and the
LoopFollowApp.init() bootstrap is removed.

Known follow-up: lifecycle work in viewDidLoad (Combine sinks,
scheduleAllTasks, migrations) again only runs when the Home view is
first rendered, so a user who has moved Home off the tab bar gets
degraded behavior until they navigate to it.

* MoreMenuView: make tab-switch rows full-row tappable

Wrap Button labels in an HStack with a trailing Spacer and contentShape
so the entire row is tappable, matching the hit area of NavigationLink
rows. Extract the pattern into a small FullRowButton helper, used for
both tab-switch rows and Share Logs.

* Align units-selection conflict resolution with integration branch

Move the Diagnostics section out of Section("Speak BG") (was nested at
the wrong indent), match StatsDisplayModel field order, and add the
spacing line in updateStats.

* MoreMenuView: fix cross-row tap routing in Features section

Mixing Button and NavigationLink rows in the same List ForEach caused
taps on a NavigationLink row to fire a sibling Button row's action —
e.g. tapping Alarms with Stats in the tab bar would switch to the Stats
tab instead of pushing the alarms detail.

Make every row in the menu's List a uniform FullRowButton and drive
pushes from state via .navigationDestination(isPresented:). Add an
opt-in chevron to FullRowButton so navigating rows render the standard
disclosure indicator.

* MoreMenuView: keep Settings as a value-based NavigationLink

Mixing .navigationDestination(isPresented:) with
.navigationDestination(for:) on the same view shadowed the value-based
SettingsRoute registration once SettingsMenuView was on the stack, so
sub-rows like Units and Metrics couldn't push.

Settings sits alone in its section, so it doesn't need the uniform-Button
treatment used in Features and Logging — restore it to a NavigationLink
and route it through the existing .navigationDestination(for:) channel.

* Fix navigation between alarms and menu

* Drive Before-First-Unlock recovery from AppDelegate

Move BFU recovery (Storage.reloadAll) out of MainViewController and into
AppDelegate so it runs even when the home tab's UIHostingController has
not yet materialized — necessary under the SwiftUI App lifecycle, where a
BG-only launch (BGAppRefreshTask, BLE wake, prewarming) may complete and
the device may unlock without MainViewController ever being created.

AppDelegate observes protectedDataDidBecomeAvailable (authoritative signal)
and willEnterForeground (fallback), with a race-guard re-check immediately
after observer registration. Recovery is idempotent via needsBFUReload.

MainViewController now reacts to a new .bfuReloadCompleted notification by
showing the loading overlay and rescheduling tasks; if it is not alive when
the notification fires, its viewDidLoad will later see the already-reloaded
Storage values and schedule tasks correctly on first load.

* Keep MainViewController alive regardless of tab layout

Make MainViewController.shared a strong, long-lived singleton created
once via bootstrap() on first foreground, so the data pipeline, alarms
and background audio run even when Home is moved into the Menu rather
than a tab. Home views reuse the single instance instead of creating
new ones, so the singleton is never displaced.

Defer the one-shot BG graph zoom until the chart has a real frame and
re-render the graph on every appearance, so the curve stays visible
when Home is reached from the Menu or moved between tab bar and Menu.

Restore the one-time telemetry consent prompt that was lost when
SceneDelegate was removed, presenting it from MainTabView on first
appearance for undecided installs.

* Fix issues found in post-migration review

- Speak BG quick action: under the SwiftUI scene lifecycle UIKit
  delivers Home Screen quick actions to the window scene delegate and
  never calls application(_:performActionFor:). Install a scene
  delegate via configurationForConnecting that handles warm taps,
  cold-launch shortcut delivery, and mirrors the Live Activity la-tap
  URL handling that moves with it.
- Nightscout tab: show a hint instead of a blank page when no URL is
  configured, and recreate the web view when the URL or token changes
  (the page was loaded once in viewDidLoad and stayed stale until app
  restart).
- Stats: resolve MainViewController lazily with a fallback to the
  shared instance, so stats work when the tab is built before
  bootstrap() has run (cold launch with Statistics as selected tab).
- Remove the unwired NightscoutSettingsViewModel delegate chain.
- Make LineChartWrapper.updateUIView a no-op; MainViewController
  already notifies the charts whenever it mutates their data.

* Fix squished Home layout caused by phantom keyboard inset

iOS sometimes replays a stale keyboard frame when the app returns to the
foreground, compressing the Home screen by a keyboard's height until a
rotation forces the safe area to recompute. Home has no text input, so
opt out of keyboard avoidance at both hosting layers.
Jonas Björkert 1 ヶ月 前
コミット
a1e8e95822
53 ファイル変更1627 行追加3035 行削除
  1. 44 40
      LoopFollow.xcodeproj/project.pbxproj
  2. 25 18
      LoopFollow/Alarm/AlarmsContainerView.swift
  3. 104 72
      LoopFollow/Application/AppDelegate.swift
  4. 0 458
      LoopFollow/Application/Base.lproj/Main.storyboard
  5. 23 0
      LoopFollow/Application/LoopFollowApp.swift
  6. 86 0
      LoopFollow/Application/MainTabView.swift
  7. 0 121
      LoopFollow/Application/SceneDelegate.swift
  8. 11 13
      LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift
  9. 5 1
      LoopFollow/Controllers/AlarmSound.swift
  10. 8 1
      LoopFollow/Controllers/Graphs.swift
  11. 15 59
      LoopFollow/Controllers/MainViewController+updateStats.swift
  12. 2 2
      LoopFollow/Controllers/Nightscout/BGData.swift
  13. 5 25
      LoopFollow/Controllers/Nightscout/DeviceStatus.swift
  14. 7 8
      LoopFollow/Controllers/Nightscout/DeviceStatusLoop.swift
  15. 7 8
      LoopFollow/Controllers/Nightscout/DeviceStatusOpenAPS.swift
  16. 18 0
      LoopFollow/Controllers/StatsDisplayModel.swift
  17. 84 0
      LoopFollow/Controllers/StatsDisplayView.swift
  18. 0 24
      LoopFollow/Extensions/UIViewExtension.swift
  19. 0 4
      LoopFollow/Helpers/AppearanceMode.swift
  20. 1 31
      LoopFollow/Helpers/BackgroundRefreshManager.swift
  21. 3 8
      LoopFollow/Helpers/Views/NavigationRow.swift
  22. 0 35
      LoopFollow/Info.plist
  23. 25 27
      LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift
  24. 5 3
      LoopFollow/InfoTable/InfoData.swift
  25. 15 28
      LoopFollow/InfoTable/InfoManager.swift
  26. 34 0
      LoopFollow/InfoTable/InfoTableView.swift
  27. 0 10
      LoopFollow/Nightscout/NightscoutSettingsViewModel.swift
  28. 48 0
      LoopFollow/Remote/RemoteContentView.swift
  29. 0 126
      LoopFollow/Remote/RemoteViewController.swift
  30. 13 15
      LoopFollow/Settings/AdvancedSettingsView.swift
  31. 42 44
      LoopFollow/Settings/CalendarSettingsView.swift
  32. 78 80
      LoopFollow/Settings/ContactSettingsView.swift
  33. 86 88
      LoopFollow/Settings/GeneralSettingsView.swift
  34. 80 82
      LoopFollow/Settings/GraphSettingsView.swift
  35. 23 23
      LoopFollow/Settings/HomeContentView.swift
  36. 44 46
      LoopFollow/Settings/ImportExport/ImportExportSettingsView.swift
  37. 81 159
      LoopFollow/Settings/SettingsMenuView.swift
  38. 0 65
      LoopFollow/Snoozer/SnoozerViewController.swift
  39. 8 2
      LoopFollow/Stats/StatsDataFetcher.swift
  40. 8 2
      LoopFollow/Stats/StatsDataService.swift
  41. 9 2
      LoopFollow/Storage/Observable.swift
  42. 5 17
      LoopFollow/Task/MinAgoTask.swift
  43. 2 39
      LoopFollow/Treatments/TreatmentsView.swift
  44. 0 60
      LoopFollow/ViewControllers/AlarmViewController.swift
  45. 74 0
      LoopFollow/ViewControllers/BGDisplayView.swift
  46. 20 0
      LoopFollow/ViewControllers/LineChartWrapper.swift
  47. 70 0
      LoopFollow/ViewControllers/MainHomeView.swift
  48. 134 631
      LoopFollow/ViewControllers/MainViewController.swift
  49. 220 0
      LoopFollow/ViewControllers/MoreMenuView.swift
  50. 0 466
      LoopFollow/ViewControllers/MoreMenuViewController.swift
  51. 17 19
      LoopFollow/ViewControllers/NightScoutViewController.swift
  52. 38 0
      LoopFollow/ViewControllers/NightscoutContentView.swift
  53. 0 73
      LoopFollow/ViewControllers/SettingsViewController.swift

+ 44 - 40
LoopFollow.xcodeproj/project.pbxproj

@@ -88,18 +88,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 */; };
 		DD1D52C22E4C100000000002 /* PredictionDisplayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1D52C12E4C100000000002 /* PredictionDisplayType.swift */; };
 		DD2C2E4F2D3B8AF1006413A5 /* NightscoutSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2C2E4E2D3B8AEC006413A5 /* NightscoutSettingsView.swift */; };
@@ -219,7 +225,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 */; };
@@ -237,7 +242,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 */; };
@@ -289,6 +293,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 */; };
@@ -410,10 +416,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 */; };
@@ -431,7 +435,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 */
@@ -544,18 +547,24 @@
 		DD0C0C672C48529400DBADDF /* Metric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metric.swift; sourceTree = "<group>"; };
 		DD0C0C6A2C48562000DBADDF /* InsulinMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsulinMetric.swift; sourceTree = "<group>"; };
 		DD0C0C6C2C48606200DBADDF /* CarbMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbMetric.swift; sourceTree = "<group>"; };
-		DD0C0C6F2C4AFFE800DBADDF /* RemoteViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteViewController.swift; sourceTree = "<group>"; };
+		DD4E5F6A7B8C9D0E1F2A3B4C /* RemoteContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteContentView.swift; sourceTree = "<group>"; };
 		DD0C0C712C4B000800DBADDF /* TrioNightscoutRemoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioNightscoutRemoteView.swift; sourceTree = "<group>"; };
-		DD12D4842E1705D9004E0112 /* AlarmViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmViewController.swift; sourceTree = "<group>"; };
+		AA1B2C3D4E5F6A7B8C9D0E1F /* LoopFollowApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopFollowApp.swift; sourceTree = "<group>"; };
+		BB2C3D4E5F6A7B8C9D0E1F2A /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = "<group>"; };
 		DD12D4862E1705E6004E0112 /* AlarmsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmsContainerView.swift; sourceTree = "<group>"; };
 		DD13BC742C3FD6200062313B /* InfoType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoType.swift; sourceTree = "<group>"; };
 		DD13BC762C3FD64E0062313B /* InfoData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoData.swift; sourceTree = "<group>"; };
 		DD13BC782C3FE63A0062313B /* InfoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoManager.swift; sourceTree = "<group>"; };
+		DD13BC7A2C3FE64A0062313B /* InfoTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoTableView.swift; sourceTree = "<group>"; };
 		DD16AF0C2C98485400FB655A /* SecureStorageValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureStorageValue.swift; sourceTree = "<group>"; };
 		DD16AF0E2C99592F00FB655A /* HKQuantityInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKQuantityInputView.swift; sourceTree = "<group>"; };
 		DD16AF102C997B4600FB655A /* LoadingButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingButtonView.swift; sourceTree = "<group>"; };
 		DD1D52B82E1EB5DC00432050 /* TabPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPosition.swift; sourceTree = "<group>"; };
-		DD1D52BA2E1EB60B00432050 /* MoreMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreMenuViewController.swift; sourceTree = "<group>"; };
+		CC3D4E5F6A7B8C9D0E1F2A3B /* MoreMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreMenuView.swift; sourceTree = "<group>"; };
+		DD7A3B5C2F1E8D9A00B4C6E1 /* BGDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGDisplayView.swift; sourceTree = "<group>"; };
+		DD7A3B5E2F1E8DA000B4C6E1 /* LineChartWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineChartWrapper.swift; sourceTree = "<group>"; };
+		DD7A3B602F1E8DA600B4C6E1 /* MainHomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeView.swift; sourceTree = "<group>"; };
+		EE5F6A7B8C9D0E1F2A3B4C5D /* NightscoutContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutContentView.swift; sourceTree = "<group>"; };
 		DD1D52BF2E4C100000000001 /* AppearanceMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceMode.swift; sourceTree = "<group>"; };
 		DD1D52C12E4C100000000002 /* PredictionDisplayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PredictionDisplayType.swift; sourceTree = "<group>"; };
 		DD2C2E4E2D3B8AEC006413A5 /* NightscoutSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutSettingsView.swift; sourceTree = "<group>"; };
@@ -676,7 +685,6 @@
 		DDC6CA482DD8E47A0060EE25 /* PumpVolumeCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpVolumeCondition.swift; sourceTree = "<group>"; };
 		DDC6CA4A2DD8E4960060EE25 /* PumpVolumeAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpVolumeAlarmEditor.swift; sourceTree = "<group>"; };
 		DDC7E5122DBCE1B900EB1127 /* SnoozerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozerView.swift; sourceTree = "<group>"; };
-		DDC7E5132DBCE1B900EB1127 /* SnoozerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnoozerViewController.swift; sourceTree = "<group>"; };
 		DDC7E5372DBD887400EB1127 /* isOnPhoneCall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = isOnPhoneCall.swift; sourceTree = "<group>"; };
 		DDC7E5392DBD8A1600EB1127 /* AlarmGeneralSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmGeneralSection.swift; sourceTree = "<group>"; };
 		DDC7E53A2DBD8A1600EB1127 /* SoundFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundFile.swift; sourceTree = "<group>"; };
@@ -695,7 +703,6 @@
 		DDCC3A5C2DDE2000006F1C10 /* PumpBatteryAlarmEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpBatteryAlarmEditor.swift; sourceTree = "<group>"; };
 		DDCC3ABF2DDE10B0006F1C10 /* Testing.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Testing.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/Testing.framework; sourceTree = DEVELOPER_DIR; };
 		DDCC3AD62DDE1790006F1C10 /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
-		DDCF979324C0D380002C9752 /* UIViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewExtension.swift; sourceTree = "<group>"; };
 		DDCF9A7F2D85FD09004DF4DD /* Alarm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alarm.swift; sourceTree = "<group>"; };
 		DDCF9A812D85FD14004DF4DD /* AlarmType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmType.swift; sourceTree = "<group>"; };
 		DDCF9A872D85FD33004DF4DD /* AlarmData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlarmData.swift; sourceTree = "<group>"; };
@@ -747,6 +754,8 @@
 		FC16A98024996C07003D6245 /* DateTime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTime.swift; sourceTree = "<group>"; };
 		FC1BDD2A24A22650001B652C /* Stats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stats.swift; sourceTree = "<group>"; };
 		FC1BDD2C24A23204001B652C /* MainViewController+updateStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainViewController+updateStats.swift"; sourceTree = "<group>"; };
+		A1B2C3D4E5F6A7B8C9D0E1F2 /* StatsDisplayModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsDisplayModel.swift; sourceTree = "<group>"; };
+		A1B2C3D4E5F6A7B8C9D0E1F4 /* StatsDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsDisplayView.swift; sourceTree = "<group>"; };
 		FC1BDD2E24A232A3001B652C /* DataStructs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStructs.swift; sourceTree = "<group>"; };
 		FC3AE7B4249E8E0E00AAE1E0 /* LoopFollow.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LoopFollow.xcdatamodel; sourceTree = "<group>"; };
 		FC5A5C3C2497B229009C550E /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
@@ -869,10 +878,8 @@
 		FC8DEEE62485D1ED0075863F /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		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 = "<group>"; };
-		FC9788192485969B00A7906C /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
 		FC97881B2485969B00A7906C /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
 		FC97881D2485969B00A7906C /* NightScoutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightScoutViewController.swift; sourceTree = "<group>"; };
-		FC9788202485969B00A7906C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
 		FC9788252485969C00A7906C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
 		FC9788282485969C00A7906C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
 		FCA2DDE52501095000254A8C /* Timers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timers.swift; sourceTree = "<group>"; };
@@ -894,7 +901,6 @@
 		FCEF87AA24A1417900AE6FA0 /* Localizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Localizer.swift; sourceTree = "<group>"; };
 		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 = "<group>"; };
-		FCFEECA1248857A600402A7F /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFileSystemSynchronizedRootGroup section */
@@ -1059,7 +1065,7 @@
 				DD4878112C7B74F90048F05C /* TRC */,
 				DD4878062C7B2E9E0048F05C /* Settings */,
 				DDF699972C5AA2E50058A8D9 /* TempTargetPreset */,
-				DD0C0C6F2C4AFFE800DBADDF /* RemoteViewController.swift */,
+				DD4E5F6A7B8C9D0E1F2A3B4C /* RemoteContentView.swift */,
 				DDE69ED12C7256260013EAEC /* RemoteType.swift */,
 			);
 			path = Remote;
@@ -1072,6 +1078,7 @@
 				DD13BC762C3FD64E0062313B /* InfoData.swift */,
 				DD13BC782C3FE63A0062313B /* InfoManager.swift */,
 				DD0C0C652C46E54C00DBADDF /* InfoDataSeparator.swift */,
+				DD13BC7A2C3FE64A0062313B /* InfoTableView.swift */,
 			);
 			path = InfoTable;
 			sourceTree = "<group>";
@@ -1180,7 +1187,6 @@
 			isa = PBXGroup;
 			children = (
 				DD98F54324BCEFEE0007425A /* ShareClientExtension.swift */,
-				DDCF979324C0D380002C9752 /* UIViewExtension.swift */,
 				DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */,
 				DD0C0C632C45A59400DBADDF /* HKUnit+Extensions.swift */,
 				DD4AFB662DB68C5500BB593F /* UUID+Identifiable.swift */,
@@ -1236,7 +1242,6 @@
 			children = (
 				DDC7E5CE2DC77C2000EB1127 /* SnoozerViewModel.swift */,
 				DDC7E5122DBCE1B900EB1127 /* SnoozerView.swift */,
-				DDC7E5132DBCE1B900EB1127 /* SnoozerViewController.swift */,
 			);
 			path = Snoozer;
 			sourceTree = "<group>";
@@ -1444,9 +1449,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;
@@ -1462,6 +1467,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 */,
@@ -1723,11 +1730,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 = "<group>";
@@ -1966,7 +1975,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 */,
@@ -2150,7 +2158,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 */,
@@ -2213,7 +2220,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 */,
@@ -2224,12 +2230,12 @@
 				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 */,
 				AB1CD0012C7B30D40048F05C /* RemoteDiagnostics.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 */,
@@ -2245,7 +2251,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 */,
@@ -2318,7 +2328,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 */,
@@ -2344,7 +2355,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 */,
@@ -2360,6 +2370,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 */,
@@ -2419,14 +2431,6 @@
 /* End PBXTargetDependency section */
 
 /* Begin PBXVariantGroup section */
-		FC97881F2485969B00A7906C /* Main.storyboard */ = {
-			isa = PBXVariantGroup;
-			children = (
-				FC9788202485969B00A7906C /* Base */,
-			);
-			name = Main.storyboard;
-			sourceTree = "<group>";
-		};
 		FC9788272485969C00A7906C /* LaunchScreen.storyboard */ = {
 			isa = PBXVariantGroup;
 			children = (

+ 25 - 18
LoopFollow/Alarm/AlarmsContainerView.swift

@@ -4,28 +4,35 @@
 import SwiftUI
 
 struct AlarmsContainerView: View {
-    var onBack: (() -> Void)?
+    private let embedsInNavigationStack: Bool
+
+    init(embedsInNavigationStack: Bool = true) {
+        self.embedsInNavigationStack = embedsInNavigationStack
+    }
 
     var body: some View {
-        NavigationStack {
-            AlarmListView()
-                .toolbar {
-                    if let onBack {
-                        ToolbarItem(placement: .navigationBarLeading) {
-                            Button(action: onBack) {
-                                Image(systemName: "chevron.left")
-                            }
-                        }
-                    }
-                    ToolbarItem(placement: .navigationBarTrailing) {
-                        NavigationLink {
-                            AlarmSettingsView()
-                        } label: {
-                            Image(systemName: "gearshape")
-                        }
-                    }
+        Group {
+            if embedsInNavigationStack {
+                NavigationStack {
+                    content
                 }
+            } else {
+                content
+            }
         }
         .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme)
     }
+
+    private var content: some View {
+        AlarmListView()
+            .toolbar {
+                ToolbarItem(placement: .navigationBarTrailing) {
+                    NavigationLink {
+                        AlarmSettingsView()
+                    } label: {
+                        Image(systemName: "gearshape")
+                    }
+                }
+            }
+    }
 }

+ 104 - 72
LoopFollow/Application/AppDelegate.swift

@@ -1,14 +1,12 @@
 // LoopFollow
 // AppDelegate.swift
 
-import CoreData
+import AVFoundation
 import EventKit
 import UIKit
 import UserNotifications
 
-@main
 class AppDelegate: UIResponder, UIApplicationDelegate {
-    var window: UIWindow?
     let notificationCenter = UNUserNotificationCenter.current()
 
     func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
@@ -65,14 +63,49 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
 
         // Detect Before-First-Unlock launch. If protected data is unavailable here,
         // StorageValues were cached from encrypted UserDefaults and need a reload
-        // on the first foreground after the user unlocks.
+        // once the device is unlocked.
         let bfu = !UIApplication.shared.isProtectedDataAvailable
         Storage.shared.needsBFUReload = bfu
         LogManager.shared.log(category: .general, message: "BFU check: isProtectedDataAvailable=\(!bfu), needsBFUReload=\(bfu)")
 
+        // Recovery is driven from AppDelegate (not MainViewController) because under
+        // the SwiftUI App lifecycle the home tab's UIHostingController is materialized
+        // lazily — on a BG-only launch (BGAppRefreshTask, BLE wake) MainViewController
+        // may not exist when the device is unlocked, and would miss willEnterForeground.
+        // protectedDataDidBecomeAvailable fires the moment file protection lifts and
+        // is the authoritative signal; willEnterForeground is a fallback.
+        let nc = NotificationCenter.default
+        nc.addObserver(self, selector: #selector(protectedDataDidBecomeAvailable), name: UIApplication.protectedDataDidBecomeAvailableNotification, object: nil)
+        nc.addObserver(self, selector: #selector(handleWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
+
+        // Race guard: protected data may have become available between the check
+        // above and the observer registration just now.
+        if Storage.shared.needsBFUReload, UIApplication.shared.isProtectedDataAvailable {
+            performBFUReloadIfNeeded()
+        }
+
         return true
     }
 
+    // MARK: - BFU recovery
+
+    @objc private func protectedDataDidBecomeAvailable() {
+        performBFUReloadIfNeeded()
+    }
+
+    @objc private func handleWillEnterForeground() {
+        performBFUReloadIfNeeded()
+    }
+
+    private func performBFUReloadIfNeeded() {
+        guard Storage.shared.needsBFUReload else { return }
+        Storage.shared.needsBFUReload = false
+        LogManager.shared.log(category: .general, message: "BFU reload triggered — reloading all StorageValues")
+        Storage.shared.reloadAll()
+        LogManager.shared.log(category: .general, message: "BFU reload complete: url='\(Storage.shared.url.value)'")
+        NotificationCenter.default.post(name: .bfuReloadCompleted, object: nil)
+    }
+
     func applicationWillTerminate(_: UIApplication) {
         #if !targetEnvironment(macCatalyst)
             LiveActivityManager.shared.endOnTerminate()
@@ -127,85 +160,31 @@ 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 <urlScheme>://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
     }
 
+    // MARK: - Scene configuration
+
+    // Under the scene-based lifecycle (which the SwiftUI App lifecycle uses),
+    // UIKit delivers Home Screen quick actions and opened URLs to the window
+    // scene delegate — application(_:performActionFor:) is never called.
+    // Injecting a delegate class here is the supported way to receive those
+    // events; SwiftUI still creates and manages the window itself.
     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)
-    }
-
-    func application(_: UIApplication, didDiscardSceneSessions _: Set<UISceneSession>) {
-        // 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.
-    }
-
-    // MARK: - Core Data stack
-
-    lazy var persistentContainer: NSPersistentCloudKitContainer = {
-        /*
-         The persistent container for the application. This implementation
-         creates and returns a container, having loaded the store for the
-         application to it. This property is optional since there are legitimate
-         error conditions that could cause the creation of the store to fail.
-         */
-        let container = NSPersistentCloudKitContainer(name: "LoopFollow")
-        container.loadPersistentStores(completionHandler: { _, error in
-            if let error = error as NSError? {
-                // Replace this implementation with code to handle the error appropriately.
-                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
-
-                /*
-                 Typical reasons for an error here include:
-                 * The parent directory does not exist, cannot be created, or disallows writing.
-                 * The persistent store is not accessible, due to permissions or data protection when the device is locked.
-                 * The device is out of space.
-                 * The store could not be migrated to the current model version.
-                 Check the error message to determine what the actual problem was.
-                 */
-                fatalError("Unresolved error \(error), \(error.userInfo)")
-            }
-        })
-        return container
-    }()
-
-    // MARK: - Core Data Saving support
-
-    func saveContext() {
-        let context = persistentContainer.viewContext
-        if context.hasChanges {
-            do {
-                try context.save()
-            } catch {
-                // Replace this implementation with code to handle the error appropriately.
-                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
-                let nserror = error as NSError
-                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
-            }
+        let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
+        if connectingSceneSession.role == .windowApplication {
+            configuration.delegateClass = AppSceneDelegate.self
         }
+        return configuration
     }
 
     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)
-            }
+            // Dismiss any presented modal/sheet so the user actually sees Home
+            UIApplication.shared.topMost?.dismiss(animated: true)
+            Observable.shared.selectedTabIndex.value = 0
         }
 
         if response.actionIdentifier == "snooze" {
@@ -226,6 +205,59 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
     }
 }
 
+extension Notification.Name {
+    /// Posted by AppDelegate after a Before-First-Unlock recovery completes
+    /// (Storage.reloadAll has run with the now-decrypted UserDefaults).
+    static let bfuReloadCompleted = Notification.Name("LoopFollow.bfuReloadCompleted")
+}
+
+/// Window scene delegate installed via configurationForConnecting. SwiftUI owns
+/// the window; this class only handles the events UIKit routes to the scene
+/// delegate instead of the application delegate.
+final class AppSceneDelegate: NSObject, UIWindowSceneDelegate {
+    private let speechSynthesizer = AVSpeechSynthesizer()
+
+    /// A quick action used to cold-launch the app arrives in the connection
+    /// options; windowScene(_:performActionFor:) is not called for that launch.
+    func scene(_: UIScene, willConnectTo _: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
+        if let shortcutItem = connectionOptions.shortcutItem {
+            handleShortcutItem(shortcutItem)
+        }
+    }
+
+    /// Called when the user taps the "Speak BG" Home Screen quick action while
+    /// the app is already running.
+    func windowScene(_: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
+        completionHandler(handleShortcutItem(shortcutItem))
+    }
+
+    @discardableResult
+    private func handleShortcutItem(_ shortcutItem: UIApplicationShortcutItem) -> Bool {
+        guard let bundleIdentifier = Bundle.main.bundleIdentifier,
+              shortcutItem.type == bundleIdentifier + ".toggleSpeakBG"
+        else {
+            return false
+        }
+        Storage.shared.speakBG.value.toggle()
+        let message = Storage.shared.speakBG.value ? "BG Speak is now on" : "BG Speak is now off"
+        speechSynthesizer.speak(AVSpeechUtterance(string: message))
+        return true
+    }
+
+    /// With a custom scene delegate installed, UIKit delivers opened URLs here
+    /// rather than through SwiftUI's onOpenURL, so the Live Activity tap
+    /// handling from LoopFollowApp is mirrored. Posting twice is harmless —
+    /// the navigation it triggers is idempotent.
+    func scene(_: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
+        guard URLContexts.contains(where: { $0.url.scheme == AppGroupID.urlScheme && $0.url.host == "la-tap" }) else { return }
+        #if !targetEnvironment(macCatalyst)
+            DispatchQueue.main.async {
+                NotificationCenter.default.post(name: .liveActivityDidForeground, object: nil)
+            }
+        #endif
+    }
+}
+
 extension AppDelegate: UNUserNotificationCenterDelegate {
     func userNotificationCenter(_: UNUserNotificationCenter,
                                 willPresent notification: UNNotification,

+ 0 - 458
LoopFollow/Application/Base.lproj/Main.storyboard

@@ -1,458 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23727" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="49e-Tb-3d3">
-    <device id="retina6_5" orientation="portrait" appearance="light"/>
-    <dependencies>
-        <deployment identifier="iOS"/>
-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23721"/>
-        <capability name="Safe area layout guides" minToolsVersion="9.0"/>
-        <capability name="Stack View standard spacing" minToolsVersion="9.0"/>
-        <capability name="System colors in document resources" minToolsVersion="11.0"/>
-        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
-    </dependencies>
-    <scenes>
-        <!--Home-->
-        <scene sceneID="hNz-n2-bh7">
-            <objects>
-                <viewController storyboardIdentifier="MainViewController" id="9pv-A4-QxB" userLabel="Home" customClass="MainViewController" customModule="LoopFollow" customModuleProvider="target" sceneMemberID="viewController">
-                    <view key="view" contentMode="scaleToFill" id="tsR-hK-woN">
-                        <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
-                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
-                        <subviews>
-                            <stackView opaque="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="250" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="mJy-Ly-Jze">
-                                <rect key="frame" x="8" y="96" width="398" height="339"/>
-                                <subviews>
-                                    <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="Ptf-fu-Ue4" userLabel="BGview">
-                                        <rect key="frame" x="0.0" y="0.0" width="144.66666666666666" height="339"/>
-                                        <subviews>
-                                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Server" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2mX-T9-SAm">
-                                                <rect key="frame" x="0.0" y="0.0" width="144.66666666666666" height="95.333333333333329"/>
-                                                <fontDescription key="fontDescription" type="system" pointSize="13"/>
-                                                <nil key="textColor"/>
-                                                <nil key="highlightedColor"/>
-                                            </label>
-                                            <label opaque="NO" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="1000" text="BG" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vXU-IW-GOL">
-                                                <rect key="frame" x="0.0" y="95.333333333333343" width="144.66666666666666" height="101.66666666666666"/>
-                                                <fontDescription key="fontDescription" type="system" weight="black" pointSize="85"/>
-                                                <nil key="textColor"/>
-                                                <nil key="highlightedColor"/>
-                                            </label>
-                                            <stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" translatesAutoresizingMaskIntoConstraints="NO" id="KZc-fa-qmt">
-                                                <rect key="frame" x="0.0" y="197" width="144.66666666666666" height="71.666666666666686"/>
-                                                <subviews>
-                                                    <label opaque="NO" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="1000" text="--" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="4sd-6h-LSj" userLabel="Direction">
-                                                        <rect key="frame" x="0.0" y="0.0" width="72.333333333333329" height="71.666666666666671"/>
-                                                        <fontDescription key="fontDescription" type="system" weight="black" pointSize="60"/>
-                                                        <nil key="textColor"/>
-                                                        <nil key="highlightedColor"/>
-                                                    </label>
-                                                    <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="1000" text="Delta" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sb7-g9-JNv" userLabel="Delta">
-                                                        <rect key="frame" x="72.333333333333343" y="0.0" width="72.333333333333343" height="71.666666666666671"/>
-                                                        <fontDescription key="fontDescription" type="system" pointSize="32"/>
-                                                        <nil key="textColor"/>
-                                                        <color key="highlightedColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
-                                                    </label>
-                                                </subviews>
-                                            </stackView>
-                                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="MinAgo" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="UkG-hw-DVo">
-                                                <rect key="frame" x="0.0" y="268.66666666666669" width="144.66666666666666" height="20.333333333333314"/>
-                                                <fontDescription key="fontDescription" type="system" pointSize="17"/>
-                                                <nil key="textColor"/>
-                                                <nil key="highlightedColor"/>
-                                            </label>
-                                            <stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" spacingType="standard" translatesAutoresizingMaskIntoConstraints="NO" id="TDG-yw-5fS">
-                                                <rect key="frame" x="0.0" y="289" width="144.66666666666666" height="50"/>
-                                                <subviews>
-                                                    <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="XEm-uK-Xui">
-                                                        <rect key="frame" x="0.0" y="0.0" width="68.333333333333329" height="50"/>
-                                                        <fontDescription key="fontDescription" type="system" pointSize="17"/>
-                                                        <nil key="textColor"/>
-                                                        <nil key="highlightedColor"/>
-                                                    </label>
-                                                    <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oiR-SM-6sg">
-                                                        <rect key="frame" x="76.333333333333343" y="0.0" width="68.333333333333343" height="50"/>
-                                                        <fontDescription key="fontDescription" type="system" pointSize="17"/>
-                                                        <nil key="textColor"/>
-                                                        <nil key="highlightedColor"/>
-                                                    </label>
-                                                </subviews>
-                                            </stackView>
-                                        </subviews>
-                                    </stackView>
-                                    <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" allowsSelection="NO" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="kTs-7g-lp9">
-                                        <rect key="frame" x="154.66666666666663" y="0.0" width="243.33333333333337" height="339"/>
-                                        <color key="backgroundColor" systemColor="systemBackgroundColor"/>
-                                        <constraints>
-                                            <constraint firstAttribute="width" priority="750" constant="250" id="Gxe-cX-1oe"/>
-                                        </constraints>
-                                        <prototypes>
-                                            <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="LabelCell" textLabel="gif-qe-9mv" detailTextLabel="hfT-xF-eNt" style="IBUITableViewCellStyleValue1" id="GJo-f1-ajA">
-                                                <rect key="frame" x="0.0" y="50" width="243.33333333333337" height="43.666667938232422"/>
-                                                <autoresizingMask key="autoresizingMask"/>
-                                                <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="GJo-f1-ajA" id="2H1-eI-xS5">
-                                                    <rect key="frame" x="0.0" y="0.0" width="243.33333333333337" height="43.666667938232422"/>
-                                                    <autoresizingMask key="autoresizingMask"/>
-                                                    <subviews>
-                                                        <label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="gif-qe-9mv">
-                                                            <rect key="frame" x="16" y="11.999999999999998" width="33" height="20.333333333333332"/>
-                                                            <autoresizingMask key="autoresizingMask"/>
-                                                            <fontDescription key="fontDescription" type="system" pointSize="17"/>
-                                                            <nil key="textColor"/>
-                                                            <nil key="highlightedColor"/>
-                                                        </label>
-                                                        <label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="right" lineBreakMode="characterWrap" baselineAdjustment="none" minimumFontSize="8" adjustsLetterSpacingToFitWidth="YES" adjustsFontForContentSizeCategory="YES" id="hfT-xF-eNt">
-                                                            <rect key="frame" x="183.66666666666671" y="11.999999999999998" width="43.666666666666664" height="20.333333333333332"/>
-                                                            <autoresizingMask key="autoresizingMask"/>
-                                                            <fontDescription key="fontDescription" type="system" pointSize="17"/>
-                                                            <nil key="textColor"/>
-                                                            <nil key="highlightedColor"/>
-                                                        </label>
-                                                    </subviews>
-                                                </tableViewCellContentView>
-                                            </tableViewCell>
-                                        </prototypes>
-                                    </tableView>
-                                </subviews>
-                            </stackView>
-                            <stackView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="1000" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="751-02-Xte">
-                                <rect key="frame" x="8" y="443" width="398" height="328"/>
-                                <subviews>
-                                    <view contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" verticalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="51B-JU-npv" customClass="LineChartView" customModule="Charts">
-                                        <rect key="frame" x="0.0" y="0.0" width="398" height="172"/>
-                                        <color key="backgroundColor" systemColor="systemBackgroundColor"/>
-                                    </view>
-                                    <view autoresizesSubviews="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="Nml-lr-dgQ" userLabel="BGChartFull" customClass="LineChartView" customModule="Charts">
-                                        <rect key="frame" x="0.0" y="180" width="398" height="40"/>
-                                        <color key="backgroundColor" systemColor="systemBackgroundColor"/>
-                                        <constraints>
-                                            <constraint firstAttribute="height" constant="40" id="qmO-ga-QWl"/>
-                                        </constraints>
-                                    </view>
-                                    <view contentMode="scaleToFill" verticalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="ikj-at-auF" userLabel="statsView">
-                                        <rect key="frame" x="0.0" y="228" width="398" height="100"/>
-                                        <subviews>
-                                            <stackView opaque="NO" contentMode="scaleToFill" ambiguous="YES" alignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="XsT-6R-Vym">
-                                                <rect key="frame" x="0.0" y="0.0" width="398" height="100"/>
-                                                <subviews>
-                                                    <view userInteractionEnabled="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Hhh-F1-s1p" customClass="PieChartView" customModule="Charts">
-                                                        <rect key="frame" x="0.0" y="0.0" width="100" height="100"/>
-                                                        <color key="backgroundColor" systemColor="systemBackgroundColor"/>
-                                                        <constraints>
-                                                            <constraint firstAttribute="width" constant="100" id="hv8-WM-AJ7"/>
-                                                            <constraint firstAttribute="height" constant="100" id="yN0-Vb-FTT"/>
-                                                        </constraints>
-                                                    </view>
-                                                    <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="fillEqually" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="wy3-R2-YQR">
-                                                        <rect key="frame" x="100" y="0.0" width="298" height="100"/>
-                                                        <subviews>
-                                                            <stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" alignment="top" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="FTW-Gd-zoi">
-                                                                <rect key="frame" x="0.0" y="0.0" width="298" height="45"/>
-                                                                <subviews>
-                                                                    <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="Xt9-qX-IQe">
-                                                                        <rect key="frame" x="0.0" y="0.0" width="92.666666666666671" height="45"/>
-                                                                        <subviews>
-                                                                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Low:" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sUB-nL-H1B">
-                                                                                <rect key="frame" x="30.333333333333343" y="0.0" width="32" height="18"/>
-                                                                                <fontDescription key="fontDescription" type="system" pointSize="15"/>
-                                                                                <nil key="textColor"/>
-                                                                                <nil key="highlightedColor"/>
-                                                                            </label>
-                                                                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="TzL-hn-9qu">
-                                                                                <rect key="frame" x="14.333333333333329" y="18" width="64" height="27"/>
-                                                                                <fontDescription key="fontDescription" type="system" pointSize="15"/>
-                                                                                <nil key="textColor"/>
-                                                                                <nil key="highlightedColor"/>
-                                                                            </label>
-                                                                        </subviews>
-                                                                    </stackView>
-                                                                    <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="n6P-oO-ivx">
-                                                                        <rect key="frame" x="102.66666666666666" y="0.0" width="92.666666666666657" height="45"/>
-                                                                        <subviews>
-                                                                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="In Range:" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zBu-Uf-0IS">
-                                                                                <rect key="frame" x="14.333333333333343" y="0.0" width="64" height="18"/>
-                                                                                <fontDescription key="fontDescription" type="system" pointSize="15"/>
-                                                                                <nil key="textColor"/>
-                                                                                <nil key="highlightedColor"/>
-                                                                            </label>
-                                                                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7mH-Np-j0L">
-                                                                                <rect key="frame" x="14.333333333333343" y="18" width="64" height="27"/>
-                                                                                <fontDescription key="fontDescription" type="system" pointSize="15"/>
-                                                                                <nil key="textColor"/>
-                                                                                <nil key="highlightedColor"/>
-                                                                            </label>
-                                                                        </subviews>
-                                                                    </stackView>
-                                                                    <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="K2J-AH-Hj1">
-                                                                        <rect key="frame" x="205.33333333333331" y="0.0" width="92.666666666666686" height="45"/>
-                                                                        <subviews>
-                                                                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="High:" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0Zq-6s-y8B">
-                                                                                <rect key="frame" x="28.333333333333375" y="0.0" width="36.333333333333343" height="18"/>
-                                                                                <fontDescription key="fontDescription" type="system" pointSize="15"/>
-                                                                                <nil key="textColor"/>
-                                                                                <nil key="highlightedColor"/>
-                                                                            </label>
-                                                                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="HON-rt-8pC">
-                                                                                <rect key="frame" x="14.333333333333371" y="18" width="64" height="27"/>
-                                                                                <fontDescription key="fontDescription" type="system" pointSize="15"/>
-                                                                                <nil key="textColor"/>
-                                                                                <nil key="highlightedColor"/>
-                                                                            </label>
-                                                                        </subviews>
-                                                                    </stackView>
-                                                                </subviews>
-                                                            </stackView>
-                                                            <stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" alignment="top" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="K8G-Ph-ZNA">
-                                                                <rect key="frame" x="0.0" y="55" width="298" height="45"/>
-                                                                <subviews>
-                                                                    <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="0aR-E9-gpC">
-                                                                        <rect key="frame" x="0.0" y="0.0" width="92.666666666666671" height="45"/>
-                                                                        <subviews>
-                                                                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Avg BG:" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Rrh-Zo-cis" userLabel="Avg BG:">
-                                                                                <rect key="frame" x="19.333333333333332" y="0.0" width="54.333333333333343" height="18"/>
-                                                                                <fontDescription key="fontDescription" type="system" pointSize="15"/>
-                                                                                <nil key="textColor"/>
-                                                                                <nil key="highlightedColor"/>
-                                                                            </label>
-                                                                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="jpA-Nb-pU7" userLabel="Stats AvBG">
-                                                                                <rect key="frame" x="21.333333333333343" y="18" width="50" height="27"/>
-                                                                                <fontDescription key="fontDescription" type="system" pointSize="15"/>
-                                                                                <nil key="textColor"/>
-                                                                                <nil key="highlightedColor"/>
-                                                                            </label>
-                                                                        </subviews>
-                                                                    </stackView>
-                                                                    <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="xGF-Pj-QE0">
-                                                                        <rect key="frame" x="102.66666666666666" y="0.0" width="92.666666666666657" height="45"/>
-                                                                        <subviews>
-                                                                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Est. A1C:" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WV0-Jy-FPs" userLabel="Est. A1C:">
-                                                                                <rect key="frame" x="18.333333333333346" y="0.0" width="56.333333333333343" height="18"/>
-                                                                                <fontDescription key="fontDescription" type="system" pointSize="15"/>
-                                                                                <nil key="textColor"/>
-                                                                                <nil key="highlightedColor"/>
-                                                                            </label>
-                                                                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="7Jx-XF-1vS" userLabel="Stas EstA1C">
-                                                                                <rect key="frame" x="21.333333333333343" y="18" width="50" height="27"/>
-                                                                                <fontDescription key="fontDescription" type="system" pointSize="15"/>
-                                                                                <nil key="textColor"/>
-                                                                                <nil key="highlightedColor"/>
-                                                                            </label>
-                                                                        </subviews>
-                                                                    </stackView>
-                                                                    <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="ohT-Dn-cjf">
-                                                                        <rect key="frame" x="205.33333333333331" y="0.0" width="92.666666666666686" height="45"/>
-                                                                        <subviews>
-                                                                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Std Dev:" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="iXC-Mz-I09">
-                                                                                <rect key="frame" x="16.999999999999996" y="0.0" width="58.666666666666657" height="18"/>
-                                                                                <fontDescription key="fontDescription" type="system" pointSize="15"/>
-                                                                                <nil key="textColor"/>
-                                                                                <nil key="highlightedColor"/>
-                                                                            </label>
-                                                                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wAI-Tp-784" userLabel="Stats StdDev">
-                                                                                <rect key="frame" x="21.333333333333371" y="18" width="50" height="27"/>
-                                                                                <fontDescription key="fontDescription" type="system" pointSize="15"/>
-                                                                                <nil key="textColor"/>
-                                                                                <nil key="highlightedColor"/>
-                                                                            </label>
-                                                                        </subviews>
-                                                                    </stackView>
-                                                                </subviews>
-                                                            </stackView>
-                                                        </subviews>
-                                                    </stackView>
-                                                </subviews>
-                                            </stackView>
-                                        </subviews>
-                                        <color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/>
-                                        <constraints>
-                                            <constraint firstAttribute="height" constant="100" id="6is-PH-c9b"/>
-                                            <constraint firstItem="XsT-6R-Vym" firstAttribute="height" secondItem="ikj-at-auF" secondAttribute="height" id="Flw-2U-D7z"/>
-                                            <constraint firstItem="XsT-6R-Vym" firstAttribute="width" secondItem="ikj-at-auF" secondAttribute="width" id="XAh-NE-9ei"/>
-                                        </constraints>
-                                    </view>
-                                </subviews>
-                                <constraints>
-                                    <constraint firstAttribute="trailing" secondItem="ikj-at-auF" secondAttribute="trailing" id="7F0-z5-mHA"/>
-                                    <constraint firstItem="ikj-at-auF" firstAttribute="leading" secondItem="751-02-Xte" secondAttribute="leading" id="FGR-1b-B5S"/>
-                                    <constraint firstItem="51B-JU-npv" firstAttribute="leading" secondItem="751-02-Xte" secondAttribute="leading" id="OoJ-3b-t1s"/>
-                                    <constraint firstItem="Nml-lr-dgQ" firstAttribute="leading" secondItem="751-02-Xte" secondAttribute="leading" id="Qcj-fm-xhF"/>
-                                    <constraint firstAttribute="trailing" secondItem="51B-JU-npv" secondAttribute="trailing" id="RFB-pP-aNp"/>
-                                    <constraint firstAttribute="trailing" secondItem="Nml-lr-dgQ" secondAttribute="trailing" id="gGi-6c-hji"/>
-                                </constraints>
-                            </stackView>
-                        </subviews>
-                        <viewLayoutGuide key="safeArea" id="PQr-Ze-W5v"/>
-                        <color key="backgroundColor" systemColor="systemBackgroundColor"/>
-                        <constraints>
-                            <constraint firstItem="PQr-Ze-W5v" firstAttribute="bottom" secondItem="751-02-Xte" secondAttribute="bottom" constant="8" id="Ca5-iu-dEv"/>
-                            <constraint firstItem="751-02-Xte" firstAttribute="top" secondItem="mJy-Ly-Jze" secondAttribute="bottom" constant="8" id="FjL-ZS-HMO"/>
-                            <constraint firstItem="PQr-Ze-W5v" firstAttribute="trailing" secondItem="mJy-Ly-Jze" secondAttribute="trailing" constant="8" id="KcX-yW-Vnd"/>
-                            <constraint firstItem="mJy-Ly-Jze" firstAttribute="leading" secondItem="PQr-Ze-W5v" secondAttribute="leading" constant="8" id="b30-Y0-qsu"/>
-                            <constraint firstItem="PQr-Ze-W5v" firstAttribute="trailing" secondItem="751-02-Xte" secondAttribute="trailing" constant="8" id="cR5-mq-6Yo"/>
-                            <constraint firstItem="751-02-Xte" firstAttribute="leading" secondItem="PQr-Ze-W5v" secondAttribute="leading" constant="8" id="t26-9v-oUV"/>
-                            <constraint firstItem="mJy-Ly-Jze" firstAttribute="top" secondItem="PQr-Ze-W5v" secondAttribute="top" constant="8" id="xem-Lc-bdU"/>
-                        </constraints>
-                    </view>
-                    <tabBarItem key="tabBarItem" title="Home" image="house" catalog="system" id="acW-dT-cKf" userLabel="Home"/>
-                    <connections>
-                        <outlet property="BGChart" destination="51B-JU-npv" id="oF1-UA-KHE"/>
-                        <outlet property="BGChartFull" destination="Nml-lr-dgQ" id="Z21-fG-GK9"/>
-                        <outlet property="BGText" destination="vXU-IW-GOL" id="2aC-pv-Qbg"/>
-                        <outlet property="DeltaText" destination="sb7-g9-JNv" id="h7s-Yj-i4M"/>
-                        <outlet property="DirectionText" destination="4sd-6h-LSj" id="lB5-Z0-p25"/>
-                        <outlet property="LoopStatusLabel" destination="XEm-uK-Xui" id="EBm-Ta-tZ1"/>
-                        <outlet property="MinAgoText" destination="UkG-hw-DVo" id="Xd2-fL-qYC"/>
-                        <outlet property="PredictionLabel" destination="oiR-SM-6sg" id="2Sw-TR-SFo"/>
-                        <outlet property="infoTable" destination="kTs-7g-lp9" id="Khq-sV-EZ1"/>
-                        <outlet property="serverText" destination="2mX-T9-SAm" id="Jot-3l-Wim"/>
-                        <outlet property="smallGraphHeightConstraint" destination="qmO-ga-QWl" id="VsZ-zJ-LsJ"/>
-                        <outlet property="statsAvgBG" destination="jpA-Nb-pU7" id="Uo8-a4-Aus"/>
-                        <outlet property="statsEstA1C" destination="7Jx-XF-1vS" id="4RD-nm-JxO"/>
-                        <outlet property="statsEstA1CTitle" destination="WV0-Jy-FPs" id="WnU-h8-2hf"/>
-                        <outlet property="statsHighPercent" destination="HON-rt-8pC" id="283-3S-PCR"/>
-                        <outlet property="statsInRangePercent" destination="7mH-Np-j0L" id="vUp-Pv-Mva"/>
-                        <outlet property="statsLowPercent" destination="TzL-hn-9qu" id="0QR-Mz-KJe"/>
-                        <outlet property="statsPieChart" destination="Hhh-F1-s1p" id="Rhh-Up-Kr0"/>
-                        <outlet property="statsStdDev" destination="wAI-Tp-784" id="BUZ-lS-JfA"/>
-                        <outlet property="statsStdDevTitle" destination="iXC-Mz-I09" id="QHf-Q8-J7B"/>
-                        <outlet property="statsView" destination="ikj-at-auF" id="7AQ-VA-Pw2"/>
-                    </connections>
-                </viewController>
-                <placeholder placeholderIdentifier="IBFirstResponder" id="W5J-7L-Pyd" sceneMemberID="firstResponder"/>
-            </objects>
-            <point key="canvasLocation" x="-1497.1014492753625" y="745.98214285714278"/>
-        </scene>
-        <!--Snoozer-->
-        <scene sceneID="gSd-1V-vbL">
-            <objects>
-                <viewController storyboardIdentifier="SnoozerViewController" id="e0E-cx-0Wr" customClass="SnoozerViewController" customModule="LoopFollow" customModuleProvider="target" sceneMemberID="viewController">
-                    <view key="view" contentMode="scaleToFill" id="oqe-Oz-Hai">
-                        <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
-                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
-                        <viewLayoutGuide key="safeArea" id="Dt4-CR-Iqb"/>
-                        <color key="backgroundColor" systemColor="systemBackgroundColor"/>
-                    </view>
-                    <tabBarItem key="tabBarItem" title="Snoozer" image="zzz" catalog="system" id="TdI-rW-rdW"/>
-                </viewController>
-                <placeholder placeholderIdentifier="IBFirstResponder" id="5xM-cL-5VG" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
-            </objects>
-            <point key="canvasLocation" x="2100" y="745.98214285714278"/>
-        </scene>
-        <!--Nightscout-->
-        <scene sceneID="wg7-f3-ORb">
-            <objects>
-                <viewController storyboardIdentifier="NightscoutViewController" id="8rJ-Kc-sve" customClass="NightscoutViewController" customModule="LoopFollow" customModuleProvider="target" sceneMemberID="viewController">
-                    <view key="view" contentMode="scaleToFill" id="QS5-Rx-YEW">
-                        <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
-                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
-                        <subviews>
-                            <wkWebView contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="5Xw-bG-4Kg">
-                                <rect key="frame" x="0.0" y="88" width="414" height="691"/>
-                                <color key="backgroundColor" red="0.36078431370000003" green="0.38823529410000002" blue="0.4039215686" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
-                                <wkWebViewConfiguration key="configuration">
-                                    <audiovisualMediaTypes key="mediaTypesRequiringUserActionForPlayback" none="YES"/>
-                                    <wkPreferences key="preferences"/>
-                                </wkWebViewConfiguration>
-                            </wkWebView>
-                        </subviews>
-                        <viewLayoutGuide key="safeArea" id="O1u-W8-tvY"/>
-                        <color key="backgroundColor" systemColor="systemBackgroundColor"/>
-                        <constraints>
-                            <constraint firstItem="5Xw-bG-4Kg" firstAttribute="top" secondItem="O1u-W8-tvY" secondAttribute="top" id="5V4-Ga-6op"/>
-                            <constraint firstItem="5Xw-bG-4Kg" firstAttribute="leading" secondItem="QS5-Rx-YEW" secondAttribute="leading" id="FaP-vT-thb"/>
-                            <constraint firstItem="O1u-W8-tvY" firstAttribute="bottom" secondItem="5Xw-bG-4Kg" secondAttribute="bottom" id="QgW-jN-ThE"/>
-                            <constraint firstAttribute="trailing" secondItem="5Xw-bG-4Kg" secondAttribute="trailing" id="hoj-B8-6r3"/>
-                        </constraints>
-                    </view>
-                    <tabBarItem key="tabBarItem" title="Nightscout" image="safari" catalog="system" id="cPa-gy-q4n"/>
-                    <connections>
-                        <outlet property="WebView" destination="5Xw-bG-4Kg" id="WKZ-Pf-oAY"/>
-                    </connections>
-                </viewController>
-                <placeholder placeholderIdentifier="IBFirstResponder" id="4Nw-L8-lE0" sceneMemberID="firstResponder"/>
-            </objects>
-            <point key="canvasLocation" x="-703" y="746"/>
-        </scene>
-        <!--Remote-->
-        <scene sceneID="sEH-4N-YEn">
-            <objects>
-                <viewController storyboardIdentifier="RemoteViewController" id="7an-Fz-r9u" customClass="RemoteViewController" customModule="LoopFollow" customModuleProvider="target" sceneMemberID="viewController">
-                    <view key="view" contentMode="scaleToFill" id="53u-XU-GCk">
-                        <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
-                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
-                        <viewLayoutGuide key="safeArea" id="SlM-Sc-PIr"/>
-                        <color key="backgroundColor" systemColor="systemBackgroundColor"/>
-                    </view>
-                    <tabBarItem key="tabBarItem" title="Remote" image="antenna.radiowaves.left.and.right" catalog="system" id="c7L-5b-Di9"/>
-                    <navigationItem key="navigationItem" id="Lph-Pi-ZZL"/>
-                </viewController>
-                <placeholder placeholderIdentifier="IBFirstResponder" id="Rld-0Y-q4t" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
-            </objects>
-            <point key="canvasLocation" x="130" y="700"/>
-        </scene>
-        <!--Alarm View Controller-->
-        <scene sceneID="a12-B3-C4d">
-            <objects>
-                <viewController storyboardIdentifier="AlarmViewController" id="b34-C5-D6e" customClass="AlarmViewController" customModule="LoopFollow" customModuleProvider="target" sceneMemberID="viewController">
-                    <view key="view" contentMode="scaleToFill" id="c56-D7-E8f">
-                        <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
-                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
-                        <viewLayoutGuide key="safeArea" id="d78-E9-F0g"/>
-                        <color key="backgroundColor" systemColor="systemBackgroundColor"/>
-                    </view>
-                    <navigationItem key="navigationItem" id="e90-F1-G2h"/>
-                </viewController>
-                <placeholder placeholderIdentifier="IBFirstResponder" id="f12-G3-H4i" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
-            </objects>
-            <point key="canvasLocation" x="130" y="700"/>
-        </scene>
-        <!--Settings-->
-        <scene sceneID="ORV-wr-Fd3">
-            <objects>
-                <viewController storyboardIdentifier="SettingsViewController" id="hDv-qK-Udb" customClass="SettingsViewController" customModule="LoopFollow" customModuleProvider="target" sceneMemberID="viewController">
-                    <view key="view" contentMode="scaleToFill" id="ljg-dW-5uR">
-                        <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
-                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
-                        <viewLayoutGuide key="safeArea" id="aVb-ap-Pwt"/>
-                        <color key="backgroundColor" systemColor="systemBackgroundColor"/>
-                    </view>
-                    <tabBarItem key="tabBarItem" title="Settings" image="gear" catalog="system" id="CBE-5V-OzJ"/>
-                </viewController>
-                <placeholder placeholderIdentifier="IBFirstResponder" id="mou-xo-Vm3" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
-            </objects>
-            <point key="canvasLocation" x="1037.68115942029" y="745.98214285714278"/>
-        </scene>
-        <!--Tab Bar Controller-->
-        <scene sceneID="yl2-sM-qoP">
-            <objects>
-                <tabBarController id="49e-Tb-3d3" sceneMemberID="viewController">
-                    <tabBar key="tabBar" contentMode="scaleToFill" id="W28-zg-YXA">
-                        <rect key="frame" x="0.0" y="975" width="768" height="49"/>
-                        <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
-                        <color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
-                    </tabBar>
-                    <connections>
-                        <segue destination="9pv-A4-QxB" kind="relationship" relationship="viewControllers" id="u7Y-xg-7CH"/>
-                        <segue destination="7an-Fz-r9u" kind="relationship" relationship="viewControllers" id="o3X-OO-dh3"/>
-                        <segue destination="e0E-cx-0Wr" kind="relationship" relationship="viewControllers" id="NKn-DR-nLR"/>
-                        <segue destination="8rJ-Kc-sve" kind="relationship" relationship="viewControllers" id="lzU-1b-eKA"/>
-                        <segue destination="hDv-qK-Udb" kind="relationship" relationship="viewControllers" id="maf-Gu-a58"/>
-                    </connections>
-                </tabBarController>
-                <placeholder placeholderIdentifier="IBFirstResponder" id="HuB-VB-40B" sceneMemberID="firstResponder"/>
-            </objects>
-            <point key="canvasLocation" x="0.0" y="0.0"/>
-        </scene>
-    </scenes>
-    <resources>
-        <image name="antenna.radiowaves.left.and.right" catalog="system" width="128" height="111"/>
-        <image name="gear" catalog="system" width="128" height="122"/>
-        <image name="house" catalog="system" width="128" height="104"/>
-        <image name="safari" catalog="system" width="128" height="123"/>
-        <image name="zzz" catalog="system" width="119" height="128"/>
-        <systemColor name="secondarySystemBackgroundColor">
-            <color red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
-        </systemColor>
-        <systemColor name="systemBackgroundColor">
-            <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
-        </systemColor>
-    </resources>
-</document>

+ 23 - 0
LoopFollow/Application/LoopFollowApp.swift

@@ -0,0 +1,23 @@
+// 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 }
+                    #if !targetEnvironment(macCatalyst)
+                        DispatchQueue.main.async {
+                            NotificationCenter.default.post(name: .liveActivityDidForeground, object: nil)
+                        }
+                    #endif
+                }
+        }
+    }
+}

+ 86 - 0
LoopFollow/Application/MainTabView.swift

@@ -0,0 +1,86 @@
+// LoopFollow
+// MainTabView.swift
+
+import SwiftUI
+
+struct MainTabView: View {
+    @ObservedObject private var selectedTab = Observable.shared.selectedTabIndex
+    @ObservedObject private var appearanceMode = Storage.shared.appearanceMode
+    @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
+
+    @State private var showTelemetryConsent = false
+
+    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(appearanceMode.value.colorScheme)
+        .onAppear {
+            // Start the data pipeline as soon as the UI appears, independent of
+            // tab layout. Without this, a user who moves Home into the Menu would
+            // have no MainViewController — and therefore no data fetching, alarms,
+            // or background audio — until they manually opened Home. Tying it to
+            // onAppear (not app launch) keeps it off the BG-only refresh path.
+            MainViewController.bootstrap()
+
+            // One-time consent prompt. Previously presented by SceneDelegate,
+            // which was removed in the storyboard→SwiftUI migration; without
+            // this, fresh installs stay permanently undecided and telemetry
+            // never sends. The storage flag keeps it to a single appearance.
+            if !Storage.shared.telemetryConsentDecisionMade.value {
+                showTelemetryConsent = true
+            }
+        }
+        .sheet(isPresented: $showTelemetryConsent) {
+            // User must explicitly choose — no swipe-to-dismiss.
+            TelemetryConsentView()
+                .interactiveDismissDisabled(true)
+        }
+    }
+
+    @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)
+            }
+        }
+    }
+}

+ 0 - 121
LoopFollow/Application/SceneDelegate.swift

@@ -1,121 +0,0 @@
-// LoopFollow
-// SceneDelegate.swift
-
-import AVFoundation
-import SwiftUI
-import UIKit
-
-class SceneDelegate: UIResponder, UIWindowSceneDelegate {
-    var window: UIWindow?
-    let synthesizer = AVSpeechSynthesizer()
-
-    /// One-shot guard so the consent prompt is only attempted once per
-    /// process lifetime even if the scene activates repeatedly.
-    private var consentPromptShownThisProcess = false
-
-    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.
-        runTelemetryFirstForegroundHook()
-    }
-
-    /// Presents the one-time consent sheet on first foreground. Sending is
-    /// handled by AppDelegate at launch and by TaskScheduler thereafter —
-    /// firing maybeSend here would duplicate the launch-time send.
-    private func runTelemetryFirstForegroundHook() {
-        if !Storage.shared.telemetryConsentDecisionMade.value,
-           !consentPromptShownThisProcess
-        {
-            consentPromptShownThisProcess = true
-            presentTelemetryConsentSheet()
-        }
-    }
-
-    private func presentTelemetryConsentSheet() {
-        guard let root = window?.rootViewController else { return }
-        // Find the topmost presented controller so we don't try to present
-        // over a sheet that's already up.
-        var top = root
-        while let presented = top.presentedViewController {
-            top = presented
-        }
-
-        let host = UIHostingController(rootView: TelemetryConsentView())
-        host.isModalInPresentation = true // user must explicitly choose
-        // Defer to the next runloop so view hierarchy is settled when the
-        // scene first becomes active on a fresh install.
-        DispatchQueue.main.async {
-            top.present(host, animated: true)
-        }
-    }
-
-    func scene(_: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
-        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.
-        #if !targetEnvironment(macCatalyst)
-            DispatchQueue.main.async {
-                NotificationCenter.default.post(name: .liveActivityDidForeground, object: nil)
-            }
-        #endif
-    }
-
-    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)
-    }
-}

+ 11 - 13
LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift

@@ -11,22 +11,20 @@ struct BackgroundRefreshSettingsView: View {
     @ObservedObject var bleManager = BLEManager.shared
 
     var body: some View {
-        NavigationView {
-            Form {
-                refreshTypeSection
+        Form {
+            refreshTypeSection
 
-                if viewModel.backgroundRefreshType.isBluetooth {
-                    selectedDeviceSection
-                    availableDevicesSection
-                }
-            }
-            .onAppear {
-                startTimer()
-            }
-            .onDisappear {
-                stopTimer()
+            if viewModel.backgroundRefreshType.isBluetooth {
+                selectedDeviceSection
+                availableDevicesSection
             }
         }
+        .onAppear {
+            startTimer()
+        }
+        .onDisappear {
+            stopTimer()
+        }
         .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme)
         .navigationBarTitle("Background Refresh Settings", displayMode: .inline)
     }

+ 5 - 1
LoopFollow/Controllers/AlarmSound.swift

@@ -339,7 +339,11 @@ extension MPVolumeView {
             slider?.value = volume
         }
         // Optional - Remove the HUD
-        if let app = UIApplication.shared.delegate as? AppDelegate, let window = app.window {
+        let activeWindow = UIApplication.shared.connectedScenes
+            .compactMap { $0 as? UIWindowScene }
+            .first { $0.activationState == .foregroundActive }?
+            .windows.first(where: \.isKeyWindow)
+        if let window = activeWindow {
             volumeView.alpha = 0.000001
             window.addSubview(volumeView)
         }

+ 8 - 1
LoopFollow/Controllers/Graphs.swift

@@ -882,7 +882,14 @@ extension MainViewController {
         BGChartFull.data?.notifyDataChanged()
         BGChartFull.notifyDataSetChanged()
 
-        if firstGraphLoad {
+        // The initial zoom is a one-shot, relative to the chart's current
+        // viewport. Skip it until the chart actually has a width — otherwise a
+        // refresh that lands while the view is loaded but off-screen (e.g. Home
+        // lives in the Menu, so MainViewController is force-loaded headless by
+        // bootstrap()) would consume firstGraphLoad against a zero-size viewport
+        // and leave the main graph blank. viewDidAppear re-runs updateBGGraph
+        // once a real frame exists, applying the zoom correctly.
+        if firstGraphLoad, BGChart.bounds.width > 0 {
             var scaleX = CGFloat(Storage.shared.chartScaleX.value)
             if scaleX > CGFloat(ScaleXMax) {
                 scaleX = CGFloat(ScaleXMax)

+ 15 - 59
LoopFollow/Controllers/MainViewController+updateStats.swift

@@ -1,9 +1,7 @@
 // LoopFollow
 // MainViewController+updateStats.swift
 
-import Charts
 import Foundation
-import UIKit
 
 extension MainViewController {
     func updateStats() {
@@ -22,75 +20,33 @@ 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))
-            statsEstA1CTitle.text = UnitSettingsStore.shared.glycemicMetricMode == .gmi ? "GMI:" : "Est. A1C:"
+            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))
 
+            statsDisplayModel.estA1CTitle = UnitSettingsStore.shared.glycemicMetricMode == .gmi ? "GMI:" : "Est. A1C:"
             if UnitSettingsStore.shared.glycemicOutputUnit == .mmolMol {
-                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)
             }
 
             if UnitSettingsStore.shared.variabilityMetricMode == .stdDeviation {
-                statsStdDevTitle.text = "Std Dev:"
+                statsDisplayModel.stdDevTitle = "Std Dev:"
                 if UnitSettingsStore.shared.glucoseUnit == .mgdL {
-                    statsStdDev.text = String(format: "%.0f", stats.stdDev)
+                    statsDisplayModel.stdDev = String(format: "%.0f", stats.stdDev)
                 } else {
-                    statsStdDev.text = String(format: "%.1f", stats.stdDev)
+                    statsDisplayModel.stdDev = String(format: "%.1f", stats.stdDev)
                 }
             } else {
-                statsStdDevTitle.text = "CV:"
-                statsStdDev.text = String(format: "%.1f%%", stats.coefficientOfVariation)
+                statsDisplayModel.stdDevTitle = "CV:"
+                statsDisplayModel.stdDev = String(format: "%.1f%%", stats.coefficientOfVariation)
             }
 
-            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
-    }
 }

+ 2 - 2
LoopFollow/Controllers/Nightscout/BGData.swift

@@ -216,9 +216,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
         }
     }
 

+ 5 - 25
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

+ 7 - 8
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 = "↻"
             }
 

+ 7 - 8
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 {
@@ -218,15 +217,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 = "↻"
             }
 

+ 18 - 0
LoopFollow/Controllers/StatsDisplayModel.swift

@@ -0,0 +1,18 @@
+// 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 estA1CTitle: String = "Est A1C:"
+    @Published var stdDev: String = ""
+    @Published var stdDevTitle: String = "Std Dev:"
+    @Published var pieLow: Double = 0
+    @Published var pieRange: Double = 0
+    @Published var pieHigh: Double = 0
+}

+ 84 - 0
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: model.estA1CTitle, value: model.estA1C)
+                    statColumn(title: model.stdDevTitle, 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)
+    }
+}

+ 0 - 24
LoopFollow/Extensions/UIViewExtension.swift

@@ -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)
-    }
-}

+ 0 - 4
LoopFollow/Helpers/AppearanceMode.swift

@@ -3,10 +3,6 @@
 
 import SwiftUI
 
-extension Notification.Name {
-    static let appearanceDidChange = Notification.Name("appearanceDidChange")
-}
-
 enum AppearanceMode: String, CaseIterable, Codable {
     case system
     case light

+ 1 - 31
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
     }
 }

+ 3 - 8
LoopFollow/Helpers/Views/NavigationRow.swift

@@ -3,23 +3,18 @@
 
 import SwiftUI
 
-struct NavigationRow: View {
+struct NavigationRow<Value: Hashable>: View {
     let title: String
     let icon: String
     var iconTint: Color = .white
-    let action: () -> Void
+    let value: Value
 
     var body: some View {
-        Button(action: action) {
+        NavigationLink(value: value) {
             HStack {
                 Glyph(symbol: icon, tint: iconTint)
                 Text(title)
-                Spacer()
-                Image(systemName: "chevron.right")
-                    .foregroundColor(Color(uiColor: .tertiaryLabel))
             }
-            .contentShape(Rectangle())
         }
-        .buttonStyle(.plain)
     }
 }

+ 0 - 35
LoopFollow/Info.plist

@@ -66,25 +66,6 @@
 	<string></string>
 	<key>NSSupportsLiveActivities</key>
 	<true/>
-	<key>UIApplicationSceneManifest</key>
-	<dict>
-		<key>UIApplicationSupportsMultipleScenes</key>
-		<false/>
-		<key>UISceneConfigurations</key>
-		<dict>
-			<key>UIWindowSceneSessionRoleApplication</key>
-			<array>
-				<dict>
-					<key>UISceneConfigurationName</key>
-					<string>Default Configuration</string>
-					<key>UISceneDelegateClassName</key>
-					<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
-					<key>UISceneStoryboardFile</key>
-					<string>Main</string>
-				</dict>
-			</array>
-		</dict>
-	</dict>
 	<key>UIBackgroundModes</key>
 	<array>
 		<string>audio</string>
@@ -97,22 +78,6 @@
 	<true/>
 	<key>UILaunchStoryboardName</key>
 	<string>LaunchScreen</string>
-	<key>UIMainStoryboardFile</key>
-	<string>Main</string>
-	<key>UIRequiredDeviceCapabilities</key>
-	<array>
-		<string>armv7</string>
-	</array>
-	<key>UIStatusBarTintParameters</key>
-	<dict>
-		<key>UINavigationBar</key>
-		<dict>
-			<key>Style</key>
-			<string>UIBarStyleDefault</string>
-			<key>Translucent</key>
-			<false/>
-		</dict>
-	</dict>
 	<key>UISupportedInterfaceOrientations</key>
 	<array>
 		<string>UIInterfaceOrientationLandscapeLeft</string>

+ 25 - 27
LoopFollow/InfoDisplaySettings/InfoDisplaySettingsView.swift

@@ -7,38 +7,36 @@ struct InfoDisplaySettingsView: View {
     @ObservedObject var viewModel: InfoDisplaySettingsViewModel
 
     var body: some View {
-        NavigationView {
-            Form {
-                Section(header: Text("General")) {
-                    Toggle(isOn: Binding(
-                        get: { Storage.shared.hideInfoTable.value },
-                        set: { Storage.shared.hideInfoTable.value = $0 }
-                    )) {
-                        Text("Hide Information Table")
-                    }
+        Form {
+            Section(header: Text("General")) {
+                Toggle(isOn: Binding(
+                    get: { Storage.shared.hideInfoTable.value },
+                    set: { Storage.shared.hideInfoTable.value = $0 }
+                )) {
+                    Text("Hide Information Table")
                 }
+            }
 
-                Section(header: Text("Information Display Settings")) {
-                    ForEach(viewModel.infoSort, id: \.self) { sortedIndex in
-                        HStack {
-                            Text(viewModel.getName(for: sortedIndex))
-                            Spacer()
-                            Toggle("", isOn: Binding(
-                                get: { viewModel.infoVisible[sortedIndex] },
-                                set: { _ in
-                                    viewModel.toggleVisibility(for: sortedIndex)
-                                }
-                            ))
-                            .labelsHidden()
-                        }
+            Section(header: Text("Information Display Settings")) {
+                ForEach(viewModel.infoSort, id: \.self) { sortedIndex in
+                    HStack {
+                        Text(viewModel.getName(for: sortedIndex))
+                        Spacer()
+                        Toggle("", isOn: Binding(
+                            get: { viewModel.infoVisible[sortedIndex] },
+                            set: { _ in
+                                viewModel.toggleVisibility(for: sortedIndex)
+                            }
+                        ))
+                        .labelsHidden()
                     }
-                    .onMove(perform: viewModel.move)
                 }
+                .onMove(perform: viewModel.move)
             }
-            .environment(\.editMode, .constant(.active))
-            .onDisappear {
-                NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil)
-            }
+        }
+        .environment(\.editMode, .constant(.active))
+        .onDisappear {
+            NotificationCenter.default.post(name: NSNotification.Name("refresh"), object: nil)
         }
         .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme)
         .navigationBarTitle("Information Display Settings", displayMode: .inline)

+ 5 - 3
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
     }

+ 15 - 28
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]
+            }
     }
 }

+ 34 - 0
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))
+    }
+}

+ 0 - 10
LoopFollow/Nightscout/NightscoutSettingsViewModel.swift

@@ -4,13 +4,7 @@
 import Combine
 import Foundation
 
-protocol NightscoutSettingsViewModelDelegate: AnyObject {
-    func nightscoutSettingsDidFinish()
-}
-
 class NightscoutSettingsViewModel: ObservableObject {
-    weak var delegate: NightscoutSettingsViewModelDelegate?
-
     private var initialURL: String
     private var initialToken: String
 
@@ -147,8 +141,4 @@ class NightscoutSettingsViewModel: ObservableObject {
             }
         }
     }
-
-    func dismiss() {
-        delegate?.nightscoutSettingsDidFinish()
-    }
 }

+ 48 - 0
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
+            }
+        }
+    }
+}

+ 0 - 126
LoopFollow/Remote/RemoteViewController.swift

@@ -1,126 +0,0 @@
-// LoopFollow
-// RemoteViewController.swift
-
-import Combine
-import SwiftUI
-import UIKit
-
-class RemoteViewController: UIViewController {
-    private var cancellables = Set<AnyCancellable>()
-    private var hostingController: UIHostingController<AnyView>?
-
-    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
-        }
-    }
-}

+ 13 - 15
LoopFollow/Settings/AdvancedSettingsView.swift

@@ -7,24 +7,22 @@ struct AdvancedSettingsView: View {
     @ObservedObject var viewModel: AdvancedSettingsViewModel
 
     var body: some View {
-        NavigationView {
-            Form {
-                Section(header: Text("Advanced Settings")) {
-                    Toggle("Download Treatments", isOn: $viewModel.downloadTreatments)
-                    Toggle("Download Prediction", isOn: $viewModel.downloadPrediction)
-                    Toggle("Graph Basal", isOn: $viewModel.graphBasal)
-                    Toggle("Graph Bolus", isOn: $viewModel.graphBolus)
-                    Toggle("Graph Carbs", isOn: $viewModel.graphCarbs)
-                    Toggle("Graph Other Treatments", isOn: $viewModel.graphOtherTreatments)
+        Form {
+            Section(header: Text("Advanced Settings")) {
+                Toggle("Download Treatments", isOn: $viewModel.downloadTreatments)
+                Toggle("Download Prediction", isOn: $viewModel.downloadPrediction)
+                Toggle("Graph Basal", isOn: $viewModel.graphBasal)
+                Toggle("Graph Bolus", isOn: $viewModel.graphBolus)
+                Toggle("Graph Carbs", isOn: $viewModel.graphCarbs)
+                Toggle("Graph Other Treatments", isOn: $viewModel.graphOtherTreatments)
 
-                    Stepper(value: $viewModel.bgUpdateDelay, in: 1 ... 30, step: 1) {
-                        Text("BG Update Delay (Sec): \(viewModel.bgUpdateDelay)")
-                    }
+                Stepper(value: $viewModel.bgUpdateDelay, in: 1 ... 30, step: 1) {
+                    Text("BG Update Delay (Sec): \(viewModel.bgUpdateDelay)")
                 }
+            }
 
-                Section(header: Text("Logging Options")) {
-                    Toggle("Debug Log Level", isOn: $viewModel.debugLogLevel)
-                }
+            Section(header: Text("Logging Options")) {
+                Toggle("Debug Log Level", isOn: $viewModel.debugLogLevel)
             }
         }
         .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme)

+ 42 - 44
LoopFollow/Settings/CalendarSettingsView.swift

@@ -20,59 +20,57 @@ struct CalendarSettingsView: View {
     // MARK: Body
 
     var body: some View {
-        NavigationView {
-            Form {
-                // ------------- Calendar write -------------
-                Section {
-                    Toggle("Save BG to Calendar",
-                           isOn: $writeCalendarEvent.value)
-                        .disabled(accessDenied) // prevent use when no access
-                } footer: {
-                    Text("""
-                    Add the Apple-Calendar complication to your watch or CarPlay \
-                    to see BG readings. Create a separate calendar (e.g. “Follow”) \
-                    — this view will **delete** events on the same calendar each time \
-                    it writes new readings.
-                    """)
-                }
+        Form {
+            // ------------- Calendar write -------------
+            Section {
+                Toggle("Save BG to Calendar",
+                       isOn: $writeCalendarEvent.value)
+                    .disabled(accessDenied) // prevent use when no access
+            } footer: {
+                Text("""
+                Add the Apple-Calendar complication to your watch or CarPlay \
+                to see BG readings. Create a separate calendar (e.g. “Follow”) \
+                — this view will **delete** events on the same calendar each time \
+                it writes new readings.
+                """)
+            }
 
-                // ------------- Access / calendar picker -------------
-                if accessDenied {
-                    Text("Calendar access denied")
-                        .foregroundColor(.red)
-                } else {
-                    if !calendars.isEmpty {
-                        Picker("Calendar",
-                               selection: $calendarIdentifier.value)
-                        {
-                            ForEach(calendars, id: \.calendarIdentifier) { cal in
-                                Text(cal.title).tag(cal.calendarIdentifier)
-                            }
+            // ------------- Access / calendar picker -------------
+            if accessDenied {
+                Text("Calendar access denied")
+                    .foregroundColor(.red)
+            } else {
+                if !calendars.isEmpty {
+                    Picker("Calendar",
+                           selection: $calendarIdentifier.value)
+                    {
+                        ForEach(calendars, id: \.calendarIdentifier) { cal in
+                            Text(cal.title).tag(cal.calendarIdentifier)
                         }
                     }
                 }
+            }
 
-                // ------------- Template lines -------------
-                Section("Calendar Text") {
-                    TextField("Line 1", text: $watchLine1.value)
-                        .textInputAutocapitalization(.never)
-                        .disableAutocorrection(true)
+            // ------------- Template lines -------------
+            Section("Calendar Text") {
+                TextField("Line 1", text: $watchLine1.value)
+                    .textInputAutocapitalization(.never)
+                    .disableAutocorrection(true)
 
-                    TextField("Line 2", text: $watchLine2.value)
-                        .textInputAutocapitalization(.never)
-                        .disableAutocorrection(true)
-                }
+                TextField("Line 2", text: $watchLine2.value)
+                    .textInputAutocapitalization(.never)
+                    .disableAutocorrection(true)
+            }
 
-                // ------------- Variable cheat-sheet -------------
-                Section("Available Variables") {
-                    ForEach(variableDescriptions, id: \.self) { desc in
-                        Text(desc)
-                    }
+            // ------------- Variable cheat-sheet -------------
+            Section("Available Variables") {
+                ForEach(variableDescriptions, id: \.self) { desc in
+                    Text(desc)
                 }
             }
-            .task { // runs once on appear
-                await requestCalendarAccessAndLoad()
-            }
+        }
+        .task { // runs once on appear
+            await requestCalendarAccessAndLoad()
         }
         .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme)
         .navigationBarTitle("Calendar", displayMode: .inline)

+ 78 - 80
LoopFollow/Settings/ContactSettingsView.swift

@@ -12,117 +12,115 @@ struct ContactSettingsView: View {
     @State private var alertMessage: String = ""
 
     var body: some View {
-        NavigationView {
-            Form {
-                Section(header: Text("Contact Integration")) {
-                    Text("Add the contact named '\(viewModel.contactName)' to your watch face to show the current BG value in real time. Make sure to give the app full access to Contacts when prompted.")
+        Form {
+            Section(header: Text("Contact Integration")) {
+                Text("Add the contact named '\(viewModel.contactName)' to your watch face to show the current BG value in real time. Make sure to give the app full access to Contacts when prompted.")
+                    .font(.footnote)
+                    .foregroundColor(.secondary)
+                    .padding(.vertical, 4)
+
+                Toggle("Enable Contact BG Updates", isOn: $viewModel.contactEnabled)
+                    .toggleStyle(SwitchToggleStyle())
+                    .onChange(of: viewModel.contactEnabled) { isEnabled in
+                        if isEnabled {
+                            requestContactAccess()
+                        }
+                    }
+            }
+
+            if viewModel.contactEnabled {
+                Section(header: Text("Color Options")) {
+                    Text("Select the colors for your BG values. Note: not all watch faces allow control over colors. Recommend options like Activity or Modular Duo if you want to customize colors.")
                         .font(.footnote)
                         .foregroundColor(.secondary)
                         .padding(.vertical, 4)
 
-                    Toggle("Enable Contact BG Updates", isOn: $viewModel.contactEnabled)
-                        .toggleStyle(SwitchToggleStyle())
-                        .onChange(of: viewModel.contactEnabled) { isEnabled in
-                            if isEnabled {
-                                requestContactAccess()
-                            }
+                    Picker("Background Color", selection: $viewModel.contactBackgroundColor) {
+                        ForEach(ContactColorOption.allCases, id: \.rawValue) { option in
+                            Text(option.rawValue.capitalized).tag(option.rawValue)
                         }
-                }
+                    }
 
-                if viewModel.contactEnabled {
-                    Section(header: Text("Color Options")) {
-                        Text("Select the colors for your BG values. Note: not all watch faces allow control over colors. Recommend options like Activity or Modular Duo if you want to customize colors.")
-                            .font(.footnote)
-                            .foregroundColor(.secondary)
-                            .padding(.vertical, 4)
+                    Picker("Color Mode", selection: $viewModel.contactColorMode) {
+                        ForEach(ContactColorMode.allCases, id: \.self) { mode in
+                            Text(mode.displayName).tag(mode)
+                        }
+                    }
 
-                        Picker("Background Color", selection: $viewModel.contactBackgroundColor) {
+                    if viewModel.contactColorMode == .staticColor {
+                        Picker("Text Color", selection: $viewModel.contactTextColor) {
                             ForEach(ContactColorOption.allCases, id: \.rawValue) { option in
                                 Text(option.rawValue.capitalized).tag(option.rawValue)
                             }
                         }
-
-                        Picker("Color Mode", selection: $viewModel.contactColorMode) {
-                            ForEach(ContactColorMode.allCases, id: \.self) { mode in
-                                Text(mode.displayName).tag(mode)
-                            }
-                        }
-
-                        if viewModel.contactColorMode == .staticColor {
-                            Picker("Text Color", selection: $viewModel.contactTextColor) {
-                                ForEach(ContactColorOption.allCases, id: \.rawValue) { option in
-                                    Text(option.rawValue.capitalized).tag(option.rawValue)
-                                }
-                            }
-                        } else {
-                            Text("Dynamic mode colors text based on BG range: Green (in range), Yellow (high), Red (low)")
-                                .font(.footnote)
-                                .foregroundColor(.secondary)
-                        }
-                    }
-
-                    Section(header: Text("Additional Information")) {
-                        Text("To see your trend, delta, or IOB, include them in another contact or create separate contacts. When using 'Include', select which contact to add the value to.")
+                    } else {
+                        Text("Dynamic mode colors text based on BG range: Green (in range), Yellow (high), Red (low)")
                             .font(.footnote)
                             .foregroundColor(.secondary)
-                            .padding(.vertical, 4)
+                    }
+                }
 
-                        Text("Trend")
-                            .font(.subheadline)
-                        Picker("Show Trend", selection: $viewModel.contactTrend) {
-                            ForEach(ContactIncludeOption.allCases, id: \.self) { option in
-                                Text(option.rawValue).tag(option)
-                            }
+                Section(header: Text("Additional Information")) {
+                    Text("To see your trend, delta, or IOB, include them in another contact or create separate contacts. When using 'Include', select which contact to add the value to.")
+                        .font(.footnote)
+                        .foregroundColor(.secondary)
+                        .padding(.vertical, 4)
+
+                    Text("Trend")
+                        .font(.subheadline)
+                    Picker("Show Trend", selection: $viewModel.contactTrend) {
+                        ForEach(ContactIncludeOption.allCases, id: \.self) { option in
+                            Text(option.rawValue).tag(option)
                         }
-                        .pickerStyle(SegmentedPickerStyle())
+                    }
+                    .pickerStyle(SegmentedPickerStyle())
 
-                        if viewModel.contactTrend == .include {
-                            Picker("Include Trend in", selection: $viewModel.contactTrendTarget) {
-                                ForEach(viewModel.availableTargets(for: .Trend), id: \.self) { target in
-                                    Text(target.rawValue).tag(target)
-                                }
+                    if viewModel.contactTrend == .include {
+                        Picker("Include Trend in", selection: $viewModel.contactTrendTarget) {
+                            ForEach(viewModel.availableTargets(for: .Trend), id: \.self) { target in
+                                Text(target.rawValue).tag(target)
                             }
                         }
+                    }
 
-                        Text("Delta")
-                            .font(.subheadline)
-                        Picker("Show Delta", selection: $viewModel.contactDelta) {
-                            ForEach(ContactIncludeOption.allCases, id: \.self) { option in
-                                Text(option.rawValue).tag(option)
-                            }
+                    Text("Delta")
+                        .font(.subheadline)
+                    Picker("Show Delta", selection: $viewModel.contactDelta) {
+                        ForEach(ContactIncludeOption.allCases, id: \.self) { option in
+                            Text(option.rawValue).tag(option)
                         }
-                        .pickerStyle(SegmentedPickerStyle())
+                    }
+                    .pickerStyle(SegmentedPickerStyle())
 
-                        if viewModel.contactDelta == .include {
-                            Picker("Include Delta in", selection: $viewModel.contactDeltaTarget) {
-                                ForEach(viewModel.availableTargets(for: .Delta), id: \.self) { target in
-                                    Text(target.rawValue).tag(target)
-                                }
+                    if viewModel.contactDelta == .include {
+                        Picker("Include Delta in", selection: $viewModel.contactDeltaTarget) {
+                            ForEach(viewModel.availableTargets(for: .Delta), id: \.self) { target in
+                                Text(target.rawValue).tag(target)
                             }
                         }
+                    }
 
-                        Text("IOB")
-                            .font(.subheadline)
-                        Picker("Show IOB", selection: $viewModel.contactIOB) {
-                            ForEach(ContactIncludeOption.allCases, id: \.self) { option in
-                                Text(option.rawValue).tag(option)
-                            }
+                    Text("IOB")
+                        .font(.subheadline)
+                    Picker("Show IOB", selection: $viewModel.contactIOB) {
+                        ForEach(ContactIncludeOption.allCases, id: \.self) { option in
+                            Text(option.rawValue).tag(option)
                         }
-                        .pickerStyle(SegmentedPickerStyle())
+                    }
+                    .pickerStyle(SegmentedPickerStyle())
 
-                        if viewModel.contactIOB == .include {
-                            Picker("Include IOB in", selection: $viewModel.contactIOBTarget) {
-                                ForEach(viewModel.availableTargets(for: .IOB), id: \.self) { target in
-                                    Text(target.rawValue).tag(target)
-                                }
+                    if viewModel.contactIOB == .include {
+                        Picker("Include IOB in", selection: $viewModel.contactIOBTarget) {
+                            ForEach(viewModel.availableTargets(for: .IOB), id: \.self) { target in
+                                Text(target.rawValue).tag(target)
                             }
                         }
                     }
                 }
             }
-            .alert(isPresented: $showAlert) {
-                Alert(title: Text(alertTitle), message: Text(alertMessage), dismissButton: .default(Text("OK")))
-            }
+        }
+        .alert(isPresented: $showAlert) {
+            Alert(title: Text(alertTitle), message: Text(alertMessage), dismissButton: .default(Text("OK")))
         }
         .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme)
         .navigationBarTitle("Contact", displayMode: .inline)

+ 86 - 88
LoopFollow/Settings/GeneralSettingsView.swift

@@ -32,118 +32,116 @@ struct GeneralSettingsView: View {
     @ObservedObject var telemetryEnabled = Storage.shared.telemetryEnabled
 
     var body: some View {
-        NavigationView {
-            Form {
-                Section("App Settings") {
-                    Toggle("Display App Badge", isOn: $appBadge.value)
-                    Toggle("Persistent Notification", isOn: $persistentNotification.value)
-                }
+        Form {
+            Section("App Settings") {
+                Toggle("Display App Badge", isOn: $appBadge.value)
+                Toggle("Persistent Notification", isOn: $persistentNotification.value)
+            }
 
-                Section("Display") {
-                    Picker("Appearance", selection: $appearanceMode.value) {
-                        ForEach(AppearanceMode.allCases, id: \.self) { mode in
-                            Text(mode.displayName).tag(mode)
-                        }
+            Section("Display") {
+                Picker("Appearance", selection: $appearanceMode.value) {
+                    ForEach(AppearanceMode.allCases, id: \.self) { mode in
+                        Text(mode.displayName).tag(mode)
                     }
-                    Toggle("Display Stats", isOn: $showStats.value)
-                    Toggle("Display Small Graph", isOn: $showSmallGraph.value)
-                    Toggle("Color BG Text", isOn: $colorBGText.value)
-                    Toggle("Keep Screen Active", isOn: $screenlockSwitchState.value)
-                    Toggle("Show Display Name", isOn: $showDisplayName.value)
-                    Toggle("Snoozer emoji", isOn: $snoozerEmoji.value)
-                    Toggle("Force portrait mode", isOn: $forcePortraitMode.value)
-                        .onChange(of: forcePortraitMode.value) { _ in
-                            let window = UIApplication.shared.connectedScenes
-                                .compactMap { $0 as? UIWindowScene }
-                                .flatMap { $0.windows }
-                                .first
-
-                            window?.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations()
-                        }
                 }
+                Toggle("Display Stats", isOn: $showStats.value)
+                Toggle("Display Small Graph", isOn: $showSmallGraph.value)
+                Toggle("Color BG Text", isOn: $colorBGText.value)
+                Toggle("Keep Screen Active", isOn: $screenlockSwitchState.value)
+                Toggle("Show Display Name", isOn: $showDisplayName.value)
+                Toggle("Snoozer emoji", isOn: $snoozerEmoji.value)
+                Toggle("Force portrait mode", isOn: $forcePortraitMode.value)
+                    .onChange(of: forcePortraitMode.value) { _ in
+                        let window = UIApplication.shared.connectedScenes
+                            .compactMap { $0 as? UIWindowScene }
+                            .flatMap { $0.windows }
+                            .first
+
+                        window?.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations()
+                    }
+            }
 
-                Section("Time Zone") {
-                    Toggle("Time Zone Override", isOn: $graphTimeZoneEnabled.value)
-                        .onChange(of: graphTimeZoneEnabled.value) { _ in markChartSettingsDirty() }
+            Section("Time Zone") {
+                Toggle("Time Zone Override", isOn: $graphTimeZoneEnabled.value)
+                    .onChange(of: graphTimeZoneEnabled.value) { _ in markChartSettingsDirty() }
 
-                    if graphTimeZoneEnabled.value {
-                        Picker("Time Zone", selection: $graphTimeZoneIdentifier.value) {
-                            ForEach(Self.sortedTimeZones, id: \.identifier) { tz in
-                                Text(Self.timeZoneLabel(tz)).tag(tz.identifier)
-                            }
+                if graphTimeZoneEnabled.value {
+                    Picker("Time Zone", selection: $graphTimeZoneIdentifier.value) {
+                        ForEach(Self.sortedTimeZones, id: \.identifier) { tz in
+                            Text(Self.timeZoneLabel(tz)).tag(tz.identifier)
                         }
-                        .onChange(of: graphTimeZoneIdentifier.value) { _ in markChartSettingsDirty() }
                     }
+                    .onChange(of: graphTimeZoneIdentifier.value) { _ in markChartSettingsDirty() }
                 }
+            }
 
-                Section("Speak BG") {
-                    Toggle("Speak BG", isOn: $speakBG.value.animation())
+            Section("Speak BG") {
+                Toggle("Speak BG", isOn: $speakBG.value.animation())
 
-                    if speakBG.value {
-                        Picker("Language", selection: $speakLanguage.value) {
-                            Text("English").tag("en")
-                            Text("French").tag("fr")
-                            Text("Italian").tag("it")
-                            Text("Slovak").tag("sk")
-                            Text("Swedish").tag("sv")
-                        }
+                if speakBG.value {
+                    Picker("Language", selection: $speakLanguage.value) {
+                        Text("English").tag("en")
+                        Text("French").tag("fr")
+                        Text("Italian").tag("it")
+                        Text("Slovak").tag("sk")
+                        Text("Swedish").tag("sv")
+                    }
 
-                        Toggle("Always", isOn: $speakBGAlways.value.animation())
+                    Toggle("Always", isOn: $speakBGAlways.value.animation())
 
-                        if !speakBGAlways.value {
-                            Toggle("Low", isOn: $speakLowBG.value.animation())
-                                .onChange(of: speakLowBG.value) { newValue in
-                                    if newValue {
-                                        speakProactiveLowBG.value = false
-                                    }
+                    if !speakBGAlways.value {
+                        Toggle("Low", isOn: $speakLowBG.value.animation())
+                            .onChange(of: speakLowBG.value) { newValue in
+                                if newValue {
+                                    speakProactiveLowBG.value = false
                                 }
+                            }
 
-                            Toggle("Proactive Low", isOn: $speakProactiveLowBG.value.animation())
-                                .onChange(of: speakProactiveLowBG.value) { newValue in
-                                    if newValue {
-                                        speakLowBG.value = false
-                                    }
+                        Toggle("Proactive Low", isOn: $speakProactiveLowBG.value.animation())
+                            .onChange(of: speakProactiveLowBG.value) { newValue in
+                                if newValue {
+                                    speakLowBG.value = false
                                 }
-
-                            if speakLowBG.value || speakProactiveLowBG.value {
-                                BGPicker(
-                                    title: "Low BG Limit",
-                                    range: 40 ... 108,
-                                    value: $speakLowBGLimit.value
-                                )
                             }
 
-                            if speakProactiveLowBG.value {
-                                BGPicker(
-                                    title: "Fast Drop Delta",
-                                    range: 3 ... 20,
-                                    value: $speakFastDropDelta.value
-                                )
-                            }
+                        if speakLowBG.value || speakProactiveLowBG.value {
+                            BGPicker(
+                                title: "Low BG Limit",
+                                range: 40 ... 108,
+                                value: $speakLowBGLimit.value
+                            )
+                        }
 
-                            Toggle("High", isOn: $speakHighBG.value.animation())
+                        if speakProactiveLowBG.value {
+                            BGPicker(
+                                title: "Fast Drop Delta",
+                                range: 3 ... 20,
+                                value: $speakFastDropDelta.value
+                            )
+                        }
 
-                            if speakHighBG.value {
-                                BGPicker(
-                                    title: "High BG Limit",
-                                    range: 140 ... 300,
-                                    value: $speakHighBGLimit.value
-                                )
-                            }
+                        Toggle("High", isOn: $speakHighBG.value.animation())
+
+                        if speakHighBG.value {
+                            BGPicker(
+                                title: "High BG Limit",
+                                range: 140 ... 300,
+                                value: $speakHighBGLimit.value
+                            )
                         }
                     }
                 }
+            }
 
-                Section("Diagnostics") {
-                    Toggle("Send anonymous usage stats", isOn: $telemetryEnabled.value)
-                        .onChange(of: telemetryEnabled.value) { newValue in
-                            if newValue {
-                                TelemetryClient.shared.scheduleRecurring()
-                            }
+            Section("Diagnostics") {
+                Toggle("Send anonymous usage stats", isOn: $telemetryEnabled.value)
+                    .onChange(of: telemetryEnabled.value) { newValue in
+                        if newValue {
+                            TelemetryClient.shared.scheduleRecurring()
                         }
-                    NavigationLink("What's sent") { TelemetryPreviewView() }
-                    NavigationLink("Privacy") { TelemetryPrivacyView() }
-                }
+                    }
+                NavigationLink("What's sent") { TelemetryPreviewView() }
+                NavigationLink("Privacy") { TelemetryPrivacyView() }
             }
         }
         .preferredColorScheme(Storage.shared.appearanceMode.value.colorScheme)

+ 80 - 82
LoopFollow/Settings/GraphSettingsView.swift

@@ -24,106 +24,104 @@ struct GraphSettingsView: View {
     private var nightscoutEnabled: Bool { IsNightscoutEnabled() }
 
     var body: some View {
-        NavigationView {
-            Form {
-                // ── Graph Display ────────────────────────────────────────────
-                Section("Graph Display") {
-                    Toggle("Display Dots", isOn: $showDots.value)
-                        .onChange(of: showDots.value) { _ in markDirty() }
+        Form {
+            // ── Graph Display ────────────────────────────────────────────
+            Section("Graph Display") {
+                Toggle("Display Dots", isOn: $showDots.value)
+                    .onChange(of: showDots.value) { _ in markDirty() }
 
-                    Toggle("Display Lines", isOn: $showLines.value)
-                        .onChange(of: showLines.value) { _ in markDirty() }
+                Toggle("Display Lines", isOn: $showLines.value)
+                    .onChange(of: showLines.value) { _ in markDirty() }
 
-                    if nightscoutEnabled {
-                        Toggle("Show DIA Lines", isOn: $showDIALines.value)
-                            .onChange(of: showDIALines.value) { _ in markDirty() }
-
-                        Toggle("Show −30 min Line", isOn: $show30MinLine.value)
-                            .onChange(of: show30MinLine.value) { _ in markDirty() }
+                if nightscoutEnabled {
+                    Toggle("Show DIA Lines", isOn: $showDIALines.value)
+                        .onChange(of: showDIALines.value) { _ in markDirty() }
 
-                        Toggle("Show −90 min Line", isOn: $show90MinLine.value)
-                            .onChange(of: show90MinLine.value) { _ in markDirty() }
-                    }
+                    Toggle("Show −30 min Line", isOn: $show30MinLine.value)
+                        .onChange(of: show30MinLine.value) { _ in markDirty() }
 
-                    Toggle("Show Midnight Lines", isOn: $showMidnightLines.value)
-                        .onChange(of: showMidnightLines.value) { _ in markDirty() }
+                    Toggle("Show −90 min Line", isOn: $show90MinLine.value)
+                        .onChange(of: show90MinLine.value) { _ in markDirty() }
                 }
 
-                // ── Treatments ───────────────────────────────────────────────
-                if nightscoutEnabled {
-                    Section("Treatments") {
-                        Toggle("Show Carb/Bolus Values", isOn: $showValues.value)
-                        Toggle("Show Carb Absorption", isOn: $showAbsorption.value)
-                        Toggle("Treatments on Small Graph",
-                               isOn: $smallGraphTreatments.value)
-                    }
+                Toggle("Show Midnight Lines", isOn: $showMidnightLines.value)
+                    .onChange(of: showMidnightLines.value) { _ in markDirty() }
+            }
+
+            // ── Treatments ───────────────────────────────────────────────
+            if nightscoutEnabled {
+                Section("Treatments") {
+                    Toggle("Show Carb/Bolus Values", isOn: $showValues.value)
+                    Toggle("Show Carb Absorption", isOn: $showAbsorption.value)
+                    Toggle("Treatments on Small Graph",
+                           isOn: $smallGraphTreatments.value)
                 }
+            }
+
+            // ── Small Graph ──────────────────────────────────────────────
+            Section("Small Graph") {
+                SettingsStepperRow(
+                    title: "Height",
+                    range: 40 ... 80,
+                    step: 5,
+                    value: $smallGraphHeight.value,
+                    format: { "\(Int($0)) pt" }
+                )
+                .onChange(of: smallGraphHeight.value) { _ in markDirty() }
+            }
 
-                // ── Small Graph ──────────────────────────────────────────────
-                Section("Small Graph") {
+            // ── Prediction ───────────────────────────────────────────────
+            if nightscoutEnabled {
+                Section("Prediction") {
                     SettingsStepperRow(
-                        title: "Height",
-                        range: 40 ... 80,
-                        step: 5,
-                        value: $smallGraphHeight.value,
-                        format: { "\(Int($0)) pt" }
+                        title: "Hours of Prediction",
+                        range: 0 ... 6,
+                        step: 0.25,
+                        value: $predictionToLoad.value,
+                        format: { "\($0.localized(maxFractionDigits: 2)) h" }
                     )
-                    .onChange(of: smallGraphHeight.value) { _ in markDirty() }
-                }
 
-                // ── Prediction ───────────────────────────────────────────────
-                if nightscoutEnabled {
-                    Section("Prediction") {
-                        SettingsStepperRow(
-                            title: "Hours of Prediction",
-                            range: 0 ... 6,
-                            step: 0.25,
-                            value: $predictionToLoad.value,
-                            format: { "\($0.localized(maxFractionDigits: 2)) h" }
-                        )
-
-                        if Storage.shared.device.value != "Loop" {
-                            Picker("Prediction Style", selection: $predictionDisplayType.value) {
-                                ForEach(PredictionDisplayType.allCases, id: \.self) { type in
-                                    Text(type.displayName).tag(type)
-                                }
+                    if Storage.shared.device.value != "Loop" {
+                        Picker("Prediction Style", selection: $predictionDisplayType.value) {
+                            ForEach(PredictionDisplayType.allCases, id: \.self) { type in
+                                Text(type.displayName).tag(type)
                             }
-                            .onChange(of: predictionDisplayType.value) { _ in markDirty() }
                         }
+                        .onChange(of: predictionDisplayType.value) { _ in markDirty() }
                     }
                 }
+            }
 
-                // ── Basal / BG scale ─────────────────────────────────────────
-                if nightscoutEnabled {
-                    Section("Basal / BG Scale") {
-                        SettingsStepperRow(
-                            title: "Min Basal",
-                            range: 0.5 ... 20,
-                            step: 0.5,
-                            value: $minBasalScale.value,
-                            format: { "\($0.localized(maxFractionDigits: 1)) U/h" }
-                        )
-
-                        BGPicker(
-                            title: "Min BG Scale",
-                            range: 40 ... 400,
-                            value: $minBGScale.value
-                        )
-                        .onChange(of: minBGScale.value) { _ in markDirty() }
-                    }
+            // ── Basal / BG scale ─────────────────────────────────────────
+            if nightscoutEnabled {
+                Section("Basal / BG Scale") {
+                    SettingsStepperRow(
+                        title: "Min Basal",
+                        range: 0.5 ... 20,
+                        step: 0.5,
+                        value: $minBasalScale.value,
+                        format: { "\($0.localized(maxFractionDigits: 1)) U/h" }
+                    )
+
+                    BGPicker(
+                        title: "Min BG Scale",
+                        range: 40 ... 400,
+                        value: $minBGScale.value
+                    )
+                    .onChange(of: minBGScale.value) { _ in markDirty() }
                 }
+            }
 
-                // ── History window ───────────────────────────────────────────
-                if nightscoutEnabled {
-                    Section("History") {
-                        SettingsStepperRow(
-                            title: "Show Days Back",
-                            range: 1 ... 4,
-                            step: 1,
-                            value: $downloadDays.value,
-                            format: { "\(Int($0)) d" }
-                        )
-                    }
+            // ── History window ───────────────────────────────────────────
+            if nightscoutEnabled {
+                Section("History") {
+                    SettingsStepperRow(
+                        title: "Show Days Back",
+                        range: 1 ... 4,
+                        step: 1,
+                        value: $downloadDays.value,
+                        format: { "\(Int($0)) d" }
+                    )
                 }
             }
         }

+ 23 - 23
LoopFollow/Settings/HomeContentView.swift

@@ -6,36 +6,36 @@ import UIKit
 
 /// A SwiftUI wrapper around MainViewController that displays the full Home screen.
 /// This can be used both in the tab bar and as a modal from the Menu.
-struct HomeContentView: UIViewControllerRepresentable {
+struct HomeContentView: View {
     let isModal: Bool
 
     init(isModal: Bool = false) {
         self.isModal = isModal
     }
 
-    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
-        }
+    var body: some View {
+        MainViewControllerRepresentable()
+            // Home has no text input, yet iOS sometimes replays a stale keyboard
+            // frame when the app returns to the foreground, which squeezes the
+            // whole screen up by a keyboard's height until a rotation forces the
+            // safe area to recompute. Opting out of keyboard avoidance prevents it.
+            .ignoresSafeArea(.keyboard)
+    }
+}
 
+private struct MainViewControllerRepresentable: UIViewControllerRepresentable {
+    func makeUIViewController(context _: Context) -> UIViewController {
+        // Reuse the single long-lived instance rather than creating a new one,
+        // so there is exactly one data pipeline and MainViewController.shared is
+        // never displaced. bootstrap() is a no-op if it already exists.
+        MainViewController.bootstrap()
+        let mainVC = MainViewController.shared!
+        // Detach from any previous SwiftUI host (e.g. after a Menu push was
+        // popped and is now being re-pushed) before this representable embeds it.
+        mainVC.willMove(toParent: nil)
+        mainVC.removeFromParent()
+        mainVC.view.removeFromSuperview()
         mainVC.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle
-
-        mainVC.isPresentedAsModal = isModal
-
         return mainVC
     }
 
@@ -50,7 +50,7 @@ struct HomeModalView: View {
     @Environment(\.dismiss) private var dismiss
 
     var body: some View {
-        NavigationView {
+        NavigationStack {
             HomeContentView(isModal: true)
                 .navigationTitle("Home")
                 .navigationBarTitleDisplayMode(.inline)

+ 44 - 46
LoopFollow/Settings/ImportExport/ImportExportSettingsView.swift

@@ -9,67 +9,65 @@ struct ImportExportSettingsView: View {
     @StateObject private var viewModel = ImportExportSettingsViewModel()
 
     var body: some View {
-        NavigationView {
-            List {
-                // MARK: - Import Section
+        List {
+            // MARK: - Import Section
 
-                Section("Import Settings") {
-                    Button(action: {
-                        viewModel.isShowingQRCodeScanner = true
-                    }) {
-                        HStack {
-                            Image(systemName: "qrcode.viewfinder")
-                                .foregroundColor(.blue)
-                            Text("Scan QR Code to Import Settings")
-                        }
+            Section("Import Settings") {
+                Button(action: {
+                    viewModel.isShowingQRCodeScanner = true
+                }) {
+                    HStack {
+                        Image(systemName: "qrcode.viewfinder")
+                            .foregroundColor(.blue)
+                        Text("Scan QR Code to Import Settings")
                     }
-                    .buttonStyle(.plain)
                 }
+                .buttonStyle(.plain)
+            }
 
-                // MARK: - Export Section
+            // MARK: - Export Section
 
-                Section("Export Settings To QR Code") {
-                    ForEach(ImportExportSettingsViewModel.ExportType.allCases, id: \.self) { exportType in
-                        Button(action: {
-                            if exportType == .alarms {
-                                viewModel.showAlarmSelection()
-                            } else {
-                                viewModel.exportType = exportType
-                                if let qrString = viewModel.generateQRCodeForExport() {
-                                    viewModel.qrCodeString = qrString
-                                    viewModel.isShowingQRCodeDisplay = true
-                                }
-                            }
-                        }) {
-                            HStack {
-                                Image(systemName: exportType.icon)
-                                    .foregroundColor(.blue)
-                                Text("Export \(exportType.rawValue)")
-                                Spacer()
-                                Image(systemName: exportType == .alarms ? "list.bullet" : "qrcode")
-                                    .foregroundColor(.secondary)
+            Section("Export Settings To QR Code") {
+                ForEach(ImportExportSettingsViewModel.ExportType.allCases, id: \.self) { exportType in
+                    Button(action: {
+                        if exportType == .alarms {
+                            viewModel.showAlarmSelection()
+                        } else {
+                            viewModel.exportType = exportType
+                            if let qrString = viewModel.generateQRCodeForExport() {
+                                viewModel.qrCodeString = qrString
+                                viewModel.isShowingQRCodeDisplay = true
                             }
                         }
-                        .buttonStyle(.plain)
+                    }) {
+                        HStack {
+                            Image(systemName: exportType.icon)
+                                .foregroundColor(.blue)
+                            Text("Export \(exportType.rawValue)")
+                            Spacer()
+                            Image(systemName: exportType == .alarms ? "list.bullet" : "qrcode")
+                                .foregroundColor(.secondary)
+                        }
                     }
+                    .buttonStyle(.plain)
                 }
+            }
 
-                // MARK: - Status Message
+            // MARK: - Status Message
 
-                if !viewModel.qrCodeErrorMessage.isEmpty {
-                    Section {
-                        let isSuccess = viewModel.qrCodeErrorMessage.contains("successfully") || viewModel.qrCodeErrorMessage.contains("Successfully imported")
-                        let displayText = isSuccess ? "✅ \(viewModel.qrCodeErrorMessage)" : viewModel.qrCodeErrorMessage
+            if !viewModel.qrCodeErrorMessage.isEmpty {
+                Section {
+                    let isSuccess = viewModel.qrCodeErrorMessage.contains("successfully") || viewModel.qrCodeErrorMessage.contains("Successfully imported")
+                    let displayText = isSuccess ? "✅ \(viewModel.qrCodeErrorMessage)" : viewModel.qrCodeErrorMessage
 
-                        Text(displayText)
-                            .foregroundColor(isSuccess ? .green : .red)
-                            .font(.caption)
-                    }
+                    Text(displayText)
+                        .foregroundColor(isSuccess ? .green : .red)
+                        .font(.caption)
                 }
             }
-            .navigationTitle("Import/Export Settings")
-            .navigationBarTitleDisplayMode(.inline)
         }
+        .navigationTitle("Import/Export Settings")
+        .navigationBarTitleDisplayMode(.inline)
         .sheet(isPresented: $viewModel.isShowingQRCodeScanner) {
             SimpleQRCodeScannerView { result in
                 viewModel.handleQRCodeScanResult(result)

+ 81 - 159
LoopFollow/Settings/SettingsMenuView.swift

@@ -5,138 +5,85 @@ import SwiftUI
 import UIKit
 
 struct SettingsMenuView: View {
-    // MARK: - Observed Objects
-
     @ObservedObject private var nightscoutURL = Storage.shared.url
-    @ObservedObject private var settingsPath = Observable.shared.settingsPath
-
-    // MARK: – Local state
-
-    var onBack: (() -> Void)?
-
-    // MARK: – Observed objects
-
-    @ObservedObject private var url = Storage.shared.url
-
-    // MARK: – Body
 
     var body: some View {
-        NavigationStack(path: $settingsPath.value) {
-            List {
-                dataSection
-
-                Section("Display Settings") {
-                    NavigationRow(title: "General",
-                                  icon: "gearshape")
-                    {
-                        settingsPath.value.append(Sheet.general)
-                    }
-
-                    NavigationRow(title: "Graph",
-                                  icon: "chart.xyaxis.line")
-                    {
-                        settingsPath.value.append(Sheet.graph)
-                    }
-
-                    if !nightscoutURL.value.isEmpty {
-                        NavigationRow(title: "Information Display",
-                                      icon: "info.circle")
-                        {
-                            settingsPath.value.append(Sheet.infoDisplay)
-                        }
-                    }
-                    NavigationRow(title: "Units and Metrics",
-                                  icon: "scalemass")
-                    {
-                        settingsPath.value.append(Sheet.units)
-                    }
-
-                    NavigationRow(title: "Tabs",
-                                  icon: "rectangle.3.group")
-                    {
-                        settingsPath.value.append(Sheet.tabSettings)
-                    }
+        List {
+            dataSection
+
+            Section("Display Settings") {
+                NavigationRow(title: "General",
+                              icon: "gearshape",
+                              value: SettingsRoute.general)
+                NavigationRow(title: "Graph",
+                              icon: "chart.xyaxis.line",
+                              value: SettingsRoute.graph)
+
+                if !nightscoutURL.value.isEmpty {
+                    NavigationRow(title: "Information Display",
+                                  icon: "info.circle",
+                                  value: SettingsRoute.infoDisplay)
                 }
 
-                Section("App Settings") {
-                    NavigationRow(title: "Background Refresh",
-                                  icon: "arrow.clockwise")
-                    {
-                        settingsPath.value.append(Sheet.backgroundRefresh)
-                    }
-
-                    NavigationRow(title: "Import/Export",
-                                  icon: "square.and.arrow.down")
-                    {
-                        settingsPath.value.append(Sheet.importExport)
-                    }
-
-                    NavigationRow(title: "APN",
-                                  icon: "bell.and.waves.left.and.right")
-                    {
-                        settingsPath.value.append(Sheet.apn)
-                    }
-
-                    #if !targetEnvironment(macCatalyst)
-                        NavigationRow(title: "Live Activity",
-                                      icon: "dot.radiowaves.left.and.right")
-                        {
-                            settingsPath.value.append(Sheet.liveActivity)
-                        }
-                    #endif
+                NavigationRow(title: "Units and Metrics",
+                              icon: "scalemass",
+                              value: SettingsRoute.units)
 
-                    if !nightscoutURL.value.isEmpty {
-                        NavigationRow(title: "Remote",
-                                      icon: "antenna.radiowaves.left.and.right")
-                        {
-                            settingsPath.value.append(Sheet.remote)
-                        }
-                    }
-                }
+                NavigationRow(title: "Tabs",
+                              icon: "rectangle.3.group",
+                              value: SettingsRoute.tabSettings)
+            }
 
-                Section("Alarms") {
-                    NavigationRow(title: "Alarms",
-                                  icon: "bell.badge")
-                    {
-                        settingsPath.value.append(Sheet.alarmSettings)
-                    }
+            Section("App Settings") {
+                NavigationRow(title: "Background Refresh",
+                              icon: "arrow.clockwise",
+                              value: SettingsRoute.backgroundRefresh)
+
+                NavigationRow(title: "Import/Export",
+                              icon: "square.and.arrow.down",
+                              value: SettingsRoute.importExport)
+
+                NavigationRow(title: "APN",
+                              icon: "bell.and.waves.left.and.right",
+                              value: SettingsRoute.apn)
+
+                #if !targetEnvironment(macCatalyst)
+                    NavigationRow(title: "Live Activity",
+                                  icon: "dot.radiowaves.left.and.right",
+                                  value: SettingsRoute.liveActivity)
+                #endif
+
+                if !nightscoutURL.value.isEmpty {
+                    NavigationRow(title: "Remote",
+                                  icon: "antenna.radiowaves.left.and.right",
+                                  value: SettingsRoute.remote)
                 }
+            }
 
-                Section("Integrations") {
-                    NavigationRow(title: "Calendar",
-                                  icon: "calendar")
-                    {
-                        settingsPath.value.append(Sheet.calendar)
-                    }
+            Section("Alarms") {
+                NavigationRow(title: "Alarms",
+                              icon: "bell.badge",
+                              value: SettingsRoute.alarmSettings)
+            }
 
-                    NavigationRow(title: "Contact",
-                                  icon: "person.circle")
-                    {
-                        settingsPath.value.append(Sheet.contact)
-                    }
-                }
+            Section("Integrations") {
+                NavigationRow(title: "Calendar",
+                              icon: "calendar",
+                              value: SettingsRoute.calendar)
 
-                Section("Advanced Settings") {
-                    NavigationRow(title: "Advanced",
-                                  icon: "exclamationmark.shield")
-                    {
-                        settingsPath.value.append(Sheet.advanced)
-                    }
-                }
+                NavigationRow(title: "Contact",
+                              icon: "person.circle",
+                              value: SettingsRoute.contact)
             }
-            .navigationTitle("Settings")
-            .navigationBarTitleDisplayMode(.large)
-            .navigationDestination(for: Sheet.self) { $0.destination }
-            .toolbar {
-                if let onBack {
-                    ToolbarItem(placement: .navigationBarLeading) {
-                        Button(action: onBack) {
-                            Image(systemName: "chevron.left")
-                        }
-                    }
-                }
+
+            Section("Advanced Settings") {
+                NavigationRow(title: "Advanced",
+                              icon: "exclamationmark.shield",
+                              value: SettingsRoute.advanced)
             }
         }
+        .navigationTitle("Settings")
+        .navigationBarTitleDisplayMode(.large)
     }
 
     // MARK: – Section builders
@@ -145,23 +92,20 @@ struct SettingsMenuView: View {
     private var dataSection: some View {
         Section("Data Settings") {
             NavigationRow(title: "Nightscout",
-                          icon: "network")
-            {
-                settingsPath.value.append(Sheet.nightscout)
-            }
+                          icon: "network",
+                          value: SettingsRoute.nightscout)
 
             NavigationRow(title: "Dexcom",
-                          icon: "sensor.tag.radiowaves.forward")
-            {
-                settingsPath.value.append(Sheet.dexcom)
-            }
+                          icon: "sensor.tag.radiowaves.forward",
+                          value: SettingsRoute.dexcom)
         }
     }
 }
 
 // MARK: – Sheet routing
 
-private enum Sheet: Hashable, Identifiable {
+enum SettingsRoute: Hashable, Identifiable {
+    case settings
     case units
     case nightscout, dexcom
     case backgroundRefresh
@@ -184,6 +128,7 @@ private enum Sheet: Hashable, Identifiable {
     @ViewBuilder
     var destination: some View {
         switch self {
+        case .settings: SettingsMenuView()
         case .units: UnitsSettingsView()
         case .nightscout: NightscoutSettingsView(viewModel: .init())
         case .dexcom: DexcomSettingsView(viewModel: .init())
@@ -226,37 +171,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
     }
 }
 
@@ -266,7 +181,14 @@ import UIKit
 
 extension UIApplication {
     var topMost: UIViewController? {
-        guard var top = keyWindow?.rootViewController else { return nil }
+        // `keyWindow` is deprecated and returns nil on Mac Catalyst / multi-window iPad.
+        // Walk connected scenes instead and prefer the foreground-active one.
+        let windowScenes = connectedScenes.compactMap { $0 as? UIWindowScene }
+        let activeScene = windowScenes.first { $0.activationState == .foregroundActive }
+            ?? windowScenes.first
+        let rootVC = activeScene?.windows.first(where: \.isKeyWindow)?.rootViewController
+            ?? activeScene?.windows.first?.rootViewController
+        guard var top = rootVC else { return nil }
         while let presented = top.presentedViewController {
             top = presented
         }

+ 0 - 65
LoopFollow/Snoozer/SnoozerViewController.swift

@@ -1,65 +0,0 @@
-// LoopFollow
-// SnoozerViewController.swift
-
-import Combine
-import SwiftUI
-import UIKit
-
-class SnoozerViewController: UIViewController {
-    private var hostingController: UIHostingController<SnoozerView>?
-    private var cancellables = Set<AnyCancellable>()
-
-    @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
-        }
-    }
-}

+ 8 - 2
LoopFollow/Stats/StatsDataFetcher.swift

@@ -4,11 +4,17 @@
 import Foundation
 
 class StatsDataFetcher {
-    weak var mainViewController: MainViewController?
+    /// See StatsDataService.mainViewController — the injected reference can be
+    /// nil at cold launch, so fall back to the shared engine.
+    private weak var injectedMainViewController: MainViewController?
+    var mainViewController: MainViewController? {
+        injectedMainViewController ?? MainViewController.shared
+    }
+
     weak var dataService: StatsDataService?
 
     init(mainViewController: MainViewController?) {
-        self.mainViewController = mainViewController
+        injectedMainViewController = mainViewController
     }
 
     func fetchBGData(days: Int, completion: @escaping () -> Void) {

+ 8 - 2
LoopFollow/Stats/StatsDataService.swift

@@ -4,7 +4,13 @@
 import Foundation
 
 class StatsDataService {
-    weak var mainViewController: MainViewController?
+    /// The injected reference can be nil when the stats UI is built before
+    /// MainViewController.bootstrap() has run (cold launch with Statistics as
+    /// the selected tab), so resolve lazily and fall back to the shared engine.
+    private weak var injectedMainViewController: MainViewController?
+    var mainViewController: MainViewController? {
+        injectedMainViewController ?? MainViewController.shared
+    }
 
     var daysToAnalyze: Int = 14 // Keep for backward compatibility
     var startDate: Date = dateTimeUtils.displayCalendar().date(byAdding: .day, value: -14, to: Date()) ?? Date()
@@ -23,7 +29,7 @@ class StatsDataService {
     }
 
     init(mainViewController: MainViewController?) {
-        self.mainViewController = mainViewController
+        injectedMainViewController = mainViewController
         dataFetcher = StatsDataFetcher(mainViewController: mainViewController)
         dataFetcher.dataService = self
     }

+ 9 - 2
LoopFollow/Storage/Observable.swift

@@ -24,6 +24,12 @@ class Observable {
     var deltaText = ObservableValue<String>(default: "+0")
     var iobText = ObservableValue<String>(default: "--")
 
+    var serverText = ObservableValue<String>(default: "Server")
+    var loopStatusText = ObservableValue<String>(default: "")
+    var loopStatusColor = ObservableValue<Color>(default: .primary)
+    var predictionText = ObservableValue<String>(default: "")
+    var predictionColor = ObservableValue<Color>(default: .purple)
+
     var currentAlarm = ObservableValue<UUID?>(default: nil)
     var alarmSoundPlaying = ObservableValue<Bool>(default: false)
 
@@ -38,13 +44,14 @@ class Observable {
     var pumpBatteryLevel = ObservableValue<Double?>(default: nil)
     var enactedOrSuggested = ObservableValue<TimeInterval?>(default: nil)
 
-    var settingsPath = ObservableValue<NavigationPath>(default: NavigationPath())
-
     var lastSentTOTP = ObservableValue<String?>(default: nil)
 
     var loopFollowDeviceToken = ObservableValue<String>(default: "")
 
     var isNotLooping = ObservableValue<Bool>(default: false)
 
+    /// Selected tab index used by SwiftUI TabView — set from MainViewController to switch tabs
+    var selectedTabIndex = ObservableValue<Int>(default: 0)
+
     private init() {}
 }

+ 5 - 17
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

+ 2 - 39
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
     }
 }
 

+ 0 - 60
LoopFollow/ViewControllers/AlarmViewController.swift

@@ -1,60 +0,0 @@
-// LoopFollow
-// AlarmViewController.swift
-
-import Combine
-import SwiftUI
-import UIKit
-
-class AlarmViewController: UIViewController {
-    private var hostingController: UIHostingController<AlarmsContainerView>!
-    private var cancellables = Set<AnyCancellable>()
-
-    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
-        }
-    }
-}

+ 74 - 0
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?()
+        }
+    }
+}

+ 20 - 0
LoopFollow/ViewControllers/LineChartWrapper.swift

@@ -0,0 +1,20 @@
+// LoopFollow
+// LineChartWrapper.swift
+
+import Charts
+import SwiftUI
+
+struct LineChartWrapper: UIViewRepresentable {
+    let chartView: LineChartView
+
+    func makeUIView(context _: Context) -> LineChartView {
+        chartView
+    }
+
+    func updateUIView(_: LineChartView, context _: Context) {
+        // Intentionally empty. MainViewController owns the chart and calls
+        // notifyDataSetChanged itself whenever it mutates the data; doing it
+        // here too would redo that work on every unrelated SwiftUI re-render
+        // of MainHomeView (e.g. the once-a-second minAgoText tick).
+    }
+}

+ 70 - 0
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)
+    }
+}

+ 134 - 631
LoopFollow/ViewControllers/MainViewController.swift

@@ -23,34 +23,30 @@ private struct APNSCredentialSnapshot: Equatable {
     let lfKeyId: String
 }
 
-class MainViewController: UIViewController, UITableViewDataSource, ChartViewDelegate, UNUserNotificationCenterDelegate, UIScrollViewDelegate {
-    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 statsEstA1CTitle: UILabel!
-    @IBOutlet var statsEstA1C: UILabel!
-    @IBOutlet var statsStdDevTitle: UILabel!
-    @IBOutlet var statsStdDev: UILabel!
-    @IBOutlet var serverText: UILabel!
-    @IBOutlet var statsView: UIView!
-    @IBOutlet var smallGraphHeightConstraint: NSLayoutConstraint!
-    var refreshScrollView: UIScrollView!
-    var refreshControl: UIRefreshControl!
+class MainViewController: UIViewController, ChartViewDelegate, UNUserNotificationCenterDelegate {
+    /// The single, long-lived MainViewController that owns the app's data
+    /// pipeline (scheduleAllTasks). Held strongly so it stays alive — and the
+    /// engine keeps running — regardless of which tabs are visible or whether
+    /// Home has been opened. Created once via bootstrap() on first foreground.
+    private(set) static var shared: MainViewController?
+
+    /// Creates and force-loads the shared instance if it does not yet exist.
+    /// loadViewIfNeeded() triggers viewDidLoad, which starts scheduleAllTasks.
+    /// Idempotent and main-thread only. Called from MainTabView on appear so
+    /// the engine runs even when Home lives in the Menu rather than a tab.
+    static func bootstrap() {
+        guard shared == nil else { return }
+        let vc = MainViewController()
+        shared = vc
+        vc.loadViewIfNeeded()
+    }
+
+    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!
@@ -137,7 +133,6 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
     let contactImageUpdater = ContactImageUpdater()
 
     private var cancellables = Set<AnyCancellable>()
-    private var isViewHierarchyReady = false
 
     // Loading state management
     private var loadingOverlay: UIView?
@@ -149,8 +144,58 @@ 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)
+        // Exclude the keyboard from the hosting controller's safe area. Home has
+        // no text input, but a stale keyboard frame replayed on foregrounding can
+        // otherwise compress the layout until a rotation recomputes the safe area.
+        hosting.safeAreaRegions = .container
+        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()
+        // Adopt the singleton only if it is not already set (normally bootstrap()
+        // has set it to this same instance). Guarding prevents a stray instance
+        // from displacing the long-lived engine and spawning a second pipeline.
+        if MainViewController.shared == nil {
+            MainViewController.shared = self
+        }
+
+        setupUI()
 
         loadDebugData()
 
@@ -160,29 +205,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
@@ -198,6 +227,10 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
         // when runMigrationsIfNeeded() is called. This catches migrations deferred by a
         // background BGAppRefreshTask launch in Before-First-Unlock state.
         notificationCenter.addObserver(self, selector: #selector(appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
+        // Posted by AppDelegate after Storage.reloadAll has refreshed every StorageValue
+        // following a BFU launch. If we're alive when this fires, our scheduled tasks
+        // were set up with BFU defaults (url='') and need to be redone.
+        notificationCenter.addObserver(self, selector: #selector(handleBFUReloadCompleted), name: .bfuReloadCompleted, object: nil)
 
         #if !targetEnvironment(macCatalyst)
             notificationCenter.addObserver(self, selector: #selector(navigateOnLAForeground), name: .liveActivityDidForeground, object: nil)
@@ -214,59 +247,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)
 
@@ -285,13 +276,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)
-
         Publishers.MergeMany(
             Storage.shared.units.$value.map { _ in () }.eraseToAnyPublisher(),
             Storage.shared.useIFCC.$value.map { _ in () }.eraseToAnyPublisher(),
@@ -312,13 +296,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
@@ -333,20 +310,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
@@ -354,26 +317,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)
@@ -458,13 +404,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
@@ -577,199 +516,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
-        }
-    }
-
-    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
-            }
+            Observable.shared.selectedTabIndex.value = 0
         }
-
-        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
@@ -781,94 +543,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"
@@ -883,7 +557,7 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
     }
 
     deinit {
-        NotificationCenter.default.removeObserver(self, name: NSNotification.Name("refresh"), object: nil)
+        NotificationCenter.default.removeObserver(self)
     }
 
     // Clean all timers and start new ones when refreshing
@@ -913,98 +587,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()
@@ -1091,28 +697,21 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
         runMigrationsIfNeeded()
     }
 
+    @objc func handleBFUReloadCompleted() {
+        // Show the loading overlay so the user sees feedback during the 2-5s
+        // while tasks re-run with the now-correct credentials. Tasks scheduled
+        // before reload used url='' and rescheduled themselves 60s out — reset
+        // them so they run within their normal 2-5s initial delay.
+        loadingStates = ["bg": false, "profile": false, "deviceStatus": false]
+        isInitialLoad = true
+        setupLoadingState()
+        showLoadingOverlay()
+        scheduleAllTasks()
+    }
+
     @objc func appCameToForeground() {
-        // If the app was cold-launched in Before-First-Unlock state (e.g. by BGAppRefreshTask
-        // after a reboot), all StorageValues were cached from encrypted UserDefaults and hold
-        // their defaults. Reload everything from disk now that the device is unlocked, firing
-        // Combine observers only for values that actually changed.
-        LogManager.shared.log(category: .general, message: "appCameToForeground: needsBFUReload=\(Storage.shared.needsBFUReload), url='\(LogRedactor.url(Storage.shared.url.value))'")
-        if Storage.shared.needsBFUReload {
-            Storage.shared.needsBFUReload = false
-            LogManager.shared.log(category: .general, message: "BFU reload triggered — reloading all StorageValues")
-            Storage.shared.reloadAll()
-            LogManager.shared.log(category: .general, message: "BFU reload complete: url='\(LogRedactor.url(Storage.shared.url.value))'")
-            // Show the loading overlay so the user sees feedback during the 2-5s
-            // while tasks re-run with the now-correct credentials.
-            loadingStates = ["bg": false, "profile": false, "deviceStatus": false]
-            isInitialLoad = true
-            setupLoadingState()
-            showLoadingOverlay()
-            // Tasks were scheduled during BFU viewDidLoad with url="" — they fired, found no
-            // data source, and rescheduled themselves 60s out. Reset them now so they run
-            // within their normal 2-5s initial delay using the now-correct credentials.
-            scheduleAllTasks()
-        }
+        // BFU recovery (Storage.reloadAll) is driven by AppDelegate; this controller
+        // reacts via .bfuReloadCompleted in handleBFUReloadCompleted() above.
 
         // reset screenlock state if needed
         UIApplication.shared.isIdleTimerDisabled = Storage.shared.screenlockSwitchState.value
@@ -1184,8 +783,25 @@ class MainViewController: UIViewController, UITableViewDataSource, ChartViewDele
         }
     }
 
-    @objc override func viewDidAppear(_: Bool) {
+    override func viewDidAppear(_ animated: Bool) {
+        super.viewDidAppear(animated)
         showHideNSDetails()
+
+        // Re-render the graph every time Home appears. The single MainViewController
+        // is reused across tab/Menu hosts, so its chart view gets re-parented when
+        // the user switches to Home or moves it between the tab bar and the Menu —
+        // and Charts does not redraw itself after a re-parent. Rebuilding here keeps
+        // the curve visible regardless of how Home was reached. It also recovers the
+        // one-shot firstGraphLoad zoom that is skipped while the view is off-screen
+        // (force-loaded headless when Home lives in the Menu). Deferred one runloop
+        // so the nested SwiftUI chart has its final frame; updateBGGraph's own
+        // width>0 guard skips the initial zoom until it does.
+        if !bgData.isEmpty {
+            DispatchQueue.main.async { [weak self] in
+                self?.updateBGGraph()
+            }
+        }
+
         #if !targetEnvironment(macCatalyst)
             LiveActivityManager.shared.startFromCurrentState()
         #endif
@@ -1198,42 +814,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) {
@@ -1248,30 +830,18 @@ 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 {
                 let thresholds = UnitSettingsStore.shared.effectiveThresholds()
                 if Double(latestBG) >= thresholds.high {
-                    color = NSUIColor.systemYellow
                     Observable.shared.bgTextColor.value = .yellow
                 } else if Double(latestBG) <= thresholds.low {
-                    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)
-            }
         }
     }
 
@@ -1293,25 +863,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": "-", "": "-"]
@@ -1648,15 +1203,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
@@ -1664,61 +1210,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
         }
     }
 

+ 220 - 0
LoopFollow/ViewControllers/MoreMenuView.swift

@@ -0,0 +1,220 @@
+// LoopFollow
+// MoreMenuView.swift
+
+import SwiftUI
+import UIKit
+
+struct MoreMenuView: View {
+    @State private var pendingRoute: MenuRoute?
+    @State private var latestVersion: String?
+    @State private var versionTint: Color = .secondary
+    @State private var alertTitle = ""
+    @State private var alertMessage = ""
+    @State private var showAlert = false
+    @State private var currentVersion: String = AppVersionManager().version()
+
+    var body: some View {
+        List {
+            // Settings
+            Section {
+                NavigationLink(value: SettingsRoute.settings) {
+                    Label("Settings", systemImage: "gearshape")
+                }
+            }
+
+            // Features
+            Section("Features") {
+                ForEach(TabItem.featureOrder) { item in
+                    FullRowButton(showsChevron: true) {
+                        let tabs = Storage.shared.orderedTabBarItems()
+                        if let tabIndex = tabs.firstIndex(of: item) {
+                            Observable.shared.selectedTabIndex.value = tabIndex
+                        } else {
+                            pendingRoute = MenuRoute(item)
+                        }
+                    } label: {
+                        Label(item.displayName, systemImage: item.icon)
+                    }
+                }
+            }
+
+            // Logging
+            Section("Logging") {
+                FullRowButton(showsChevron: true) { pendingRoute = .log } label: {
+                    Label("View Log", systemImage: "doc.text.magnifyingglass")
+                }
+
+                FullRowButton { shareLogs() } label: {
+                    Label("Share Logs", systemImage: "square.and.arrow.up")
+                }
+            }
+
+            // 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: currentVersion, 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()
+        }
+        .alert(alertTitle, isPresented: $showAlert) {
+            Button("OK", role: .cancel) {}
+        } message: {
+            Text(alertMessage)
+        }
+        .navigationDestination(for: SettingsRoute.self) { $0.destination }
+        .navigationDestination(
+            isPresented: Binding(
+                get: { pendingRoute != nil },
+                set: { if !$0 { pendingRoute = nil } }
+            )
+        ) {
+            if let route = pendingRoute {
+                route.destination
+            }
+        }
+    }
+
+    // MARK: - Helpers
+
+    private func buildInfoRow(title: String, value: String, color: Color) -> some View {
+        HStack {
+            Text(title)
+            Spacer()
+            Text(value)
+                .foregroundStyle(color)
+        }
+    }
+
+    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
+        }
+        let avc = UIActivityViewController(activityItems: files, applicationActivities: nil)
+        UIApplication.shared.topMost?.present(avc, animated: true)
+    }
+
+    private func fetchVersionInfo() async {
+        let mgr = AppVersionManager()
+        let (latest, newer, blacklisted) = await mgr.checkForNewVersionAsync()
+        latestVersion = latest ?? "Unknown"
+
+        versionTint = blacklisted ? .red
+            : newer ? .orange
+            : latest == currentVersion ? .green
+            : .secondary
+    }
+}
+
+// MARK: – Full-row tappable button
+
+private struct FullRowButton<Label: View>: View {
+    var showsChevron: Bool = false
+    let action: () -> Void
+    @ViewBuilder let label: () -> Label
+
+    var body: some View {
+        Button(action: action) {
+            HStack {
+                label()
+                Spacer(minLength: 0)
+                if showsChevron {
+                    Image(systemName: "chevron.right")
+                        .font(.footnote.weight(.semibold))
+                        .foregroundStyle(.tertiary)
+                }
+            }
+            .contentShape(Rectangle())
+        }
+        .buttonStyle(.plain)
+    }
+}
+
+// MARK: – Menu routing
+
+enum MenuRoute: Hashable {
+    case home
+    case alarms
+    case remote
+    case nightscout
+    case snoozer
+    case treatments
+    case stats
+    case log
+
+    init(_ item: TabItem) {
+        switch item {
+        case .home: self = .home
+        case .alarms: self = .alarms
+        case .remote: self = .remote
+        case .nightscout: self = .nightscout
+        case .snoozer: self = .snoozer
+        case .treatments: self = .treatments
+        case .stats: self = .stats
+        }
+    }
+
+    @ViewBuilder
+    var destination: some View {
+        switch self {
+        case .home: HomeContentView(isModal: true)
+        case .alarms: AlarmsContainerView(embedsInNavigationStack: false)
+        case .remote: RemoteContentView()
+        case .nightscout: NightscoutContentView()
+        case .snoozer: SnoozerView()
+        case .treatments: TreatmentsView()
+        case .stats: AggregatedStatsContentView(mainViewController: MainViewController.shared)
+        case .log: LogView()
+        }
+    }
+}

+ 0 - 466
LoopFollow/ViewControllers/MoreMenuViewController.swift

@@ -1,466 +0,0 @@
-// LoopFollow
-// MoreMenuViewController.swift
-
-import Combine
-import SwiftUI
-import UIKit
-
-class MoreMenuViewController: UIViewController {
-    private var tableView: UITableView!
-    private var cancellables = Set<AnyCancellable>()
-    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<Content: View>: UIHostingController<Content> {
-    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()
-    }
-}

+ 17 - 19
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<AnyCancellable>()
 
     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])

+ 38 - 0
LoopFollow/ViewControllers/NightscoutContentView.swift

@@ -0,0 +1,38 @@
+// LoopFollow
+// NightscoutContentView.swift
+
+import SwiftUI
+
+struct NightscoutContentView: View {
+    @ObservedObject private var url = Storage.shared.url
+    @ObservedObject private var token = Storage.shared.token
+
+    var body: some View {
+        if url.value.isEmpty {
+            VStack(spacing: 16) {
+                Image(systemName: "network")
+                    .font(.system(size: 60))
+                    .foregroundStyle(.secondary)
+                Text("Please enter your Nightscout URL in Settings.")
+                    .foregroundStyle(.secondary)
+                    .multilineTextAlignment(.center)
+            }
+            .padding()
+        } else {
+            NightscoutWebView()
+                // NightscoutViewController loads the page once in viewDidLoad,
+                // so recreate it when the URL or token changes.
+                .id(url.value + "|" + token.value)
+        }
+    }
+}
+
+private struct NightscoutWebView: UIViewControllerRepresentable {
+    func makeUIViewController(context _: Context) -> NightscoutViewController {
+        NightscoutViewController()
+    }
+
+    func updateUIViewController(_ uiViewController: NightscoutViewController, context _: Context) {
+        uiViewController.overrideUserInterfaceStyle = Storage.shared.appearanceMode.value.userInterfaceStyle
+    }
+}

+ 0 - 73
LoopFollow/ViewControllers/SettingsViewController.swift

@@ -1,73 +0,0 @@
-// LoopFollow
-// SettingsViewController.swift
-
-import Combine
-import SwiftUI
-import UIKit
-
-final class SettingsViewController: UIViewController {
-    // MARK: Stored properties
-
-    private var host: UIHostingController<SettingsMenuView>!
-    private var cancellables = Set<AnyCancellable>()
-
-    // 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
-    }
-}