diff --git a/Loop/Views/StatusTableView.swift b/Loop/Views/StatusTableView.swift index ab7a6cfbe8..6f59b1be3a 100644 --- a/Loop/Views/StatusTableView.swift +++ b/Loop/Views/StatusTableView.swift @@ -301,12 +301,30 @@ enum ActionTabBarMetrics { static let iconHeight: CGFloat = 34 - static var bottomSafeAreaInset: CGFloat { + private static var keyWindow: UIWindow? { UIApplication.shared.connectedScenes .compactMap { $0 as? UIWindowScene } .flatMap { $0.windows } - .first { $0.isKeyWindow }? - .safeAreaInsets.bottom ?? 0 + .first { $0.isKeyWindow } + } + + static var bottomSafeAreaInset: CGFloat { + keyWindow?.safeAreaInsets.bottom ?? 0 + } + + static var hasVerticalStatusBar: Bool { + guard let insets = keyWindow?.safeAreaInsets else { return false } + return verticalStatusBarEdge(for: EdgeInsets(top: insets.top, leading: insets.left, bottom: insets.bottom, trailing: insets.right)) != nil + } + + // A vertical status bar (iPhone Duo) leaves no top inset and insets one side only. + static func verticalStatusBarEdge(for insets: EdgeInsets) -> HorizontalEdge? { + guard insets.top == 0 else { return nil } + switch (insets.leading > 0, insets.trailing > 0) { + case (true, false): return .leading + case (false, true): return .trailing + default: return nil + } } static var isCompactHeight: Bool { @@ -318,7 +336,7 @@ enum ActionTabBarMetrics { } static var tableContentInset: CGFloat { - guard !isCompactHeight else { return 0 } + guard !isCompactHeight, !hasVerticalStatusBar else { return 0 } if #available(iOS 26.0, *), bottomSafeAreaInset == 0 { return barHeight + 40 } @@ -371,11 +389,62 @@ struct ActionTabView: View { } var body: some View { - let isVisible = verticalSizeClass != .compact - content - .safeAreaInset(edge: .bottom, spacing: 0) { - ActionTabBar(items: tabs, isHidden: !isVisible) - .modifier(LegacyTabBarBackground(isVisible: isVisible)) + GeometryReader { proxy in + let insets = proxy.safeAreaInsets + let verticalEdge = verticalSizeClass == .compact ? nil : ActionTabBarMetrics.verticalStatusBarEdge(for: insets) + let isVisible = verticalSizeClass != .compact && verticalEdge == nil + content + .safeAreaInset(edge: .bottom, spacing: 0) { + ActionTabBar(items: tabs, isHidden: !isVisible) + .modifier(LegacyTabBarBackground(isVisible: isVisible)) + } + .overlay(alignment: verticalEdge == .leading ? .bottomLeading : .bottomTrailing) { + // In the status bar's column, below it, as the system places vertical toolbar buttons + if let verticalEdge { + VerticalActionBar(items: tabs) + .frame(width: verticalEdge == .leading ? insets.leading : insets.trailing) + .padding(.bottom, max(insets.bottom, 12)) + .ignoresSafeArea(edges: verticalEdge == .leading ? [.leading, .bottom] : [.trailing, .bottom]) + } + } + } + } +} + +struct VerticalActionBar: View { + + let items: [ActionTab] + + var body: some View { + VStack(spacing: 4) { + ForEach(items) { item in + Button(action: item.action) { + Image(item.icon) + .renderingMode(.template) + .resizable() + .scaledToFit() + .frame(height: ActionTabBarMetrics.iconHeight) + .foregroundStyle(Color(item.tintColor)) + .frame(width: 52, height: 52) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .accessibilityLabel(item.title) } + } + .padding(.vertical, 6) + .modifier(VerticalActionBarBackground()) + } +} + +private struct VerticalActionBarBackground: ViewModifier { + func body(content: Content) -> some View { + if #available(iOS 26.0, *) { + content.background { + Color.clear.glassEffect(.regular, in: .capsule) + } + } else { + content.background(Color(UIColor.secondarySystemBackground), in: Capsule()) + } } }