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,16 @@
# Core/Map 模块
## 职责
封装高德地图 UIKit 组件,供运营区域围栏展示与打卡点编辑选点使用。
## 组件
- `OperatingAreaMapView`:绘制运营围栏 polygon模拟器降级为坐标摘要。
- `PunchPointMapPickerView`:打卡点编辑页地图点选;模拟器提示使用「当前位置」。
## 构建说明
- 真机构建:`AMAP_ENABLED` + CocoaPods 高德 SDK。
- 模拟器:不链接 AMap自动展示文字/坐标兜底 UI。
-`Info.plist` 配置 `AMapAPIKey` 为高德控制台 Key需与 Bundle ID 绑定)。

View File

@ -0,0 +1,199 @@
//
// OperatingAreaMapView.swift
// suixinkan_ios
//
import CoreLocation
import SnapKit
import UIKit
/// polygon
final class OperatingAreaMapView: UIView {
var rings: [OperatingFenceRing] = [] {
didSet { updateContent() }
}
private let contentContainer = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
contentContainer.clipsToBounds = true
contentContainer.layer.cornerRadius = AppMetrics.CornerRadius.card
addSubview(contentContainer)
contentContainer.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
updateContent()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateContent() {
contentContainer.subviews.forEach { $0.removeFromSuperview() }
#if AMAP_ENABLED
let mapView = OperatingAreaAMapView(rings: rings)
contentContainer.addSubview(mapView)
mapView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
#else
let fallback = OperatingAreaMapFallbackView(rings: rings)
contentContainer.addSubview(fallback)
fallback.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
#endif
}
}
#if AMAP_ENABLED
import MAMapKit
private final class OperatingAreaAMapView: UIView, MAMapViewDelegate {
private let mapView = MAMapView()
private var ringsByOverlay: [ObjectIdentifier: OperatingFenceRing] = [:]
private var rings: [OperatingFenceRing]
init(rings: [OperatingFenceRing]) {
self.rings = rings
super.init(frame: .zero)
mapView.delegate = self
mapView.showsCompass = false
mapView.showsScale = false
mapView.isRotateEnabled = false
addSubview(mapView)
mapView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
applyRings()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func applyRings() {
ringsByOverlay.removeAll()
mapView.removeOverlays(mapView.overlays)
mapView.removeAnnotations(mapView.annotations)
var allCoordinates: [CLLocationCoordinate2D] = []
for ring in rings {
var coordinates = ring.points.map {
CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude)
}
guard coordinates.count >= 3 else { continue }
guard let polygon = MAPolygon(coordinates: &coordinates, count: UInt(coordinates.count)) else {
continue
}
ringsByOverlay[ObjectIdentifier(polygon)] = ring
mapView.add(polygon)
allCoordinates.append(contentsOf: coordinates)
if let center = ring.centerCoordinate {
let annotation = MAPointAnnotation()
annotation.coordinate = center
annotation.title = ring.regionName
mapView.addAnnotation(annotation)
}
}
if let region = Self.region(for: allCoordinates) {
mapView.setRegion(region, animated: false)
}
}
func mapView(_ mapView: MAMapView!, rendererFor overlay: MAOverlay!) -> MAOverlayRenderer! {
guard let polygon = overlay as? MAPolygon else { return nil }
let ring = ringsByOverlay[ObjectIdentifier(polygon)]
let renderer = MAPolygonRenderer(polygon: polygon)
let color = ring?.isCurrentStore == true ? UIColor.systemRed : UIColor.systemBlue
renderer?.strokeColor = color
renderer?.fillColor = color.withAlphaComponent(0.18)
renderer?.lineWidth = 3
return renderer
}
private static func region(for coordinates: [CLLocationCoordinate2D]) -> MACoordinateRegion? {
guard !coordinates.isEmpty else { return nil }
let minLat = coordinates.map(\.latitude).min() ?? 0
let maxLat = coordinates.map(\.latitude).max() ?? 0
let minLng = coordinates.map(\.longitude).min() ?? 0
let maxLng = coordinates.map(\.longitude).max() ?? 0
let center = CLLocationCoordinate2D(
latitude: (minLat + maxLat) / 2,
longitude: (minLng + maxLng) / 2
)
let span = MACoordinateSpan(
latitudeDelta: max((maxLat - minLat) * 1.4, 0.01),
longitudeDelta: max((maxLng - minLng) * 1.4, 0.01)
)
return MACoordinateRegion(center: center, span: span)
}
}
private extension OperatingFenceRing {
var centerCoordinate: CLLocationCoordinate2D? {
guard !points.isEmpty else { return nil }
let latitude = points.reduce(0) { $0 + $1.latitude } / Double(points.count)
let longitude = points.reduce(0) { $0 + $1.longitude } / Double(points.count)
return CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
}
}
#endif
private final class OperatingAreaMapFallbackView: UIView {
init(rings: [OperatingFenceRing]) {
super.init(frame: .zero)
backgroundColor = UIColor(hex: 0xEEF2F7)
let titleLabel = UILabel()
titleLabel.text = "模拟器未启用高德地图,以下为围栏坐标摘要"
titleLabel.font = .systemFont(ofSize: AppMetrics.FontSize.subheadline, weight: .medium)
titleLabel.textColor = AppDesign.textSecondary
titleLabel.numberOfLines = 0
let stack = UIStackView()
stack.axis = .vertical
stack.spacing = 8
addSubview(titleLabel)
addSubview(stack)
titleLabel.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview().inset(AppMetrics.Spacing.medium)
}
stack.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(AppMetrics.Spacing.small)
make.leading.trailing.bottom.equalToSuperview().inset(AppMetrics.Spacing.medium)
}
for ring in rings.prefix(6) {
let label = UILabel()
label.numberOfLines = 0
label.font = .systemFont(ofSize: AppMetrics.FontSize.caption)
label.textColor = AppDesign.textSecondary
let name = ring.regionName.isEmpty ? "未命名区域" : ring.regionName
let coords = ring.points.prefix(4)
.map { String(format: "%.6f, %.6f", $0.latitude, $0.longitude) }
.joined(separator: " ")
label.text = "\(name)\n\(coords)"
stack.addArrangedSubview(label)
}
if rings.isEmpty {
let empty = UILabel()
empty.text = "暂无围栏数据"
empty.textColor = AppDesign.placeholder
stack.addArrangedSubview(empty)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -0,0 +1,139 @@
//
// PunchPointMapPickerView.swift
// suixinkan_ios
//
import CoreLocation
import SnapKit
import UIKit
/// +
@MainActor
final class PunchPointMapPickerView: UIView {
var onLocationPicked: ((Double, Double, String) -> Void)?
var coordinate: CLLocationCoordinate2D? {
didSet { updatePin() }
}
private let hintLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
clipsToBounds = true
layer.cornerRadius = AppMetrics.CornerRadius.card
backgroundColor = UIColor(hex: 0xEEF2F7)
hintLabel.font = .systemFont(ofSize: AppMetrics.FontSize.caption)
hintLabel.textColor = AppDesign.textSecondary
hintLabel.numberOfLines = 0
hintLabel.textAlignment = .center
#if AMAP_ENABLED
hintLabel.text = "点击地图选择打卡位置"
let mapView = PunchPointAMapPickerView { [weak self] lat, lng, address in
self?.onLocationPicked?(lat, lng, address)
}
addSubview(mapView)
addSubview(hintLabel)
mapView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
hintLabel.snp.makeConstraints { make in
make.leading.trailing.bottom.equalToSuperview().inset(8)
}
#else
hintLabel.text = "模拟器未启用高德地图,请使用「当前位置」按钮选点"
addSubview(hintLabel)
hintLabel.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(AppMetrics.Spacing.medium)
}
#endif
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updatePin() {
// Pin updates handled inside AMap subview when coordinate is set externally.
}
func setCoordinate(latitude: Double, longitude: Double) {
coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
}
}
#if AMAP_ENABLED
import AMapSearchKit
import MAMapKit
private final class PunchPointAMapPickerView: UIView, MAMapViewDelegate, AMapSearchDelegate {
private let mapView = MAMapView()
private let searchAPI = AMapSearchAPI()
private var pin: MAPointAnnotation?
private var pendingRegeo: CheckedContinuation<String, Never>?
private let onPick: (Double, Double, String) -> Void
init(onPick: @escaping (Double, Double, String) -> Void) {
self.onPick = onPick
super.init(frame: .zero)
searchAPI?.delegate = self
mapView.delegate = self
mapView.showsUserLocation = true
mapView.userTrackingMode = .none
mapView.zoomLevel = 16
addSubview(mapView)
mapView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func mapView(_ mapView: MAMapView!, didSingleTappedAt coordinate: CLLocationCoordinate2D) {
Task { await handlePick(coordinate) }
}
private func handlePick(_ coordinate: CLLocationCoordinate2D) async {
if let pin {
mapView.removeAnnotation(pin)
}
let annotation = MAPointAnnotation()
annotation.coordinate = coordinate
pin = annotation
mapView.addAnnotation(annotation)
let address = await reverseGeocode(coordinate)
onPick(coordinate.latitude, coordinate.longitude, address)
}
private func reverseGeocode(_ coordinate: CLLocationCoordinate2D) async -> String {
await withCheckedContinuation { continuation in
pendingRegeo = continuation
let request = AMapReGeocodeSearchRequest()
request.location = AMapGeoPoint.location(
withLatitude: CGFloat(coordinate.latitude),
longitude: CGFloat(coordinate.longitude)
)
request.requireExtension = true
searchAPI?.aMapReGoecodeSearch(request)
}
}
func onReGeocodeSearchDone(_ request: AMapReGeocodeSearchRequest!, response: AMapReGeocodeSearchResponse!) {
let address = response.regeocode.formattedAddress ?? "已选位置"
pendingRegeo?.resume(returning: address)
pendingRegeo = nil
}
func aMapSearchRequest(_ request: Any!, didFailWithError error: Error!) {
pendingRegeo?.resume(returning: "已选位置")
pendingRegeo = nil
}
}
#endif