From 7146484948dc6c570cb63012a523bb63d2e06afd Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Fri, 23 Oct 2026 18:31:13 -0500 Subject: [PATCH] Status screen layout for iPhone Duo's inner display Landscape mode (charts only, no HUD, status row or action bar) was keyed to the screen being wider than tall. The unfolded inner display is wider than tall but has regular height, so after a fold/unfold the HUD and status row disappeared and the action bar was always hidden. Key it to compact vertical size class instead, in both the table view controller and ActionTabView. Chart row heights were sized from the longer screen dimension, which only makes sense in phone landscape; elsewhere use the table height so the charts fit the inner display. In inner landscape the status bar is vertical, so there is no top safe area and the first row met the display corners. The warning banner now runs to the top edge (the display corners round it) with its content inset by the corner-adapted safe area; without a banner, the table is inset by that amount. The status bar background view now tracks the table width, so it no longer stops at the launch width after unfolding. --- .../StatusTableViewController.swift | 56 +++++++++++++------ Loop/Views/StatusTableView.swift | 26 +++------ 2 files changed, 48 insertions(+), 34 deletions(-) diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index 6f31ab976e..b41971409d 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -219,7 +219,7 @@ final class StatusTableViewController: LoopChartsTableViewController { tableView.estimatedRowHeight = 74 // Estimate an initial value - landscapeMode = UIScreen.main.bounds.size.width > UIScreen.main.bounds.size.height + landscapeMode = ActionTabBarMetrics.isCompactHeight addScenarioStepGestureRecognizers() @@ -346,6 +346,7 @@ final class StatusTableViewController: LoopChartsTableViewController { let backgroundContainerView = UIView() backgroundContainerView.backgroundColor = .systemBackground let statusBarBackgroundView = UIView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: 0)) + statusBarBackgroundView.autoresizingMask = .flexibleWidth self.statusBarBackgroundView = statusBarBackgroundView backgroundContainerView.addSubview(statusBarBackgroundView) tableView.backgroundView = backgroundContainerView @@ -379,7 +380,7 @@ final class StatusTableViewController: LoopChartsTableViewController { } } - // Toggles the display mode based on the screen aspect ratio. Should not be updated outside of reloadData(). + // Charts only, for compact height (an iPhone in landscape). Should not be updated outside of reloadData(). private var landscapeMode = false { didSet { setupPresetsStatusBar() @@ -400,6 +401,12 @@ final class StatusTableViewController: LoopChartsTableViewController { return !landscapeMode } + // With a vertical status bar there is no top safe area inset, so keep the first row clear of the display corners. + private var cornerClearanceTopInset: CGFloat { + guard #available(iOS 26.0, *), let window = view.window else { return 0 } + return max(0, window.edgeInsets(for: .safeArea(cornerAdaptation: .vertical)).top - window.safeAreaInsets.top) + } + private var shouldShowStatus: Bool { return !landscapeMode && statusRowMode.hasRow } @@ -656,7 +663,7 @@ final class StatusTableViewController: LoopChartsTableViewController { let statusRowMode = self.determineStatusRowMode() updateBannerAndHUDandStatusRows(statusRowMode: statusRowMode, newSize: currentContext.newSize, animated: animated) - tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: ActionTabBarMetrics.tableContentInset, right: 0) + tableView.contentInset = UIEdgeInsets(top: shouldShowBannerWarning ? 0 : cornerClearanceTopInset, left: 0, bottom: ActionTabBarMetrics.tableContentInset, right: 0) redrawCharts() @@ -822,8 +829,8 @@ final class StatusTableViewController: LoopChartsTableViewController { self.presetsRowMode = determinePresetsRowMode() self.statusRowMode = statusRowMode - if let newSize = newSize { - landscapeMode = newSize.width > newSize.height + if newSize != nil { + landscapeMode = traitCollection.verticalSizeClass == .compact } let presetsIsVisible = self.shouldShowPresets @@ -968,7 +975,8 @@ final class StatusTableViewController: LoopChartsTableViewController { case .alertWarning: let cell = UITableViewCell() let alert = AlertPermissionsChecker.UnsafeNotificationPermissionAlert(permissions: alertPermissionsChecker.notificationCenterSettings) - + let fullBleedTopInset: CGFloat? = cornerClearanceTopInset > 0 ? cornerClearanceTopInset : nil + cell.contentConfiguration = UIHostingConfiguration { if alertPermissionsChecker.showWarning { if let alert { @@ -988,9 +996,7 @@ final class StatusTableViewController: LoopChartsTableViewController { .font(.headline) } .foregroundStyle(Color.white) - .padding(8) - .background(Color.critical.cornerRadius(10)) - .padding([.top, .horizontal], 8) + .modifier(WarningBannerStyle(fullBleedTopInset: fullBleedTopInset)) } } else { HStack { @@ -1011,15 +1017,13 @@ final class StatusTableViewController: LoopChartsTableViewController { .font(.title) } .foregroundStyle(Color.white) - .padding(8) - .background(Color.critical.cornerRadius(10)) - .padding([.top, .horizontal], 8) + .modifier(WarningBannerStyle(fullBleedTopInset: fullBleedTopInset)) } } .margins(.all, 0) - - cell.backgroundColor = .secondarySystemBackground - + + cell.backgroundColor = fullBleedTopInset == nil ? .secondarySystemBackground : .critical + return cell case .hud: let cell = tableView.dequeueReusableCell(withIdentifier: HUDViewTableViewCell.className, for: indexPath) as! HUDViewTableViewCell @@ -1245,7 +1249,7 @@ final class StatusTableViewController: LoopChartsTableViewController { case .charts: // Compute the height of the HUD, defaulting to 70 let hudHeight = ceil(hudView?.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height ?? 74) - var availableSize = max(tableView.bounds.width, tableView.bounds.height) + var availableSize = landscapeMode ? max(tableView.bounds.width, tableView.bounds.height) : tableView.bounds.height availableSize -= (tableView.safeAreaInsets.top + tableView.safeAreaInsets.bottom + tableView.contentInset.bottom + hudHeight) switch ChartRow(rawValue: indexPath.row)! { @@ -2322,3 +2326,23 @@ extension StatusTableViewController { updateStatusBar() } } + +// Without a top status bar, the banner runs to the top edge and the display corners round it. +private struct WarningBannerStyle: ViewModifier { + let fullBleedTopInset: CGFloat? + + func body(content: Content) -> some View { + if let fullBleedTopInset { + content + .padding(.top, fullBleedTopInset) + .padding(.horizontal, 24) + .padding(.bottom, 12) + .background(Color.critical) + } else { + content + .padding(8) + .background(Color.critical.cornerRadius(10)) + .padding([.top, .horizontal], 8) + } + } +} diff --git a/Loop/Views/StatusTableView.swift b/Loop/Views/StatusTableView.swift index 47d18b3d2c..ab7a6cfbe8 100644 --- a/Loop/Views/StatusTableView.swift +++ b/Loop/Views/StatusTableView.swift @@ -309,16 +309,16 @@ enum ActionTabBarMetrics { .safeAreaInsets.bottom ?? 0 } - static var interfaceOrientation: UIInterfaceOrientation { + static var isCompactHeight: Bool { let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene } let scene = scenes.first { $0.windows.contains { $0.isKeyWindow } } ?? scenes.first { $0.activationState == .foregroundActive } ?? scenes.first - return scene?.interfaceOrientation ?? .portrait + return scene?.traitCollection.verticalSizeClass == .compact } static var tableContentInset: CGFloat { - guard !interfaceOrientation.isLandscape else { return 0 } + guard !isCompactHeight else { return 0 } if #available(iOS 26.0, *), bottomSafeAreaInset == 0 { return barHeight + 40 } @@ -357,35 +357,25 @@ struct LegacyTabBarBackground: ViewModifier { struct ActionTabView: View { - @State private var orientation: UIInterfaceOrientation + @Environment(\.verticalSizeClass) private var verticalSizeClass private let content: Content private let tabs: [ActionTab] - + init( @ViewBuilder content: @escaping () -> Content, @ActionTabBuilder tabs: @escaping () -> [ActionTab], ) { self.content = content() self.tabs = tabs() - self.orientation = ActionTabBarMetrics.interfaceOrientation } var body: some View { + let isVisible = verticalSizeClass != .compact content .safeAreaInset(edge: .bottom, spacing: 0) { - ActionTabBar(items: tabs, isHidden: !orientation.isPortrait) - .modifier(LegacyTabBarBackground(isVisible: orientation.isPortrait)) - } - .onAppear { - UIDevice.current.beginGeneratingDeviceOrientationNotifications() - orientation = ActionTabBarMetrics.interfaceOrientation - } - .onDisappear { - UIDevice.current.endGeneratingDeviceOrientationNotifications() - } - .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in - orientation = ActionTabBarMetrics.interfaceOrientation + ActionTabBar(items: tabs, isHidden: !isVisible) + .modifier(LegacyTabBarBackground(isVisible: isVisible)) } } }