Add location report map page and history list aligned with Android.
Extract shared LocationReportService, wire location_report menu routing, and add unit tests for reporting cooldown and history pagination. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@ -382,65 +382,56 @@ final class HomeViewController: BaseViewController {
|
||||
}
|
||||
|
||||
private func presentOnlineStatusDialog() {
|
||||
let goingOnline = !viewModel.isOnline
|
||||
let alert = UIAlertController(
|
||||
title: goingOnline ? "切换在线" : "切换离线",
|
||||
message: goingOnline ? "确认切换到在线状态并上报位置?" : "确认切换到离线状态?",
|
||||
preferredStyle: .alert
|
||||
viewModel.hideOnlineStatusSwitchDialog()
|
||||
LocationReportDialogPresenter.presentOnlineStatusSwitch(
|
||||
from: self,
|
||||
isOnline: viewModel.isOnline,
|
||||
onConfirm: { [weak self] in
|
||||
guard let self else { return }
|
||||
self.activeDialog = nil
|
||||
Task { await self.viewModel.switchOnlineStatus(api: self.homeAPI) }
|
||||
},
|
||||
onCancel: { [weak self] in
|
||||
self?.viewModel.hideOnlineStatusSwitchDialog()
|
||||
self?.activeDialog = nil
|
||||
}
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel) { [weak self] _ in
|
||||
self?.viewModel.hideOnlineStatusSwitchDialog()
|
||||
self?.activeDialog = nil
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: "确定", style: .default) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
self.activeDialog = nil
|
||||
Task { await self.viewModel.switchOnlineStatus(api: self.homeAPI) }
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func presentLocationTimeoutDialog() {
|
||||
let alert = UIAlertController(
|
||||
title: "位置上报提醒",
|
||||
message: "您的位置上报即将超时,请立即上报",
|
||||
preferredStyle: .alert
|
||||
viewModel.hideLocationTimeoutDialog()
|
||||
LocationReportDialogPresenter.presentLocationTimeout(
|
||||
from: self,
|
||||
onReport: { [weak self] in
|
||||
guard let self else { return }
|
||||
self.activeDialog = nil
|
||||
Task { await self.viewModel.confirmLocationTimeoutReport(api: self.homeAPI) }
|
||||
},
|
||||
onLater: { [weak self] in
|
||||
self?.viewModel.hideLocationTimeoutDialog()
|
||||
self?.activeDialog = nil
|
||||
}
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "稍后", style: .cancel) { [weak self] _ in
|
||||
self?.viewModel.hideLocationTimeoutDialog()
|
||||
self?.activeDialog = nil
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: "立即上报", style: .default) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
self.activeDialog = nil
|
||||
Task { await self.viewModel.confirmLocationTimeoutReport(api: self.homeAPI) }
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func presentLocationSuccessDialog() {
|
||||
let alert = UIAlertController(
|
||||
title: "上报成功",
|
||||
message: "上报时间:\(viewModel.reportTimeText)",
|
||||
preferredStyle: .alert
|
||||
let reportTime = viewModel.reportTimeText
|
||||
let countdown = viewModel.countdownDisplayText
|
||||
viewModel.hideLocationReportSuccessDialog()
|
||||
LocationReportDialogPresenter.presentReportSuccess(
|
||||
from: self,
|
||||
reportTime: reportTime,
|
||||
countdown: countdown,
|
||||
onDismiss: { [weak self] in
|
||||
self?.activeDialog = nil
|
||||
}
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "确定", style: .default) { [weak self] _ in
|
||||
self?.viewModel.hideLocationReportSuccessDialog()
|
||||
self?.activeDialog = nil
|
||||
})
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func presentReminderPicker() {
|
||||
let alert = UIAlertController(title: "提前提醒", message: "选择位置超时提前提醒时间", preferredStyle: .actionSheet)
|
||||
[0, 5, 10, 15, 30].forEach { minutes in
|
||||
let title = minutes == 0 ? "不提醒" : "提前\(minutes)分钟"
|
||||
alert.addAction(UIAlertAction(title: title, style: .default) { [weak self] _ in
|
||||
self?.viewModel.updateReminderMinutes(minutes)
|
||||
})
|
||||
LocationReportDialogPresenter.presentReminderPicker(from: self) { [weak self] minutes in
|
||||
self?.viewModel.updateReminderMinutes(minutes)
|
||||
}
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func presentScenicSelection() {
|
||||
|
||||
@ -0,0 +1,73 @@
|
||||
//
|
||||
// LocationReportDialogPresenter.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
/// 位置上报相关弹窗,对齐 Android 文案。
|
||||
enum LocationReportDialogPresenter {
|
||||
|
||||
/// 展示切换在线状态确认弹窗。
|
||||
@MainActor
|
||||
static func presentOnlineStatusSwitch(
|
||||
from viewController: UIViewController,
|
||||
isOnline: Bool,
|
||||
onConfirm: @escaping () -> Void,
|
||||
onCancel: (() -> Void)? = nil
|
||||
) {
|
||||
let message = isOnline
|
||||
? "是否确认切换为离线状态?\n离线后将暂停位置上报,直到下次手动切换为在线状态"
|
||||
: "是否确认切换为在线状态?\n在线后将开始位置上报和计时"
|
||||
let alert = UIAlertController(title: "切换在线状态", message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel) { _ in onCancel?() })
|
||||
alert.addAction(UIAlertAction(title: "确定", style: .default) { _ in onConfirm() })
|
||||
viewController.present(alert, animated: true)
|
||||
}
|
||||
|
||||
/// 展示上报成功弹窗。
|
||||
@MainActor
|
||||
static func presentReportSuccess(
|
||||
from viewController: UIViewController,
|
||||
reportTime: String,
|
||||
countdown: String,
|
||||
onDismiss: (() -> Void)? = nil
|
||||
) {
|
||||
let message = "上报已成功,上报时间为\(reportTime)\n距离下次上报时间\(countdown)"
|
||||
let alert = UIAlertController(title: "上报成功", message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "确定", style: .default) { _ in onDismiss?() })
|
||||
viewController.present(alert, animated: true)
|
||||
}
|
||||
|
||||
/// 展示位置超时提醒弹窗。
|
||||
@MainActor
|
||||
static func presentLocationTimeout(
|
||||
from viewController: UIViewController,
|
||||
onReport: @escaping () -> Void,
|
||||
onLater: (() -> Void)? = nil
|
||||
) {
|
||||
let alert = UIAlertController(
|
||||
title: "位置上报提醒",
|
||||
message: "倒计时即将结束,请立即上报位置",
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "稍后", style: .cancel) { _ in onLater?() })
|
||||
alert.addAction(UIAlertAction(title: "立即上报", style: .default) { _ in onReport() })
|
||||
viewController.present(alert, animated: true)
|
||||
}
|
||||
|
||||
/// 展示提醒设置选择器。
|
||||
@MainActor
|
||||
static func presentReminderPicker(
|
||||
from viewController: UIViewController,
|
||||
onSelect: @escaping (Int) -> Void
|
||||
) {
|
||||
let alert = UIAlertController(title: "提醒设置", message: "提前提醒时间", preferredStyle: .actionSheet)
|
||||
[0, 5, 10, 15, 30].forEach { minutes in
|
||||
let title = minutes == 0 ? "不提醒" : "提前\(minutes)分钟"
|
||||
alert.addAction(UIAlertAction(title: title, style: .default) { _ in onSelect(minutes) })
|
||||
}
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
viewController.present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,184 @@
|
||||
//
|
||||
// LocationReportHistoryViewController.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 位置上报历史页,对齐 Android `LocationReportHistoryScreen`。
|
||||
final class LocationReportHistoryViewController: BaseViewController, UITableViewDelegate, UITableViewDataSource {
|
||||
|
||||
private let viewModel: LocationReportHistoryViewModel
|
||||
private let homeAPI: HomeAPI
|
||||
|
||||
private let filterBar = LocationReportHistoryFilterBar()
|
||||
private let tableView = UITableView(frame: .zero, style: .plain)
|
||||
private let emptyLabel = UILabel()
|
||||
private let footerLabel = UILabel()
|
||||
private let refreshControl = UIRefreshControl()
|
||||
|
||||
init(
|
||||
viewModel: LocationReportHistoryViewModel = LocationReportHistoryViewModel(),
|
||||
homeAPI: HomeAPI = NetworkServices.shared.homeAPI
|
||||
) {
|
||||
self.viewModel = viewModel
|
||||
self.homeAPI = homeAPI
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func setupNavigationBar() {
|
||||
title = "位置上报历史"
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
tableView.separatorStyle = .none
|
||||
tableView.backgroundColor = AppColor.pageBackground
|
||||
tableView.register(LocationReportHistoryCell.self, forCellReuseIdentifier: LocationReportHistoryCell.reuseIdentifier)
|
||||
tableView.refreshControl = refreshControl
|
||||
|
||||
emptyLabel.text = "暂无位置上报记录"
|
||||
emptyLabel.textColor = AppColor.textTertiary
|
||||
emptyLabel.textAlignment = .center
|
||||
emptyLabel.isHidden = true
|
||||
|
||||
footerLabel.text = "没有更多"
|
||||
footerLabel.font = .systemFont(ofSize: 13)
|
||||
footerLabel.textColor = AppColor.textTertiary
|
||||
footerLabel.textAlignment = .center
|
||||
footerLabel.isHidden = true
|
||||
|
||||
view.addSubview(filterBar)
|
||||
view.addSubview(tableView)
|
||||
view.addSubview(emptyLabel)
|
||||
view.addSubview(footerLabel)
|
||||
|
||||
filterBar.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide)
|
||||
}
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.top.equalTo(filterBar.snp.bottom)
|
||||
make.leading.trailing.bottom.equalTo(view.safeAreaLayoutGuide)
|
||||
}
|
||||
emptyLabel.snp.makeConstraints { make in
|
||||
make.center.equalTo(tableView)
|
||||
}
|
||||
footerLabel.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
make.bottom.equalTo(view.safeAreaLayoutGuide).offset(-12)
|
||||
}
|
||||
}
|
||||
|
||||
override func bindActions() {
|
||||
viewModel.onStateChange = { [weak self] in
|
||||
Task { @MainActor in self?.applyViewModel() }
|
||||
}
|
||||
viewModel.onShowMessage = { [weak self] message in
|
||||
Task { @MainActor in self?.showToast(message) }
|
||||
}
|
||||
|
||||
refreshControl.addTarget(self, action: #selector(refreshTapped), for: .valueChanged)
|
||||
filterBar.onFilterTap = { [weak self] in self?.presentFilterPicker() }
|
||||
filterBar.onDateFilterTap = { [weak self] in self?.presentDateFilter() }
|
||||
filterBar.onClearDates = { [weak self] in
|
||||
guard let self else { return }
|
||||
Task { await self.viewModel.clearDateRange(api: self.homeAPI) }
|
||||
}
|
||||
|
||||
Task { await viewModel.loadReportList(api: homeAPI, refresh: true) }
|
||||
}
|
||||
|
||||
private func applyViewModel() {
|
||||
filterBar.apply(
|
||||
filterTitle: viewModel.selectedFilter.displayName,
|
||||
startText: viewModel.startDateText,
|
||||
endText: viewModel.endDateText,
|
||||
showsDateRange: viewModel.hasDateFilter
|
||||
)
|
||||
tableView.reloadData()
|
||||
emptyLabel.isHidden = !viewModel.reportList.isEmpty
|
||||
footerLabel.isHidden = viewModel.reportList.isEmpty || viewModel.canLoadMore
|
||||
if !viewModel.isRefreshing {
|
||||
refreshControl.endRefreshing()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func refreshTapped() {
|
||||
Task { await viewModel.loadReportList(api: homeAPI, refresh: true) }
|
||||
}
|
||||
|
||||
private func presentFilterPicker() {
|
||||
let alert = UIAlertController(title: "筛选类型", message: nil, preferredStyle: .actionSheet)
|
||||
LocationReportFilterType.allCases.forEach { filter in
|
||||
alert.addAction(UIAlertAction(title: filter.displayName, style: .default) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
Task { await self.viewModel.selectFilter(filter, api: self.homeAPI) }
|
||||
})
|
||||
}
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func presentDateFilter() {
|
||||
let alert = UIAlertController(title: "时间筛选", message: nil, preferredStyle: .actionSheet)
|
||||
alert.addAction(UIAlertAction(title: "选择开始日期", style: .default) { [weak self] _ in
|
||||
self?.presentDatePicker(isStart: true)
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: "选择结束日期", style: .default) { [weak self] _ in
|
||||
self?.presentDatePicker(isStart: false)
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
private func presentDatePicker(isStart: Bool) {
|
||||
let alert = UIAlertController(title: isStart ? "开始日期" : "结束日期", message: "\n\n\n\n\n\n\n\n", preferredStyle: .actionSheet)
|
||||
let picker = UIDatePicker()
|
||||
picker.datePickerMode = .date
|
||||
if #available(iOS 13.4, *) {
|
||||
picker.preferredDatePickerStyle = .wheels
|
||||
}
|
||||
picker.date = isStart ? (viewModel.startDate ?? Date()) : (viewModel.endDate ?? Date())
|
||||
alert.view.addSubview(picker)
|
||||
picker.snp.makeConstraints { make in
|
||||
make.centerX.equalToSuperview()
|
||||
make.top.equalToSuperview().offset(24)
|
||||
}
|
||||
alert.addAction(UIAlertAction(title: "确定", style: .default) { [weak self] _ in
|
||||
guard let self else { return }
|
||||
Task {
|
||||
if isStart {
|
||||
await self.viewModel.setDateRange(start: picker.date, end: self.viewModel.endDate, api: self.homeAPI)
|
||||
} else {
|
||||
await self.viewModel.setDateRange(start: self.viewModel.startDate, end: picker.date, api: self.homeAPI)
|
||||
}
|
||||
}
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
viewModel.reportList.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(
|
||||
withIdentifier: LocationReportHistoryCell.reuseIdentifier,
|
||||
for: indexPath
|
||||
) as! LocationReportHistoryCell
|
||||
cell.apply(item: viewModel.reportList[indexPath.row])
|
||||
return cell
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
Task { await viewModel.loadMoreIfNeeded(currentIndex: indexPath.row, api: homeAPI) }
|
||||
}
|
||||
}
|
||||
177
suixinkan/UI/LocationReport/LocationReportViewController.swift
Normal file
177
suixinkan/UI/LocationReport/LocationReportViewController.swift
Normal file
@ -0,0 +1,177 @@
|
||||
//
|
||||
// LocationReportViewController.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 位置上报独立页,对齐 Android `LocationReportScreen`。
|
||||
final class LocationReportViewController: BaseViewController {
|
||||
|
||||
private let viewModel: LocationReportViewModel
|
||||
private let homeAPI: HomeAPI
|
||||
|
||||
private let mapContainer = LocationReportMapView()
|
||||
private let mapControls = LocationReportMapControlStack()
|
||||
private let statusCard = LocationReportStatusCardView()
|
||||
private let actionCard = LocationReportActionCardView()
|
||||
private let markButton = LocationReportBottomActionView(title: "标记上报", symbol: "mappin.and.ellipse")
|
||||
private let historyButton = LocationReportBottomActionView(title: "历史记录", symbol: "clock")
|
||||
private let onlineButton = LocationReportBottomActionView(title: "离线", symbol: "power")
|
||||
private var hasCenteredMap = false
|
||||
|
||||
init(
|
||||
viewModel: LocationReportViewModel = LocationReportViewModel(),
|
||||
homeAPI: HomeAPI = NetworkServices.shared.homeAPI
|
||||
) {
|
||||
self.viewModel = viewModel
|
||||
self.homeAPI = homeAPI
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func setupNavigationBar() {
|
||||
title = "位置上报"
|
||||
}
|
||||
|
||||
override func setupUI() {
|
||||
view.backgroundColor = AppColor.pageBackground
|
||||
|
||||
let bottomStack = UIStackView(arrangedSubviews: [markButton, historyButton, onlineButton])
|
||||
bottomStack.axis = .horizontal
|
||||
bottomStack.spacing = 12
|
||||
bottomStack.distribution = .fillEqually
|
||||
|
||||
let panelStack = UIStackView(arrangedSubviews: [statusCard, actionCard, bottomStack])
|
||||
panelStack.axis = .vertical
|
||||
panelStack.spacing = 12
|
||||
|
||||
view.addSubview(mapContainer)
|
||||
view.addSubview(mapControls)
|
||||
view.addSubview(panelStack)
|
||||
|
||||
mapContainer.snp.makeConstraints { make in
|
||||
make.top.leading.trailing.equalTo(view.safeAreaLayoutGuide)
|
||||
make.height.equalTo(view.snp.height).multipliedBy(0.45)
|
||||
}
|
||||
mapControls.snp.makeConstraints { make in
|
||||
make.trailing.equalTo(mapContainer).offset(-16)
|
||||
make.centerY.equalTo(mapContainer)
|
||||
}
|
||||
panelStack.snp.makeConstraints { make in
|
||||
make.top.equalTo(mapContainer.snp.bottom).offset(-20)
|
||||
make.leading.trailing.equalToSuperview().inset(15)
|
||||
make.bottom.lessThanOrEqualTo(view.safeAreaLayoutGuide).offset(-12)
|
||||
}
|
||||
}
|
||||
|
||||
override func bindActions() {
|
||||
viewModel.onStateChange = { [weak self] in
|
||||
Task { @MainActor in self?.applyViewModel() }
|
||||
}
|
||||
viewModel.onShowMessage = { [weak self] message in
|
||||
Task { @MainActor in self?.showToast(message) }
|
||||
}
|
||||
|
||||
mapControls.onZoomIn = { [weak self] in self?.mapContainer.zoomIn() }
|
||||
mapControls.onZoomOut = { [weak self] in self?.mapContainer.zoomOut() }
|
||||
mapControls.onRelocate = { [weak self] in
|
||||
self?.viewModel.startLocation()
|
||||
self?.mapContainer.centerOnUserLocation()
|
||||
}
|
||||
mapContainer.onMapTap = { [weak self] coordinate in
|
||||
self?.viewModel.setMarkerLocation(latitude: coordinate.latitude, longitude: coordinate.longitude)
|
||||
}
|
||||
|
||||
statusCard.onOnlineStatusTap = { [weak self] in self?.viewModel.showOnlineStatusSwitchDialog() }
|
||||
statusCard.onReminderTap = { [weak self] in
|
||||
guard let self else { return }
|
||||
LocationReportDialogPresenter.presentReminderPicker(from: self) { minutes in
|
||||
self.viewModel.updateReminderMinutes(minutes)
|
||||
}
|
||||
}
|
||||
actionCard.onReportTap = { [weak self] in
|
||||
guard let self else { return }
|
||||
Task { await self.viewModel.reportLocation(api: self.homeAPI) }
|
||||
}
|
||||
markButton.addTarget(self, action: #selector(markTapped), for: .touchUpInside)
|
||||
historyButton.addTarget(self, action: #selector(historyTapped), for: .touchUpInside)
|
||||
onlineButton.addTarget(self, action: #selector(onlineTapped), for: .touchUpInside)
|
||||
|
||||
viewModel.restoreStateIfNeeded()
|
||||
requestLocationIfNeeded()
|
||||
applyViewModel()
|
||||
}
|
||||
|
||||
private func requestLocationIfNeeded() {
|
||||
let status = CLLocationManager.authorizationStatus()
|
||||
switch status {
|
||||
case .authorizedAlways, .authorizedWhenInUse:
|
||||
viewModel.startLocation()
|
||||
case .notDetermined:
|
||||
CLLocationManager().requestWhenInUseAuthorization()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||
self?.viewModel.startLocation()
|
||||
}
|
||||
default:
|
||||
showToast("需要定位权限才能使用此功能")
|
||||
}
|
||||
}
|
||||
|
||||
private func applyViewModel() {
|
||||
statusCard.apply(
|
||||
isOnline: viewModel.isOnline,
|
||||
countdown: viewModel.countdownDisplayText,
|
||||
reminderMinutes: viewModel.reminderMinutes
|
||||
)
|
||||
actionCard.setReporting(viewModel.isReporting)
|
||||
markButton.isSelected = viewModel.isMarking
|
||||
onlineButton.applyOnlineStyle(isOnline: viewModel.isOnline)
|
||||
|
||||
mapContainer.updateMarker(latitude: viewModel.markerLatitude, longitude: viewModel.markerLongitude)
|
||||
if !hasCenteredMap, let lat = viewModel.currentLatitude, let lng = viewModel.currentLongitude {
|
||||
mapContainer.setCenter(CLLocationCoordinate2D(latitude: lat, longitude: lng), animated: false)
|
||||
hasCenteredMap = true
|
||||
}
|
||||
|
||||
if viewModel.showOnlineStatusDialog {
|
||||
viewModel.hideOnlineStatusSwitchDialog()
|
||||
LocationReportDialogPresenter.presentOnlineStatusSwitch(
|
||||
from: self,
|
||||
isOnline: viewModel.isOnline
|
||||
) { [weak self] in
|
||||
guard let self else { return }
|
||||
Task { await self.viewModel.switchOnlineStatus(api: self.homeAPI) }
|
||||
}
|
||||
}
|
||||
|
||||
if viewModel.showLocationReportSuccessDialog {
|
||||
let reportTime = viewModel.reportTimeText
|
||||
let countdown = viewModel.countdownDisplayText
|
||||
viewModel.hideLocationReportSuccessDialog()
|
||||
LocationReportDialogPresenter.presentReportSuccess(
|
||||
from: self,
|
||||
reportTime: reportTime,
|
||||
countdown: countdown
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func markTapped() {
|
||||
viewModel.toggleMarking()
|
||||
}
|
||||
|
||||
@objc private func historyTapped() {
|
||||
navigationController?.pushViewController(LocationReportHistoryViewController(), animated: true)
|
||||
}
|
||||
|
||||
@objc private func onlineTapped() {
|
||||
viewModel.showOnlineStatusSwitchDialog()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,177 @@
|
||||
//
|
||||
// LocationReportHistoryViews.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 历史上报列表卡片。
|
||||
final class LocationReportHistoryCell: UITableViewCell {
|
||||
|
||||
static let reuseIdentifier = "LocationReportHistoryCell"
|
||||
|
||||
private let typeBadge = UILabel()
|
||||
private let timeLabel = UILabel()
|
||||
private let coordinateLabel = UILabel()
|
||||
private let addressLabel = UILabel()
|
||||
private let remarkLabel = UILabel()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
let card = UIView()
|
||||
card.backgroundColor = .white
|
||||
card.layer.cornerRadius = 10
|
||||
|
||||
typeBadge.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
typeBadge.textAlignment = .center
|
||||
typeBadge.layer.cornerRadius = 4
|
||||
typeBadge.clipsToBounds = true
|
||||
|
||||
timeLabel.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
timeLabel.textColor = AppColor.textPrimary
|
||||
|
||||
coordinateLabel.font = .systemFont(ofSize: 12)
|
||||
coordinateLabel.textColor = AppColor.textSecondary
|
||||
|
||||
addressLabel.font = .systemFont(ofSize: 13)
|
||||
addressLabel.textColor = AppColor.textPrimary
|
||||
addressLabel.numberOfLines = 2
|
||||
|
||||
remarkLabel.font = .systemFont(ofSize: 12)
|
||||
remarkLabel.textColor = AppColor.textTertiary
|
||||
remarkLabel.numberOfLines = 2
|
||||
|
||||
let stack = UIStackView(arrangedSubviews: [timeLabel, coordinateLabel, addressLabel, remarkLabel])
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 6
|
||||
|
||||
card.addSubview(typeBadge)
|
||||
card.addSubview(stack)
|
||||
contentView.addSubview(card)
|
||||
|
||||
card.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(UIEdgeInsets(top: 6, left: 16, bottom: 6, right: 16))
|
||||
}
|
||||
typeBadge.snp.makeConstraints { make in
|
||||
make.top.leading.equalToSuperview().offset(12)
|
||||
make.height.equalTo(22)
|
||||
make.width.greaterThanOrEqualTo(64)
|
||||
}
|
||||
stack.snp.makeConstraints { make in
|
||||
make.top.equalTo(typeBadge.snp.bottom).offset(8)
|
||||
make.leading.trailing.bottom.equalToSuperview().inset(12)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func apply(item: LocationReportHistoryItem) {
|
||||
let filterType = item.filterType
|
||||
typeBadge.text = " \(filterType.displayName) "
|
||||
typeBadge.textColor = UIColor(hex: filterType.tagColorHex)
|
||||
typeBadge.backgroundColor = UIColor(hex: filterType.tagColorHex).withAlphaComponent(0.12)
|
||||
timeLabel.text = item.createdAt.isEmpty ? "--" : item.createdAt
|
||||
coordinateLabel.text = "经纬度:\(item.latitude), \(item.longitude)"
|
||||
addressLabel.text = item.address.isEmpty ? "--" : item.address
|
||||
if item.remark.isEmpty {
|
||||
remarkLabel.isHidden = true
|
||||
} else {
|
||||
remarkLabel.isHidden = false
|
||||
remarkLabel.text = "备注:\(item.remark)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 筛选与日期工具栏。
|
||||
final class LocationReportHistoryFilterBar: UIView {
|
||||
|
||||
var onFilterTap: (() -> Void)?
|
||||
var onDateFilterTap: (() -> Void)?
|
||||
var onClearDates: (() -> Void)?
|
||||
|
||||
private let filterButton = UIButton(type: .system)
|
||||
private let dateButton = UIButton(type: .system)
|
||||
private let startLabel = UILabel()
|
||||
private let endLabel = UILabel()
|
||||
private let clearButton = UIButton(type: .system)
|
||||
private let dateRangeStack = UIStackView()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .white
|
||||
|
||||
filterButton.setTitle("全部", for: .normal)
|
||||
filterButton.setTitleColor(AppColor.textPrimary, for: .normal)
|
||||
filterButton.titleLabel?.font = .systemFont(ofSize: 14)
|
||||
filterButton.setImage(UIImage(systemName: "chevron.down"), for: .normal)
|
||||
filterButton.semanticContentAttribute = .forceRightToLeft
|
||||
filterButton.addTarget(self, action: #selector(filterTapped), for: .touchUpInside)
|
||||
|
||||
dateButton.setTitle("时间筛选", for: .normal)
|
||||
dateButton.setTitleColor(AppColor.primary, for: .normal)
|
||||
dateButton.titleLabel?.font = .systemFont(ofSize: 14)
|
||||
dateButton.addTarget(self, action: #selector(dateTapped), for: .touchUpInside)
|
||||
|
||||
startLabel.font = .systemFont(ofSize: 13)
|
||||
endLabel.font = .systemFont(ofSize: 13)
|
||||
startLabel.textColor = AppColor.textSecondary
|
||||
endLabel.textColor = AppColor.textSecondary
|
||||
|
||||
clearButton.setTitle("清除", for: .normal)
|
||||
clearButton.setTitleColor(AppColor.danger, for: .normal)
|
||||
clearButton.titleLabel?.font = .systemFont(ofSize: 13)
|
||||
clearButton.addTarget(self, action: #selector(clearTapped), for: .touchUpInside)
|
||||
|
||||
dateRangeStack.axis = .horizontal
|
||||
dateRangeStack.spacing = 8
|
||||
dateRangeStack.alignment = .center
|
||||
dateRangeStack.addArrangedSubview(startLabel)
|
||||
dateRangeStack.addArrangedSubview(UILabel(text: "至"))
|
||||
dateRangeStack.addArrangedSubview(endLabel)
|
||||
dateRangeStack.addArrangedSubview(clearButton)
|
||||
dateRangeStack.isHidden = true
|
||||
|
||||
let topRow = UIStackView(arrangedSubviews: [filterButton, UIView(), dateButton])
|
||||
topRow.axis = .horizontal
|
||||
|
||||
let stack = UIStackView(arrangedSubviews: [topRow, dateRangeStack])
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 8
|
||||
addSubview(stack)
|
||||
stack.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(12)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func apply(filterTitle: String, startText: String, endText: String, showsDateRange: Bool) {
|
||||
filterButton.setTitle(filterTitle, for: .normal)
|
||||
startLabel.text = startText
|
||||
endLabel.text = endText
|
||||
dateRangeStack.isHidden = !showsDateRange
|
||||
}
|
||||
|
||||
@objc private func filterTapped() { onFilterTap?() }
|
||||
@objc private func dateTapped() { onDateFilterTap?() }
|
||||
@objc private func clearTapped() { onClearDates?() }
|
||||
}
|
||||
|
||||
private extension UILabel {
|
||||
convenience init(text: String) {
|
||||
self.init()
|
||||
self.text = text
|
||||
font = .systemFont(ofSize: 13)
|
||||
textColor = AppColor.textSecondary
|
||||
}
|
||||
}
|
||||
357
suixinkan/UI/LocationReport/Views/LocationReportViews.swift
Normal file
357
suixinkan/UI/LocationReport/Views/LocationReportViews.swift
Normal file
@ -0,0 +1,357 @@
|
||||
//
|
||||
// LocationReportViews.swift
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import MapKit
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
/// 地图缩放/定位控件。
|
||||
final class LocationReportMapControlStack: UIView {
|
||||
|
||||
var onZoomIn: (() -> Void)?
|
||||
var onZoomOut: (() -> Void)?
|
||||
var onRelocate: (() -> Void)?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
let zoomIn = makeButton(symbol: "plus", action: #selector(zoomInTapped))
|
||||
let zoomOut = makeButton(symbol: "minus", action: #selector(zoomOutTapped))
|
||||
let relocate = makeButton(symbol: "location.fill", action: #selector(relocateTapped))
|
||||
|
||||
let zoomStack = UIStackView(arrangedSubviews: [zoomIn, zoomOut])
|
||||
zoomStack.axis = .vertical
|
||||
zoomStack.spacing = 0
|
||||
zoomStack.backgroundColor = .white
|
||||
zoomStack.layer.cornerRadius = 12
|
||||
zoomStack.clipsToBounds = true
|
||||
|
||||
relocate.backgroundColor = .white
|
||||
relocate.layer.cornerRadius = 12
|
||||
|
||||
let stack = UIStackView(arrangedSubviews: [zoomStack, relocate])
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 12
|
||||
addSubview(stack)
|
||||
stack.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
[zoomIn, zoomOut, relocate].forEach { button in
|
||||
button.snp.makeConstraints { make in
|
||||
make.size.equalTo(44)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func makeButton(symbol: String, action: Selector) -> UIButton {
|
||||
let button = UIButton(type: .system)
|
||||
button.setImage(UIImage(systemName: symbol), for: .normal)
|
||||
button.tintColor = AppColor.primary
|
||||
button.addTarget(self, action: action, for: .touchUpInside)
|
||||
return button
|
||||
}
|
||||
|
||||
@objc private func zoomInTapped() { onZoomIn?() }
|
||||
@objc private func zoomOutTapped() { onZoomOut?() }
|
||||
@objc private func relocateTapped() { onRelocate?() }
|
||||
}
|
||||
|
||||
/// 状态信息卡片。
|
||||
final class LocationReportStatusCardView: UIView {
|
||||
|
||||
var onOnlineStatusTap: (() -> Void)?
|
||||
var onReminderTap: (() -> Void)?
|
||||
|
||||
private let onlineBadge = UILabel()
|
||||
private let countdownLabel = UILabel()
|
||||
private let reminderLabel = UILabel()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .white
|
||||
layer.cornerRadius = 10
|
||||
|
||||
let statusTitle = makeTitle("当前状态")
|
||||
onlineBadge.font = .systemFont(ofSize: 12, weight: .medium)
|
||||
onlineBadge.textAlignment = .center
|
||||
onlineBadge.layer.cornerRadius = 4
|
||||
onlineBadge.clipsToBounds = true
|
||||
onlineBadge.isUserInteractionEnabled = true
|
||||
onlineBadge.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onlineTapped)))
|
||||
|
||||
let countdownTitle = makeTitle("距离下次上报时间")
|
||||
countdownLabel.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
countdownLabel.textColor = AppColor.primary
|
||||
|
||||
let reminderTitle = makeTitle("提醒设置")
|
||||
reminderLabel.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
reminderLabel.textColor = AppColor.primary
|
||||
reminderLabel.isUserInteractionEnabled = true
|
||||
reminderLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(reminderTapped)))
|
||||
|
||||
let stack = UIStackView()
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 12
|
||||
stack.addArrangedSubview(makeRow(left: statusTitle, right: onlineBadge))
|
||||
stack.addArrangedSubview(makeRow(left: countdownTitle, right: countdownLabel))
|
||||
stack.addArrangedSubview(makeRow(left: reminderTitle, right: reminderLabel))
|
||||
addSubview(stack)
|
||||
stack.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(15)
|
||||
}
|
||||
onlineBadge.snp.makeConstraints { make in
|
||||
make.width.greaterThanOrEqualTo(52)
|
||||
make.height.equalTo(28)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func apply(isOnline: Bool, countdown: String, reminderMinutes: Int) {
|
||||
onlineBadge.text = isOnline ? "在线" : "离线"
|
||||
onlineBadge.textColor = isOnline ? AppColor.online : UIColor(hex: 0x7B8EAA)
|
||||
onlineBadge.backgroundColor = isOnline ? UIColor(hex: 0xF0FDF4) : AppColor.inputBackground
|
||||
countdownLabel.text = countdown
|
||||
reminderLabel.text = reminderMinutes == 0 ? "不提醒" : "提前\(reminderMinutes)分钟"
|
||||
}
|
||||
|
||||
@objc private func onlineTapped() { onOnlineStatusTap?() }
|
||||
@objc private func reminderTapped() { onReminderTap?() }
|
||||
|
||||
private func makeTitle(_ text: String) -> UILabel {
|
||||
let label = UILabel()
|
||||
label.text = text
|
||||
label.font = .systemFont(ofSize: 14)
|
||||
label.textColor = AppColor.textPrimary
|
||||
return label
|
||||
}
|
||||
|
||||
private func makeRow(left: UIView, right: UIView) -> UIView {
|
||||
let row = UIStackView(arrangedSubviews: [left, right])
|
||||
row.axis = .horizontal
|
||||
row.distribution = .equalSpacing
|
||||
row.alignment = .center
|
||||
return row
|
||||
}
|
||||
}
|
||||
|
||||
/// 立即上报卡片。
|
||||
final class LocationReportActionCardView: UIView {
|
||||
|
||||
var onReportTap: (() -> Void)?
|
||||
|
||||
private let reportButton = UIButton(type: .system)
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
backgroundColor = .white
|
||||
layer.cornerRadius = 10
|
||||
|
||||
let icon = UIImageView(image: UIImage(systemName: "arrow.up"))
|
||||
icon.tintColor = AppColor.primary
|
||||
let title = UILabel()
|
||||
title.text = "立即上报"
|
||||
title.font = .systemFont(ofSize: 16, weight: .bold)
|
||||
title.textColor = AppColor.textPrimary
|
||||
let subtitle = UILabel()
|
||||
subtitle.text = "您已进入打卡范围"
|
||||
subtitle.font = .systemFont(ofSize: 12)
|
||||
subtitle.textColor = AppColor.textTertiary
|
||||
|
||||
let textStack = UIStackView(arrangedSubviews: [title, subtitle])
|
||||
textStack.axis = .vertical
|
||||
textStack.spacing = 4
|
||||
|
||||
reportButton.backgroundColor = AppColor.primary
|
||||
reportButton.tintColor = .white
|
||||
reportButton.setImage(UIImage(systemName: "location.fill"), for: .normal)
|
||||
reportButton.layer.cornerRadius = 36
|
||||
reportButton.addTarget(self, action: #selector(reportTapped), for: .touchUpInside)
|
||||
|
||||
addSubview(icon)
|
||||
addSubview(textStack)
|
||||
addSubview(reportButton)
|
||||
|
||||
icon.snp.makeConstraints { make in
|
||||
make.leading.equalToSuperview().offset(15)
|
||||
make.centerY.equalToSuperview()
|
||||
make.size.equalTo(24)
|
||||
}
|
||||
textStack.snp.makeConstraints { make in
|
||||
make.leading.equalTo(icon.snp.trailing).offset(8)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
reportButton.snp.makeConstraints { make in
|
||||
make.trailing.equalToSuperview().offset(-15)
|
||||
make.centerY.equalToSuperview()
|
||||
make.size.equalTo(72)
|
||||
}
|
||||
snp.makeConstraints { make in
|
||||
make.height.equalTo(96)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func setReporting(_ reporting: Bool) {
|
||||
reportButton.isEnabled = !reporting
|
||||
reportButton.alpha = reporting ? 0.6 : 1
|
||||
}
|
||||
|
||||
@objc private func reportTapped() { onReportTap?() }
|
||||
}
|
||||
|
||||
/// 底部快捷操作按钮。
|
||||
final class LocationReportBottomActionView: UIControl {
|
||||
|
||||
private let iconView = UIImageView()
|
||||
private let titleLabel = UILabel()
|
||||
|
||||
init(title: String, symbol: String) {
|
||||
super.init(frame: .zero)
|
||||
backgroundColor = .white
|
||||
layer.cornerRadius = 10
|
||||
|
||||
iconView.image = UIImage(systemName: symbol)
|
||||
iconView.tintColor = AppColor.primary
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
|
||||
titleLabel.text = title
|
||||
titleLabel.font = .systemFont(ofSize: 12)
|
||||
titleLabel.textColor = AppColor.textPrimary
|
||||
titleLabel.textAlignment = .center
|
||||
|
||||
let stack = UIStackView(arrangedSubviews: [iconView, titleLabel])
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 4
|
||||
stack.alignment = .center
|
||||
stack.isUserInteractionEnabled = false
|
||||
addSubview(stack)
|
||||
stack.snp.makeConstraints { make in
|
||||
make.center.equalToSuperview()
|
||||
}
|
||||
iconView.snp.makeConstraints { make in
|
||||
make.size.equalTo(24)
|
||||
}
|
||||
snp.makeConstraints { make in
|
||||
make.height.equalTo(80)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override var isSelected: Bool {
|
||||
didSet { applySelectedStyle() }
|
||||
}
|
||||
|
||||
func applyOnlineStyle(isOnline: Bool) {
|
||||
if isOnline {
|
||||
backgroundColor = AppColor.online
|
||||
iconView.tintColor = .white
|
||||
titleLabel.text = "在线"
|
||||
titleLabel.textColor = .white
|
||||
} else {
|
||||
backgroundColor = .white
|
||||
iconView.tintColor = UIColor(hex: 0x7B8EAA)
|
||||
titleLabel.text = "离线"
|
||||
titleLabel.textColor = UIColor(hex: 0x7B8EAA)
|
||||
}
|
||||
}
|
||||
|
||||
private func applySelectedStyle() {
|
||||
guard titleLabel.text == "标记上报" else { return }
|
||||
if isSelected {
|
||||
backgroundColor = AppColor.online
|
||||
iconView.tintColor = UIColor(hex: 0xF0FDF4)
|
||||
titleLabel.textColor = UIColor(hex: 0xF0FDF4)
|
||||
} else {
|
||||
backgroundColor = .white
|
||||
iconView.tintColor = AppColor.primary
|
||||
titleLabel.textColor = AppColor.textPrimary
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 地图容器,封装 MKMapView。
|
||||
final class LocationReportMapView: UIView, MKMapViewDelegate {
|
||||
|
||||
var onMapTap: ((CLLocationCoordinate2D) -> Void)?
|
||||
|
||||
let mapView = MKMapView()
|
||||
private var markerAnnotation: MKPointAnnotation?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
mapView.delegate = self
|
||||
mapView.showsUserLocation = true
|
||||
mapView.showsCompass = false
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
|
||||
mapView.addGestureRecognizer(tap)
|
||||
addSubview(mapView)
|
||||
mapView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func centerOnUserLocation() {
|
||||
guard let coordinate = mapView.userLocation.location?.coordinate else { return }
|
||||
setCenter(coordinate)
|
||||
}
|
||||
|
||||
func setCenter(_ coordinate: CLLocationCoordinate2D, animated: Bool = true) {
|
||||
let region = MKCoordinateRegion(center: coordinate, latitudinalMeters: 800, longitudinalMeters: 800)
|
||||
mapView.setRegion(region, animated: animated)
|
||||
}
|
||||
|
||||
func zoomIn() {
|
||||
var region = mapView.region
|
||||
region.span.latitudeDelta *= 0.5
|
||||
region.span.longitudeDelta *= 0.5
|
||||
mapView.setRegion(region, animated: true)
|
||||
}
|
||||
|
||||
func zoomOut() {
|
||||
var region = mapView.region
|
||||
region.span.latitudeDelta = min(region.span.latitudeDelta * 2, 180)
|
||||
region.span.longitudeDelta = min(region.span.longitudeDelta * 2, 180)
|
||||
mapView.setRegion(region, animated: true)
|
||||
}
|
||||
|
||||
func updateMarker(latitude: Double?, longitude: Double?) {
|
||||
if let annotation = markerAnnotation {
|
||||
mapView.removeAnnotation(annotation)
|
||||
markerAnnotation = nil
|
||||
}
|
||||
guard let latitude, let longitude else { return }
|
||||
let annotation = MKPointAnnotation()
|
||||
annotation.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
|
||||
markerAnnotation = annotation
|
||||
mapView.addAnnotation(annotation)
|
||||
}
|
||||
|
||||
@objc private func handleTap(_ gesture: UITapGestureRecognizer) {
|
||||
let point = gesture.location(in: mapView)
|
||||
let coordinate = mapView.convert(point, toCoordinateFrom: mapView)
|
||||
onMapTap?(coordinate)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user