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:
@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user