Create a NavigationTabCoordinator to manage a TabView.
This commit is contained in:
@@ -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