// // 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