Integrate Amap SDK to replace CoreLocation and MapKit for all location features.

Unify GPS, reverse geocoding, and map display behind LocationProvider with privacy-gated bootstrap after login, aligned with Android.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-07 11:49:22 +08:00
parent d2879ad7e2
commit cfcd3eee6f
29 changed files with 748 additions and 240 deletions

View File

@ -13,7 +13,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
if AppStore.shared.privacyAgreementAccepted, !AppStore.shared.token.isEmpty {
AMapBootstrap.configureIfNeeded()
}
return true
}

View File

@ -0,0 +1,12 @@
//
// AMapConfig.swift
// suixinkan
//
import Foundation
/// SDK
enum AMapConfig {
/// iOS Key Bundle ID `com.yuanzhixiang.suixinkan`
nonisolated static let apiKey = "ea25441168468f39e276687dc94b8e7c"
}

View File

@ -0,0 +1,22 @@
//
// AppAgreementURLs.swift
// suixinkan
//
import Foundation
/// App Android `AgreementRoutes`
enum AppAgreementURLs {
/// H5
nonisolated static var privacyPolicy: URL {
APIEnvironment.current.baseURL.appending(path: "h5/app/privacy-policy")
}
/// H5
nonisolated static var userAgreement: URL {
APIEnvironment.current.baseURL.appending(path: "h5/app/user-agreement")
}
///
nonisolated static let amapPrivacyPolicy = URL(string: "https://lbs.amap.com/pages/privacy/")!
}

View File

@ -23,6 +23,10 @@ enum AuthSessionHelper {
AppStore.shared.privacyAgreementAccepted = privacyAgreementAccepted
applyAccountSession(from: selectedAccount, in: response)
if privacyAgreementAccepted {
AMapBootstrap.configureIfNeeded()
}
NotificationCenter.default.post(name: NotificationName.userDidLogin, object: nil)
}

View File

@ -1,98 +0,0 @@
//
// HomeLocationProvider.swift
// suixinkan
//
import CoreLocation
///
struct HomeLocationSnapshot: Sendable {
let latitude: Double
let longitude: Double
let address: String
}
/// CoreLocation
final class HomeLocationProvider: NSObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
private var coordinateContinuation: CheckedContinuation<CLLocationCoordinate2D, Error>?
override init() {
super.init()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
}
///
func requestSnapshot() async throws -> HomeLocationSnapshot {
let status = manager.authorizationStatus
if status == .notDetermined {
manager.requestWhenInUseAuthorization()
try await Task.sleep(nanoseconds: 300_000_000)
}
let resolvedStatus = manager.authorizationStatus
guard resolvedStatus == .authorizedWhenInUse || resolvedStatus == .authorizedAlways else {
throw HomeLocationProviderError.permissionDenied
}
let coordinate = try await requestCoordinate()
let address = await reverseGeocode(latitude: coordinate.latitude, longitude: coordinate.longitude)
return HomeLocationSnapshot(
latitude: coordinate.latitude,
longitude: coordinate.longitude,
address: address
)
}
/// 使
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
let location = CLLocation(latitude: latitude, longitude: longitude)
let geocoder = CLGeocoder()
do {
let placemarks = try await geocoder.reverseGeocodeLocation(location)
return placemarks.first?.formattedAddress ?? ""
} catch {
return ""
}
}
private func requestCoordinate() async throws -> CLLocationCoordinate2D {
try await withCheckedThrowingContinuation { continuation in
self.coordinateContinuation = continuation
manager.requestLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
coordinateContinuation?.resume(returning: location.coordinate)
coordinateContinuation = nil
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
coordinateContinuation?.resume(throwing: error)
coordinateContinuation = nil
}
}
///
enum HomeLocationProviderError: LocalizedError {
case permissionDenied
var errorDescription: String? {
switch self {
case .permissionDenied: "需要定位权限才能使用此功能"
}
}
}
private extension CLPlacemark {
var formattedAddress: String {
[subLocality, locality, administrativeArea]
.compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
.joined(separator: "")
}
}

View File

@ -30,7 +30,7 @@ final class HomeViewModel {
let locationReportService: LocationReportService
private let appStore: AppStore
private let commonMenuStore: HomeCommonMenuStore
private let locationProvider: HomeLocationProvider
private let locationProvider: any LocationProviding
private var timeoutDialogTriggered = false
private var dismissedLocationTimeoutDialog = false
@ -39,7 +39,7 @@ final class HomeViewModel {
appStore: AppStore = .shared,
locationStateStore: HomeLocationStateStore = HomeLocationStateStore(),
commonMenuStore: HomeCommonMenuStore = HomeCommonMenuStore(),
locationProvider: HomeLocationProvider = HomeLocationProvider()
locationProvider: any LocationProviding = LocationProvider.shared
) {
self.appStore = appStore
self.locationStateStore = locationStateStore

View File

@ -27,11 +27,11 @@ final class ScenicSelectionViewModel {
var onNavigateApplyStatus: ((String) -> Void)?
private let appStore: AppStore
private let locationProvider: HomeLocationProvider
private let locationProvider: any LocationProviding
private var currentLat: Double?
private var currentLng: Double?
init(appStore: AppStore = .shared, locationProvider: HomeLocationProvider = HomeLocationProvider()) {
init(appStore: AppStore = .shared, locationProvider: any LocationProviding = LocationProvider.shared) {
self.appStore = appStore
self.locationProvider = locationProvider
}
@ -63,7 +63,7 @@ final class ScenicSelectionViewModel {
let address = snapshot.address.trimmingCharacters(in: .whitespacesAndNewlines)
currentLocationText = address.isEmpty ? "最近的景区" : address
loadFromLocal()
} catch HomeLocationProviderError.permissionDenied {
} catch LocationProviderError.permissionDenied {
onShowMessage?("未授予定位权限,无法获取附近景区")
} catch {
onShowMessage?("定位失败: \(error.localizedDescription)")

View File

@ -0,0 +1,49 @@
//
// AMapBootstrap.swift
// suixinkan
//
import Foundation
/// SDK Key SDK
enum AMapBootstrap {
private(set) static var isConfigured = false
private static let lock = NSLock()
/// App SDK
@discardableResult
static func configureIfNeeded(appStore: AppStore = .shared) -> Bool {
guard appStore.privacyAgreementAccepted else { return false }
lock.lock()
defer { lock.unlock() }
guard !isConfigured else { return true }
AMapLocationManager.updatePrivacyShow(.didShow, privacyInfo: .didContain)
AMapLocationManager.updatePrivacyAgree(.didAgree)
MAMapView.updatePrivacyShow(.didShow, privacyInfo: .didContain)
MAMapView.updatePrivacyAgree(.didAgree)
AMapSearchAPI.updatePrivacyShow(.didShow, privacyInfo: .didContain)
AMapSearchAPI.updatePrivacyAgree(.didAgree)
AMapServices.shared().apiKey = AMapConfig.apiKey
AMapServices.shared().enableHTTPS = true
isConfigured = true
return true
}
/// SDK
static func requireConfigured(appStore: AppStore = .shared) throws {
guard configureIfNeeded(appStore: appStore) else {
throw LocationProviderError.privacyNotAccepted
}
}
///
static func resetForTesting() {
lock.lock()
defer { lock.unlock() }
isConfigured = false
}
}

View File

@ -0,0 +1,64 @@
//
// LocationGeocoder.swift
// suixinkan
//
import Foundation
/// Android `GeocodeSearch`
final class LocationGeocoder: NSObject, AMapSearchDelegate {
static let shared = LocationGeocoder()
private var searchAPI: AMapSearchAPI?
private var continuation: CheckedContinuation<String, Never>?
private override init() {
super.init()
}
///
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
do {
try AMapBootstrap.requireConfigured()
} catch {
return ""
}
return await withCheckedContinuation { continuation in
self.continuation?.resume(returning: "")
self.continuation = continuation
guard let api = makeSearchAPI() else {
continuation.resume(returning: "")
return
}
let request = AMapReGeocodeSearchRequest()
request.location = AMapGeoPoint.location(withLatitude: CGFloat(latitude), longitude: CGFloat(longitude))
request.requireExtension = true
request.radius = 200
api.aMapReGoecodeSearch(request)
}
}
func onReGeocodeSearchDone(_ request: AMapReGeocodeSearchRequest!, response: AMapReGeocodeSearchResponse!) {
let address = response?.regeocode?.formattedAddress ?? ""
continuation?.resume(returning: address)
continuation = nil
}
func aMapSearchRequest(_ request: Any!, didFailWithError error: Error!) {
continuation?.resume(returning: "")
continuation = nil
}
private func makeSearchAPI() -> AMapSearchAPI? {
if let searchAPI {
return searchAPI
}
guard let api = AMapSearchAPI() else { return nil }
api.delegate = self
searchAPI = api
return api
}
}

View File

@ -0,0 +1,100 @@
//
// LocationProvider.swift
// suixinkan
//
import CoreLocation
import Foundation
/// `AMapLocationManager` Android `LocationProvider`
final class LocationProvider: NSObject, LocationProviding {
static let shared = LocationProvider()
private let permissionManager = CLLocationManager()
private let geocoder: LocationGeocoder
init(geocoder: LocationGeocoder = .shared) {
self.geocoder = geocoder
super.init()
permissionManager.desiredAccuracy = kCLLocationAccuracyBest
}
func requestSnapshot() async throws -> HomeLocationSnapshot {
try AMapBootstrap.requireConfigured()
try await ensureLocationPermission()
let manager = makeLocationManager()
return try await withCheckedThrowingContinuation { continuation in
let started = manager.requestLocation(withReGeocode: true) { location, regeocode, error in
if let error {
continuation.resume(throwing: error)
return
}
guard let location else {
continuation.resume(throwing: LocationProviderError.locationFailed)
return
}
let address = regeocode?.formattedAddress ?? ""
continuation.resume(returning: HomeLocationSnapshot(
latitude: location.coordinate.latitude,
longitude: location.coordinate.longitude,
address: address
))
}
if !started {
continuation.resume(throwing: LocationProviderError.locationFailed)
}
}
}
func requestCoordinate() async throws -> CLLocationCoordinate2D {
try AMapBootstrap.requireConfigured()
try await ensureLocationPermission()
let manager = makeLocationManager()
return try await withCheckedThrowingContinuation { continuation in
let started = manager.requestLocation(withReGeocode: false) { location, _, error in
if let error {
continuation.resume(throwing: error)
return
}
guard let location else {
continuation.resume(throwing: LocationProviderError.locationFailed)
return
}
continuation.resume(returning: location.coordinate)
}
if !started {
continuation.resume(throwing: LocationProviderError.locationFailed)
}
}
}
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
await geocoder.reverseGeocode(latitude: latitude, longitude: longitude)
}
///
func ensureLocationPermission() async throws {
let initialStatus = permissionManager.authorizationStatus
if initialStatus == .notDetermined {
permissionManager.requestWhenInUseAuthorization()
try await Task.sleep(nanoseconds: 500_000_000)
}
let status = permissionManager.authorizationStatus
guard status == .authorizedWhenInUse || status == .authorizedAlways else {
throw LocationProviderError.permissionDenied
}
}
private func makeLocationManager() -> AMapLocationManager {
let manager = AMapLocationManager()
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.locationTimeout = 30
manager.reGeocodeTimeout = 10
manager.locatingWithReGeocode = true
return manager
}
}

View File

@ -0,0 +1,54 @@
//
// LocationProviding.swift
// suixinkan
//
import CoreLocation
import Foundation
///
struct HomeLocationSnapshot: Sendable, Equatable {
let latitude: Double
let longitude: Double
let address: String
}
/// 便 ViewModel mock
protocol LocationProviding: Sendable {
///
func requestSnapshot() async throws -> HomeLocationSnapshot
///
func requestCoordinate() async throws -> CLLocationCoordinate2D
///
func reverseGeocode(latitude: Double, longitude: Double) async -> String
}
///
enum LocationProviderError: LocalizedError, Equatable {
case privacyNotAccepted
case permissionDenied
case locationFailed
var errorDescription: String? {
switch self {
case .privacyNotAccepted:
"请先阅读并同意隐私政策"
case .permissionDenied:
"需要定位权限才能使用此功能"
case .locationFailed:
"定位失败,请稍后重试"
}
}
}
///
enum OrderLocationError: LocalizedError {
case permissionDenied
var errorDescription: String? {
switch self {
case .permissionDenied:
"缺少定位权限,功能无法进行"
}
}
}

View File

@ -23,12 +23,12 @@ final class LocationReportService {
private let appStore: AppStore
private let locationStateStore: HomeLocationStateStore
private let locationProvider: HomeLocationProvider
private let locationProvider: any LocationProviding
init(
appStore: AppStore = .shared,
locationStateStore: HomeLocationStateStore,
locationProvider: HomeLocationProvider = HomeLocationProvider()
locationProvider: any LocationProviding = LocationProvider.shared
) {
self.appStore = appStore
self.locationStateStore = locationStateStore

View File

@ -26,11 +26,11 @@ final class LocationReportViewModel {
var onStateChange: (() -> Void)?
var onShowMessage: ((String) -> Void)?
private let locationProvider: HomeLocationProvider
private let locationProvider: any LocationProviding
init(
locationStateStore: HomeLocationStateStore = HomeLocationStateStore(),
locationProvider: HomeLocationProvider = HomeLocationProvider()
locationProvider: any LocationProviding = LocationProvider.shared
) {
self.locationStateStore = locationStateStore
self.locationProvider = locationProvider

View File

@ -3,12 +3,11 @@
// suixinkan
//
import CoreLocation
import UIKit
/// Tab Android `MainTabViewModel.signIn`
@MainActor
final class MainTabScanHandler: NSObject, CLLocationManagerDelegate {
final class MainTabScanHandler {
var onShowMessage: ((String) -> Void)?
var onShowSignIn: ((OrderSignInResponse) -> Void)?
@ -18,15 +17,12 @@ final class MainTabScanHandler: NSObject, CLLocationManagerDelegate {
var onNavigateBindAcquirer: ((Int) -> Void)?
private let api: OrderAPI
private let locationManager = CLLocationManager()
private var locationContinuation: ((Result<CLLocationCoordinate2D, Error>) -> Void)?
private let locationProvider: any LocationProviding
private(set) var pendingMultiVerify: MultiVerifyParseData?
init(api: OrderAPI) {
init(api: OrderAPI, locationProvider: any LocationProviding = LocationProvider.shared) {
self.api = api
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationProvider = locationProvider
}
func handleScanResult(_ scanResult: String) async {
@ -141,14 +137,8 @@ final class MainTabScanHandler: NSObject, CLLocationManagerDelegate {
}
private func fetchSpotsForMultiVerify(pending: MultiVerifyParseData) async {
let status = locationManager.authorizationStatus
guard status == .authorizedWhenInUse || status == .authorizedAlways else {
locationManager.requestWhenInUseAuthorization()
onShowMessage?("缺少定位权限,功能无法进行")
return
}
do {
let coordinate = try await requestLocation()
let coordinate = try await locationProvider.requestCoordinate()
let response = try await api.multiTravelScenicSpotList(
orderNumber: pending.orderNumber,
lat: coordinate.latitude,
@ -160,35 +150,12 @@ final class MainTabScanHandler: NSObject, CLLocationManagerDelegate {
} else {
onShowMultiVerifySheet?(spots, spots.first)
}
} catch LocationProviderError.permissionDenied {
onShowMessage?(OrderLocationError.permissionDenied.errorDescription ?? "缺少定位权限,功能无法进行")
} catch {
onShowMessage?(error.localizedDescription)
onShowMessage?((error as? LocalizedError)?.errorDescription ?? error.localizedDescription)
}
}
private func requestLocation() async throws -> CLLocationCoordinate2D {
try await withCheckedThrowingContinuation { continuation in
locationContinuation = { result in
switch result {
case .success(let coordinate):
continuation.resume(returning: coordinate)
case .failure(let error):
continuation.resume(throwing: error)
}
}
locationManager.requestLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
locationContinuation?(.success(location.coordinate))
locationContinuation = nil
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
locationContinuation?(.failure(error))
locationContinuation = nil
}
}
private extension MainTabScanHandler {

View File

@ -1,55 +0,0 @@
//
// OrderLocationProvider.swift
// suixinkan
//
import CoreLocation
///
final class OrderLocationProvider: NSObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
private var continuation: ((Result<CLLocationCoordinate2D, Error>) -> Void)?
override init() {
super.init()
manager.delegate = self
manager.desiredAccuracy = kCLLocationAccuracyBest
}
func requestCoordinate() async throws -> CLLocationCoordinate2D {
let status = manager.authorizationStatus
if status == .notDetermined {
manager.requestWhenInUseAuthorization()
}
guard status == .authorizedWhenInUse || status == .authorizedAlways || manager.authorizationStatus == .authorizedWhenInUse else {
throw OrderLocationError.permissionDenied
}
return try await withCheckedThrowingContinuation { continuation in
self.continuation = { result in
continuation.resume(with: result)
}
manager.requestLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last else { return }
continuation?(.success(location.coordinate))
continuation = nil
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
continuation?(.failure(error))
continuation = nil
}
}
enum OrderLocationError: LocalizedError {
case permissionDenied
var errorDescription: String? {
switch self {
case .permissionDenied: "缺少定位权限,功能无法进行"
}
}
}

View File

@ -7,7 +7,7 @@
<key>NSCameraUsageDescription</key>
<string>需要访问相机以扫描订单核销二维码</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>需要定位以获取多点位核销打卡点列表</string>
<string>需要获取您的位置信息,用于位置上报、景区距离排序及多点位核销打卡</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要访问相册以选择头像、身份证和银行卡照片</string>
<key>NSPhotoLibraryAddUsageDescription</key>

View File

@ -110,17 +110,13 @@ final class LocationReportViewController: BaseViewController {
}
private func requestLocationIfNeeded() {
let status = CLLocationManager.authorizationStatus()
switch status {
case .authorizedAlways, .authorizedWhenInUse:
viewModel.startLocation()
case .notDetermined:
CLLocationManager().requestWhenInUseAuthorization()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
self?.viewModel.startLocation()
Task {
do {
try await LocationProvider.shared.ensureLocationPermission()
viewModel.startLocation()
} catch {
showToast((error as? LocalizedError)?.errorDescription ?? error.localizedDescription)
}
default:
showToast("需要定位权限才能使用此功能")
}
}

View File

@ -3,7 +3,7 @@
// suixinkan
//
import MapKit
import CoreLocation
import SnapKit
import UIKit
@ -287,21 +287,22 @@ final class LocationReportBottomActionView: UIControl {
}
}
/// MKMapView
final class LocationReportMapView: UIView, MKMapViewDelegate {
/// `MAMapView`
final class LocationReportMapView: UIView, MAMapViewDelegate {
var onMapTap: ((CLLocationCoordinate2D) -> Void)?
let mapView = MKMapView()
private var markerAnnotation: MKPointAnnotation?
let mapView: MAMapView
private var markerAnnotation: MAPointAnnotation?
override init(frame: CGRect) {
_ = AMapBootstrap.configureIfNeeded()
mapView = MAMapView(frame: .zero)
super.init(frame: frame)
mapView.delegate = self
mapView.showsUserLocation = true
mapView.showsCompass = false
let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
mapView.addGestureRecognizer(tap)
mapView.zoomLevel = 15
addSubview(mapView)
mapView.snp.makeConstraints { make in
make.edges.equalToSuperview()
@ -314,27 +315,22 @@ final class LocationReportMapView: UIView, MKMapViewDelegate {
}
func centerOnUserLocation() {
guard let coordinate = mapView.userLocation.location?.coordinate else { return }
let coordinate = mapView.userLocation.location?.coordinate
guard let coordinate, CLLocationCoordinate2DIsValid(coordinate) else { return }
setCenter(coordinate)
}
func setCenter(_ coordinate: CLLocationCoordinate2D, animated: Bool = true) {
let region = MKCoordinateRegion(center: coordinate, latitudinalMeters: 800, longitudinalMeters: 800)
mapView.setRegion(region, animated: animated)
mapView.setCenter(coordinate, animated: animated)
mapView.setZoomLevel(15, animated: animated)
}
func zoomIn() {
var region = mapView.region
region.span.latitudeDelta *= 0.5
region.span.longitudeDelta *= 0.5
mapView.setRegion(region, animated: true)
mapView.setZoomLevel(mapView.zoomLevel + 1, animated: true)
}
func zoomOut() {
var region = mapView.region
region.span.latitudeDelta = min(region.span.latitudeDelta * 2, 180)
region.span.longitudeDelta = min(region.span.longitudeDelta * 2, 180)
mapView.setRegion(region, animated: true)
mapView.setZoomLevel(mapView.zoomLevel - 1, animated: true)
}
func updateMarker(latitude: Double?, longitude: Double?) {
@ -343,15 +339,13 @@ final class LocationReportMapView: UIView, MKMapViewDelegate {
markerAnnotation = nil
}
guard let latitude, let longitude else { return }
let annotation = MKPointAnnotation()
let annotation = MAPointAnnotation()
annotation.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
markerAnnotation = annotation
mapView.addAnnotation(annotation)
}
@objc private func handleTap(_ gesture: UITapGestureRecognizer) {
let point = gesture.location(in: mapView)
let coordinate = mapView.convert(point, toCoordinateFrom: mapView)
func mapView(_ mapView: MAMapView!, didSingleTappedAt coordinate: CLLocationCoordinate2D) {
onMapTap?(coordinate)
}
}

View File

@ -3,6 +3,7 @@
// suixinkan
//
import SafariServices
import SnapKit
import UIKit
@ -212,10 +213,10 @@ final class LoginViewController: BaseViewController {
self?.showToast("注册待接入")
}
viewModel.onUserAgreement = { [weak self] in
self?.showToast("用户协议待接入")
self?.openAgreementURL(AppAgreementURLs.userAgreement)
}
viewModel.onPrivacyPolicy = { [weak self] in
self?.showToast("隐私政策待接入")
self?.openAgreementURL(AppAgreementURLs.privacyPolicy)
}
}
@ -345,6 +346,11 @@ final class LoginViewController: BaseViewController {
view.endEditing(true)
viewModel.onRegister?()
}
private func openAgreementURL(_ url: URL) {
let controller = SFSafariViewController(url: url)
present(controller, animated: true)
}
}
extension LoginViewController: UIGestureRecognizerDelegate {

View File

@ -23,7 +23,7 @@ final class OrdersViewController: BaseViewController, UITableViewDataSource, UIT
private weak var orderSourcePicker: OrderSourcePickerViewController?
private var pendingGiftOrderNumber: String?
private var isFetchingMultiVerifySpots = false
private let locationProvider = OrderLocationProvider()
private let locationProvider: any LocationProviding = LocationProvider.shared
override func setupNavigationBar() {
title = "订单"

View File

@ -0,0 +1,9 @@
//
// suixinkan-Bridging-Header.h
// suixinkan
//
#import <AMapFoundationKit/AMapFoundationKit.h>
#import <AMapLocationKit/AMapLocationKit.h>
#import <MAMapKit/MAMapKit.h>
#import <AMapSearchKit/AMapSearchKit.h>