mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:31:27 +00:00
TabRail layout fixes (#6172)
* Attempt another TabRail layout fix by just keeping it visible all the time * Move one of the sidebar dividers to the tab rail.
This commit is contained in:
@@ -397,8 +397,7 @@ private struct NavigationSplitCoordinatorView: View {
|
||||
.environment(\.isInSidebar, true)
|
||||
// The tab rail's background tracks the detail module, so exclude the sidebar's background value.
|
||||
.transformPreference(CompoundBackgroundPreferenceKey.self) { $0 = nil }
|
||||
.overlay(alignment: .leading) { sidebarDivider }
|
||||
.overlay(alignment: .trailing) { sidebarDivider }
|
||||
.overlay(alignment: .trailing) { NavigationSidebarDivider() }
|
||||
} detail: {
|
||||
if let detailModule = navigationSplitCoordinator.detailModule {
|
||||
detailModule.coordinator?.toPresentable()
|
||||
@@ -416,9 +415,11 @@ private struct NavigationSplitCoordinatorView: View {
|
||||
.animation(.elementDefault, value: navigationSplitCoordinator.sidebarModule)
|
||||
.animation(.noAnimation, value: navigationSplitCoordinator.detailModule) // Don't crossfade the detail transition on iPad.
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var sidebarDivider: some View {
|
||||
}
|
||||
|
||||
/// The vertical divider that separates the sidebar from its neighbours on iOS 27, where the sidebar is no longer inset.
|
||||
struct NavigationSidebarDivider: View {
|
||||
var body: some View {
|
||||
if #available(iOS 27, *) {
|
||||
HStack(spacing: 0) { // The stack is necessary for the divider's orientation.
|
||||
Divider()
|
||||
|
||||
@@ -307,48 +307,6 @@ extension EnvironmentValues {
|
||||
@Entry var tabViewHorizontalSizeClass: UserInterfaceSizeClass?
|
||||
}
|
||||
|
||||
/// Tracks whether the tab rail should be shown alongside the split view's sidebar.
|
||||
///
|
||||
/// The rail is laid out beside the split view, so hiding it hands that width back to the split, which may
|
||||
/// then have room for its sidebar again, which would show the rail, ad infinitum. The split reports the
|
||||
/// same column visibility whether it collapsed on its own or because the sidebar was toggled, and at the
|
||||
/// same width, so the two are told apart by when they arrive: the rebound lands in the next layout pass.
|
||||
private struct TabRailVisibility {
|
||||
/// The longest a collapse can follow the rail being shown and still be considered caused by it.
|
||||
private static let reboundDuration: TimeInterval = 0.5
|
||||
|
||||
private(set) var isVisible = true
|
||||
|
||||
private var containerWidth: CGFloat = 0
|
||||
/// The container width at which the rail and the sidebar have been shown not to fit together.
|
||||
private var blockedWidth: CGFloat?
|
||||
/// When the rail was last shown, used to spot the split view collapsing as a direct result of it.
|
||||
private var shownDate = Date.distantPast
|
||||
|
||||
mutating func containerWidthChanged(to width: CGFloat) {
|
||||
guard width != containerWidth else { return }
|
||||
|
||||
containerWidth = width
|
||||
blockedWidth = nil // The rail and the sidebar may well fit at the new width.
|
||||
}
|
||||
|
||||
mutating func splitVisibilityChanged(to visibility: NavigationSplitViewVisibility) {
|
||||
if visibility == .detailOnly {
|
||||
// A collapse this soon after showing the rail was caused by the rail's own width, so showing
|
||||
// it again would only collapse the split view again, flickering forever. Note that the two
|
||||
// don't fit at this width and leave the rail hidden until that changes.
|
||||
if shownDate.timeIntervalSinceNow > -Self.reboundDuration {
|
||||
blockedWidth = containerWidth
|
||||
}
|
||||
|
||||
isVisible = false
|
||||
} else if !isVisible, blockedWidth != containerWidth {
|
||||
isVisible = true
|
||||
shownDate = .now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct NavigationTabCoordinatorView<Tag: Hashable>: View {
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
|
||||
@@ -357,7 +315,6 @@ private struct NavigationTabCoordinatorView<Tag: Hashable>: View {
|
||||
@State private var standardAppearance = UITabBarAppearance()
|
||||
@State private var window: UIWindow?
|
||||
@State private var isFullScreen = true
|
||||
@State private var railVisibility = TabRailVisibility()
|
||||
@State private var railBackgroundColor: Color = .compound.bgCanvasDefault
|
||||
|
||||
var body: some View {
|
||||
@@ -394,20 +351,16 @@ private struct NavigationTabCoordinatorView<Tag: Hashable>: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// The column visibility of the split coordinator for the currently selected tab, if any.
|
||||
private var selectedTabSplitColumnVisibility: NavigationSplitViewVisibility {
|
||||
let selectedTabDetails = navigationTabCoordinator.tabModules
|
||||
.first { $0.details.tag == navigationTabCoordinator.selectedTab }?.details
|
||||
return selectedTabDetails?.navigationSplitCoordinator?.columnVisibility ?? .all
|
||||
}
|
||||
|
||||
var tabRailLayout: some View {
|
||||
// The rail is always shown. Removing it hands its width back to the split view, which then has
|
||||
// room for its sidebar again, which shows the rail, ad infinitum. Keeping it means the split's
|
||||
// width never depends on it and the two can't drive each other.
|
||||
HStack(spacing: 0) {
|
||||
if railVisibility.isVisible {
|
||||
TabRailView(navigationTabCoordinator: navigationTabCoordinator, isFullScreen: isFullScreen)
|
||||
.background(railBackgroundColor.ignoresSafeArea())
|
||||
.animation(.easeInOut(duration: 0.4).disabledDuringTests(), value: railBackgroundColor)
|
||||
}
|
||||
TabRailView(navigationTabCoordinator: navigationTabCoordinator, isFullScreen: isFullScreen)
|
||||
.background(railBackgroundColor.ignoresSafeArea())
|
||||
.animation(.easeInOut(duration: 0.4).disabledDuringTests(), value: railBackgroundColor)
|
||||
.overlay(alignment: .trailing) { NavigationSidebarDivider() }
|
||||
.zIndex(1)
|
||||
|
||||
if let module = navigationTabCoordinator.tabModules.first(where: { $0.details.tag == navigationTabCoordinator.selectedTab }) {
|
||||
module.coordinator?.toPresentable()
|
||||
@@ -428,26 +381,14 @@ private struct NavigationTabCoordinatorView<Tag: Hashable>: View {
|
||||
}
|
||||
}
|
||||
.onGeometryChange(for: CGSize.self) { geometry in
|
||||
geometry.size
|
||||
} action: { size in
|
||||
railVisibility.containerWidthChanged(to: size.width)
|
||||
|
||||
geometry.size // We don't need the size, but it's a signal to re-compute.
|
||||
} action: { _ in
|
||||
guard let newValue = window?.isFullScreen else { return }
|
||||
|
||||
if newValue != isFullScreen {
|
||||
isFullScreen = newValue
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedTabSplitColumnVisibility, initial: true) {
|
||||
// The rail consumes width, which feeds back into the split's column visibility (not the
|
||||
// same as flattening everything into the single stack), creating an observation tracking
|
||||
// feedback loop that locks up the app while resizing.
|
||||
//
|
||||
// Use a stored value (instead of a computed value) that is updated on the next run loop to break the loop.
|
||||
DispatchQueue.main.async {
|
||||
railVisibility.splitVisibilityChanged(to: selectedTabSplitColumnVisibility)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tabViewLayout: some View {
|
||||
@@ -489,14 +430,6 @@ struct TabRailView<Tag: Hashable>: View {
|
||||
isFullScreen || ProcessInfo.processInfo.isiOSAppOnMac ? 0 : 48
|
||||
}
|
||||
|
||||
var trailingPadding: CGFloat {
|
||||
if #available(iOS 27, *) {
|
||||
8
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
@@ -516,8 +449,7 @@ struct TabRailView<Tag: Hashable>: View {
|
||||
.badge(10) // TODO: Check if this works.
|
||||
}
|
||||
}
|
||||
.padding(.leading, 8)
|
||||
.padding(.trailing, trailingPadding)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.top, topPadding)
|
||||
.padding(.bottom)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user