Create a NavigationTabCoordinator to manage a TabView.

This commit is contained in:
Doug
2025-07-29 14:54:20 +01:00
committed by Doug
parent 81b733fb6a
commit 26bc46e6ff
2 changed files with 110 additions and 6 deletions

View File

@@ -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)
}
}
}
}