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:
2026-06-26 15:16:12 +08:00
parent 9edf993432
commit d99a5b1bf8
124 changed files with 5195 additions and 1536 deletions

View File

@ -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?()
}
}