Scale info data table text with Dynamic Type#667
Draft
bjorkert wants to merge 26 commits into
Draft
Conversation
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.
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.
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().
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.
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.
- 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
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.
Pass MainViewController.shared instead of nil when creating AggregatedStatsContentView in MainTabView and MoreMenuView. Replace view-hierarchy-walking getMainViewController() in TreatmentsViewModel with MainViewController.shared.
The storyboard used system 17pt for both title and detail labels. The SwiftUI migration used .subheadline (~15pt) making text smaller.
Present UIActivityViewController via UIApplication.topMost instead of wrapping it in a SwiftUI .sheet, which rendered an empty view.
# Conflicts: # LoopFollow/Application/SceneDelegate.swift
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.
* 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).
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.
# Conflicts: # LoopFollow/Settings/GraphSettingsView.swift
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.
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.
# Conflicts: # LoopFollow/Application/Base.lproj/Main.storyboard # LoopFollow/Application/SceneDelegate.swift # LoopFollow/Controllers/MainViewController+updateStats.swift # LoopFollow/Nightscout/NightscoutSettingsView.swift # LoopFollow/Settings/DexcomSettingsView.swift # LoopFollow/Settings/GeneralSettingsView.swift # LoopFollow/Settings/GraphSettingsView.swift # LoopFollow/Settings/SettingsMenuView.swift # LoopFollow/ViewControllers/MainViewController.swift
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.
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.
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.
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.
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.
# Conflicts: # LoopFollow/ViewControllers/MainViewController.swift
Drive the info table font size and row height from @ScaledMetric so the top-right info data grows and shrinks with the iPhone's text-size setting, keeping the existing 17pt/21pt look at the default size. Cap the scaling at accessibility1 so the compact top strip stays within its layout.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The top-right info data (IOB / COB / Basal / etc.) used a hardcoded font size and fixed row heights, so it ignored the iPhone's text-size / accessibility (Dynamic Type) setting.
This drives the info table's font size and row height from
@ScaledMetric, so the text grows and shrinks with the user's text-size setting while keeping today's compact 17 pt / 21 pt look at the default size. Both the font and the row height scale together, so larger text never clips. The scaling is capped ataccessibility1so the compact top strip (shared with the BG display, constrained to 250 pt wide) stays within its layout at extreme sizes.Scope is intentionally limited to the info table.
Changes
LoopFollow/InfoTable/InfoTableView.swift—@ScaledMetric(relativeTo: .body)for font size (17) and row height (21); applied to the font, the row frame height, anddefaultMinListRowHeight.LoopFollow/ViewControllers/MainHomeView.swift—.dynamicTypeSize(...DynamicTypeSize.accessibility1)on the info table to cap the scaling.remove-storyboardThis branch is based on
remove-storyboard, not ondev. The diff shown here therefore includes the storyboard-removal work. This PR must not be merged untilremove-storyboardhas been merged intodevfirst, after which this diff will reduce to just the two files above. Keeping it as a draft until then.