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

@ -6,8 +6,18 @@
import SnapKit
import UIKit
// MARK: - Diffable
private typealias AccountSwitchSection = Int
private typealias AccountSwitchItem = String
///
private enum AccountSwitchItemID {
static let empty = "accountSwitch:empty"
}
///
final class AccountSwitchViewController: UIViewController {
final class AccountSwitchViewController: UIViewController, UITableViewDelegate {
private let viewModel = AccountSwitchViewModel()
@ -15,7 +25,6 @@ final class AccountSwitchViewController: UIViewController {
let table = UITableView(frame: .zero, style: .plain)
table.backgroundColor = UIColor(hex: 0xF5F7FB)
table.separatorStyle = .none
table.dataSource = self
table.delegate = self
table.register(AccountSwitchCell.self, forCellReuseIdentifier: AccountSwitchCell.reuseID)
return table
@ -27,11 +36,16 @@ final class AccountSwitchViewController: UIViewController {
return button
}()
/// Diffable
private var tableDataSource: UITableViewDiffableDataSource<AccountSwitchSection, AccountSwitchItem>!
/// UI
override func viewDidLoad() {
super.viewDidLoad()
title = "账号切换"
view.backgroundColor = UIColor(hex: 0xF5F7FB)
configureTableDataSource()
view.addSubview(tableView)
view.addSubview(confirmButton)
tableView.snp.makeConstraints { make in
@ -45,18 +59,75 @@ final class AccountSwitchViewController: UIViewController {
}
viewModel.onChange = { [weak self] in
self?.tableView.reloadData()
self?.applyTableSnapshot(reconfigure: true)
self?.updateConfirmButton()
}
Task { await loadAccounts() }
}
/// Diffable
private func configureTableDataSource() {
tableDataSource = UITableViewDiffableDataSource<AccountSwitchSection, AccountSwitchItem>(
tableView: tableView
) { [weak self] (tableView: UITableView, indexPath: IndexPath, item: AccountSwitchItem) -> UITableViewCell? in
guard let self else { return UITableViewCell() }
if item == AccountSwitchItemID.empty {
let cell = UITableViewCell()
cell.selectionStyle = .none
cell.backgroundColor = .clear
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
let empty = makeEmptyStateView(
title: "暂无可切换账号",
message: "当前登录账号下没有其他可切换账号。",
systemImage: "person.crop.circle.badge.exclamationmark"
)
cell.contentView.addSubview(empty)
empty.snp.makeConstraints { make in make.edges.equalToSuperview(); make.height.equalTo(280) }
return cell
}
guard let account = self.viewModel.accounts.first(where: { $0.id == item }) else {
return UITableViewCell()
}
let cell = tableView.dequeueReusableCell(withIdentifier: AccountSwitchCell.reuseID, for: indexPath) as! AccountSwitchCell
cell.configure(
account: account,
selected: self.viewModel.selectedAccountId == account.id,
isCurrent: account.isCurrent || self.isCurrentAccount(account)
)
return cell
}
applyTableSnapshot(animated: false)
}
/// Diffable snapshot
private func buildTableSnapshot() -> NSDiffableDataSourceSnapshot<AccountSwitchSection, AccountSwitchItem> {
var snapshot = NSDiffableDataSourceSnapshot<AccountSwitchSection, AccountSwitchItem>()
snapshot.appendSections([0])
if viewModel.accounts.isEmpty, !viewModel.loading {
snapshot.appendItems([AccountSwitchItemID.empty], toSection: 0)
} else {
snapshot.appendItems(viewModel.accounts.map(\.id), toSection: 0)
}
return snapshot
}
/// snapshot
private func applyTableSnapshot(animated: Bool = true, reconfigure: Bool = false) {
var snapshot = buildTableSnapshot()
if reconfigure, !snapshot.itemIdentifiers.isEmpty {
snapshot.reconfigureItems(snapshot.itemIdentifiers)
}
tableDataSource.apply(snapshot, animatingDifferences: animated)
}
/// ConfirmButton
private func updateConfirmButton() {
let enabled = viewModel.selectedAccount != nil && !viewModel.loading && !viewModel.switching
confirmButton.isEnabled = enabled
confirmButton.alpha = enabled ? 1 : 0.5
}
/// Accounts
private func loadAccounts(force: Bool = false) async {
do {
try await appServices.globalLoading.withOptionalLoading(!force && viewModel.accounts.isEmpty, message: "加载中...") {
@ -82,10 +153,12 @@ final class AccountSwitchViewController: UIViewController {
return current.userId.isEmpty ? nil : current.userId
}
/// isCurrentAccount
private func isCurrentAccount(_ account: AccountSwitchAccount) -> Bool {
viewModel.isCurrent(account, currentAccountId: currentAccountId)
}
/// confirm
@objc private func confirmTapped() {
guard let account = viewModel.selectedAccount else { return }
if account.isCurrent || isCurrentAccount(account) {
@ -95,6 +168,7 @@ final class AccountSwitchViewController: UIViewController {
Task { await switchAccount(account) }
}
/// switch
private func switchAccount(_ account: AccountSwitchAccount) async {
do {
let response = try await appServices.globalLoading.withLoading(message: "切换中...") {
@ -121,53 +195,29 @@ final class AccountSwitchViewController: UIViewController {
}
}
/// non
private func nonEmpty(_ value: String?) -> String? {
let text = value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return text.isEmpty ? nil : text
}
}
extension AccountSwitchViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
viewModel.accounts.isEmpty && !viewModel.loading ? 1 : viewModel.accounts.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard !viewModel.accounts.isEmpty else {
let cell = UITableViewCell()
cell.selectionStyle = .none
cell.backgroundColor = .clear
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
let empty = makeEmptyStateView(
title: "暂无可切换账号",
message: "当前登录账号下没有其他可切换账号。",
systemImage: "person.crop.circle.badge.exclamationmark"
)
cell.contentView.addSubview(empty)
empty.snp.makeConstraints { make in make.edges.equalToSuperview(); make.height.equalTo(280) }
return cell
}
let cell = tableView.dequeueReusableCell(withIdentifier: AccountSwitchCell.reuseID, for: indexPath) as! AccountSwitchCell
let account = viewModel.accounts[indexPath.row]
cell.configure(
account: account,
selected: viewModel.selectedAccountId == account.id,
isCurrent: account.isCurrent || isCurrentAccount(account)
)
return cell
}
/// UITableView
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard indexPath.row < viewModel.accounts.count else { return }
viewModel.select(viewModel.accounts[indexPath.row])
guard let item = tableDataSource.itemIdentifier(for: indexPath),
item != AccountSwitchItemID.empty,
let account = viewModel.accounts.first(where: { $0.id == item }) else { return }
viewModel.select(account)
}
/// UITableView
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
viewModel.accounts.isEmpty ? 280 : 92
guard let item = tableDataSource.itemIdentifier(for: indexPath) else { return 92 }
return item == AccountSwitchItemID.empty ? 280 : 92
}
}
/// AccountSwitch Cell
private final class AccountSwitchCell: UITableViewCell {
static let reuseID = "AccountSwitchCell"
@ -178,6 +228,7 @@ private final class AccountSwitchCell: UITableViewCell {
private let tagLabel = UILabel()
private let checkView = UIImageView()
///
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
@ -245,6 +296,7 @@ private final class AccountSwitchCell: UITableViewCell {
@available(*, unavailable)
required init?(coder: NSCoder) { fatalError() }
///
func configure(account: AccountSwitchAccount, selected: Bool, isCurrent: Bool) {
let title = account.title.trimmingCharacters(in: .whitespacesAndNewlines)
titleLabel.text = title.isEmpty ? account.accountTypeLabel : title