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,9 +6,14 @@
import SnapKit
import UIKit
// MARK: - Diffable
private typealias AccountSelectionSection = Int
private typealias AccountSelectionItem = String
@MainActor
/// /
final class AccountSelectionViewController: UIViewController {
final class AccountSelectionViewController: UIViewController, UITableViewDelegate {
private let payload: AccountSelectionPayload
private var isLoading: Bool
@ -17,9 +22,13 @@ final class AccountSelectionViewController: UIViewController {
private var selectedAccountId: String?
private let tableView = UITableView(frame: .zero, style: .plain)
/// Diffable
private var tableDataSource: UITableViewDiffableDataSource<AccountSelectionSection, AccountSelectionItem>!
private let confirmButton = UIButton(type: .system)
private let bottomBar = UIView()
///
init(
payload: AccountSelectionPayload,
isLoading: Bool,
@ -38,6 +47,7 @@ final class AccountSelectionViewController: UIViewController {
fatalError("init(coder:) has not been implemented")
}
/// UI
override func viewDidLoad() {
super.viewDidLoad()
title = "选择账号"
@ -47,9 +57,47 @@ final class AccountSelectionViewController: UIViewController {
selectedAccountId = payload.accounts.first?.id
configureTableView()
configureTableDataSource()
configureBottomBar()
}
/// Diffable
private func configureTableDataSource() {
tableDataSource = UITableViewDiffableDataSource<AccountSelectionSection, AccountSelectionItem>(
tableView: tableView
) { [weak self] (tableView: UITableView, indexPath: IndexPath, item: AccountSelectionItem) -> UITableViewCell? in
guard let self,
let cell = tableView.dequeueReusableCell(
withIdentifier: AccountSelectionCell.reuseIdentifier,
for: indexPath
) as? AccountSelectionCell,
let account = self.payload.accounts.first(where: { $0.id == item }) else {
return UITableViewCell()
}
cell.configure(account: account, selected: account.id == self.selectedAccountId)
return cell
}
applyTableSnapshot(animated: false)
}
/// Diffable snapshot
private func buildTableSnapshot() -> NSDiffableDataSourceSnapshot<AccountSelectionSection, AccountSelectionItem> {
var snapshot = NSDiffableDataSourceSnapshot<AccountSelectionSection, AccountSelectionItem>()
snapshot.appendSections([0])
snapshot.appendItems(payload.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)
}
/// Loading
func updateLoading(_ loading: Bool) {
isLoading = loading
isModalInPresentation = loading
@ -65,10 +113,10 @@ final class AccountSelectionViewController: UIViewController {
selectedAccount != nil && !isLoading
}
/// TableView
private func configureTableView() {
tableView.backgroundColor = .clear
tableView.separatorStyle = .none
tableView.dataSource = self
tableView.delegate = self
tableView.register(AccountSelectionCell.self, forCellReuseIdentifier: AccountSelectionCell.reuseIdentifier)
view.addSubview(tableView)
@ -78,6 +126,7 @@ final class AccountSelectionViewController: UIViewController {
}
}
/// BottomBar
private func configureBottomBar() {
bottomBar.backgroundColor = .white
@ -112,37 +161,26 @@ final class AccountSelectionViewController: UIViewController {
}
}
/// cancel
@objc private func cancelTapped() {
onCancel()
dismiss(animated: true)
}
/// confirm
@objc private func confirmTapped() {
guard let selectedAccount else { return }
onConfirm(selectedAccount)
}
}
extension AccountSelectionViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
payload.accounts.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(
withIdentifier: AccountSelectionCell.reuseIdentifier,
for: indexPath
) as? AccountSelectionCell else {
return UITableViewCell()
}
let account = payload.accounts[indexPath.row]
cell.configure(account: account, selected: account.id == selectedAccountId)
return cell
}
extension AccountSelectionViewController {
/// UITableView
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selectedAccountId = payload.accounts[indexPath.row].id
tableView.reloadData()
tableView.deselectRow(at: indexPath, animated: true)
guard let item = tableDataSource.itemIdentifier(for: indexPath) else { return }
selectedAccountId = item
applyTableSnapshot(animated: false, reconfigure: true)
confirmButton.isEnabled = canConfirm
confirmButton.backgroundColor = canConfirm ? AppDesign.primary : UIColor(hex: 0xC9CED6)
}
@ -161,6 +199,7 @@ private final class AccountSelectionCell: UITableViewCell {
private let currentTag = UILabel()
private let checkmark = UIImageView()
///
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
@ -257,6 +296,7 @@ private final class AccountSelectionCell: UITableViewCell {
nil
}
///
func configure(account: AccountSwitchAccount, selected: Bool) {
titleLabel.text = account.title.isEmpty ? account.accountTypeLabel : account.title
subtitleLabel.text = account.subtitle

View File

@ -26,6 +26,7 @@ final class LoginViewController: UIViewController {
private let privacyPolicyButton = UIButton(type: .system)
private let loginButton = UIButton(type: .system)
///
init(services: AppServices) {
self.services = services
super.init(nibName: nil, bundle: nil)
@ -36,6 +37,7 @@ final class LoginViewController: UIViewController {
fatalError("init(coder:) has not been implemented")
}
/// UI
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(hex: 0x0B1220)
@ -44,6 +46,7 @@ final class LoginViewController: UIViewController {
viewModel.applyPreferences(services.authSessionCoordinator.loginPreferences())
}
/// Views
private func configureViews() {
backgroundImageView.contentMode = .scaleAspectFill
backgroundImageView.clipsToBounds = true
@ -161,6 +164,7 @@ final class LoginViewController: UIViewController {
view.addGestureRecognizer(tap)
}
/// AgreementText
private func configureAgreementText() {
privacyLabel.numberOfLines = 0
privacyLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline)
@ -197,6 +201,7 @@ final class LoginViewController: UIViewController {
}
}
/// ViewModel
private func bindViewModel() {
viewModel.onChange = { [weak self] in
self?.renderViewModel()
@ -204,6 +209,7 @@ final class LoginViewController: UIViewController {
renderViewModel()
}
/// render
private func renderViewModel() {
if usernameField.textField.text != viewModel.username {
usernameField.textField.text = viewModel.username
@ -241,38 +247,46 @@ final class LoginViewController: UIViewController {
private weak var accountSelectionController: AccountSelectionViewController?
/// username
@objc private func usernameChanged() {
viewModel.username = usernameField.textField.text ?? ""
viewModel.normalizeUsernameCountryCodeIfNeeded()
services.toastCenter.dismiss()
}
/// password
@objc private func passwordChanged() {
viewModel.password = passwordField.textField.text ?? ""
services.toastCenter.dismiss()
}
/// togglePrivacy
@objc private func togglePrivacy() {
viewModel.privacyChecked.toggle()
}
/// openUserAgreement
@objc private func openUserAgreement() {
showToast("用户协议页面待接入")
}
/// openPrivacyPolicy
@objc private func openPrivacyPolicy() {
showToast("隐私政策页面待接入")
}
/// login
@objc private func loginTapped() {
view.endEditing(true)
performLogin()
}
/// dismiss
@objc private func dismissKeyboard() {
view.endEditing(true)
}
/// perform
private func performLogin() {
if let validationError = viewModel.validateForLogin() {
if validationError == .privacyUnchecked {
@ -307,6 +321,7 @@ final class LoginViewController: UIViewController {
}
}
/// AgreementSheet
private func presentAgreementSheet() {
let controller = LoginAgreementConsentViewController(
onOpenAgreement: { [weak self] title in
@ -324,6 +339,7 @@ final class LoginViewController: UIViewController {
present(controller, animated: true)
}
/// AccountSelection
private func presentAccountSelection(_ payload: AccountSelectionPayload) {
let controller = AccountSelectionViewController(
payload: payload,
@ -342,6 +358,7 @@ final class LoginViewController: UIViewController {
present(navigation, animated: true)
}
/// select
private func selectAccount(_ account: AccountSwitchAccount) {
Task {
do {
@ -359,6 +376,7 @@ final class LoginViewController: UIViewController {
}
}
/// complete
private func completeLogin(with response: V9AuthResponse) async {
do {
try await services.authSessionCoordinator.completeLogin(
@ -378,6 +396,7 @@ final class LoginViewController: UIViewController {
}
extension LoginViewController: UITextFieldDelegate {
/// textShouldReturn
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if textField === usernameField.textField {
passwordField.textField.becomeFirstResponder()
@ -396,6 +415,7 @@ private final class LoginInputField: UIView {
private let toggleButton = UIButton(type: .custom)
private var isSecure = false
///
init(iconName: String, placeholder: String, isSecure: Bool) {
self.isSecure = isSecure
super.init(frame: .zero)
@ -450,12 +470,14 @@ private final class LoginInputField: UIView {
nil
}
/// setSecureEntry
func setSecureEntry(_ secure: Bool) {
textField.isSecureTextEntry = secure
let imageName = secure ? "LoginPwdInvisible" : "LoginPwdVisible"
toggleButton.setImage(UIImage(named: imageName), for: .normal)
}
/// toggleVisibility
@objc private func toggleVisibility() {
onToggleVisibility?()
}
@ -467,6 +489,7 @@ private final class LoginAgreementConsentViewController: UIViewController {
private let onOpenAgreement: (String) -> Void
private let onAgreeAndContinue: () -> Void
///
init(onOpenAgreement: @escaping (String) -> Void, onAgreeAndContinue: @escaping () -> Void) {
self.onOpenAgreement = onOpenAgreement
self.onAgreeAndContinue = onAgreeAndContinue
@ -477,6 +500,7 @@ private final class LoginAgreementConsentViewController: UIViewController {
nil
}
/// UI
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
@ -524,6 +548,7 @@ private final class LoginAgreementConsentViewController: UIViewController {
}
}
/// continue
@objc private func continueTapped() {
dismiss(animated: true) { [onAgreeAndContinue] in
onAgreeAndContinue()