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:
173
suixinkan_ios/Core/UIKit/ListDiffableSupport.swift
Normal file
173
suixinkan_ios/Core/UIKit/ListDiffableSupport.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user