Files
汉秋 7469f92177 Replace CYLTabBarController with native UITabBarController for main tabs.
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>
2026-06-26 19:17:38 +08:00

242 lines
8.5 KiB
Swift
Raw Permalink 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.

//
// 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)
}
}