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:
@ -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
|
||||
|
||||
@ -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 {
|
||||
/// text字段ShouldReturn相关逻辑。
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user