494 lines
16 KiB
Swift
494 lines
16 KiB
Swift
//
|
|
// LocationReportViews.swift
|
|
// suixinkan
|
|
//
|
|
|
|
import CoreLocation
|
|
import SnapKit
|
|
import UIKit
|
|
|
|
#if targetEnvironment(simulator)
|
|
import MapKit
|
|
#endif
|
|
|
|
/// 地图缩放/定位控件。
|
|
final class LocationReportMapControlStack: UIView {
|
|
|
|
var onZoomIn: (() -> Void)?
|
|
var onZoomOut: (() -> Void)?
|
|
var onRelocate: (() -> Void)?
|
|
private weak var relocateButton: UIButton?
|
|
|
|
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))
|
|
relocateButton = relocate
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 更新定位按钮是否可用。
|
|
func setRelocateEnabled(_ isEnabled: Bool) {
|
|
relocateButton?.isEnabled = isEnabled
|
|
relocateButton?.alpha = isEnabled ? 1 : 0.46
|
|
}
|
|
|
|
@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
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 位置上报地图容器,模拟器使用 MapKit。
|
|
#if targetEnvironment(simulator)
|
|
final class LocationReportMapView: UIView, MKMapViewDelegate {
|
|
|
|
var onMapTap: ((CLLocationCoordinate2D) -> Void)?
|
|
|
|
let mapView: MKMapView
|
|
private var markerAnnotation: MKPointAnnotation?
|
|
private var shouldCenterOnNextUserLocation = false
|
|
|
|
override init(frame: CGRect) {
|
|
mapView = MKMapView(frame: .zero)
|
|
super.init(frame: frame)
|
|
mapView.delegate = self
|
|
mapView.showsUserLocation = true
|
|
mapView.showsCompass = false
|
|
addSubview(mapView)
|
|
mapView.snp.makeConstraints { make in
|
|
make.edges.equalToSuperview()
|
|
}
|
|
|
|
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(mapTapped(_:)))
|
|
tapGesture.cancelsTouchesInView = false
|
|
mapView.addGestureRecognizer(tapGesture)
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
/// 进入页面后,在首次拿到用户位置时将地图居中。
|
|
func enableInitialUserLocationCentering() {
|
|
shouldCenterOnNextUserLocation = true
|
|
centerOnUserLocationIfNeeded()
|
|
}
|
|
|
|
func centerOnUserLocation() {
|
|
guard let coordinate = validUserCoordinate else { return }
|
|
setCenter(coordinate)
|
|
}
|
|
|
|
func setCenter(_ coordinate: CLLocationCoordinate2D, animated: Bool = true) {
|
|
let region = MKCoordinateRegion(
|
|
center: coordinate,
|
|
span: MKCoordinateSpan(latitudeDelta: 0.012, longitudeDelta: 0.012)
|
|
)
|
|
mapView.setRegion(region, animated: animated)
|
|
}
|
|
|
|
func zoomIn() {
|
|
updateZoom(scale: 0.5)
|
|
}
|
|
|
|
func zoomOut() {
|
|
updateZoom(scale: 2)
|
|
}
|
|
|
|
func updateMarker(latitude: Double?, longitude: Double?) {
|
|
if let markerAnnotation {
|
|
mapView.removeAnnotation(markerAnnotation)
|
|
self.markerAnnotation = nil
|
|
}
|
|
guard let latitude, let longitude else { return }
|
|
let annotation = MKPointAnnotation()
|
|
annotation.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
|
|
markerAnnotation = annotation
|
|
mapView.addAnnotation(annotation)
|
|
}
|
|
|
|
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
|
|
centerOnUserLocationIfNeeded()
|
|
}
|
|
|
|
@objc private func mapTapped(_ gesture: UITapGestureRecognizer) {
|
|
guard gesture.state == .ended else { return }
|
|
let point = gesture.location(in: mapView)
|
|
onMapTap?(mapView.convert(point, toCoordinateFrom: mapView))
|
|
}
|
|
|
|
private var validUserCoordinate: CLLocationCoordinate2D? {
|
|
guard let coordinate = mapView.userLocation.location?.coordinate,
|
|
CLLocationCoordinate2DIsValid(coordinate),
|
|
coordinate.latitude != 0 || coordinate.longitude != 0 else {
|
|
return nil
|
|
}
|
|
return coordinate
|
|
}
|
|
|
|
private func centerOnUserLocationIfNeeded() {
|
|
guard shouldCenterOnNextUserLocation, let coordinate = validUserCoordinate else { return }
|
|
setCenter(coordinate, animated: false)
|
|
shouldCenterOnNextUserLocation = false
|
|
}
|
|
|
|
private func updateZoom(scale: Double) {
|
|
let current = mapView.region
|
|
let span = MKCoordinateSpan(
|
|
latitudeDelta: min(max(current.span.latitudeDelta * scale, 0.0005), 120),
|
|
longitudeDelta: min(max(current.span.longitudeDelta * scale, 0.0005), 120)
|
|
)
|
|
mapView.setRegion(MKCoordinateRegion(center: current.center, span: span), animated: true)
|
|
}
|
|
}
|
|
#else
|
|
/// 位置上报地图容器,真机封装高德 `MAMapView`。
|
|
final class LocationReportMapView: UIView, MAMapViewDelegate {
|
|
|
|
var onMapTap: ((CLLocationCoordinate2D) -> Void)?
|
|
|
|
let mapView: MAMapView
|
|
private var markerAnnotation: MAPointAnnotation?
|
|
private var shouldCenterOnNextUserLocation = false
|
|
|
|
override init(frame: CGRect) {
|
|
_ = AMapBootstrap.configureIfNeeded()
|
|
mapView = MAMapView(frame: .zero)
|
|
super.init(frame: frame)
|
|
mapView.delegate = self
|
|
mapView.showsUserLocation = true
|
|
mapView.showsCompass = false
|
|
mapView.zoomLevel = 15
|
|
addSubview(mapView)
|
|
mapView.snp.makeConstraints { make in
|
|
make.edges.equalToSuperview()
|
|
}
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
/// 进入页面后,在首次拿到用户位置时将地图居中(不持续跟随,便于手动拖动)。
|
|
func enableInitialUserLocationCentering() {
|
|
shouldCenterOnNextUserLocation = true
|
|
centerOnUserLocationIfNeeded()
|
|
}
|
|
|
|
func centerOnUserLocation() {
|
|
guard let coordinate = validUserCoordinate else { return }
|
|
setCenter(coordinate)
|
|
}
|
|
|
|
func setCenter(_ coordinate: CLLocationCoordinate2D, animated: Bool = true) {
|
|
mapView.setCenter(coordinate, animated: animated)
|
|
mapView.setZoomLevel(15, animated: animated)
|
|
}
|
|
|
|
func zoomIn() {
|
|
mapView.setZoomLevel(mapView.zoomLevel + 1, animated: true)
|
|
}
|
|
|
|
func zoomOut() {
|
|
mapView.setZoomLevel(mapView.zoomLevel - 1, 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 = MAPointAnnotation()
|
|
annotation.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
|
|
markerAnnotation = annotation
|
|
mapView.addAnnotation(annotation)
|
|
}
|
|
|
|
func mapView(_ mapView: MAMapView!, didUpdate userLocation: MAUserLocation!, updatingLocation: Bool) {
|
|
guard updatingLocation else { return }
|
|
centerOnUserLocationIfNeeded()
|
|
}
|
|
|
|
func mapView(_ mapView: MAMapView!, didSingleTappedAt coordinate: CLLocationCoordinate2D) {
|
|
onMapTap?(coordinate)
|
|
}
|
|
|
|
private var validUserCoordinate: CLLocationCoordinate2D? {
|
|
let coordinate = mapView.userLocation.location?.coordinate
|
|
guard let coordinate, CLLocationCoordinate2DIsValid(coordinate) else { return nil }
|
|
guard coordinate.latitude != 0 || coordinate.longitude != 0 else { return nil }
|
|
return coordinate
|
|
}
|
|
|
|
private func centerOnUserLocationIfNeeded() {
|
|
guard shouldCenterOnNextUserLocation, let coordinate = validUserCoordinate else { return }
|
|
setCenter(coordinate, animated: false)
|
|
shouldCenterOnNextUserLocation = false
|
|
}
|
|
}
|
|
#endif
|