Integrate CYLTabBar center scan button and unify UIKit navigation.

Add CYLTabBarController with a global scan entry, promote MainTabBarController to the window root after login, replace RouterPath-based navigation with AppNavigator, and fix Profile diffable cell reconfiguration crashes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-26 16:36:18 +08:00
parent 24a7339b68
commit a1c031c9b7
31 changed files with 838 additions and 517 deletions

View File

@ -172,12 +172,3 @@ enum AppRouteViewControllerFactory {
return String(describing: route)
}
}
@MainActor
/// ViewController `UIKitAppNavigation`
enum HomeRouteViewControllerFactory {
///
static func make(for route: HomeRoute) -> UIViewController {
AppRouteViewControllerFactory.makeViewController(for: route, services: AppServices.shared)
}
}

View File

@ -6,6 +6,7 @@
//
import Foundation
import UIKit
/// Tab
enum AppRoute: Hashable {
@ -21,90 +22,140 @@ enum AppRoute: Hashable {
}
@MainActor
/// Tab
final class RouterPath {
var onChange: (() -> Void)?
var path: [AppRoute] = [] { didSet { onChange?() } }
/// Tab
func navigate(to route: AppRoute) {
path.append(route)
}
/// Tab
func navigateBack() {
guard !path.isEmpty else { return }
path.removeLast()
}
/// Tab
func reset() {
path = []
}
/// UIKit
enum AppNavigationPolicy: Equatable {
case currentStack
case tabRoot(AppTab)
case preserveTab(AppTab)
}
@MainActor
/// Tab Tab
final class AppRouter {
var onChange: (() -> Void)?
var selectedTab: AppTab = .home { didSet { onChange?() } }
var selectedOrdersEntry: OrdersEntry = .storeOrders { didSet { onChange?() } }
private(set) var pendingOrderScanCode: String? { didSet { onChange?() } }
/// Tab 宿 `MainTabBarController` UIKit
protocol AppNavigationHost: AnyObject {
/// Tab
func selectTab(_ tab: AppTab)
private let routers: [AppTab: RouterPath]
/// Tab
func navigationController(for tab: AppTab) -> TabNavigationController?
///
init() {
var builtRouters: [AppTab: RouterPath] = [:]
for tab in AppTab.allCases {
builtRouters[tab] = RouterPath()
/// Tab
func selectedNavigationController() -> TabNavigationController?
/// Tab
func resetAllStacks()
///
func applyOrdersEntry(_ entry: OrdersEntry, scannedCode: String?)
}
@MainActor
/// UIKit `UINavigationController.viewControllers`
final class AppNavigator {
private weak var host: AppNavigationHost?
private var pendingActions: [(@MainActor (AppNavigationHost) -> Void)] = []
/// Tab 宿 FIFO
func attach(host: AppNavigationHost) {
self.host = host
let actions = pendingActions
pendingActions.removeAll()
actions.forEach { $0(host) }
}
/// Tab 宿
func detach(host: AppNavigationHost? = nil) {
if let host, self.host !== host {
return
}
routers = builtRouters
routers.values.forEach { router in
router.onChange = { [weak self] in
self?.onChange?()
self.host = nil
pendingActions.removeAll()
}
/// Tab Tab
func selectTab(_ tab: AppTab) {
performOrEnqueue { host in
host.selectTab(tab)
}
}
/// push
func push(_ route: AppRoute, from source: UIViewController?, animated: Bool = true) {
if let navigationController = source?.navigationController as? TabNavigationController {
navigationController.push(route: route, animated: animated)
return
}
performOrEnqueue { host in
guard let navigationController = source?.navigationController as? TabNavigationController
?? host.selectedNavigationController()
else { return }
navigationController.push(route: route, animated: animated)
}
}
///
func openHome(_ route: HomeRoute, policy: AppNavigationPolicy = .currentStack, animated: Bool = true) {
open(.home(route), defaultTab: .home, policy: policy, animated: animated)
}
///
func openProfile(_ route: ProfileRoute, policy: AppNavigationPolicy = .currentStack, animated: Bool = true) {
open(.profile(route), defaultTab: .profile, policy: policy, animated: animated)
}
/// Tab
func openOrdersEntry(_ entry: OrdersEntry, scannedCode: String? = nil) {
performOrEnqueue { host in
host.selectTab(.orders)
host.navigationController(for: .orders)?.popToRootViewController(animated: false)
host.applyOrdersEntry(entry, scannedCode: scannedCode)
}
}
/// Tab
func resetAllStacks() {
pendingActions.removeAll()
host?.resetAllStacks()
}
///
private func open(
_ route: AppRoute,
defaultTab: AppTab,
policy: AppNavigationPolicy,
animated: Bool
) {
performOrEnqueue { host in
let targetTab: AppTab
switch policy {
case .currentStack:
guard let navigationController = host.selectedNavigationController() else { return }
navigationController.push(route: route, animated: animated)
return
case .tabRoot(let tab):
targetTab = tab
host.selectTab(tab)
guard let navigationController = host.navigationController(for: tab) else { return }
navigationController.popToRootViewController(animated: false)
navigationController.push(route: route, animated: animated)
case .preserveTab(let tab):
targetTab = tab
host.selectTab(tab)
host.navigationController(for: tab)?.push(route: route, animated: animated)
}
if targetTab != defaultTab {
assertionFailure("Route \(route) opened on unexpected tab \(targetTab)")
}
}
}
/// Tab
func router(for tab: AppTab) -> RouterPath {
guard let router = routers[tab] else {
assertionFailure("Missing router path for tab: \(tab)")
return RouterPath()
/// Tab
private func performOrEnqueue(_ action: @escaping @MainActor (AppNavigationHost) -> Void) {
if let host {
action(host)
} else {
pendingActions.append(action)
}
return router
}
/// Tab
func select(_ tab: AppTab) {
selectedTab = tab
}
/// Tab
func selectOrders(entry: OrdersEntry) {
selectedOrdersEntry = entry
selectedTab = .orders
}
///
func routeToOrderVerification(scannedCode: String) {
pendingOrderScanCode = scannedCode
selectOrders(entry: .verificationOrders)
}
///
func consumePendingOrderScanCode() -> String? {
let code = pendingOrderScanCode
pendingOrderScanCode = nil
return code
}
/// Tab Tab
func reset() {
selectedTab = .home
selectedOrdersEntry = .storeOrders
pendingOrderScanCode = nil
routers.values.forEach { $0.reset() }
}
}

View File

@ -6,25 +6,19 @@
import UIKit
@MainActor
/// Tab `AppRouter` push TabBar
final class TabNavigationController: UINavigationController, UINavigationControllerDelegate {
/// Tab UIKit Tab
final class TabNavigationController: UINavigationController {
let appTab: AppTab
private let services: AppServices
private var isSyncingStack = false
///
init(tab: AppTab, services: AppServices) {
self.appTab = tab
self.services = services
super.init(nibName: nil, bundle: nil)
delegate = self
navigationBar.prefersLargeTitles = false
setViewControllers([tab.makeRootViewController(services: services)], animated: false)
registerNavigationBridge()
services.appRouter.router(for: appTab).onChange = { [weak self] in
self?.syncFromRouterPath()
}
}
@available(*, unavailable)
@ -32,76 +26,10 @@ final class TabNavigationController: UINavigationController, UINavigationControl
fatalError("init(coder:) has not been implemented")
}
/// Push RouterPath
func pushRoute(_ route: AppRoute, animated: Bool = true) {
/// Push TabBar
func push(route: AppRoute, animated: Bool = true) {
let viewController = AppRouteViewControllerFactory.makeViewController(for: route, services: services)
viewController.hidesBottomBarWhenPushed = route.hidesTabBarWhenPushed
isSyncingStack = true
services.appRouter.router(for: appTab).navigate(to: route)
isSyncingStack = false
pushViewController(viewController, animated: animated)
}
/// navigationController
func navigationController(
_ navigationController: UINavigationController,
didShow viewController: UIViewController,
animated: Bool
) {
syncRouterPathFromStack()
}
/// NavigationBridge
private func registerNavigationBridge() {
switch appTab {
case .home:
UIKitAppNavigation.homeNavigationController = self
case .profile:
UIKitAppNavigation.profileNavigationController = self
default:
break
}
}
/// FromRouterPath
private func syncFromRouterPath() {
guard !isSyncingStack else { return }
let routerPath = services.appRouter.router(for: appTab)
let targetDepth = routerPath.path.count
let currentDepth = max(viewControllers.count - 1, 0)
if targetDepth > currentDepth {
// Router push
for index in currentDepth..<targetDepth {
let route = routerPath.path[index]
let viewController = AppRouteViewControllerFactory.makeViewController(for: route, services: services)
viewController.hidesBottomBarWhenPushed = route.hidesTabBarWhenPushed
pushViewController(viewController, animated: true)
}
return
}
if targetDepth < currentDepth {
// Router pop
if targetDepth == 0 {
popToRootViewController(animated: true)
} else if targetDepth < viewControllers.count {
popToViewController(viewControllers[targetDepth], animated: true)
}
}
}
/// RouterPathFromStack
private func syncRouterPathFromStack() {
guard !isSyncingStack else { return }
let routerPath = services.appRouter.router(for: appTab)
let depth = max(viewControllers.count - 1, 0)
if depth < routerPath.path.count {
isSyncingStack = true
routerPath.path = Array(routerPath.path.prefix(depth))
isSyncingStack = false
}
}
}

View File

@ -1,46 +0,0 @@
//
// UIKitAppNavigation.swift
// suixinkan
//
// Created by Codex on 2026/6/26.
//
import UIKit
@MainActor
/// UIKit AppRouter UI Test
enum UIKitAppNavigation {
weak static var homeNavigationController: UINavigationController?
weak static var profileNavigationController: UINavigationController?
/// Push
static func pushHomeRoute(_ route: HomeRoute, animated: Bool = true) {
let viewController = AppRouteViewControllerFactory.makeViewController(for: route, services: AppServices.shared)
homeNavigationController?.pushViewController(viewController, animated: animated)
}
/// Push
static func pushProfileRoute(_ route: ProfileRoute, animated: Bool = true) {
let viewController = AppRouteViewControllerFactory.makeViewController(
for: .profile(route),
services: AppServices.shared
)
profileNavigationController?.pushViewController(viewController, animated: animated)
}
}
extension AppRouter {
/// UIKit push
func navigateHome(_ route: HomeRoute, animated: Bool = true) {
select(.home)
router(for: .home).navigate(to: .home(route))
UIKitAppNavigation.pushHomeRoute(route, animated: animated)
}
/// UIKit push
func navigateProfile(_ route: ProfileRoute, animated: Bool = true) {
select(.profile)
router(for: .profile).navigate(to: .profile(route))
UIKitAppNavigation.pushProfileRoute(route, animated: animated)
}
}