Files
suixinkan_uikit/suixinkan/UI/MainTab/MainTabBarController.swift

248 lines
9.0 KiB
Swift

//
// MainTabBarController.swift
// suixinkan
//
import UIKit
/// 主 Tab 容器,展示四个一级入口及中间扫码按钮。
final class MainTabBarController: UITabBarController {
private let badgeViewModel = MainTabBadgeViewModel()
private var tabNavigationControllers: [AppTab: TabNavigationController] = [:]
private var isSyncingTabSelection = false
private let scanHandler = MainTabScanHandler(api: NetworkServices.shared.orderAPI)
/// 测试时可注入扫码点击处理。
var onScanTabSelected: (() -> Void)?
private enum TabSlot: CaseIterable {
case home
case orders
case scan
case statistics
case profile
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
}
}
static func index(for tab: AppTab) -> Int? {
allCases.firstIndex { $0.appTab == tab }
}
static var scanIndex: Int? {
allCases.firstIndex(of: .scan)
}
}
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
configureTabBarAppearance()
configureTabs()
bindBadgeViewModel()
bindScanHandler()
badgeViewModel.refreshPendingWriteOffCount()
}
/// 选中指定主 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 {
badgeViewModel.refreshPendingWriteOffCount()
}
}
/// 响应推送点击,切换到目标 Tab 并打开对应业务页面。
func openPushDestination(_ destination: PushDestination) {
loadViewIfNeeded()
let targetTab: AppTab = destination == .orders ? .orders : .home
let wasAlreadyOnOrders = targetTab == .orders && selectedViewController === tabNavigationControllers[.orders]
selectTab(targetTab)
guard let navigationController = tabNavigationControllers[targetTab] else { return }
_ = navigationController.popToRootViewController(animated: false)
let controller: UIViewController?
switch destination {
case .orders:
(navigationController.viewControllers.first as? OrdersViewController)?.openFromOrderNotification()
if wasAlreadyOnOrders { badgeViewModel.refreshPendingWriteOffCount() }
controller = nil
case .paymentDetails:
controller = PaymentCollectionDetailsViewController()
case .messageCenter:
controller = MessageCenterViewController()
case .aiRetouchTaskList:
controller = TravelAlbumAIJobListViewController()
case .aiRetouchTaskDetail(let batchId):
controller = TravelAlbumAIJobDetailViewController(batchId: batchId)
}
if let controller {
navigationController.pushViewController(controller, animated: false)
}
}
private func configureTabBarAppearance() {
tabBar.tintColor = AppColor.primary
tabBar.unselectedItemTintColor = AppColor.textTabInactive
tabBar.backgroundColor = AppColor.cardBackground
tabBar.shadowImage = UIImage()
let normalAttributes: [NSAttributedString.Key: Any] = [
.foregroundColor: AppColor.textTabInactive,
.font: UIFont.app(.caption),
]
let selectedAttributes: [NSAttributedString.Key: Any] = [
.foregroundColor: AppColor.primary,
.font: UIFont.app(.captionMedium),
]
let appearance = UITabBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = AppColor.cardBackground
appearance.shadowColor = AppColor.tabBarShadow
appearance.stackedLayoutAppearance.normal.titleTextAttributes = normalAttributes
appearance.stackedLayoutAppearance.selected.titleTextAttributes = selectedAttributes
tabBar.standardAppearance = appearance
tabBar.scrollEdgeAppearance = appearance
}
private func configureTabs() {
tabNavigationControllers.removeAll()
viewControllers = TabSlot.allCases.map { slot in
if let tab = slot.appTab {
let navigationController = TabNavigationController(tab: tab)
navigationController.tabBarItem = tab.makeTabBarItem()
tabNavigationControllers[tab] = navigationController
return navigationController
}
let scannerPlaceholder = UIViewController()
scannerPlaceholder.tabBarItem = MainScanTabItem.makeTabBarItem()
return scannerPlaceholder
}
}
private func bindBadgeViewModel() {
badgeViewModel.onChange = { [weak self] in
self?.updateOrderBadge()
}
}
private func updateOrderBadge() {
let badgeValue: String?
if let count = badgeViewModel.pendingWriteOffCount, count > 0 {
badgeValue = "\(count)"
} else {
badgeValue = nil
}
tabNavigationControllers[.orders]?.tabBarItem.badgeValue = badgeValue
}
private func bindScanHandler() {
scanHandler.onShowMessage = { [weak self] message in
self?.showScanToast(message)
}
scanHandler.onShowSignIn = { [weak self] response in
guard let self else { return }
OrderSignInAlertController.present(from: self, response: response)
}
scanHandler.onShowWriteOff = { [weak self] response in
guard let self else { return }
WriteOffResultAlertController.present(from: self, response: response)
}
scanHandler.onShowMultiVerifySheet = { [weak self] spots, selected in
self?.presentMultiVerifySheet(spots: spots, selected: selected)
}
scanHandler.onNavigateBindAcquirer = { [weak self] saleUserId in
self?.presentBindAcquirerAfterScan(saleUserId: saleUserId)
}
}
private func presentBindAcquirerAfterScan(saleUserId: Int) {
dismiss(animated: true) { [weak self] in
guard let self else { return }
let controller = BindAcquirerViewController(saleUserId: saleUserId)
self.topNavigationController()?.pushViewController(controller, animated: true)
}
}
private func topNavigationController() -> UINavigationController? {
if let selected = selectedViewController as? UINavigationController {
return selected
}
return selectedViewController?.navigationController
}
private func showScanToast(_ message: String) {
let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert)
present(alert, animated: true)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
alert.dismiss(animated: true)
}
}
private func presentMultiVerifySheet(spots: [MultiTravelScenicSpotEntity], selected: MultiTravelScenicSpotEntity?) {
let controller = MultiVerifyScenicSpotViewController(spots: spots, selected: selected)
controller.onConfirm = { [weak self] spot in
guard let self, let pending = self.scanHandler.pendingMultiVerify else { return }
Task { await self.scanHandler.confirmMultiVerify(pending: pending, spot: spot) }
}
let nav = UINavigationController(rootViewController: controller)
nav.modalPresentationStyle = .pageSheet
present(nav, animated: true)
}
private func presentGlobalScanner() {
guard presentedViewController == nil else { return }
let scanner = QRCodeScannerViewController()
scanner.onScanResult = { [weak self] result in
guard let self else { return }
Task { await self.scanHandler.handleScanResult(result) }
}
let navigationController = UINavigationController(rootViewController: scanner)
navigationController.modalPresentationStyle = .fullScreen
present(navigationController, animated: true)
}
}
extension MainTabBarController: UITabBarControllerDelegate {
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
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
guard !isSyncingTabSelection,
let navigationController = viewController as? TabNavigationController else { return }
if navigationController.appTab == .orders {
badgeViewModel.refreshPendingWriteOffCount()
}
}
}