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