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