352 lines
12 KiB
Swift
352 lines
12 KiB
Swift
//
|
||
// 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?()
|
||
}
|
||
}
|