mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +00:00
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.overlayPresentationMode)
|
||||||
.animation(.elementDefault, value: navigationSplitCoordinator.overlayModule)
|
.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
|
/// 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)? {
|
var rootCoordinator: (any CoordinatorProtocol)? {
|
||||||
rootModule?.coordinator
|
rootModule?.coordinator
|
||||||
}
|
}
|
||||||
@@ -533,8 +534,8 @@ class NavigationStackCoordinator: ObservableObject, CoordinatorProtocol, CustomS
|
|||||||
|
|
||||||
var presentationDetents: Set<PresentationDetent> = []
|
var presentationDetents: Set<PresentationDetent> = []
|
||||||
|
|
||||||
// The currently presented sheet coordinator
|
/// The currently presented sheet coordinator
|
||||||
// Sheets will be presented through the NavigationSplitCoordinator if provided
|
/// Sheets will be presented through the NavigationSplitCoordinator if provided
|
||||||
var sheetCoordinator: (any CoordinatorProtocol)? {
|
var sheetCoordinator: (any CoordinatorProtocol)? {
|
||||||
if let navigationSplitCoordinator {
|
if let navigationSplitCoordinator {
|
||||||
return navigationSplitCoordinator.sheetCoordinator
|
return navigationSplitCoordinator.sheetCoordinator
|
||||||
@@ -558,8 +559,8 @@ class NavigationStackCoordinator: ObservableObject, CoordinatorProtocol, CustomS
|
|||||||
}
|
}
|
||||||
|
|
||||||
// periphery:ignore - might be useful to have
|
// periphery:ignore - might be useful to have
|
||||||
// The currently presented fullscreen cover coordinator
|
/// The currently presented fullscreen cover coordinator
|
||||||
// Fullscreen covers will be presented through the NavigationSplitCoordinator if provided
|
/// Fullscreen covers will be presented through the NavigationSplitCoordinator if provided
|
||||||
var fullScreenCoverCoordinator: (any CoordinatorProtocol)? {
|
var fullScreenCoverCoordinator: (any CoordinatorProtocol)? {
|
||||||
if let navigationSplitCoordinator {
|
if let navigationSplitCoordinator {
|
||||||
return navigationSplitCoordinator.fullScreenCoverCoordinator
|
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] {
|
var stackCoordinators: [any CoordinatorProtocol] {
|
||||||
stackModules.compactMap(\.coordinator)
|
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