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

@ -0,0 +1,173 @@
//
// ListDiffableSupport.swift
// suixinkan
//
// Diffable Data Source
//
import SnapKit
import UIKit
// MARK: - Module Table / section
/// section 使 section
typealias ModuleTableSection = Int
/// `section-row`
typealias ModuleTableRow = String
///
func moduleTableRowID(section: Int, row: Int) -> ModuleTableRow {
"\(section)-\(row)"
}
/// indexPath
func moduleTableIndexPath(from rowID: ModuleTableRow) -> IndexPath? {
let parts = rowID.split(separator: "-", omittingEmptySubsequences: false)
guard parts.count == 2, let section = Int(parts[0]), let row = Int(parts[1]) else { return nil }
return IndexPath(row: row, section: section)
}
// MARK: - Simple Table
/// section
typealias SimpleTableSection = Int
///
typealias SimpleTableRow = String
// MARK: - Collection Layout
/// UICollectionView section Compositional Layout
enum CollectionDiffableLayout {
/// section estimated absolute
static func fullWidthSection(estimatedHeight: CGFloat = 100) -> NSCollectionLayoutSection {
let item = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .estimated(estimatedHeight)
)
)
let group = NSCollectionLayoutGroup.vertical(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .estimated(estimatedHeight)
),
subitems: [item]
)
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(
top: AppMetrics.Spacing.xxSmall,
leading: AppMetrics.Spacing.pageHorizontal,
bottom: AppMetrics.Spacing.xxSmall,
trailing: AppMetrics.Spacing.pageHorizontal
)
return section
}
/// section
static func fullWidthSection(height: CGFloat) -> NSCollectionLayoutSection {
let item = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .absolute(height)
)
)
let group = NSCollectionLayoutGroup.vertical(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .absolute(height)
),
subitems: [item]
)
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(
top: AppMetrics.Spacing.xxSmall,
leading: AppMetrics.Spacing.pageHorizontal,
bottom: AppMetrics.Spacing.xxSmall,
trailing: AppMetrics.Spacing.pageHorizontal
)
return section
}
/// section /
static func gridSection(
columns: Int = 3,
itemHeight: CGFloat = 102,
interItemSpacing: CGFloat = 15,
lineSpacing: CGFloat = 15,
contentInsets: NSDirectionalEdgeInsets? = nil
) -> NSCollectionLayoutSection {
let item = NSCollectionLayoutItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0 / CGFloat(columns)),
heightDimension: .absolute(itemHeight)
)
)
let group = NSCollectionLayoutGroup.horizontal(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .absolute(itemHeight)
),
subitem: item,
count: columns
)
group.interItemSpacing = .fixed(interItemSpacing)
let section = NSCollectionLayoutSection(group: group)
section.interGroupSpacing = lineSpacing
section.contentInsets = contentInsets ?? NSDirectionalEdgeInsets(
top: AppMetrics.Spacing.xxSmall,
leading: AppMetrics.Spacing.pageHorizontal,
bottom: AppMetrics.Spacing.xxSmall,
trailing: AppMetrics.Spacing.pageHorizontal
)
return section
}
/// section
static func addHeader(
to section: NSCollectionLayoutSection,
height: CGFloat = 36
) -> NSCollectionLayoutSection {
let header = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1),
heightDimension: .absolute(height)
),
elementKind: UICollectionView.elementKindSectionHeader,
alignment: .top
)
section.boundarySupplementaryItems = [header]
return section
}
}
/// section
final class CollectionSectionHeaderView: UICollectionReusableView {
static let reuseID = "CollectionSectionHeaderView"
private let titleLabel = UILabel()
///
override init(frame: CGRect) {
super.init(frame: frame)
titleLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline, weight: .semibold)
titleLabel.textColor = AppDesignUIKit.textSecondary
addSubview(titleLabel)
titleLabel.snp.makeConstraints { make in
make.leading.equalToSuperview().inset(AppMetrics.Spacing.pageHorizontal)
make.trailing.equalToSuperview().inset(AppMetrics.Spacing.pageHorizontal)
make.bottom.equalToSuperview().inset(4)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// section
func configure(title: String) {
titleLabel.text = title
}
}

View File

@ -12,6 +12,7 @@ import UIKit
final class TitleSubtitleTableViewCell: UITableViewCell {
static let reuseIdentifier = "TitleSubtitleTableViewCell"
///
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
selectionStyle = .default
@ -28,6 +29,7 @@ final class TitleSubtitleTableViewCell: UITableViewCell {
fatalError("init(coder:) has not been implemented")
}
///
func configure(title: String, subtitle: String? = nil, detail: String? = nil) {
textLabel?.text = title
if let subtitle, !subtitle.isEmpty {
@ -38,18 +40,20 @@ final class TitleSubtitleTableViewCell: UITableViewCell {
}
}
/// UITableView ViewModel onChange
@MainActor
class ModuleTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
/// UITableView Diffable ViewModel onChange
class ModuleTableViewController: UIViewController, UITableViewDelegate {
let services = AppServices.shared
let tableView = UITableView(frame: .zero, style: .insetGrouped)
private let refreshControl = UIRefreshControl()
private let activityIndicator = UIActivityIndicatorView(style: .medium)
private var viewModelReloadHandler: (() -> Void)?
/// Diffable
private(set) var tableDataSource: UITableViewDiffableDataSource<ModuleTableSection, ModuleTableRow>!
var isLoading = false {
didSet {
if isLoading, tableView.numberOfSections > 0, tableView.numberOfRows(inSection: 0) == 0 {
if isLoading, tableDataSource.snapshot().numberOfItems == 0 {
activityIndicator.startAnimating()
} else {
activityIndicator.stopAnimating()
@ -57,12 +61,19 @@ class ModuleTableViewController: UIViewController, UITableViewDataSource, UITabl
}
}
/// UI
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(hex: 0xF5F7FA)
navigationItem.largeTitleDisplayMode = .never
tableView.dataSource = self
tableDataSource = UITableViewDiffableDataSource<ModuleTableSection, ModuleTableRow>(
tableView: tableView
) { [weak self] (tableView: UITableView, indexPath: IndexPath, row: ModuleTableRow) -> UITableViewCell? in
guard let self else { return UITableViewCell() }
return self.tableCell(for: indexPath, row: row, in: tableView)
}
tableView.delegate = self
tableView.backgroundColor = .clear
tableView.register(
@ -85,36 +96,45 @@ class ModuleTableViewController: UIViewController, UITableViewDataSource, UITabl
Task { await reloadContent() }
}
/// ViewModel `reloadContent`
func bindViewModel(onChange: (() -> Void)?) {
viewModelReloadHandler = onChange
}
func reloadTable() {
tableView.reloadData()
/// Diffable snapshot reloadData
func reloadTable(animated: Bool = true) {
tableDataSource.apply(buildTableSnapshot(), animatingDifferences: animated)
}
/// section section
func numberOfTableSections() -> Int { 1 }
/// section
func tableRowCount(in section: Int) -> Int {
section == 0 ? tableRowCount() : 0
}
/// section
func tableRowCount() -> Int { 0 }
/// Diffable snapshot
func buildTableSnapshot() -> NSDiffableDataSourceSnapshot<ModuleTableSection, ModuleTableRow> {
var snapshot = NSDiffableDataSourceSnapshot<ModuleTableSection, ModuleTableRow>()
for section in 0..<numberOfTableSections() {
snapshot.appendSections([section])
let rows = (0..<tableRowCount(in: section)).map {
moduleTableRowID(section: section, row: $0)
}
snapshot.appendItems(rows, toSection: section)
}
return snapshot
}
/// Cell
func configureCell(_ cell: TitleSubtitleTableViewCell, at indexPath: IndexPath) {}
func didSelectTableRow(at indexPath: IndexPath) {}
func reloadContent() async {}
@objc private func handleRefresh() {
Task {
await reloadContent()
refreshControl.endRefreshing()
}
}
func numberOfSections(in tableView: UITableView) -> Int { 1 }
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
tableRowCount()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
/// Cell section
func tableCell(for indexPath: IndexPath, row: ModuleTableRow, in tableView: UITableView) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(
withIdentifier: TitleSubtitleTableViewCell.reuseIdentifier,
for: indexPath
@ -125,19 +145,46 @@ class ModuleTableViewController: UIViewController, UITableViewDataSource, UITabl
return cell
}
///
func didSelectTableRow(at indexPath: IndexPath) {}
///
func reloadContent() async {}
/// section
func tableSectionTitle(for section: Int) -> String? { nil }
///
@objc private func handleRefresh() {
Task {
await reloadContent()
refreshControl.endRefreshing()
}
}
/// UITableView section
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
tableSectionTitle(for: section)
}
/// UITableView
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
didSelectTableRow(at: indexPath)
}
/// UITableView
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
willDisplayTableRow(at: indexPath)
}
/// willDisplayTableRow
func willDisplayTableRow(at indexPath: IndexPath) {}
}
/// `ModuleTableViewController` ViewModel 便
extension ModuleTableViewController {
/// ViewModel onChange
func wireViewModel(_ viewModel: AnyObject, reload: @escaping () -> Void) {
if let bindable = viewModel as? ViewModelBindable {
bindable.onChange = { [weak self] in
@ -146,6 +193,7 @@ extension ModuleTableViewController {
}
}
reload()
reloadTable(animated: false)
}
}
@ -154,3 +202,140 @@ extension ModuleTableViewController {
protocol ViewModelBindable: AnyObject {
var onChange: (() -> Void)? { get set }
}
/// 使 UITableView Diffable
class SimpleTableDiffableViewController: UIViewController, UITableViewDelegate {
let tableView: UITableView
private(set) var tableDataSource: UITableViewDiffableDataSource<SimpleTableSection, SimpleTableRow>!
///
init(style: UITableView.Style = .insetGrouped) {
tableView = UITableView(frame: .zero, style: style)
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// snapshot
func buildSnapshot() -> NSDiffableDataSourceSnapshot<SimpleTableSection, SimpleTableRow> {
NSDiffableDataSourceSnapshot()
}
/// Cell
func configureCell(_ cell: UITableViewCell, row: SimpleTableRow, at indexPath: IndexPath) {}
///
func didSelectRow(_ row: SimpleTableRow, at indexPath: IndexPath) {}
/// section
func sectionTitle(for section: SimpleTableSection) -> String? { nil }
/// section
func sectionFooter(for section: SimpleTableSection) -> String? { nil }
/// snapshot
func applySnapshot(animated: Bool = true) {
tableDataSource.apply(buildSnapshot(), animatingDifferences: animated)
}
/// UI
override func viewDidLoad() {
super.viewDidLoad()
tableDataSource = UITableViewDiffableDataSource<SimpleTableSection, SimpleTableRow>(
tableView: tableView
) { [weak self] (_: UITableView, indexPath: IndexPath, row: SimpleTableRow) -> UITableViewCell? in
let cell = UITableViewCell(style: .value1, reuseIdentifier: nil)
self?.configureCell(cell, row: row, at: indexPath)
return cell
}
tableView.delegate = self
view.addSubview(tableView)
tableView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
applySnapshot(animated: false)
}
/// UITableView section
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
guard let sectionID = tableDataSource.snapshot().sectionIdentifiers[safe: section] else { return nil }
return sectionTitle(for: sectionID)
}
/// UITableView section
func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
guard let sectionID = tableDataSource.snapshot().sectionIdentifiers[safe: section] else { return nil }
return sectionFooter(for: sectionID)
}
/// UITableView
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard let row = tableDataSource.itemIdentifier(for: indexPath) else { return }
didSelectRow(row, at: indexPath)
}
}
/// section
private extension Array {
subscript(safe index: Int) -> Element? {
indices.contains(index) ? self[index] : nil
}
}
/// UICollectionView Diffable
class ModuleCollectionViewController: UIViewController {
let services = AppServices.shared
let collectionView: UICollectionView
private let refreshControl = UIRefreshControl()
private let activityIndicator = UIActivityIndicatorView(style: .medium)
///
init(collectionViewLayout: UICollectionViewLayout) {
collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// UI
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(hex: 0xF5F7FA)
navigationItem.largeTitleDisplayMode = .never
collectionView.backgroundColor = .clear
collectionView.refreshControl = refreshControl
refreshControl.addTarget(self, action: #selector(handleRefresh), for: .valueChanged)
activityIndicator.hidesWhenStopped = true
view.addSubview(collectionView)
view.addSubview(activityIndicator)
collectionView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
activityIndicator.snp.makeConstraints { make in
make.center.equalToSuperview()
}
Task { await reloadContent() }
}
///
func reloadContent() async {}
///
@objc private func handleRefresh() {
Task {
await reloadContent()
refreshControl.endRefreshing()
}
}
}