285 lines
10 KiB
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
|
|
}
|
|
}
|