Create a NavigationTabCoordinator to manage a TabView.
This commit is contained in:
@@ -449,6 +449,7 @@ private struct NavigationSplitCoordinatorView: View {
|
||||
.animation(.elementDefault, value: navigationSplitCoordinator.overlayPresentationMode)
|
||||
.animation(.elementDefault, value: navigationSplitCoordinator.overlayModule)
|
||||
}
|
||||
.ignoresSafeArea() // Necessary when embedded in a TabView on iPadOS otherwise there's a gap at the top (as of 18.5).
|
||||
}
|
||||
|
||||
/// The NavigationStack that will be used in compact layouts
|
||||
@@ -512,7 +513,7 @@ class NavigationStackCoordinator: ObservableObject, CoordinatorProtocol, CustomS
|
||||
}
|
||||
}
|
||||
|
||||
// The stack's current root coordinator
|
||||
/// The stack's current root coordinator
|
||||
var rootCoordinator: (any CoordinatorProtocol)? {
|
||||
rootModule?.coordinator
|
||||
}
|
||||
@@ -533,8 +534,8 @@ class NavigationStackCoordinator: ObservableObject, CoordinatorProtocol, CustomS
|
||||
|
||||
var presentationDetents: Set<PresentationDetent> = []
|
||||
|
||||
// The currently presented sheet coordinator
|
||||
// Sheets will be presented through the NavigationSplitCoordinator if provided
|
||||
/// The currently presented sheet coordinator
|
||||
/// Sheets will be presented through the NavigationSplitCoordinator if provided
|
||||
var sheetCoordinator: (any CoordinatorProtocol)? {
|
||||
if let navigationSplitCoordinator {
|
||||
return navigationSplitCoordinator.sheetCoordinator
|
||||
@@ -558,8 +559,8 @@ class NavigationStackCoordinator: ObservableObject, CoordinatorProtocol, CustomS
|
||||
}
|
||||
|
||||
// periphery:ignore - might be useful to have
|
||||
// The currently presented fullscreen cover coordinator
|
||||
// Fullscreen covers will be presented through the NavigationSplitCoordinator if provided
|
||||
/// The currently presented fullscreen cover coordinator
|
||||
/// Fullscreen covers will be presented through the NavigationSplitCoordinator if provided
|
||||
var fullScreenCoverCoordinator: (any CoordinatorProtocol)? {
|
||||
if let navigationSplitCoordinator {
|
||||
return navigationSplitCoordinator.fullScreenCoverCoordinator
|
||||
@@ -584,7 +585,7 @@ class NavigationStackCoordinator: ObservableObject, CoordinatorProtocol, CustomS
|
||||
}
|
||||
}
|
||||
|
||||
// The current navigation stack. Excludes the rootCoordinator
|
||||
/// The current navigation stack. Excludes the rootCoordinator
|
||||
var stackCoordinators: [any CoordinatorProtocol] {
|
||||
stackModules.compactMap(\.coordinator)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
//
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
/// Class responsible for displaying an arbitrary number of coordinators within the tab bar.
|
||||
@Observable class NavigationTabCoordinator: CoordinatorProtocol, CustomStringConvertible {
|
||||
struct Tab {
|
||||
let coordinator: CoordinatorProtocol
|
||||
let title: String
|
||||
let icon: KeyPath<CompoundIcons, Image>
|
||||
let selectedIcon: KeyPath<CompoundIcons, Image>
|
||||
}
|
||||
|
||||
fileprivate struct TabModule: Identifiable {
|
||||
let module: NavigationModule
|
||||
let title: String
|
||||
let icon: KeyPath<CompoundIcons, Image>
|
||||
let selectedIcon: KeyPath<CompoundIcons, Image>
|
||||
|
||||
var id: ObjectIdentifier { module.id }
|
||||
@MainActor var coordinator: CoordinatorProtocol? { module.coordinator }
|
||||
}
|
||||
|
||||
fileprivate var tabModules = [TabModule]() {
|
||||
didSet {
|
||||
let diffs = tabModules.map(\.module).difference(from: oldValue.map(\.module))
|
||||
diffs.forEach { change in
|
||||
switch change {
|
||||
case .insert(_, let module, _):
|
||||
logPresentationChange("Set tab", module)
|
||||
module.coordinator?.start()
|
||||
case .remove(_, let module, _):
|
||||
logPresentationChange("Remove tab", module)
|
||||
module.tearDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The current set of coordinators displayed by the tabs.
|
||||
var tabCoordinators: [any CoordinatorProtocol] {
|
||||
tabModules.compactMap(\.module.coordinator)
|
||||
}
|
||||
|
||||
/// Updates the displayed tabs with the provided array.
|
||||
func setTabs(_ tabs: [Tab], animated: Bool = true) {
|
||||
var transaction = Transaction()
|
||||
transaction.disablesAnimations = !animated
|
||||
|
||||
withTransaction(transaction) {
|
||||
tabModules = tabs.map { TabModule(module: .init($0.coordinator), title: $0.title, icon: $0.icon, selectedIcon: $0.selectedIcon) }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CoordinatorProtocol
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(NavigationTabCoordinatorView(navigationTabCoordinator: self))
|
||||
}
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
var description: String {
|
||||
guard !tabModules.isEmpty else { return "NavigationTabCoordinator(Empty)" }
|
||||
return "NavigationTabCoordinator(\(tabCoordinators)"
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func logPresentationChange(_ change: String, _ module: NavigationModule) {
|
||||
if let coordinator = module.coordinator {
|
||||
MXLog.info("\(self) \(change): \(coordinator)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct NavigationTabCoordinatorView: View {
|
||||
@Bindable var navigationTabCoordinator: NavigationTabCoordinator
|
||||
@State private var selectedTab: ObjectIdentifier?
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $selectedTab) {
|
||||
ForEach(navigationTabCoordinator.tabModules) { module in
|
||||
module.coordinator?.toPresentable()
|
||||
.tabItem {
|
||||
Label {
|
||||
Text(module.title)
|
||||
} icon: {
|
||||
CompoundIcon(module.id == selectedTab ? module.selectedIcon : module.icon)
|
||||
}
|
||||
}
|
||||
.tag(module.id)
|
||||
.id(module.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user