Advance UIKit rewrite with AMap integration and core UI modules.
Integrate高德 SDK with simulator-safe build flags, add map views for operating area and punch points, refactor main tabs and key feature screens to UIKit with Diffable lists, and document Swift concurrency defaults in AGENTS.md. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -3,24 +3,22 @@
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
/// 主 Tab 容器,使用自定义底部栏展示四个一级入口和中间扫码按钮。
|
||||
final class MainTabBarController: UIViewController {
|
||||
/// 主 Tab 容器,使用系统 `UITabBarController` 展示四个一级入口。
|
||||
final class MainTabBarController: UITabBarController, UITabBarControllerDelegate {
|
||||
|
||||
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]
|
||||
private var isSyncingTabSelection = false
|
||||
|
||||
/// 初始化实例。
|
||||
init(services: AppServices) {
|
||||
self.services = services
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
delegate = self
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
@ -28,124 +26,88 @@ final class MainTabBarController: UIViewController {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
/// 视图加载完成后的 UI 初始化与数据绑定。
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .white
|
||||
configureLayout()
|
||||
configureTabBarAppearance()
|
||||
configureTabs()
|
||||
bindRouter()
|
||||
bindBadgeViewModel()
|
||||
bindAccountContext()
|
||||
switchToTab(services.appRouter.selectedTab, animated: false)
|
||||
syncSelectedTabFromRouter(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)
|
||||
/// 配置系统 TabBar 外观。
|
||||
private func configureTabBarAppearance() {
|
||||
tabBar.tintColor = AppDesign.primary
|
||||
tabBar.backgroundColor = .white
|
||||
}
|
||||
|
||||
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)
|
||||
/// 为每个 Tab 创建独立导航栈并绑定 TabBarItem。
|
||||
private func configureTabs() {
|
||||
viewControllers = AppTab.allCases.map { tab in
|
||||
let navigationController = TabNavigationController(tab: tab, services: services)
|
||||
navigationController.tabBarItem = tab.makeTabBarItem()
|
||||
tabNavigationControllers[tab] = navigationController
|
||||
return navigationController
|
||||
}
|
||||
}
|
||||
|
||||
/// 绑定 Router 回调,同步程序化 Tab 切换。
|
||||
private func bindRouter() {
|
||||
services.appRouter.onChange = { [weak self] in
|
||||
self?.handleRouterChange()
|
||||
}
|
||||
}
|
||||
|
||||
/// 绑定角标 ViewModel 回调。
|
||||
private func bindBadgeViewModel() {
|
||||
badgeViewModel.onChange = { [weak self] in
|
||||
self?.updateOrderBadge()
|
||||
}
|
||||
}
|
||||
|
||||
/// 绑定账号上下文,景区或门店变化时刷新角标。
|
||||
private func bindAccountContext() {
|
||||
services.accountContext.onChange = { [weak self] in
|
||||
Task { await self?.refreshOrderBadge() }
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理 Router 变更,保持选中 Tab 与 `AppRouter` 一致。
|
||||
private func handleRouterChange() {
|
||||
switchToTab(services.appRouter.selectedTab, animated: false)
|
||||
syncSelectedTabFromRouter(animated: false)
|
||||
if services.appRouter.selectedTab == .orders {
|
||||
Task { await refreshOrderBadge() }
|
||||
}
|
||||
}
|
||||
|
||||
private func switchToTab(_ tab: AppTab, animated: Bool) {
|
||||
loadedTabs.insert(tab)
|
||||
customTabBar.selectedTab = tab
|
||||
/// 将 `AppRouter.selectedTab` 同步到系统 TabBar 选中项。
|
||||
private func syncSelectedTabFromRouter(animated: Bool) {
|
||||
guard let index = AppTab.allCases.firstIndex(of: services.appRouter.selectedTab) else { return }
|
||||
guard selectedIndex != index else { return }
|
||||
|
||||
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
|
||||
isSyncingTabSelection = true
|
||||
selectedIndex = index
|
||||
isSyncingTabSelection = false
|
||||
}
|
||||
|
||||
/// 更新订单 Tab 待核销角标。
|
||||
private func updateOrderBadge() {
|
||||
guard let count = badgeViewModel.pendingWriteOffCount, count > 0 else {
|
||||
customTabBar.orderBadgeText = nil
|
||||
return
|
||||
let badgeValue: String?
|
||||
if let count = badgeViewModel.pendingWriteOffCount, count > 0 {
|
||||
badgeValue = "\(count)"
|
||||
} else {
|
||||
badgeValue = nil
|
||||
}
|
||||
customTabBar.orderBadgeText = "\(count)"
|
||||
tabNavigationControllers[.orders]?.tabBarItem.badgeValue = badgeValue
|
||||
}
|
||||
|
||||
/// 刷新订单 Tab 待核销角标数据。
|
||||
private func refreshOrderBadge() async {
|
||||
await badgeViewModel.refreshPendingWriteOffCount(
|
||||
api: services.ordersAPI,
|
||||
@ -154,198 +116,14 @@ final class MainTabBarController: UIViewController {
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
/// 用户点击 TabBar 时同步选中状态到 `AppRouter`。
|
||||
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
|
||||
guard !isSyncingTabSelection,
|
||||
let navigationController = viewController as? TabNavigationController else { return }
|
||||
|
||||
services.appRouter.select(navigationController.appTab)
|
||||
if navigationController.appTab == .orders {
|
||||
Task { await refreshOrderBadge() }
|
||||
}
|
||||
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?()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user