// // MainTabBarController.swift // suixinkan // import SnapKit import UIKit @MainActor /// 主 Tab 容器,使用自定义底部栏展示四个一级入口和中间扫码按钮。 final class MainTabBarController: UIViewController { private let services: AppServices private let badgeViewModel = MainTabBadgeViewModel() private let contentContainer = UIView() private let customTabBar = CustomMainTabBarView() private var tabNavigationControllers: [AppTab: TabNavigationController] = [:] private var loadedTabs: Set = [.home] init(services: AppServices) { self.services = services super.init(nibName: nil, bundle: nil) } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white configureLayout() bindRouter() bindBadgeViewModel() bindAccountContext() switchToTab(services.appRouter.selectedTab, animated: false) Task { await refreshOrderBadge() } #if DEBUG Task { await AppUITestRouteDriver.applyIfNeeded(services: services) } #endif } /// 控制自定义 TabBar 显隐,push 子页面时隐藏。 func setCustomTabBarHidden(_ hidden: Bool, animated: Bool) { let updates = { self.customTabBar.alpha = hidden ? 0 : 1 self.customTabBar.isUserInteractionEnabled = !hidden } guard animated else { updates() return } UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseInOut], animations: updates) } private func configureLayout() { view.addSubview(contentContainer) view.addSubview(customTabBar) customTabBar.onTabSelected = { [weak self] tab in self?.services.appRouter.select(tab) } customTabBar.onScanTapped = { [weak self] in self?.presentScanner() } contentContainer.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() make.bottom.equalTo(customTabBar.snp.top) } customTabBar.snp.makeConstraints { make in make.leading.trailing.equalToSuperview() make.bottom.equalTo(view.safeAreaLayoutGuide) make.height.equalTo(72) } } private func bindRouter() { services.appRouter.onChange = { [weak self] in self?.handleRouterChange() } } private func bindBadgeViewModel() { badgeViewModel.onChange = { [weak self] in self?.updateOrderBadge() } } private func bindAccountContext() { services.accountContext.onChange = { [weak self] in Task { await self?.refreshOrderBadge() } } } private func handleRouterChange() { switchToTab(services.appRouter.selectedTab, animated: false) if services.appRouter.selectedTab == .orders { Task { await refreshOrderBadge() } } } private func switchToTab(_ tab: AppTab, animated: Bool) { loadedTabs.insert(tab) customTabBar.selectedTab = tab let navigationController = navigationController(for: tab) for child in children where child !== navigationController { child.willMove(toParent: nil) child.view.removeFromSuperview() child.removeFromParent() } addChild(navigationController) contentContainer.addSubview(navigationController.view) navigationController.view.snp.makeConstraints { make in make.edges.equalToSuperview() } navigationController.didMove(toParent: self) let isRoot = navigationController.viewControllers.count <= 1 setCustomTabBarHidden(!isRoot, animated: animated) } private func navigationController(for tab: AppTab) -> TabNavigationController { if let existing = tabNavigationControllers[tab] { return existing } let navigationController = TabNavigationController(tab: tab, services: services) navigationController.tabBarHost = self tabNavigationControllers[tab] = navigationController return navigationController } private func updateOrderBadge() { guard let count = badgeViewModel.pendingWriteOffCount, count > 0 else { customTabBar.orderBadgeText = nil return } customTabBar.orderBadgeText = "\(count)" } private func refreshOrderBadge() async { await badgeViewModel.refreshPendingWriteOffCount( api: services.ordersAPI, scenicId: services.accountContext.currentScenic?.id, storeId: services.accountContext.currentStore?.id ) } private func presentScanner() { let scanner = OrderCodeScannerViewController() scanner.onScanResult = { [weak self] result in guard let self else { return } switch result { case .success(let rawCode): dismiss(animated: true) { self.services.appRouter.routeToOrderVerification(scannedCode: rawCode) } case .failure(let error): showToast(error.localizedDescription) } } let navigation = UINavigationController(rootViewController: scanner) navigation.modalPresentationStyle = .fullScreen present(navigation, animated: true) } } /// 自定义主 TabBar,展示四个一级入口和中间扫码核销按钮。 private final class CustomMainTabBarView: UIView { var selectedTab: AppTab = .home { didSet { refreshSelection() } } var orderBadgeText: String? { didSet { ordersBadgeLabel.text = orderBadgeText; ordersBadgeLabel.isHidden = orderBadgeText == nil } } var onTabSelected: ((AppTab) -> Void)? var onScanTapped: (() -> Void)? private struct TabItem { let tab: AppTab let title: String let selectedImage: String let unselectedImage: String } private let items: [TabItem] = [ .init(tab: .home, title: "首页", selectedImage: "TabHomeSelected", unselectedImage: "TabHomeUnselected"), .init(tab: .orders, title: "订单", selectedImage: "TabOrderSelected", unselectedImage: "TabOrderUnselected"), .init(tab: .statistics, title: "数据", selectedImage: "TabDataSelected", unselectedImage: "TabDataUnselected"), .init(tab: .profile, title: "我的", selectedImage: "TabProfileSelected", unselectedImage: "TabProfileUnselected") ] private var tabButtons: [AppTab: UIButton] = [:] private var tabTitleLabels: [AppTab: UILabel] = [:] private let ordersBadgeLabel = UILabel() private let topDivider = UIView() override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .white configureViews() refreshSelection() } required init?(coder: NSCoder) { nil } private func configureViews() { topDivider.backgroundColor = UIColor.black.withAlphaComponent(0.04) addSubview(topDivider) topDivider.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() make.height.equalTo(0.5) } let leftStack = UIStackView() leftStack.axis = .horizontal leftStack.distribution = .fillEqually leftStack.spacing = 0 let rightStack = UIStackView() rightStack.axis = .horizontal rightStack.distribution = .fillEqually rightStack.spacing = 0 for (index, item) in items.enumerated() { let buttonContainer = makeTabButton(for: item) if index < 2 { leftStack.addArrangedSubview(buttonContainer) } else { rightStack.addArrangedSubview(buttonContainer) } } let scanButton = UIButton(type: .system) scanButton.backgroundColor = AppDesign.primary scanButton.layer.cornerRadius = 29 scanButton.tintColor = .white scanButton.setImage( UIImage(systemName: "qrcode.viewfinder")?.withConfiguration( UIImage.SymbolConfiguration(pointSize: 30, weight: .bold) ), for: .normal ) scanButton.accessibilityLabel = "扫码核销" scanButton.accessibilityIdentifier = "main.scan" scanButton.addTarget(self, action: #selector(scanTapped), for: .touchUpInside) let row = UIStackView(arrangedSubviews: [leftStack, scanButton, rightStack]) row.axis = .horizontal row.alignment = .center row.spacing = 0 addSubview(row) scanButton.snp.makeConstraints { make in make.width.equalTo(74) make.height.equalTo(64) } row.snp.makeConstraints { make in make.leading.trailing.equalToSuperview().inset(22) make.top.bottom.equalToSuperview() } leftStack.snp.makeConstraints { make in make.width.equalTo(rightStack) } } private func makeTabButton(for item: TabItem) -> UIView { let container = UIView() let button = UIButton(type: .custom) button.accessibilityLabel = item.title button.accessibilityIdentifier = "main.tab.\(item.tab.rawValue)" button.tag = items.firstIndex(where: { $0.tab == item.tab }) ?? 0 button.addTarget(self, action: #selector(tabTapped(_:)), for: .touchUpInside) tabButtons[item.tab] = button let titleLabel = UILabel() titleLabel.text = item.title titleLabel.font = .systemFont(ofSize: 12) titleLabel.textAlignment = .center tabTitleLabels[item.tab] = titleLabel container.addSubview(button) container.addSubview(titleLabel) button.snp.makeConstraints { make in make.top.equalToSuperview().offset(8) make.centerX.equalToSuperview() make.width.height.equalTo(24) } titleLabel.snp.makeConstraints { make in make.top.equalTo(button.snp.bottom).offset(3) make.centerX.equalToSuperview() make.bottom.lessThanOrEqualToSuperview() } if item.tab == .orders { ordersBadgeLabel.font = .systemFont(ofSize: 9, weight: .bold) ordersBadgeLabel.textColor = .white ordersBadgeLabel.backgroundColor = UIColor(hex: 0xEF4444) ordersBadgeLabel.textAlignment = .center ordersBadgeLabel.layer.cornerRadius = 7.5 ordersBadgeLabel.clipsToBounds = true ordersBadgeLabel.isHidden = true container.addSubview(ordersBadgeLabel) ordersBadgeLabel.snp.makeConstraints { make in make.top.equalTo(button).offset(-4) make.leading.equalTo(button.snp.trailing).offset(-6) make.height.equalTo(15) make.width.greaterThanOrEqualTo(15) } } return container } private func refreshSelection() { for item in items { let isSelected = item.tab == selectedTab let imageName = isSelected ? item.selectedImage : item.unselectedImage tabButtons[item.tab]?.setImage(UIImage(named: imageName), for: .normal) tabTitleLabels[item.tab]?.textColor = isSelected ? AppDesign.primary : UIColor(hex: 0x7D8DA3) tabTitleLabels[item.tab]?.font = .systemFont(ofSize: 12, weight: isSelected ? .medium : .regular) } } @objc private func tabTapped(_ sender: UIButton) { guard sender.tag >= 0, sender.tag < items.count else { return } onTabSelected?(items[sender.tag].tab) } @objc private func scanTapped() { onScanTapped?() } }