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

783 lines
31 KiB
Swift
Raw 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
import CoreLocation
import TagListView
import ObjectMapper
#if !targetEnvironment(simulator)
import AMapNaviKit
import AMapSearchKit
#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.navView.titleLabel.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
self.dl.show(error)
}).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, row in
switch row {
case .add:
let cell: ScheduleAddPointCell = tv.dequeueReusableCell(for: indexPath)
cell.onAdd = { [weak self] in
self?.viewModel.addPointTapped.onNext(())
}
return cell
case .point(let pointIndex, let item, let role):
let cell: SchedulePointCell = tv.dequeueReusableCell(for: indexPath)
cell.configure(item: item, role: role)
cell.onDelete = { [weak self] in
self?.viewModel.deletePointAt.onNext(pointIndex)
}
cell.onDragGesture = { [weak self] gesture in
self?.handleMiddlePointDrag(gesture, pointIndex: pointIndex, cell: cell)
}
cell.onTimeTap = { [weak self] in
self?.showTimePicker(for: pointIndex)
}
cell.onLocationTap = { [weak self] in
self?.showLocationPicker(for: pointIndex, item: item)
}
cell.remarkTextField?.rx.controlEvent(.editingDidEnd)
.subscribe(onNext: { [weak self, weak cell] in
guard let self = self, let text = cell?.remarkTextField?.text else { return }
var list = self.viewModel.pointsRelay.value
guard pointIndex < list.count else { return }
list[pointIndex].remark = text
self.viewModel.pointsRelay.accept(list)
})
.disposed(by: cell.disposeBag)
return cell
}
})
}()
fileprivate func bindViewModel() {
viewModel.dateString
.map { $0 }
.bind(to: popView.dateLab.rx.text)
.disposed(by: disposeBag)
viewModel.listItems
.map { [SchedulePointSection(model: "", items: $0)] }
.bind(to: popView.tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
popView.tableView.rx.setDelegate(self)
.disposed(by: disposeBag)
// 动态高度 + 刷新地图路线
viewModel.pointsRelay
.observe(on: MainScheduler.asyncInstance)
.subscribe(onNext: { [weak self] items in
guard let self = self else { return }
let h = CGFloat(items.count) * ScheduleTimelineMetric.pointRowHeight
+ ScheduleTimelineMetric.addRowHeight
self.popView.tableView.layoutChain.height(h)
self.refreshMapPoints()
})
.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 showTimePicker(for pointIndex: Int) {
view.endEditing(true)
let points = viewModel.pointsRelay.value
guard pointIndex < points.count else { return }
let day = viewModel.selectedDate.value
let bounds = timePickerBounds(for: pointIndex, in: points, on: day)
guard bounds.canSelect else {
DLToast.show(text: "行程时间需满足:开始 < 途径 < 结束")
return
}
let picker = BRDatePickerView(pickerMode: .HM)
picker.title = "选择到达时间"
picker.minDate = bounds.min
picker.maxDate = bounds.max
if let existing = points[pointIndex].expectedTime,
let onDay = combining(timeFrom: existing, on: day) {
picker.selectDate = clamp(onDay, min: bounds.min, max: bounds.max)
} else if let minDate = bounds.min {
picker.selectDate = minDate
} else if let maxDate = bounds.max {
picker.selectDate = maxDate
}
picker.pickerStyle = makeSchedulePickerStyle()
picker.pickerFooterView = makeSchedulePickerFooter { [weak picker] in
picker?.doneBlock?()
picker?.dismiss()
}
picker.resultBlock = { [weak self] date, _ in
guard let self = self, let d = date else { return }
var list = self.viewModel.pointsRelay.value
guard pointIndex < list.count else { return }
let cal = Calendar.current
var comps = cal.dateComponents([.year, .month, .day], from: self.viewModel.selectedDate.value)
let timeComps = cal.dateComponents([.hour, .minute], from: d)
comps.hour = timeComps.hour
comps.minute = timeComps.minute
if let merged = cal.date(from: comps) {
list[pointIndex].expectedTime = merged
self.viewModel.pointsRelay.accept(list)
}
}
showSchedulePicker(picker)
}
/// 当前点必须晚于前方已选时间、早于后方已选时间(开始 < 途径 < 结束)。
private func timePickerBounds(for index: Int, in points: [SchedulePointItem], on day: Date) -> (min: Date?, max: Date?, canSelect: Bool) {
let cal = Calendar.current
let previous = points.prefix(index).reversed().compactMap(\.expectedTime).first
let next = points.dropFirst(index + 1).compactMap(\.expectedTime).first
var minDate: Date?
if let previous, let onDay = combining(timeFrom: previous, on: day) {
guard let candidate = cal.date(byAdding: .minute, value: 1, to: onDay),
cal.isDate(candidate, inSameDayAs: day) else {
return (nil, nil, false)
}
minDate = candidate
}
var maxDate: Date?
if let next, let onDay = combining(timeFrom: next, on: day) {
guard let candidate = cal.date(byAdding: .minute, value: -1, to: onDay),
cal.isDate(candidate, inSameDayAs: day) else {
return (nil, nil, false)
}
maxDate = candidate
}
if let minDate, let maxDate, minDate > maxDate {
return (minDate, maxDate, false)
}
return (minDate, maxDate, true)
}
private func combining(timeFrom source: Date, on day: Date) -> Date? {
let cal = Calendar.current
var comps = cal.dateComponents([.year, .month, .day], from: day)
let time = cal.dateComponents([.hour, .minute], from: source)
comps.hour = time.hour
comps.minute = time.minute
comps.second = 0
return cal.date(from: comps)
}
private func clamp(_ date: Date, min: Date?, max: Date?) -> Date {
if let min, date < min { return min }
if let max, date > max { return max }
return date
}
private func showLocationPicker(for pointIndex: Int, item: SchedulePointItem) {
let coord = item.latitude != 0 || item.longitude != 0
? CLLocationCoordinate2D(latitude: item.latitude, longitude: item.longitude)
: kCLLocationCoordinate2DInvalid
let vc = LocationPickerViewController()
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 = { [weak self] picked in
guard let self = self else { return }
var list = self.viewModel.pointsRelay.value
guard pointIndex < list.count else { return }
list[pointIndex].locationName = picked.name
list[pointIndex].address = picked.address
list[pointIndex].latitude = picked.coordinate.latitude
list[pointIndex].longitude = picked.coordinate.longitude
list[pointIndex].province = picked.province
list[pointIndex].city = picked.city
list[pointIndex].district = picked.district
list[pointIndex].street = picked.street
list[pointIndex].country = picked.country
list[pointIndex].formatted_address = picked.formatted_address
self.viewModel.pointsRelay.accept(list)
}
present(vc, animated: true)
}
// MARK: - Middle point drag reorder
private var dragSnapshot: UIView?
private var dragSourcePointIndex: Int?
private var dragTargetPointIndex: Int?
private weak var dragSourceCell: SchedulePointCell?
private func handleMiddlePointDrag(_ gesture: UILongPressGestureRecognizer,
pointIndex: Int,
cell: SchedulePointCell) {
let points = viewModel.pointsRelay.value
guard points.count > 2,
pointIndex > 0,
pointIndex < points.count - 1 else { return }
let table = popView.tableView
let locationInTable = gesture.location(in: table)
switch gesture.state {
case .began:
guard let snapshot = cell.snapshotView(afterScreenUpdates: true) else { return }
let frameInPop = table.convert(cell.frame, to: popView)
snapshot.frame = frameInPop
snapshot.alpha = 0.92
snapshot.layer.shadowColor = UIColor.black.cgColor
snapshot.layer.shadowOpacity = 0.2
snapshot.layer.shadowRadius = 6
snapshot.layer.shadowOffset = CGSize(width: 0, height: 2)
popView.addSubview(snapshot)
dragSnapshot = snapshot
dragSourcePointIndex = pointIndex
dragTargetPointIndex = pointIndex
dragSourceCell = cell
popView.isReorderingPoints = true
cell.alpha = 0.15
UIView.animate(withDuration: 0.12) {
snapshot.transform = CGAffineTransform(scaleX: 1.02, y: 1.02)
}
case .changed:
guard let snapshot = dragSnapshot else { return }
let locationInPop = gesture.location(in: popView)
snapshot.center = CGPoint(x: snapshot.center.x, y: locationInPop.y)
// 拖动过程只记录目标,松手再改数据,避免 reload 打断手势
if let targetPath = table.indexPathForRow(at: locationInTable) {
let items = CreateScheduleVM.buildListItems(from: viewModel.pointsRelay.value)
if targetPath.row < items.count,
case .point(let targetPointIndex, _, .middle) = items[targetPath.row] {
dragTargetPointIndex = targetPointIndex
}
}
// 内容较多时,靠近边缘自动滚一下外层 scroll
autoScrollPopIfNeeded(locationInPop: locationInPop)
case .ended, .cancelled, .failed:
if gesture.state == .ended,
let from = dragSourcePointIndex,
let to = dragTargetPointIndex,
from != to {
viewModel.movePoint.onNext((from: from, to: to))
}
finishMiddlePointDrag()
default:
break
}
}
private func autoScrollPopIfNeeded(locationInPop: CGPoint) {
let scroll = popView.scrollView
let edge: CGFloat = 60
let maxY = popView.bounds.height - edge
var offset = scroll.contentOffset
if locationInPop.y < edge {
offset.y = max(0, offset.y - 8)
scroll.setContentOffset(offset, animated: false)
} else if locationInPop.y > maxY {
let maxOffset = max(0, scroll.contentSize.height - scroll.bounds.height)
offset.y = min(maxOffset, offset.y + 8)
scroll.setContentOffset(offset, animated: false)
}
}
private func finishMiddlePointDrag() {
let snapshot = dragSnapshot
let sourceCell = dragSourceCell
dragSnapshot = nil
dragSourcePointIndex = nil
dragTargetPointIndex = nil
dragSourceCell = nil
popView.isReorderingPoints = false
UIView.animate(withDuration: 0.18, animations: {
snapshot?.alpha = 0
snapshot?.transform = .identity
}, completion: { _ in
snapshot?.removeFromSuperview()
})
sourceCell?.alpha = 1
popView.tableView.visibleCells.forEach { $0.alpha = 1 }
}
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 }
let times = points.compactMap(\.expectedTime)
let isIncreasing = zip(times, times.dropFirst()).allSatisfy { $0 < $1 }
guard isIncreasing 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 / Time Picker
private func showDatePicker() {
view.endEditing(true)
let picker = BRDatePickerView(pickerMode: .YMD)
picker.title = "选择日期"
picker.minDate = Date()
picker.maxDate = Calendar.current.date(byAdding: .day, value: 7, to: Date())
picker.selectDate = viewModel.selectedDate.value
picker.pickerStyle = makeSchedulePickerStyle()
picker.pickerFooterView = makeSchedulePickerFooter { [weak picker] in
picker?.doneBlock?()
picker?.dismiss()
}
picker.resultBlock = { [weak self] date, _ in
if let d = date { self?.viewModel.selectedDate.accept(d) }
}
showSchedulePicker(picker)
}
private func makeSchedulePickerStyle() -> BRPickerStyle {
let style = BRPickerStyle()
let width = UIScreen.main.bounds.width
style.topCornerRadius = 20
style.hiddenShadowLine = true
style.hiddenTitleLine = true
style.hiddenDoneBtn = true
style.titleBarHeight = 56
style.titleBarColor = .white
style.alertViewColor = .white
style.maskColor = UIColor.black.withAlphaComponent(0.4)
style.titleTextColor = UIColor(hexStr: "#293445")
style.titleTextFont = .systemFont(ofSize: 18, weight: .semibold)
style.titleLabelFrame = CGRect(x: 0, y: 0, width: 150, height: 56)
style.cancelBtnImage = UIImage(named: "Common/x_black")
style.cancelBtnTitle = ""
style.cancelColor = UIColor(hexStr: "#F2F2F2")
style.cancelCornerRadius = 14
style.cancelBorderStyle = .fill
style.cancelBtnFrame = CGRect(x: width - 16 - 28, y: 14, width: 28, height: 28)
style.pickerColor = .clear
style.pickerHeight = 230
style.rowHeight = 44
style.clearPickerNewStyle = true
style.separatorColor = .clear
style.selectRowTextColor = UIColor(hexStr: "#16B3FF")
style.selectRowTextFont = .systemFont(ofSize: 18, weight: .medium)
style.pickerTextColor = UIColor(hexStr: "#333333")
style.pickerTextFont = .systemFont(ofSize: 16, weight: .regular)
return style
}
private func showSchedulePicker(_ picker: BRDatePickerView) {
picker.show()
guard let alertView = picker.alertView,
let pickerView = firstSubview(of: UIPickerView.self, in: alertView) else { return }
let selectedBackgroundView = UIView()
selectedBackgroundView.translatesAutoresizingMaskIntoConstraints = false
selectedBackgroundView.backgroundColor = UIColor(hexStr: "#E3F6FF")
selectedBackgroundView.layer.cornerRadius = 12
selectedBackgroundView.isUserInteractionEnabled = false
alertView.insertSubview(selectedBackgroundView, belowSubview: pickerView)
NSLayoutConstraint.activate([
selectedBackgroundView.leadingAnchor.constraint(equalTo: alertView.leadingAnchor, constant: 30),
selectedBackgroundView.trailingAnchor.constraint(equalTo: alertView.trailingAnchor, constant: -30),
selectedBackgroundView.centerYAnchor.constraint(equalTo: pickerView.centerYAnchor),
selectedBackgroundView.heightAnchor.constraint(equalToConstant: picker.pickerStyle?.rowHeight ?? 44)
])
}
private func firstSubview<View: UIView>(of type: View.Type, in view: UIView) -> View? {
if let match = view as? View { return match }
for subview in view.subviews {
if let match = firstSubview(of: type, in: subview) { return match }
}
return nil
}
private func makeSchedulePickerFooter(onDone: @escaping () -> Void) -> UIView {
let width = UIScreen.main.bounds.width
let footer = UIView(frame: CGRect(x: 0, y: 0, width: width, height: 78))
footer.backgroundColor = .white
let btn = UIButton(type: .custom)
btn.setTitle("确定", for: .normal)
btn.setTitleColor(.white, for: .normal)
btn.titleLabel?.font = FontManager.boboBold(18)
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
btn.layer.cornerRadius = 20
btn.clipsToBounds = true
btn.frame = CGRect(x: 30, y: 8, width: width - 60, height: 56)
btn.addAction(UIAction { _ in onDone() }, for: .touchUpInside)
footer.addSubview(btn)
return footer
}
// 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,
edgePadding: UIEdgeInsets(top: kNaviHeight + 20, left: 30, bottom: kScreenHeight / 3 + 20, right: 30),
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: - UITableViewDelegate (row height)
extension CreateScheduleVC: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let items = CreateScheduleVM.buildListItems(from: viewModel.pointsRelay.value)
guard indexPath.row < items.count else { return ScheduleTimelineMetric.pointRowHeight }
switch items[indexPath.row] {
case .add:
return ScheduleTimelineMetric.addRowHeight
case .point:
return ScheduleTimelineMetric.pointRowHeight
}
}
}
// 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