diff --git a/AuthenticatorShared/UI/Platform/Application/AppCoordinator.swift b/AuthenticatorShared/UI/Platform/Application/AppCoordinator.swift index b554847a32..0f79111877 100644 --- a/AuthenticatorShared/UI/Platform/Application/AppCoordinator.swift +++ b/AuthenticatorShared/UI/Platform/Application/AppCoordinator.swift @@ -130,7 +130,7 @@ class AppCoordinator: Coordinator, HasRootNavigator { coordinator.navigate(to: route) } else { guard let rootNavigator else { return } - let tabNavigator = UITabBarController() + let tabNavigator = BitwardenTabBarController() let coordinator = module.makeTabCoordinator( errorReporter: services.errorReporter, rootNavigator: rootNavigator, diff --git a/AuthenticatorShared/UI/Platform/Application/Utilities/TabNavigator.swift b/AuthenticatorShared/UI/Platform/Application/Utilities/TabNavigator.swift deleted file mode 100644 index 10040d27ea..0000000000 --- a/AuthenticatorShared/UI/Platform/Application/Utilities/TabNavigator.swift +++ /dev/null @@ -1,53 +0,0 @@ -import BitwardenKit -import UIKit - -// MARK: - TabNavigator - -/// A navigator that displays a child navigators in a tab interface. -/// -@MainActor -public protocol TabNavigator: Navigator { - /// The index of the navigator associated with the currently selected tab item. - var selectedIndex: Int { get set } - - /// Returns the child navigator for the specified tab. - /// - /// - Parameter tab: The tab which should be returned by the navigator. - /// - Returns: The child navigator for the specified tab. - /// - func navigator(for tab: Tab) -> Navigator? - - /// Sets the child navigators for their tabs. - /// - /// This method replaces all existing tabs with this new set of tabs. - /// - /// Tabs are ordered based on their `index` value. - /// - /// - Parameter tabs: The tab -> navigator relationship. - /// - func setNavigators(_ tabs: [Tab: Navigator]) -} - -// MARK: - UITabBarController - -extension UITabBarController: TabNavigator { - public var rootViewController: UIViewController? { - self - } - - public func navigator(for tab: Tab) -> Navigator? { - viewControllers?[tab.index] as? Navigator - } - - public func setNavigators(_ tabs: [Tab: Navigator]) { - viewControllers = tabs - .sorted { $0.key.index < $1.key.index } - .compactMap { tab in - guard let viewController = tab.value.rootViewController else { return nil } - viewController.tabBarItem.title = tab.key.title - viewController.tabBarItem.image = tab.key.image - viewController.tabBarItem.selectedImage = tab.key.selectedImage - return viewController - } - } -} diff --git a/AuthenticatorShared/UI/Platform/Application/Views/BitwardenTabBarController.swift b/AuthenticatorShared/UI/Platform/Application/Views/BitwardenTabBarController.swift new file mode 100644 index 0000000000..58b22fa254 --- /dev/null +++ b/AuthenticatorShared/UI/Platform/Application/Views/BitwardenTabBarController.swift @@ -0,0 +1,56 @@ +import BitwardenKit +import UIKit + +// MARK: - BitwardenTabBarController + +/// A `UITabBarController` subclass conforming to `TabNavigator`. This class manages +/// a set of tabs and handles dynamic appearance changes between light/dark mode. +/// +class BitwardenTabBarController: UITabBarController, TabNavigator { + // MARK: Properties + + /// The tabs used in the UITabBarController, mapping each `TabRoute` to its respective `Navigator`. + private var tabsAndNavigators: [TabRoute: any Navigator] = [:] + + // MARK: AlertPresentable + + var rootViewController: UIViewController? { + self + } + + // MARK: TabNavigator + + func navigator(for tab: Tab) -> Navigator? { + viewControllers?[tab.index] as? Navigator + } + + func setNavigators(_ tabs: [Tab: Navigator]) { + tabsAndNavigators = tabs as? [TabRoute: Navigator] ?? [:] + + viewControllers = tabs + .sorted { $0.key.index < $1.key.index } + .compactMap { tab in + guard let viewController = tab.value.rootViewController else { return nil } + viewController.tabBarItem.title = tab.key.title + viewController.tabBarItem.image = tab.key.image + viewController.tabBarItem.selectedImage = tab.key.selectedImage + return viewController + } + } + + // MARK: Lifecycle + + /// Called when the trait collection (such as light/dark mode) changes. + /// + /// UIKit does not seem to refresh the tab bar icon images dynamically when switching between + /// light/dark mode in mid-session. This override ensures the icons update correctly by re-applying + /// the navigators with the current tabs. + /// + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { + setNavigators(tabsAndNavigators) + } + } +} diff --git a/AuthenticatorShared/UI/Platform/Tabs/TabRoute.swift b/AuthenticatorShared/UI/Platform/Tabs/TabRoute.swift index da86f56e92..93ebcd8926 100644 --- a/AuthenticatorShared/UI/Platform/Tabs/TabRoute.swift +++ b/AuthenticatorShared/UI/Platform/Tabs/TabRoute.swift @@ -1,3 +1,4 @@ +import BitwardenKit import BitwardenResources import UIKit diff --git a/BitwardenKit/UI/Platform/Application/Utilities/Mocks/MockTabNavigator.swift b/BitwardenKit/UI/Platform/Application/Utilities/Mocks/MockTabNavigator.swift new file mode 100644 index 0000000000..c5256552df --- /dev/null +++ b/BitwardenKit/UI/Platform/Application/Utilities/Mocks/MockTabNavigator.swift @@ -0,0 +1,34 @@ +import BitwardenKit +import UIKit + +public final class MockTabNavigator: TabNavigator { + public var navigators: [Navigator] = [] + public var navigatorForTabValue: Int? + public var navigatorForTabReturns: Navigator? + public var rootViewController: UIViewController? + public var selectedIndex: Int = 0 + + public init() {} + + public func setChildren(_ navigators: [Navigator]) { + self.navigators = navigators + } + + public func navigator(for tab: Tab) -> Navigator? { + navigatorForTabValue = tab.index + return navigatorForTabReturns + } + + public func present( + _ viewController: UIViewController, + animated: Bool, + overFullscreen: Bool, + onCompletion: (() -> Void)?, + ) {} + + public func setNavigators(_ tabs: [Tab: Navigator]) { + navigators = tabs + .sorted { $0.key.index < $1.key.index } + .map(\.value) + } +} diff --git a/BitwardenShared/UI/Platform/Application/Utilities/TabNavigator.swift b/BitwardenKit/UI/Platform/Application/Utilities/TabNavigator.swift similarity index 90% rename from BitwardenShared/UI/Platform/Application/Utilities/TabNavigator.swift rename to BitwardenKit/UI/Platform/Application/Utilities/TabNavigator.swift index 2abf8a8362..7e30062211 100644 --- a/BitwardenShared/UI/Platform/Application/Utilities/TabNavigator.swift +++ b/BitwardenKit/UI/Platform/Application/Utilities/TabNavigator.swift @@ -1,9 +1,8 @@ -import BitwardenKit import UIKit // MARK: - TabNavigator -/// A navigator that displays a child navigators in a tab interface. +/// A navigator that displays a child navigator in a tab interface. /// @MainActor public protocol TabNavigator: Navigator { diff --git a/AuthenticatorShared/UI/Platform/Tabs/TabRepresentable.swift b/BitwardenKit/UI/Platform/Tabs/TabRepresentable.swift similarity index 100% rename from AuthenticatorShared/UI/Platform/Tabs/TabRepresentable.swift rename to BitwardenKit/UI/Platform/Tabs/TabRepresentable.swift diff --git a/BitwardenShared/UI/Platform/Application/Views/BitwardenTabBarController.swift b/BitwardenShared/UI/Platform/Application/Views/BitwardenTabBarController.swift index cf1527f56b..58b22fa254 100644 --- a/BitwardenShared/UI/Platform/Application/Views/BitwardenTabBarController.swift +++ b/BitwardenShared/UI/Platform/Application/Views/BitwardenTabBarController.swift @@ -3,7 +3,7 @@ import UIKit // MARK: - BitwardenTabBarController -/// A `UITabBarController` subclass confirming to `TabBavigator`. This class manages +/// A `UITabBarController` subclass conforming to `TabNavigator`. This class manages /// a set of tabs and handles dynamic appearance changes between light/dark mode. /// class BitwardenTabBarController: UITabBarController, TabNavigator { diff --git a/BitwardenShared/UI/Platform/Application/Utilities/TabNavigatorTests.swift b/BitwardenShared/UI/Platform/Application/Views/BitwardenTabBarControllerTests.swift similarity index 98% rename from BitwardenShared/UI/Platform/Application/Utilities/TabNavigatorTests.swift rename to BitwardenShared/UI/Platform/Application/Views/BitwardenTabBarControllerTests.swift index 18773fe47e..f7026bb9f7 100644 --- a/BitwardenShared/UI/Platform/Application/Utilities/TabNavigatorTests.swift +++ b/BitwardenShared/UI/Platform/Application/Views/BitwardenTabBarControllerTests.swift @@ -1,10 +1,11 @@ +import BitwardenKit import BitwardenKitMocks import SwiftUI import XCTest @testable import BitwardenShared -class TabNavigatorTests: BitwardenTestCase { +class BitwardenTabBarControllerTests: BitwardenTestCase { // MARK: Types enum TestRoute: Int, Equatable, Hashable, TabRepresentable { diff --git a/BitwardenShared/UI/Platform/Tabs/TabRepresentable.swift b/BitwardenShared/UI/Platform/Tabs/TabRepresentable.swift deleted file mode 100644 index ad08ba3b5b..0000000000 --- a/BitwardenShared/UI/Platform/Tabs/TabRepresentable.swift +++ /dev/null @@ -1,26 +0,0 @@ -import UIKit - -// MARK: - TabRepresentable - -/// An object that can represent a tab in a tab navigator. -/// -public protocol TabRepresentable { - // MARK: Properties - - /// The unselected image for this tab. - var image: UIImage? { get } - - /// The index for this tab. - var index: Int { get } - - /// The selected image for this tab. - var selectedImage: UIImage? { get } - - /// The title for this tab. - var title: String { get } -} - -public extension TabRepresentable where Self: RawRepresentable, Self.RawValue == Int { - /// The index for this tab. - var index: Int { rawValue } -} diff --git a/BitwardenShared/UI/Platform/Tabs/TabRoute.swift b/BitwardenShared/UI/Platform/Tabs/TabRoute.swift index c6495db51c..ff83497bb3 100644 --- a/BitwardenShared/UI/Platform/Tabs/TabRoute.swift +++ b/BitwardenShared/UI/Platform/Tabs/TabRoute.swift @@ -1,3 +1,4 @@ +import BitwardenKit import BitwardenResources import UIKit diff --git a/GlobalTestHelpers/MockAppModule.swift b/GlobalTestHelpers/MockAppModule.swift index eaa1c7bf34..eb2cb4d208 100644 --- a/GlobalTestHelpers/MockAppModule.swift +++ b/GlobalTestHelpers/MockAppModule.swift @@ -76,7 +76,7 @@ class MockAppModule: authCoordinator.asAnyCoordinator() } - func makeAuthRouter() -> BitwardenShared.AnyRouter { + func makeAuthRouter() -> AnyRouter { authRouter.asAnyRouter() } @@ -183,19 +183,19 @@ class MockAppModule: func makeTabCoordinator( // swiftlint:disable:this function_parameter_count errorReporter _: ErrorReporter, - rootNavigator _: BitwardenKit.RootNavigator, - settingsDelegate _: BitwardenShared.SettingsCoordinatorDelegate, - tabNavigator _: BitwardenShared.TabNavigator, - vaultDelegate _: BitwardenShared.VaultCoordinatorDelegate, - vaultRepository _: BitwardenShared.VaultRepository, - ) -> BitwardenShared.AnyCoordinator { + rootNavigator _: RootNavigator, + settingsDelegate _: SettingsCoordinatorDelegate, + tabNavigator _: TabNavigator, + vaultDelegate _: VaultCoordinatorDelegate, + vaultRepository _: VaultRepository, + ) -> AnyCoordinator { tabCoordinator.asAnyCoordinator() } func makeVaultCoordinator( - delegate _: BitwardenShared.VaultCoordinatorDelegate, + delegate _: VaultCoordinatorDelegate, stackNavigator _: StackNavigator, - ) -> BitwardenShared.AnyCoordinator { + ) -> AnyCoordinator { vaultCoordinator.asAnyCoordinator() } diff --git a/GlobalTestHelpers/MockTabNavigator.swift b/GlobalTestHelpers/MockTabNavigator.swift deleted file mode 100644 index d9e30d1f3a..0000000000 --- a/GlobalTestHelpers/MockTabNavigator.swift +++ /dev/null @@ -1,33 +0,0 @@ -import BitwardenKit -import BitwardenShared -import UIKit - -final class MockTabNavigator: TabNavigator { - var navigators: [Navigator] = [] - var navigatorForTabValue: Int? - var navigatorForTabReturns: Navigator? - var rootViewController: UIViewController? - var selectedIndex: Int = 0 - - func setChildren(_ navigators: [Navigator]) { - self.navigators = navigators - } - - func navigator(for tab: Tab) -> Navigator? { - navigatorForTabValue = tab.index - return navigatorForTabReturns - } - - func present( - _ viewController: UIViewController, - animated: Bool, - overFullscreen: Bool, - onCompletion: (() -> Void)?, - ) {} - - func setNavigators(_ tabs: [Tab: Navigator]) { - navigators = tabs - .sorted { $0.key.index < $1.key.index } - .map(\.value) - } -}