fix(ios): use dynamic settings bottom margin

This commit is contained in:
joshavant
2026-06-04 22:08:20 -05:00
parent 697eeb8bab
commit 17ab517047
3 changed files with 122 additions and 4 deletions

View File

@@ -59,6 +59,11 @@ struct SettingsProTab: View {
@State var notificationActionText = "Request Access"
@State var diagnosticsLastRunText = "Not run"
@State var diagnosticsIssueCount: Int?
@State var bottomOverlayInset: CGFloat = 0
var bottomScrollMargin: CGFloat {
max(0, self.bottomOverlayInset - SettingsLayout.rowHeight - SettingsLayout.bottomContentPadding)
}
var body: some View {
NavigationStack {
@@ -71,9 +76,13 @@ struct SettingsProTab: View {
self.gatewaySection
self.settingsListSection
}
.padding(.vertical, 18)
.padding(.top, 18)
.padding(.bottom, SettingsLayout.bottomContentPadding)
}
.safeAreaPadding(.bottom, OpenClawProMetric.bottomScrollInset)
.contentMargins(.bottom, self.bottomScrollMargin, for: .scrollContent)
SettingsBottomOverlayInsetReader(inset: self.$bottomOverlayInset)
.frame(width: 0, height: 0)
.allowsHitTesting(false)
}
.navigationBarHidden(true)
.navigationDestination(for: SettingsRoute.self) { route in

View File

@@ -203,9 +203,13 @@ extension SettingsProTab {
self.aboutDestination
}
}
.padding(.vertical, 18)
.padding(.top, 18)
.padding(.bottom, SettingsLayout.bottomContentPadding)
}
.safeAreaPadding(.bottom, OpenClawProMetric.bottomScrollInset)
.contentMargins(.bottom, self.bottomScrollMargin, for: .scrollContent)
SettingsBottomOverlayInsetReader(inset: self.$bottomOverlayInset)
.frame(width: 0, height: 0)
.allowsHitTesting(false)
}
.navigationTitle(self.title(for: route))
.navigationBarTitleDisplayMode(.inline)

View File

@@ -1,5 +1,6 @@
import Darwin
import SwiftUI
import UIKit
enum SettingsRoute: Hashable {
case gateway
@@ -14,6 +15,110 @@ enum SettingsRoute: Hashable {
enum SettingsLayout {
static let cardRadius: CGFloat = 12
static let rowHeight: CGFloat = 58
static let bottomContentPadding: CGFloat = 12
}
struct SettingsBottomOverlayInsetReader: UIViewRepresentable {
@Binding var inset: CGFloat
func makeCoordinator() -> Coordinator {
Coordinator(inset: self.$inset)
}
func makeUIView(context: Context) -> SettingsBottomOverlayInsetProbeView {
let view = SettingsBottomOverlayInsetProbeView()
view.onInsetChange = { value in
context.coordinator.updateInset(value)
}
return view
}
func updateUIView(_ uiView: SettingsBottomOverlayInsetProbeView, context: Context) {
context.coordinator.inset = self.$inset
uiView.onInsetChange = { value in
context.coordinator.updateInset(value)
}
uiView.updateInset()
}
final class Coordinator {
var inset: Binding<CGFloat>
init(inset: Binding<CGFloat>) {
self.inset = inset
}
func updateInset(_ value: CGFloat) {
let rounded = max(0, ceil(value))
guard abs(self.inset.wrappedValue - rounded) > 0.5 else { return }
self.inset.wrappedValue = rounded
}
}
}
final class SettingsBottomOverlayInsetProbeView: UIView {
var onInsetChange: ((CGFloat) -> Void)?
override func didMoveToWindow() {
super.didMoveToWindow()
self.updateInset()
}
override func layoutSubviews() {
super.layoutSubviews()
self.updateInset()
}
func updateInset() {
let value = self.visibleTabBarHeight()
DispatchQueue.main.async { [weak self] in
self?.onInsetChange?(value)
}
}
private func visibleTabBarHeight() -> CGFloat {
let tabBarController = self.nearestViewController()?.tabBarController
?? self.findTabBarController(in: self.window?.rootViewController)
guard let tabBar = tabBarController?.tabBar,
!tabBar.isHidden,
tabBar.alpha > 0.01,
tabBar.window != nil,
self.window != nil
else {
return 0
}
let tabFrame = tabBar.convert(tabBar.bounds, to: nil)
guard tabFrame.height.isFinite else { return 0 }
return max(0, tabFrame.height)
}
private func nearestViewController() -> UIViewController? {
var responder: UIResponder? = self
while let current = responder {
if let viewController = current as? UIViewController {
return viewController
}
responder = current.next
}
return nil
}
private func findTabBarController(in viewController: UIViewController?) -> UITabBarController? {
guard let viewController else { return nil }
if let tabBarController = viewController as? UITabBarController {
return tabBarController
}
if let tabBarController = self.findTabBarController(in: viewController.presentedViewController) {
return tabBarController
}
for child in viewController.children {
if let tabBarController = self.findTabBarController(in: child) {
return tabBarController
}
}
return nil
}
}
enum SettingsDiagnosticIssue: String, Equatable, CaseIterable {