Files
suixinkan_ios_uikit/suixinkan_ios/App/Navigation/NavigationRouter.swift
汉秋 a1c031c9b7 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>
2026-06-26 16:36:18 +08:00

162 lines
5.4 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// NavigationRouter.swift
// suixinkan
//
// Created by Codex on 2026/6/18.
//
import Foundation
import UIKit
/// Tab
enum AppRoute: Hashable {
case placeholder(title: String)
case home(HomeRoute)
case profile(ProfileRoute)
case orders(OrdersRoute)
/// push TabBar
var hidesTabBarWhenPushed: Bool {
true
}
}
@MainActor
/// UIKit
enum AppNavigationPolicy: Equatable {
case currentStack
case tabRoot(AppTab)
case preserveTab(AppTab)
}
@MainActor
/// Tab 宿 `MainTabBarController` UIKit
protocol AppNavigationHost: AnyObject {
/// Tab
func selectTab(_ tab: AppTab)
/// Tab
func navigationController(for tab: AppTab) -> TabNavigationController?
/// 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
}
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
private func performOrEnqueue(_ action: @escaping @MainActor (AppNavigationHost) -> Void) {
if let host {
action(host)
} else {
pendingActions.append(action)
}
}
}