Files
suixinkan_ios_uikit/suixinkan_ios/Features/Main/ViewControllers/MainTabBarController.swift
2026-06-26 14:33:31 +08:00

352 lines
12 KiB
Swift
Raw 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 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<AppTab> = [.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?()
}
}