Use a system scan tab slot with MainScanTabItem interception so scanner presentation no longer depends on CYL PlusButton, and remove the CYLTabBarController pod dependency. Co-authored-by: Cursor <cursoragent@cursor.com>
242 lines
8.5 KiB
Swift
242 lines
8.5 KiB
Swift
//
|
||
// MainTabBarController.swift
|
||
// suixinkan
|
||
//
|
||
|
||
import UIKit
|
||
|
||
@MainActor
|
||
/// 主 Tab 容器,使用系统 TabBar 展示四个一级入口及中间扫码按钮。
|
||
final class MainTabBarController: UITabBarController {
|
||
|
||
private let services: AppServices
|
||
private let badgeViewModel = MainTabBadgeViewModel()
|
||
private var tabNavigationControllers: [AppTab: TabNavigationController] = [:]
|
||
private var isSyncingTabSelection = false
|
||
|
||
/// 测试时可注入扫码点击处理,生产环境为空时执行真实扫码展示。
|
||
var onScanTabSelected: (() -> Void)?
|
||
|
||
/// 主 TabBar 固定槽位,扫码槽位只负责触发全局扫码,不参与业务路由。
|
||
private enum TabSlot: CaseIterable {
|
||
case home
|
||
case orders
|
||
case scan
|
||
case statistics
|
||
case profile
|
||
|
||
/// 返回槽位对应的业务 Tab,扫码槽位无业务根页面。
|
||
var appTab: AppTab? {
|
||
switch self {
|
||
case .home:
|
||
return .home
|
||
case .orders:
|
||
return .orders
|
||
case .scan:
|
||
return nil
|
||
case .statistics:
|
||
return .statistics
|
||
case .profile:
|
||
return .profile
|
||
}
|
||
}
|
||
|
||
/// 返回指定业务 Tab 在系统 TabBar 中的槽位索引。
|
||
static func index(for tab: AppTab) -> Int? {
|
||
allCases.firstIndex { $0.appTab == tab }
|
||
}
|
||
|
||
/// 返回扫码槽位在系统 TabBar 中的索引。
|
||
static var scanIndex: Int? {
|
||
allCases.firstIndex(of: .scan)
|
||
}
|
||
}
|
||
|
||
/// 初始化实例。
|
||
init(services: AppServices) {
|
||
self.services = services
|
||
super.init(nibName: nil, bundle: nil)
|
||
delegate = self
|
||
}
|
||
|
||
@available(*, unavailable)
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
/// 视图加载完成后的 UI 初始化与数据绑定。
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
configureTabBarAppearance()
|
||
configureTabs()
|
||
services.appNavigator.attach(host: self)
|
||
bindBadgeViewModel()
|
||
bindAccountContext()
|
||
Task { await refreshOrderBadge() }
|
||
#if DEBUG
|
||
Task { await AppUITestRouteDriver.applyIfNeeded(services: services) }
|
||
#endif
|
||
}
|
||
|
||
/// 配置 TabBar 外观,对齐参考工程 CustomMainTabBar 样式。
|
||
private func configureTabBarAppearance() {
|
||
tabBar.tintColor = AppDesign.primary
|
||
tabBar.backgroundColor = .white
|
||
tabBar.shadowImage = UIImage()
|
||
|
||
let normalAttributes: [NSAttributedString.Key: Any] = [
|
||
.foregroundColor: UIColor(hex: 0x7D8DA3),
|
||
.font: UIFont.systemFont(ofSize: 12, weight: .regular)
|
||
]
|
||
let selectedAttributes: [NSAttributedString.Key: Any] = [
|
||
.foregroundColor: AppDesign.primary,
|
||
.font: UIFont.systemFont(ofSize: 12, weight: .medium)
|
||
]
|
||
|
||
let appearance = UITabBarAppearance()
|
||
appearance.configureWithOpaqueBackground()
|
||
appearance.backgroundColor = .white
|
||
appearance.shadowColor = .clear
|
||
appearance.stackedLayoutAppearance.normal.titleTextAttributes = normalAttributes
|
||
appearance.stackedLayoutAppearance.selected.titleTextAttributes = selectedAttributes
|
||
tabBar.standardAppearance = appearance
|
||
tabBar.scrollEdgeAppearance = appearance
|
||
}
|
||
|
||
/// 为每个业务 Tab 创建独立导航栈,并在中间插入系统扫码槽位。
|
||
private func configureTabs() {
|
||
tabNavigationControllers.removeAll()
|
||
viewControllers = TabSlot.allCases.map { slot in
|
||
if let tab = slot.appTab {
|
||
let navigationController = TabNavigationController(tab: tab, services: services)
|
||
navigationController.tabBarItem = tab.makeTabBarItem()
|
||
tabNavigationControllers[tab] = navigationController
|
||
return navigationController
|
||
}
|
||
let scannerPlaceholder = UIViewController()
|
||
scannerPlaceholder.tabBarItem = MainScanTabItem.makeTabBarItem()
|
||
return scannerPlaceholder
|
||
}
|
||
}
|
||
|
||
/// 绑定角标 ViewModel 回调。
|
||
private func bindBadgeViewModel() {
|
||
badgeViewModel.onChange = { [weak self] in
|
||
self?.updateOrderBadge()
|
||
}
|
||
}
|
||
|
||
/// 绑定账号上下文,景区或门店变化时刷新角标。
|
||
private func bindAccountContext() {
|
||
services.accountContext.onChange = { [weak self] in
|
||
Task { await self?.refreshOrderBadge() }
|
||
}
|
||
}
|
||
|
||
/// 更新订单 Tab 待核销角标。
|
||
private func updateOrderBadge() {
|
||
let badgeValue: String?
|
||
if let count = badgeViewModel.pendingWriteOffCount, count > 0 {
|
||
badgeValue = "\(count)"
|
||
} else {
|
||
badgeValue = nil
|
||
}
|
||
tabNavigationControllers[.orders]?.tabBarItem.badgeValue = badgeValue
|
||
}
|
||
|
||
/// 刷新订单 Tab 待核销角标数据。
|
||
private func refreshOrderBadge() async {
|
||
await badgeViewModel.refreshPendingWriteOffCount(
|
||
api: services.ordersAPI,
|
||
scenicId: services.accountContext.currentScenic?.id,
|
||
storeId: services.accountContext.currentStore?.id
|
||
)
|
||
}
|
||
|
||
/// 从 TabBar 中间按钮打开全局扫码页,成功后路由至订单核销入口。
|
||
private func presentGlobalScanner() {
|
||
guard presentedViewController == nil else { return }
|
||
|
||
let scanner = OrderCodeScannerViewController()
|
||
scanner.onScanResult = { [weak self, weak scanner] result in
|
||
scanner?.dismiss(animated: true)
|
||
guard let self else { return }
|
||
switch result {
|
||
case .success(let code):
|
||
self.services.appNavigator.openOrdersEntry(.verificationOrders, scannedCode: code)
|
||
case .failure(let error):
|
||
self.services.toastCenter.show(error.localizedDescription)
|
||
}
|
||
}
|
||
let navigationController = UINavigationController(rootViewController: scanner)
|
||
navigationController.modalPresentationStyle = .fullScreen
|
||
present(navigationController, animated: true)
|
||
}
|
||
}
|
||
|
||
extension MainTabBarController: UITabBarControllerDelegate {
|
||
/// 用户选择 TabBar 槽位前拦截扫码入口,使关闭扫码页后保留原页面。
|
||
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
|
||
guard viewControllers?.firstIndex(of: viewController) == TabSlot.scanIndex else {
|
||
return true
|
||
}
|
||
|
||
if let onScanTabSelected {
|
||
onScanTabSelected()
|
||
} else {
|
||
presentGlobalScanner()
|
||
}
|
||
return false
|
||
}
|
||
|
||
/// 用户点击 TabBar 时刷新订单角标。
|
||
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
|
||
guard !isSyncingTabSelection,
|
||
let navigationController = viewController as? TabNavigationController else { return }
|
||
|
||
if navigationController.appTab == .orders {
|
||
Task { await refreshOrderBadge() }
|
||
}
|
||
}
|
||
}
|
||
|
||
extension MainTabBarController: AppNavigationHost {
|
||
/// 选中指定主 Tab。
|
||
func selectTab(_ tab: AppTab) {
|
||
guard let index = TabSlot.index(for: tab) else { return }
|
||
guard selectedIndex != index else { return }
|
||
isSyncingTabSelection = true
|
||
selectedIndex = index
|
||
isSyncingTabSelection = false
|
||
if tab == .orders {
|
||
Task { await refreshOrderBadge() }
|
||
}
|
||
}
|
||
|
||
/// 返回指定 Tab 的导航控制器。
|
||
func navigationController(for tab: AppTab) -> TabNavigationController? {
|
||
tabNavigationControllers[tab]
|
||
}
|
||
|
||
/// 返回当前选中 Tab 的导航控制器。
|
||
func selectedNavigationController() -> TabNavigationController? {
|
||
selectedViewController as? TabNavigationController
|
||
}
|
||
|
||
/// 重置全部 Tab 的导航栈。
|
||
func resetAllStacks() {
|
||
for navigationController in tabNavigationControllers.values {
|
||
navigationController.popToRootViewController(animated: false)
|
||
}
|
||
selectTab(.home)
|
||
}
|
||
|
||
/// 将订单入口和可选扫码结果应用到订单根页面。
|
||
func applyOrdersEntry(_ entry: OrdersEntry, scannedCode: String?) {
|
||
guard let ordersController = tabNavigationControllers[.orders]?.viewControllers.first as? OrdersViewController else {
|
||
return
|
||
}
|
||
ordersController.applyOrdersEntry(entry, scannedCode: scannedCode)
|
||
}
|
||
}
|