Skip to content

Scale info data table text with Dynamic Type#667

Draft
bjorkert wants to merge 26 commits into
devfrom
info-table-dynamic-type
Draft

Scale info data table text with Dynamic Type#667
bjorkert wants to merge 26 commits into
devfrom
info-table-dynamic-type

Conversation

@bjorkert
Copy link
Copy Markdown
Member

@bjorkert bjorkert commented Jun 3, 2026

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 at accessibility1 so 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.

Simulator Screenshot - iPhone 17 Pro Max - 2026-06-03 at 19 28 48

Changes

  • LoopFollow/InfoTable/InfoTableView.swift@ScaledMetric(relativeTo: .body) for font size (17) and row height (21); applied to the font, the row frame height, and defaultMinListRowHeight.
  • LoopFollow/ViewControllers/MainHomeView.swift.dynamicTypeSize(...DynamicTypeSize.accessibility1) on the info table to cap the scaling.

⚠️ Do not merge before remove-storyboard

This branch is based on remove-storyboard, not on dev. The diff shown here therefore includes the storyboard-removal work. This PR must not be merged until remove-storyboard has been merged into dev first, after which this diff will reduce to just the two files above. Keeping it as a draft until then.

bjorkert added 26 commits April 18, 2026 10:41
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant