Files
mobile-ios/Rosetta/DesignSystem/Components/CallBarSafeAreaBridge.swift

79 lines
2.9 KiB
Swift

import SwiftUI
// MARK: - Call Bar Safe Area Bridge
/// UIKit bridge that pushes NavigationStack's nav bar down by setting
/// `additionalSafeAreaInsets.top` on every UINavigationController in the window.
///
/// `UIViewController.navigationController` walks UP the parent chain but the
/// bridge VC is a SIBLING of NavigationStack's UINavigationController, not a child.
/// So we walk DOWN from the window's root VC to find all navigation controllers.
struct CallBarSafeAreaBridge: UIViewRepresentable {
let topInset: CGFloat
func makeUIView(context: Context) -> CallBarInsetView {
let view = CallBarInsetView()
view.isHidden = true
view.isUserInteractionEnabled = false
return view
}
func updateUIView(_ view: CallBarInsetView, context: Context) {
view.topInset = topInset
view.applyInset()
}
}
/// Invisible UIView that finds all UINavigationControllers in the window
/// and sets `additionalSafeAreaInsets.top` on each.
final class CallBarInsetView: UIView {
var topInset: CGFloat = 0
private var lastAppliedInset: CGFloat = -1
override func didMoveToWindow() {
super.didMoveToWindow()
lastAppliedInset = -1 // Force re-apply
applyInset()
}
func applyInset() {
guard topInset != lastAppliedInset else { return }
guard let window, let rootVC = window.rootViewController else { return }
let inset = topInset
// Animate in sync with SwiftUI spring transition to avoid jerk.
// layoutIfNeeded() forces the layout pass INSIDE the animation block
// without it, additionalSafeAreaInsets triggers layout on the next
// run loop pass, which is outside the animation scope instant jump.
UIView.animate(withDuration: 0.4, delay: 0, usingSpringWithDamping: 0.85,
initialSpringVelocity: 0, options: [.beginFromCurrentState]) {
Self.apply(inset: inset, in: rootVC)
rootVC.view.layoutIfNeeded()
}
lastAppliedInset = topInset
}
/// Recursively walk child view controllers (NOT presentedViewController)
/// and set additionalSafeAreaInsets.top on every UINavigationController.
private static func apply(inset: CGFloat, in vc: UIViewController) {
if let nav = vc as? UINavigationController,
nav.additionalSafeAreaInsets.top != inset {
nav.additionalSafeAreaInsets.top = inset
}
for child in vc.children {
apply(inset: inset, in: child)
}
}
}
extension View {
/// Push NavigationStack's nav bar down by `inset` points.
/// Uses UIKit `additionalSafeAreaInsets` the only reliable mechanism.
func callBarSafeAreaInset(_ inset: CGFloat) -> some View {
background(
CallBarSafeAreaBridge(topInset: inset)
.frame(width: 0, height: 0)
.allowsHitTesting(false)
)
}
}