328 lines
12 KiB
Swift
328 lines
12 KiB
Swift
//
|
||
// LocationPickerVC.swift
|
||
// QuickLocation
|
||
//
|
||
// Created by 八条 on 2026/6/24.
|
||
//
|
||
|
||
import UIKit
|
||
import RxSwift
|
||
import RxCocoa
|
||
import CoreLocation
|
||
import SwiftyUserDefaults
|
||
#if !targetEnvironment(simulator)
|
||
import AMapNaviKit
|
||
import AMapSearchKit
|
||
#endif
|
||
|
||
struct PickedLocation {
|
||
let name: String
|
||
let address: String
|
||
let coordinate: CLLocationCoordinate2D
|
||
var province: String = ""
|
||
var city: String = ""
|
||
var district: String = ""
|
||
var street: String = ""
|
||
var country: String = ""
|
||
var formatted_address: String = ""
|
||
}
|
||
|
||
class LocationPickerVC: BaseViewController {
|
||
|
||
override var isNavigationBarHidden: Bool { true }
|
||
|
||
var onPickedLocation: ((PickedLocation) -> Void)?
|
||
var initialLocation: PickedLocation?
|
||
|
||
fileprivate var rootView: LocationPickerView!
|
||
|
||
override func loadView() {
|
||
rootView = LocationPickerView(frame: UIScreen.main.bounds)
|
||
view = rootView
|
||
}
|
||
|
||
private var isShowPoi: Bool = false
|
||
override func viewDidDisappear(_ animated: Bool) {
|
||
super.viewDidDisappear(animated)
|
||
// 页面被 pop/dismiss 时清理地图资源
|
||
if isMovingFromParent || isBeingDismissed {
|
||
#if !targetEnvironment(simulator)
|
||
rootView.cleanupMap()
|
||
#endif
|
||
}
|
||
}
|
||
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
setupMap()
|
||
setupKeyboard()
|
||
setupSearch()
|
||
// 如果有初始位置,显示在底部面板
|
||
if let loc = initialLocation {
|
||
selectedLocation = loc
|
||
rootView.bottomView.isHidden = false
|
||
rootView.poiNameLab.text = loc.name
|
||
rootView.poiAddressLab.text = loc.address
|
||
if CLLocationCoordinate2DIsValid(loc.coordinate) {
|
||
rootView.mapView.setCenter(loc.coordinate, animated: false)
|
||
rootView.mapView.setZoomLevel(19, animated: false)
|
||
addLocationAnnotation(coordinate: loc.coordinate)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Keyboard
|
||
private var keyboardHeight: CGFloat = 0
|
||
|
||
private func setupKeyboard() {
|
||
NotificationCenter.default.rx.notification(UIResponder.keyboardWillShowNotification)
|
||
.subscribe(onNext: { [weak self] noti in
|
||
guard let self = self,
|
||
let frame = noti.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect
|
||
else { return }
|
||
self.keyboardHeight = frame.height
|
||
})
|
||
.disposed(by: disposeBag)
|
||
}
|
||
|
||
// MARK: - Search
|
||
#if !targetEnvironment(simulator)
|
||
private let searchAPI = AMapSearchAPI()
|
||
private var searchPoiRequest = AMapPOIKeywordsSearchRequest()
|
||
#endif
|
||
private var selectedLocation: PickedLocation?
|
||
private var searchKeyword: String = ""
|
||
private var poiItems: [AMapPOI] = []
|
||
private var maxTableHeight: CGFloat = 300
|
||
private var currentLocation: CLLocationCoordinate2D?
|
||
|
||
private func setupSearch() {
|
||
#if !targetEnvironment(simulator)
|
||
searchAPI?.delegate = self
|
||
|
||
searchPoiRequest.city = ""
|
||
searchPoiRequest.offset = 10
|
||
if let loc = currentLocation {
|
||
searchPoiRequest.location = AMapGeoPoint.location(withLatitude: CGFloat(loc.latitude), longitude: CGFloat(loc.longitude))
|
||
}
|
||
#endif
|
||
|
||
rootView.resultTableView.delegate = self
|
||
rootView.resultTableView.dataSource = self
|
||
|
||
// 输入联想
|
||
rootView.searchField.rx.text.orEmpty
|
||
.debounce(.milliseconds(400), scheduler: MainScheduler.instance)
|
||
// .filter { $0.count >= 2 }
|
||
.subscribe(onNext: { [weak self] keyword in
|
||
self?.poiSearch(keyword: keyword)
|
||
})
|
||
.disposed(by: disposeBag)
|
||
rootView.searchField.rx.controlEvent(.editingDidBegin)
|
||
.subscribe(onNext: {
|
||
self.isShowPoi = false
|
||
})
|
||
.disposed(by: disposeBag)
|
||
|
||
rootView.searchField.rx.controlEvent(.editingDidEndOnExit)
|
||
.subscribe(onNext: { [weak self] _ in
|
||
guard let self = self, !self.poiItems.isEmpty else { return }
|
||
self.selectPOI(at: 0)
|
||
self.rootView.searchField.resignFirstResponder()
|
||
self.rootView.resultTableView.isHidden = true
|
||
})
|
||
.disposed(by: disposeBag)
|
||
|
||
// 搜索按钮 → 选中第一条联想
|
||
rootView.searchBtn.rx.tap
|
||
.subscribe(onNext: { [weak self] _ in
|
||
guard let self = self, !self.poiItems.isEmpty else { return }
|
||
self.selectPOI(at: 0)
|
||
self.rootView.searchField.resignFirstResponder()
|
||
self.rootView.resultTableView.isHidden = true
|
||
})
|
||
.disposed(by: disposeBag)
|
||
|
||
rootView.backBtn.rx.tap
|
||
.subscribe(onNext: { [weak self] _ in
|
||
self?.dismiss(animated: true)
|
||
})
|
||
.disposed(by: disposeBag)
|
||
|
||
rootView.confirmBtn.rx.tap
|
||
.subscribe(onNext: { [weak self] _ in
|
||
guard let self = self, let loc = self.selectedLocation else { return }
|
||
self.onPickedLocation?(loc)
|
||
self.dismiss(animated: true)
|
||
})
|
||
.disposed(by: disposeBag)
|
||
}
|
||
|
||
private func poiSearch(keyword: String) {
|
||
searchKeyword = keyword
|
||
#if !targetEnvironment(simulator)
|
||
searchPoiRequest.keywords = keyword
|
||
searchAPI?.aMapPOIKeywordsSearch(searchPoiRequest)
|
||
#endif
|
||
}
|
||
|
||
private func selectPOI(at index: Int) {
|
||
guard index < poiItems.count else { return }
|
||
selectLocation(poi: poiItems[index])
|
||
}
|
||
|
||
private func selectLocation(poi: AMapPOI) {
|
||
let coord = CLLocationCoordinate2D(latitude: CGFloat(poi.location.latitude), longitude: CGFloat(poi.location.longitude))
|
||
guard CLLocationCoordinate2DIsValid(coord) else { return }
|
||
isShowPoi = true
|
||
selectedLocation = PickedLocation(
|
||
name: poi.name ?? "",
|
||
address: poi.address ?? "",
|
||
coordinate: coord,
|
||
province: poi.province ?? "",
|
||
city: poi.city ?? "",
|
||
district: poi.district ?? "",
|
||
formatted_address: poi.address ?? ""
|
||
)
|
||
rootView.resultTableView.isHidden = true
|
||
rootView.bottomView.isHidden = false
|
||
rootView.poiNameLab.text = poi.name
|
||
rootView.poiAddressLab.text = poi.address
|
||
rootView.mapView.setCenter(coord, animated: true)
|
||
rootView.mapView.setZoomLevel(19, animated: true)
|
||
addLocationAnnotation(coordinate: coord)
|
||
}
|
||
|
||
private func updateTableHeight(itemCount: Int) {
|
||
let contentH = CGFloat(itemCount) * 50
|
||
let searchBarBottom = kStatusBarHeight + 8 + 44 + 10
|
||
let bottomOffset: CGFloat = keyboardHeight > 0 ? keyboardHeight : 200
|
||
let available = kScreenHeight - searchBarBottom - bottomOffset - 10
|
||
maxTableHeight = max(80, available)
|
||
let h = min(contentH, maxTableHeight)
|
||
rootView.resultTableView.layoutChain.height(h)
|
||
}
|
||
|
||
// MARK: - Map
|
||
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) {
|
||
currentLocation = coord
|
||
rootView.mapView.setCenter(coord, animated: false)
|
||
rootView.mapView.setZoomLevel(18, animated: false)
|
||
}
|
||
}
|
||
#endif
|
||
}
|
||
|
||
private func addLocationAnnotation(coordinate: CLLocationCoordinate2D) {
|
||
#if !targetEnvironment(simulator)
|
||
for ann in rootView.mapView.annotations?.compactMap({ $0 as? MAPointAnnotation }) ?? [] {
|
||
rootView.mapView.removeAnnotation(ann)
|
||
}
|
||
let ann = MAPointAnnotation()
|
||
ann.coordinate = coordinate
|
||
rootView.mapView.addAnnotation(ann)
|
||
#endif
|
||
}
|
||
}
|
||
|
||
#if !targetEnvironment(simulator)
|
||
// MARK: - AMapSearchDelegate
|
||
extension LocationPickerVC: AMapSearchDelegate {
|
||
func onPOISearchDone(_ request: AMapPOISearchBaseRequest!, response: AMapPOISearchResponse!) {
|
||
// POI ID 搜索(来自地图点击)直接选中
|
||
if request is AMapPOIIDSearchRequest, let poi = response.pois?.first {
|
||
selectLocation(poi: poi)
|
||
return
|
||
}
|
||
|
||
// 关键字搜索
|
||
guard response.pois.count > 0, isShowPoi == false else { return }
|
||
poiItems = response.pois
|
||
rootView.resultTableView.isHidden = false
|
||
rootView.resultTableView.reloadData()
|
||
updateTableHeight(itemCount: poiItems.count)
|
||
}
|
||
|
||
func onReGeocodeSearchDone(_ request: AMapReGeocodeSearchRequest!, response: AMapReGeocodeSearchResponse!) {
|
||
guard let regeo = response.regeocode else { return }
|
||
let address = regeo.formattedAddress ?? ""
|
||
selectedLocation = PickedLocation(
|
||
name: rootView.poiNameLab.text ?? "",
|
||
address: address,
|
||
coordinate: selectedLocation?.coordinate ?? kCLLocationCoordinate2DInvalid
|
||
)
|
||
rootView.poiAddressLab.text = address
|
||
}
|
||
|
||
func aMapSearchRequest(_ request: Any!, didFailWithError error: Error!) {
|
||
print("AMapSearch error: \(error.localizedDescription)")
|
||
}
|
||
}
|
||
|
||
// MARK: - MAMapViewDelegate
|
||
extension LocationPickerVC: MAMapViewDelegate {
|
||
func mapView(_ mapView: MAMapView!, didTouchPois pois: [Any]!) {
|
||
guard let touchPoi = pois?.first as? MATouchPoi, let uid = touchPoi.uid, !uid.isEmpty else { return }
|
||
let request = AMapPOIIDSearchRequest()
|
||
request.uid = uid
|
||
searchAPI?.aMapPOIIDSearch(request)
|
||
}
|
||
|
||
func mapView(_ mapView: MAMapView!, viewFor annotation: MAAnnotation!) -> MAAnnotationView! {
|
||
if annotation is MAUserLocation { return nil }
|
||
guard annotation is MAPointAnnotation else { return nil }
|
||
let identifier = "LocationPin"
|
||
var view = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
|
||
if view == nil {
|
||
view = MAAnnotationView(annotation: annotation, reuseIdentifier: identifier)
|
||
} else {
|
||
view?.annotation = annotation
|
||
}
|
||
view?.image = UIImage(named: "Schedule/location")
|
||
view?.centerOffset = CGPoint(x: 0, y: -30)
|
||
return view
|
||
}
|
||
}
|
||
|
||
// MARK: - UITableViewDataSource / Delegate
|
||
extension LocationPickerVC: UITableViewDataSource, UITableViewDelegate {
|
||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||
poiItems.count
|
||
}
|
||
|
||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") ?? UITableViewCell(style: .subtitle, reuseIdentifier: "cell")
|
||
let poi = poiItems[indexPath.row]
|
||
let name = poi.name ?? ""
|
||
if !searchKeyword.isEmpty, let range = name.lowercased().range(of: searchKeyword.lowercased()) {
|
||
let attr = NSMutableAttributedString(string: name)
|
||
attr.addAttribute(.foregroundColor, value: UIColor(hexStr: "#16B3FF"), range: NSRange(range, in: name))
|
||
cell.textLabel?.attributedText = attr
|
||
} else {
|
||
cell.textLabel?.text = name
|
||
}
|
||
cell.textLabel?.font = .systemFont(ofSize: 14)
|
||
cell.detailTextLabel?.text = poi.address
|
||
cell.detailTextLabel?.font = .systemFont(ofSize: 12)
|
||
cell.detailTextLabel?.textColor = .gray
|
||
let arrow = UIImageView(image: UIImage(named: "Schedule/arrow"))
|
||
arrow.frame = CGRect(x: 0, y: 0, width: 10, height: 10)
|
||
cell.accessoryView = arrow
|
||
return cell
|
||
}
|
||
|
||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||
tableView.deselectRow(at: indexPath, animated: true)
|
||
rootView.searchField.resignFirstResponder()
|
||
selectPOI(at: indexPath.row)
|
||
rootView.searchField.text = poiItems[indexPath.row].name
|
||
}
|
||
}
|
||
#endif
|