jsdw_ios/QuickLocation/Section/Schedule/CreateSchedule/CreateScheduleVC.swift

495 lines
20 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// CreateScheduleVC.swift
// QuickLocation
//
// Created by on 2026/6/23.
//
import UIKit
import RxSwift
import RxCocoa
import RxDataSources
import SwiftyUserDefaults
import BRPickerView
import RxGesture
#if !targetEnvironment(simulator)
import AMapNaviKit
import AMapSearchKit
import TagListView
import ObjectMapper
#endif
class CreateScheduleVC: BaseViewController, MAMapViewDelegate {
override var isNavigationBarHidden: Bool { true }
fileprivate var rootView: CreateScheduleView!
private let viewModel: CreateScheduleVM
private var popView: CreateSchedulePopView { rootView.createSchedulePopView }
override func loadView() {
rootView = CreateScheduleView(frame: UIScreen.main.bounds)
view = rootView
}
private var groupList: [GroupInfoModel] = []
override func viewDidLoad() {
super.viewDidLoad()
popView.tagListView.delegate = self
setupMap()
bindViewModel()
reactiveAction()
requestGroupInfo()
guard let _ = viewModel.scheduModel else { return }
rootView.navTitleLabel.text = "编辑行程"
rootView.deleteBtn.isHidden = false
// requestFollowList(id: model.id)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if isMovingFromParent || isBeingDismissed {
#if !targetEnvironment(simulator)
rootView.cleanupMap()
#endif
}
}
// MARK: - Actions
private func reactiveAction() {
rootView.deleteBtn.rx.tap.subscribe(onNext: { _ in
guard let model = self.viewModel.scheduModel else { return }
self.showConfirmPop(title: "确定要删除吗?",
message: "此条行程路线将被永久删除",
confirmText: "删除",
confirmBlock: {
self.requestDelete(id: model.id)
}, cancelText: "取消")
}).disposed(by: disposeBag)
}
// MARK: - API
private func requestFollowList(id: String) {
dl.showLoading()
ItineraryService.queryFollowList(id: id).subscribe { response in
self.dl.dismiss()
}.disposed(by: disposeBag)
}
private func requestGroupInfo() {
GroupService.groupInfo().subscribe { response in
guard let model = response.model else { return }
self.groupList = model.groups
self.popView.setupTagData(model.groups)
//
#if !targetEnvironment(simulator)
if !self.viewModel.selectedGroupKeys.isEmpty {
for (i, group) in self.groupList.enumerated() {
guard i < self.popView.tagListView.tagViews.count,
self.viewModel.selectedGroupKeys.contains(group.group_key) else { continue }
self.popView.tagListView.tagViews[i].isSelected = true
}
}
#endif
}.disposed(by: disposeBag)
}
private func requestSet(id: String="", points: [[String: Any]]) {
let selectedDate = Int64(viewModel.selectedDate.value.timeIntervalSince1970 * 1000)
dl.showLoading()
ItineraryService.set(id: id,
group_keys: viewModel.selectedGroupKeys,
timestamp: selectedDate,
points: points).subscribe(onNext: { response in
self.dl.dismiss()
self.dl.show(text: id.isEmpty ? "创建成功" : "更新成功") {
AppRouter.shared.popOrDismiss()
}
}, onError: { error in
guard let code = error.underlyingError?.code else { return }
if code == 20010 { // ""
CreateScheduleVipPopView.show()
}
else {
self.dl.show(text: error.localizedDescription)
}
}).disposed(by: disposeBag)
}
private func requestDelete(id: String) {
dl.showLoading()
ItineraryService.delete(id: id).subscribe { response in
self.dl.dismiss()
self.dl.show(text: "删除成功") {
AppRouter.shared.popOrDismiss()
}
}.disposed(by: disposeBag)
}
// MARK: - Binding
private lazy var dataSource: RxTableViewSectionedReloadDataSource<SchedulePointSection> = {
RxTableViewSectionedReloadDataSource<SchedulePointSection>(
configureCell: { [weak self] _, tv, indexPath, item in
let cell: SchedulePointCell = tv.dequeueReusableCell(for: indexPath)
cell.configure(item: item, index: indexPath.row,
total: self?.viewModel.pointsRelay.value.count ?? 0,
onDelete: { [weak self] in
self?.viewModel.deletePointAt.onNext(indexPath.row)
})
//
cell.onTimeTap = { [weak self] in
guard let self = self else { return }
let picker = BRDatePickerView(pickerMode: .HM)
picker.title = "选择到达时间"
let style = BRPickerStyle()
style.selectRowTextColor = UIColor(hexStr: "#16B3FF")
picker.pickerStyle = style
picker.resultBlock = { date, _ in
guard let d = date else { return }
var list = self.viewModel.pointsRelay.value
guard indexPath.row < list.count else { return }
// +
let cal = Calendar.current
let today = Date()
var comps = cal.dateComponents([.year, .month, .day], from: today)
let timeComps = cal.dateComponents([.hour, .minute], from: d)
comps.hour = timeComps.hour
comps.minute = timeComps.minute
if let merged = cal.date(from: comps) {
list[indexPath.row].expectedTime = merged
self.viewModel.pointsRelay.accept(list)
}
}
picker.show()
}
//
cell.onLocationTap = { [weak self] in
guard let self = self else { return }
let coord = item.latitude != 0 || item.longitude != 0
? CLLocationCoordinate2D(latitude: item.latitude, longitude: item.longitude)
: kCLLocationCoordinate2DInvalid
let vc = LocationPickerVC()
vc.modalPresentationStyle = .fullScreen
if !item.locationName.isEmpty {
vc.initialLocation = PickedLocation(
name: item.locationName, address: item.address,
coordinate: coord,
province: item.province, city: item.city,
district: item.district, street: item.street,
country: item.country, formatted_address: item.formatted_address
)
}
vc.onPickedLocation = { picked in
var list = self.viewModel.pointsRelay.value
guard indexPath.row < list.count else { return }
list[indexPath.row].locationName = picked.name
list[indexPath.row].address = picked.address
list[indexPath.row].latitude = picked.coordinate.latitude
list[indexPath.row].longitude = picked.coordinate.longitude
list[indexPath.row].province = picked.province
list[indexPath.row].city = picked.city
list[indexPath.row].district = picked.district
list[indexPath.row].street = picked.street
list[indexPath.row].country = picked.country
list[indexPath.row].formatted_address = picked.formatted_address
self.viewModel.pointsRelay.accept(list)
}
self.present(vc, animated: true)
}
//
cell.remarkTF.rx.controlEvent(.editingDidEnd)
.subscribe(onNext: {
guard let self = self, let text = cell.remarkTF.text else { return }
var list = self.viewModel.pointsRelay.value
guard indexPath.row < list.count else { return }
list[indexPath.row].remark = text
self.viewModel.pointsRelay.accept(list)
})
.disposed(by: cell.disposeBag)
return cell
})
}()
fileprivate func bindViewModel() {
//
viewModel.dateString
.bind(to: popView.dateLab.rx.text)
.disposed(by: disposeBag)
// tableView
viewModel.pointsRelay
.map { [SchedulePointSection(model: "", items: $0)] }
.bind(to: popView.tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
// + 线
viewModel.pointsRelay
.observe(on: MainScheduler.asyncInstance)
.subscribe(onNext: { [weak self] items in
guard let self = self else { return }
let h = max(CGFloat(items.count) * 122, 122)
self.popView.tableView.layoutChain.height(h)
self.refreshMapPoints()
})
.disposed(by: disposeBag)
//
popView.addBtn.rx.tap
.bind(to: viewModel.addPointTapped)
.disposed(by: disposeBag)
//
popView.dateLab.rx.tapGesture
.when(.recognized)
.subscribe(onNext: { [weak self] _ in
self?.showDatePicker()
})
.disposed(by: disposeBag)
//
popView.createBtn.rx.tap
.subscribe(onNext: { [weak self] _ in
self?.handleCreate()
})
.disposed(by: disposeBag)
}
private func handleCreate() {
let points = viewModel.pointsRelay.value
let hasLocation = points.filter { $0.latitude != 0 || $0.longitude != 0 }
let hasTime = points.filter { $0.expectedTime != nil }
//
guard points.count >= 2 else { DLToast.show(text: "至少需要两个行程点"); return }
guard hasLocation.count == points.count else { DLToast.show(text: "请为每个行程点选择地点"); return }
guard hasTime.count == points.count else { DLToast.show(text: "请为每个行程点选择到达时间"); return }
guard !viewModel.selectedGroupKeys.isEmpty else { DLToast.show(text: "请选择分享的圈子"); return }
// points
let pointsJSON: [[String: Any]] = points.map { p in
let expectedTs = p.expectedTime.map { Int64($0.timeIntervalSince1970 * 1000) } ?? 0
return [
"point": ["lat": p.latitude, "lng": p.longitude],
"address": [
"formatted_address": p.formatted_address,
"country": p.country,
"province": p.province,
"city": p.city,
"district": p.district,
"street": p.street
],
"expected_timestamp": expectedTs,
"remark": p.remark
]
}
requestSet(id: viewModel.scheduModel?.id ?? "", points: pointsJSON)
}
private func setupMap() {
#if !targetEnvironment(simulator)
rootView.mapView.delegate = self
rootView.mapView.showsUserLocation = false
if let lat = Defaults[\.currentLatitude], let lon = Defaults[\.currentLongitude] {
let coord = CLLocationCoordinate2D(latitude: lat, longitude: lon)
if CLLocationCoordinate2DIsValid(coord) {
rootView.mapView.setCenter(coord, animated: false)
rootView.mapView.setZoomLevel(18, animated: false)
}
}
#endif
}
// MARK: - Date Picker
private func showDatePicker() {
let picker = BRDatePickerView(pickerMode: .YMD)
picker.minDate = Date()
picker.maxDate = Calendar.current.date(byAdding: .day, value: 7, to: Date())
let style = BRPickerStyle()
style.selectRowTextColor = UIColor(hexStr: "#16B3FF")
picker.pickerStyle = style
picker.resultBlock = { [weak self] date, _ in
if let d = date { self?.viewModel.selectedDate.accept(d) }
}
picker.show()
}
// MARK: - 线
#if !targetEnvironment(simulator)
private let routeSearch = AMapSearchAPI()
#endif
private var routeOverlays: [MAPolyline] = []
private var pointAnnotations: [MAPointAnnotation] = []
/// refreshMapPoints
private var pendingRoutePoints: [CLLocationCoordinate2D] = []
private func refreshMapPoints() {
let points = viewModel.pointsRelay.value
#if !targetEnvironment(simulator)
routeSearch?.delegate = self
// 线
for ann in pointAnnotations { rootView.mapView.removeAnnotation(ann) }
for ol in routeOverlays { rootView.mapView.remove(ol) }
pointAnnotations.removeAll()
routeOverlays.removeAll()
//
let validPoints = points.filter { $0.latitude != 0 || $0.longitude != 0 }
for (i, p) in validPoints.enumerated() {
let ann = MAPointAnnotation()
ann.coordinate = CLLocationCoordinate2D(latitude: p.latitude, longitude: p.longitude)
ann.title = "\(i + 1)"
rootView.mapView.addAnnotation(ann)
pointAnnotations.append(ann)
}
// 线
if validPoints.count >= 2 {
pendingRoutePoints = validPoints.map { CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude) }
requestRoute()
}
//
if !pointAnnotations.isEmpty {
rootView.mapView.showAnnotations(pointAnnotations, animated: true)
}
#endif
}
#if !targetEnvironment(simulator)
private func requestRoute() {
guard pendingRoutePoints.count >= 2 else { return }
let request = AMapDrivingRouteSearchRequest()
request.origin = AMapGeoPoint.location(withLatitude: CGFloat(pendingRoutePoints[0].latitude), longitude: CGFloat(pendingRoutePoints[0].longitude))
request.destination = AMapGeoPoint.location(withLatitude: CGFloat(pendingRoutePoints.last!.latitude), longitude: CGFloat(pendingRoutePoints.last!.longitude))
//
if pendingRoutePoints.count > 2 {
var waypoints: [AMapGeoPoint] = []
for i in 1..<pendingRoutePoints.count - 1 {
let p = pendingRoutePoints[i]
if let wp = AMapGeoPoint.location(withLatitude: CGFloat(p.latitude), longitude: CGFloat(p.longitude)) {
waypoints.append(wp)
}
}
request.waypoints = waypoints
}
request.strategy = 0
// request.city = ""
routeSearch?.aMapDrivingRouteSearch(request)
}
#endif
///
private static func numberImage(_ num: Int) -> UIImage? {
let size = CGSize(width: 20, height: 20)
let rect = CGRect(origin: .zero, size: size)
UIGraphicsBeginImageContextWithOptions(size, false, 0)
guard let ctx = UIGraphicsGetCurrentContext() else { return nil }
//
ctx.setLineWidth(1)
ctx.setStrokeColor(UIColor.white.cgColor)
//
ctx.setFillColor(UIColor(hexStr: "#16B3FF").cgColor)
let path = UIBezierPath(ovalIn: rect)
path.fill()
path.stroke()
//
let text = "\(num)" as NSString
let attrs: [NSAttributedString.Key: Any] = [.font: UIFont.boldSystemFont(ofSize: 11), .foregroundColor: UIColor.white]
let strSize = text.size(withAttributes: attrs)
text.draw(at: CGPoint(x: (size.width - strSize.width) / 2, y: (size.height - strSize.height) / 2))
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img
}
// MARK: - Map
func mapView(_ mapView: MAMapView!, viewFor annotation: MAAnnotation!) -> MAAnnotationView! {
guard !(annotation is MAUserLocation), let pointAnn = annotation as? MAPointAnnotation else { return nil }
if let num = Int(pointAnn.title ?? "") {
let id = "PointPin"
var view = mapView.dequeueReusableAnnotationView(withIdentifier: id)
if view == nil { view = MAAnnotationView(annotation: annotation, reuseIdentifier: id) }
else { view?.annotation = annotation }
view?.image = Self.numberImage(num)
view?.centerOffset = CGPoint(x: 0, y: -15)
return view
}
return nil
}
func mapView(_ mapView: MAMapView!, rendererFor overlay: MAOverlay!) -> MAOverlayRenderer! {
if let polyline = overlay as? MAPolyline {
let r = MAPolylineRenderer(polyline: polyline)
r?.strokeColor = UIColor(hexStr: "#16B3FF")
r?.lineWidth = 3
r?.lineDashType = kMALineDashTypeSquare
return r
}
return nil
}
// MARK: - Init
init(routeId: String, scheduleJson: [String: Any]) {
let model = ScheduleModel.init(JSON: scheduleJson)
self.viewModel = CreateScheduleVM(scheduleJson.isEmpty ? nil : model)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - TagListViewDelegate
extension CreateScheduleVC: TagListViewDelegate {
func tagPressed(_ title: String, tagView: TagView, sender: TagListView) {
tagView.isSelected = !tagView.isSelected
// tagView tagViews group_key
guard let idx = sender.tagViews.firstIndex(of: tagView),
idx < groupList.count else { return }
let key = groupList[idx].group_key
viewModel.toggleGroupKey(key)
print("📋 selectedGroupKeys: \(viewModel.selectedGroupKeys)")
}
}
#if !targetEnvironment(simulator)
// MARK: - AMapSearchDelegate
extension CreateScheduleVC: AMapSearchDelegate {
func onRouteSearchDone(_ request: AMapRouteSearchBaseRequest!, response: AMapRouteSearchResponse!) {
guard let path = response.route?.paths?.first as? AMapPath else { return }
var coords: [CLLocationCoordinate2D] = []
for step in path.steps {
guard let polylineStr = step.polyline else { continue }
for point in polylineStr.components(separatedBy: ";") {
let latLon = point.components(separatedBy: ",")
if latLon.count == 2, let lon = Double(latLon[0]), let lat = Double(latLon[1]) {
coords.append(CLLocationCoordinate2D(latitude: lat, longitude: lon))
}
}
}
guard coords.count > 1 else { return }
var mutableCoords = coords
if let polyline = MAPolyline(coordinates: &mutableCoords, count: UInt(coords.count)) {
rootView.mapView.add(polyline)
routeOverlays.append(polyline)
}
}
func aMapSearchRequest(_ request: Any!, didFailWithError error: Error!) {
print("Route error: \(error.localizedDescription)")
}
}
#endif