jsdw_ios/QuickLocation/Section/Schedule/LocationPicker/LocationPickerVC.swift

361 lines
14 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.

//
// 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 ?? "",
street: "",
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)
// street
#if !targetEnvironment(simulator)
let regeo = AMapReGeocodeSearchRequest()
regeo.location = AMapGeoPoint.location(withLatitude: CGFloat(coord.latitude),
longitude: CGFloat(coord.longitude))
regeo.requireExtension = true
searchAPI?.aMapReGoecodeSearch(regeo)
#endif
}
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 ?? ""
// POI /
let poiName = regeo.pois?.first?.name
?? regeo.pois?.first?.address
?? regeo.pois?.first?.district
?? ""
selectedLocation = PickedLocation(
name: poiName,
address: regeo.pois?.first?.address ?? "",
coordinate: selectedLocation?.coordinate ?? kCLLocationCoordinate2DInvalid,
province: regeo.pois?.first?.province ?? "",
city: regeo.pois?.first?.city ?? "",
district: regeo.pois?.first?.district ?? "",
street: regeo.addressComponent?.streetNumber?.street ?? "",
formatted_address: regeo.pois?.first?.address ?? ""
)
rootView.bottomView.isHidden = false
rootView.poiNameLab.text = poiName
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!, didSingleTappedAt coordinate: CLLocationCoordinate2D) {
isShowPoi = true
selectedLocation = PickedLocation(
name: "",
address: "",
coordinate: coordinate
)
addLocationAnnotation(coordinate: coordinate)
rootView.mapView.setCenter(coordinate, animated: true)
//
#if !targetEnvironment(simulator)
let regeo = AMapReGeocodeSearchRequest()
regeo.location = AMapGeoPoint.location(withLatitude: CGFloat(coordinate.latitude),
longitude: CGFloat(coordinate.longitude))
regeo.requireExtension = true
searchAPI?.aMapReGoecodeSearch(regeo)
#endif
}
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