jsdw_ios/QuickLocation/Section/Home/TodayTrackDetail/TodayTrackDetailVC.swift

285 lines
10 KiB
Swift

//
// TodayTrackDetailVC.swift
// QuickLocation
//
import UIKit
import RxSwift
import RxCocoa
import AMapNaviKit
import SwiftDate
import ObjectMapper
final class TodayTrackDetailVC: BaseViewController {
private var rootView: TodayTrackDetailView!
private let viewModel: TodayTrackDetailViewModel
private var routeOverlays: [MAPolyline] = []
private var panStartHeight: CGFloat = 0
private var stayList: [StayPoint] = []
init(members: [GroupMemberModel], selectedUserId: String) {
self.viewModel = TodayTrackDetailViewModel(members: members, selectedUserId: selectedUserId)
super.init(nibName: nil, bundle: nil)
}
convenience init(userInfo: [String: Any]) {
let rawMembers = userInfo["members"]
let membersJson: [[String: Any]]
if let arr = rawMembers as? [[String: Any]] {
membersJson = arr
} else if let arr = rawMembers as? [Any] {
membersJson = arr.compactMap { $0 as? [String: Any] }
} else {
membersJson = []
}
let members = membersJson.compactMap { GroupMemberModel(JSON: $0) }
let selected = (userInfo["userId"] as? String) ?? members.first?.user_id ?? ""
self.init(members: members, selectedUserId: selected)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func loadView() {
rootView = TodayTrackDetailView(frame: UIScreen.main.bounds)
view = rootView
}
override func viewDidLoad() {
super.viewDidLoad()
rootView.mapView.delegate = self
rootView.memberCV.dataSource = self
rootView.memberCV.delegate = self
rootView.dateCV.dataSource = self
rootView.dateCV.delegate = self
rootView.timelineTV.dataSource = self
rootView.timelineTV.delegate = self
bindActions()
bindSheetPan()
rootView.memberCV.reloadData()
rootView.dateCV.reloadData()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if isMovingFromParent || isBeingDismissed {
rootView.cleanupMap()
}
}
private func bindActions() {
rootView.backBtn.rx.tap
.subscribe(onNext: { [weak self] in
self?.navigationController?.popViewController(animated: true)
})
.disposed(by: disposeBag)
viewModel.trajectoryPoints
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] points in
self?.drawRoute(points)
})
.disposed(by: disposeBag)
viewModel.isEmpty
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] empty in
self?.rootView.emptyLab.isHidden = !empty
})
.disposed(by: disposeBag)
viewModel.stayPoints
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] stays in
self?.stayList = stays
self?.rootView.timelineTV.reloadData()
})
.disposed(by: disposeBag)
viewModel.selectedMemberId
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] _ in
self?.rootView.memberCV.reloadData()
})
.disposed(by: disposeBag)
viewModel.selectedDateKey
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] _ in
self?.rootView.dateCV.reloadData()
})
.disposed(by: disposeBag)
}
private func bindSheetPan() {
let pan = UIPanGestureRecognizer()
rootView.sheetView.addGestureRecognizer(pan)
pan.rx.event.subscribe(onNext: { [weak self] gesture in
guard let self, let constraint = self.rootView.sheetHeightConstraint else { return }
let minH: CGFloat = 220 + kSafeBottomMargin
let maxH: CGFloat = UIScreen.main.bounds.height * 0.7
switch gesture.state {
case .began:
self.panStartHeight = constraint.constant
case .changed:
let dy = gesture.translation(in: self.rootView).y
constraint.constant = min(maxH, max(minH, self.panStartHeight - dy))
case .ended, .cancelled:
let mid = (minH + maxH) / 2
let target = constraint.constant > mid ? maxH : minH + 100
UIView.animate(withDuration: 0.25) {
constraint.constant = target
self.rootView.layoutIfNeeded()
}
default:
break
}
}).disposed(by: disposeBag)
}
private func drawRoute(_ points: [TrackPoint]) {
rootView.mapView.removeOverlays(routeOverlays)
routeOverlays.removeAll()
if let anns = rootView.mapView.annotations {
rootView.mapView.removeAnnotations(anns)
}
let coords = points
.map { CLLocationCoordinate2D(latitude: $0.lat, longitude: $0.lng) }
.filter { CLLocationCoordinate2DIsValid($0) }
guard coords.count >= 2 else {
if let first = coords.first {
rootView.mapView.setCenter(first, animated: true)
}
return
}
var mutable = coords
if let polyline = MAPolyline(coordinates: &mutable, count: UInt(mutable.count)) {
rootView.mapView.add(polyline)
routeOverlays.append(polyline)
let padding = UIEdgeInsets(top: 160, left: 40, bottom: 360, right: 40)
rootView.mapView.showOverlays(routeOverlays, edgePadding: padding, animated: true)
}
if let start = coords.first {
let ann = MAPointAnnotation()
ann.coordinate = start
ann.title = ""
rootView.mapView.addAnnotation(ann)
}
}
private static func stayTitle(_ stay: StayPoint, isCurrent: Bool) -> String {
let addr = stay.address.isEmpty ? "**" : stay.address
if isCurrent {
return "当前 \(addr)"
}
let time = Date(timeIntervalSince1970: TimeInterval(stay.start_time)).toFormat("HH:mm")
return "\(time) \(addr)"
}
private static func stayTags(_ stay: StayPoint, isCurrent: Bool) -> [String] {
let seconds = max(0, Int(stay.end_time - stay.start_time))
let hours = seconds / 3600
let minutes = (seconds % 3600) / 60
let duration: String
if hours > 0 {
duration = "停留 \(hours)小时\(minutes)"
} else {
duration = "停留 \(minutes)"
}
if isCurrent {
return [duration, "停留中..."]
}
return [duration]
}
}
// MARK: - Collection / Table
extension TodayTrackDetailVC: UICollectionViewDataSource, UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if collectionView === rootView.memberCV {
return viewModel.members.count
}
return viewModel.dateItems.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if collectionView === rootView.memberCV {
let cell: GroupMemberListCell = collectionView.dequeueReusableCell(for: indexPath)
let model = viewModel.members[indexPath.item]
let selected = model.user_id == viewModel.selectedMemberId.value
let isMe = model.user_id == AppContextManager.shared.userId
cell.configure(model: model, isCurrentUser: isMe, isSelected: selected)
return cell
}
let cell: TodayTrackDateCell = collectionView.dequeueReusableCell(for: indexPath)
let item = viewModel.dateItems[indexPath.item]
let selected = item.key == viewModel.selectedDateKey.value
cell.configure(title: item.title, selected: selected)
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if collectionView === rootView.memberCV {
let model = viewModel.members[indexPath.item]
viewModel.selectedMemberId.accept(model.user_id)
return
}
let item = viewModel.dateItems[indexPath.item]
viewModel.selectedDateKey.accept(item.key)
}
}
extension TodayTrackDetailVC: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
stayList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: TodayTrackStayCell = tableView.dequeueReusableCell(for: indexPath)
let stay = stayList[indexPath.row]
let isFirst = indexPath.row == 0
let isLast = indexPath.row == stayList.count - 1
cell.configure(
title: Self.stayTitle(stay, isCurrent: isFirst),
tags: Self.stayTags(stay, isCurrent: isFirst),
isFirst: isFirst,
isLast: isLast
)
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let stay = stayList[indexPath.row]
let coord = CLLocationCoordinate2D(latitude: stay.lat, longitude: stay.lng)
guard CLLocationCoordinate2DIsValid(coord) else { return }
rootView.mapView.setCenter(coord, animated: true)
rootView.mapView.setZoomLevel(16, animated: true)
}
}
extension TodayTrackDetailVC: MAMapViewDelegate {
func mapView(_ mapView: MAMapView!, rendererFor overlay: MAOverlay!) -> MAOverlayRenderer! {
guard let polyline = overlay as? MAPolyline else { return nil }
let renderer = MAPolylineRenderer(polyline: polyline)
renderer?.strokeColor = UIColor(hexStr: "#5EC8FF")
renderer?.lineWidth = 6
return renderer
}
func mapView(_ mapView: MAMapView!, viewFor annotation: MAAnnotation!) -> MAAnnotationView! {
guard !(annotation is MAUserLocation) else { return nil }
let id = "todayTrack.ann"
var view = mapView.dequeueReusableAnnotationView(withIdentifier: id)
if view == nil {
view = MAAnnotationView(annotation: annotation, reuseIdentifier: id)
}
view?.annotation = annotation
view?.image = UIImage(systemName: "mappin.circle.fill")
view?.centerOffset = CGPoint(x: 0, y: -12)
return view
}
}