mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 20:09:04 +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)
|
.environment(\.isInSidebar, true)
|
||||||
// The tab rail's background tracks the detail module, so exclude the sidebar's background value.
|
// The tab rail's background tracks the detail module, so exclude the sidebar's background value.
|
||||||
.transformPreference(CompoundBackgroundPreferenceKey.self) { $0 = nil }
|
.transformPreference(CompoundBackgroundPreferenceKey.self) { $0 = nil }
|
||||||
.overlay(alignment: .leading) { sidebarDivider }
|
.overlay(alignment: .trailing) { NavigationSidebarDivider() }
|
||||||
.overlay(alignment: .trailing) { sidebarDivider }
|
|
||||||
} detail: {
|
} detail: {
|
||||||
if let detailModule = navigationSplitCoordinator.detailModule {
|
if let detailModule = navigationSplitCoordinator.detailModule {
|
||||||
detailModule.coordinator?.toPresentable()
|
detailModule.coordinator?.toPresentable()
|
||||||
@@ -416,9 +415,11 @@ private struct NavigationSplitCoordinatorView: View {
|
|||||||
.animation(.elementDefault, value: navigationSplitCoordinator.sidebarModule)
|
.animation(.elementDefault, value: navigationSplitCoordinator.sidebarModule)
|
||||||
.animation(.noAnimation, value: navigationSplitCoordinator.detailModule) // Don't crossfade the detail transition on iPad.
|
.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, *) {
|
if #available(iOS 27, *) {
|
||||||
HStack(spacing: 0) { // The stack is necessary for the divider's orientation.
|
HStack(spacing: 0) { // The stack is necessary for the divider's orientation.
|
||||||
Divider()
|
Divider()
|
||||||
|
|||||||
@@ -307,48 +307,6 @@ extension EnvironmentValues {
|
|||||||
@Entry var tabViewHorizontalSizeClass: UserInterfaceSizeClass?
|
@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 {
|
private struct NavigationTabCoordinatorView<Tag: Hashable>: View {
|
||||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||||
|
|
||||||
@@ -357,7 +315,6 @@ private struct NavigationTabCoordinatorView<Tag: Hashable>: View {
|
|||||||
@State private var standardAppearance = UITabBarAppearance()
|
@State private var standardAppearance = UITabBarAppearance()
|
||||||
@State private var window: UIWindow?
|
@State private var window: UIWindow?
|
||||||
@State private var isFullScreen = true
|
@State private var isFullScreen = true
|
||||||
@State private var railVisibility = TabRailVisibility()
|
|
||||||
@State private var railBackgroundColor: Color = .compound.bgCanvasDefault
|
@State private var railBackgroundColor: Color = .compound.bgCanvasDefault
|
||||||
|
|
||||||
var body: some View {
|
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 {
|
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) {
|
HStack(spacing: 0) {
|
||||||
if railVisibility.isVisible {
|
TabRailView(navigationTabCoordinator: navigationTabCoordinator, isFullScreen: isFullScreen)
|
||||||
TabRailView(navigationTabCoordinator: navigationTabCoordinator, isFullScreen: isFullScreen)
|
.background(railBackgroundColor.ignoresSafeArea())
|
||||||
.background(railBackgroundColor.ignoresSafeArea())
|
.animation(.easeInOut(duration: 0.4).disabledDuringTests(), value: railBackgroundColor)
|
||||||
.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 }) {
|
if let module = navigationTabCoordinator.tabModules.first(where: { $0.details.tag == navigationTabCoordinator.selectedTab }) {
|
||||||
module.coordinator?.toPresentable()
|
module.coordinator?.toPresentable()
|
||||||
@@ -428,26 +381,14 @@ private struct NavigationTabCoordinatorView<Tag: Hashable>: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onGeometryChange(for: CGSize.self) { geometry in
|
.onGeometryChange(for: CGSize.self) { geometry in
|
||||||
geometry.size
|
geometry.size // We don't need the size, but it's a signal to re-compute.
|
||||||
} action: { size in
|
} action: { _ in
|
||||||
railVisibility.containerWidthChanged(to: size.width)
|
|
||||||
|
|
||||||
guard let newValue = window?.isFullScreen else { return }
|
guard let newValue = window?.isFullScreen else { return }
|
||||||
|
|
||||||
if newValue != isFullScreen {
|
if newValue != isFullScreen {
|
||||||
isFullScreen = newValue
|
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 {
|
var tabViewLayout: some View {
|
||||||
@@ -489,14 +430,6 @@ struct TabRailView<Tag: Hashable>: View {
|
|||||||
isFullScreen || ProcessInfo.processInfo.isiOSAppOnMac ? 0 : 48
|
isFullScreen || ProcessInfo.processInfo.isiOSAppOnMac ? 0 : 48
|
||||||
}
|
}
|
||||||
|
|
||||||
var trailingPadding: CGFloat {
|
|
||||||
if #available(iOS 27, *) {
|
|
||||||
8
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(spacing: 16) {
|
VStack(spacing: 16) {
|
||||||
@@ -516,8 +449,7 @@ struct TabRailView<Tag: Hashable>: View {
|
|||||||
.badge(10) // TODO: Check if this works.
|
.badge(10) // TODO: Check if this works.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.leading, 8)
|
.padding(.horizontal, 8)
|
||||||
.padding(.trailing, trailingPadding)
|
|
||||||
.padding(.top, topPadding)
|
.padding(.top, topPadding)
|
||||||
.padding(.bottom)
|
.padding(.bottom)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user