返回

iOS开发教程:获取用户位置并在MapKit地图显示

IOS

iOS 开发:获取用户当前位置并显示在地图上

遇到一个常见的问题:想在 App 里拿到用户当前的具体位置(经纬度),并且把这个位置标在地图上。可能你已经知道怎么在地图上显示一个写死的坐标点,但不知道怎么从用户的设备上动态获取实时位置。另外,听说需要在 Info.plist 文件里加点东西,具体怎么操作呢?

别急,咱们一步步来拆解。

问题出在哪?

获取用户位置这事儿,跟直接读取一个变量可不一样。主要卡在几个点:

  1. 隐私权限 : 用户的位置是敏感信息,iOS 系统管控得非常严。你不能想拿就拿,必须先明确告诉用户你要干嘛,并且得到用户的明确授权 。这是最重要的一步,也是 Info.plist 发挥作用的地方。
  2. 系统服务 : 定位功能是由操作系统底层服务(CoreLocation.framework)提供的。你需要通过苹果提供的 API 来跟这个服务打交道,请求位置更新。
  3. 异步获取 : 获取位置不是一瞬间完成的事。设备需要跟 GPS 卫星、Wi-Fi 热点或蜂窝基站通信,这需要时间。所以,位置信息通常是异步返回 的,你需要设置好回调(Delegate)来接收。
  4. 配置先行 : 必须先在 Info.plist 里声明你要用定位权限以及为什么用,然后才能在代码里去请求授权。顺序不能错。

搞清楚了这些,解决起来就顺理成章了。

解决方案:分步走

咱们把整个过程分成三大步:请求权限 -> 获取位置 -> 地图显示。

第一步:配置权限 (Info.plist) 与发起请求

这是基础中的基础。没拿到权限,后面一切免谈。

  1. 编辑 Info.plist 文件

    你需要告诉 iOS 系统你的 App 会请求哪种类型的定位权限,并且向用户解释清楚为什么需要这个权限。用户在看到系统弹出的授权请求时,就会看到你写的这段解释。写得越清楚、越有道理,用户同意的可能性就越大。

    在 Xcode 项目导航器里找到 Info.plist 文件。右键点击空白处,选择 "Add Row"。你需要添加以下两个 Key 中的至少一个,取决于你的需求:

    • NSLocationWhenInUseUsageDescription (Privacy - Location When In Use Usage Description)

      • 作用 : 当你的 App 只在前台运行时 需要获取用户位置时,添加这个 Key。这是比较常见的场景,也相对更容易被用户接受。
      • Value (值) : 写一段清晰的,告诉用户为什么 App 在使用时需要位置信息。例如:"我们需要您的位置来在地图上标记您当前所在地点并发掘附近的精彩内容。" 切记,这段文字是直接给用户看的,一定要写明白!
    • NSLocationAlwaysAndWhenInUseUsageDescription (Privacy - Location Always and When In Use Usage Description)

      • 作用 : 当你的 App 无论在前台还是后台 都需要获取用户位置时(比如导航 App、运动记录 App),添加这个 Key。请求 "Always" 权限门槛更高,需要向用户充分说明理由。如果只需要前台定位,就不要添加这个。
      • Value (值) : 同样,写一段清晰的,解释为什么 App 需要持续访问位置。例如:"允许 App 持续访问您的位置,即使在后台运行,也能为您提供准确的导航路线和记录您的运动轨迹。"
      • 重要补充 : 如果你请求 "Always" 权限,通常还需要在 Xcode 项目设置的 "Signing & Capabilities" -> "Background Modes" 中勾选 "Location updates"。

    代码示例 (Info.plist 原始 XML 格式):

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <!-- ... 其他 key ... -->
        <key>NSLocationWhenInUseUsageDescription</key>
        <string>我们需要您的位置来在地图上标记您当前所在地点并发掘附近的精彩内容。</string>
        <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
        <string>允许 App 持续访问您的位置,即使在后台运行,也能为您提供准确的导航路线和记录您的运动轨迹。(如果不需要后台定位,请删除此项)</string>
        <!-- ... 其他 key ... -->
    </dict>
    </plist>
    

    安全与体验建议 :

    • 按需申请 : 除非你的核心功能绝对离不开 后台定位,否则只申请 "When In Use" 权限。对用户更友好,也更容易通过 App Store 审核。
    • 描述要真诚具体 : 不要用模棱两可或者欺骗性的描述。直接说明用途,比如 "显示附近餐馆"、"记录跑步路线" 等。
  2. 在代码中请求授权

    配置好 Info.plist 后,就可以在代码里使用 CoreLocation 框架来请求授权了。通常在需要定位功能的视图控制器(ViewController)或者一个专门的 Location Service 类里做这件事。

    import UIKit
    import CoreLocation // 别忘了导入 CoreLocation 框架
    
    class LocationViewController: UIViewController, CLLocationManagerDelegate { // 遵循 CLLocationManagerDelegate 协议
    
        let locationManager = CLLocationManager() // 创建 CLLocationManager 实例
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // 设置代理,这样才能接收到位置更新和权限变化的回调
            locationManager.delegate = self
    
            // 检查当前的授权状态
            checkLocationAuthorization()
        }
    
        func checkLocationAuthorization() {
            switch locationManager.authorizationStatus {
            case .notDetermined:
                // 用户还没做选择,发起请求
                // 如果需要 "Always" 权限,调用 requestAlwaysAuthorization()
                locationManager.requestWhenInUseAuthorization()
                print("请求 'When In Use' 授权")
            case .restricted:
                // 用户的设备或家长控制限制了定位服务
                print("定位服务受限")
                // 可以提示用户检查设备设置
            case .denied:
                // 用户之前拒绝了授权,或者在设置里关闭了定位服务
                print("用户已拒绝授权")
                // 引导用户去设置 App 的定位权限
                showEnableLocationServicesAlert()
            case .authorizedWhenInUse:
                // 用户授权了“使用 App 期间”
                print("'When In Use' 权限已授权")
                // 可以开始获取位置了 (见下一步)
                // locationManager.startUpdatingLocation() // 如果你想立即开始
            case .authorizedAlways:
                // 用户授权了“始终”
                print("'Always' 权限已授权")
                // 可以开始获取位置了 (见下一步)
                // locationManager.startUpdatingLocation() // 如果你想立即开始
            @unknown default:
                // 处理未来可能出现的新的 case
                fatalError("未知的 Core Location 授权状态")
            }
        }
    
        // MARK: - CLLocationManagerDelegate 方法
    
        // 当授权状态改变时调用 (用户在设置里改了权限也会触发)
        func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
            print("定位权限发生变化,重新检查状态")
            checkLocationAuthorization() // 重新检查状态,确保 UI 或逻辑相应更新
        }
    
        // MARK: - Helper
        func showEnableLocationServicesAlert() {
            let alert = UIAlertController(
                title: "需要定位权限",
                message: "我们未能获取您的位置。请前往 设置 > 隐私 > 定位服务 中开启本 App 的定位权限。",
                preferredStyle: .alert
            )
            alert.addAction(UIAlertAction(title: "取消", style: .cancel, handler: nil))
            alert.addAction(UIAlertAction(title: "去设置", style: .default) { _ in
                // 打开 App 的系统设置页面
                if let url = URL(string: UIApplication.openSettingsURLString), UIApplication.shared.canOpenURL(url) {
                    UIApplication.shared.open(url, options: [:], completionHandler: nil)
                }
            })
            present(alert, animated: true, completion: nil)
        }
    
        // ... 获取位置和显示地图的代码将在下面添加 ...
    }
    

    关键点 :

    • 创建 CLLocationManager 实例。
    • 设置 delegate = self,让当前类能接收回调。
    • 检查 locationManager.authorizationStatus 来判断当前状态,并据此行动:
      • .notDetermined: 用户没选过,发起请求 (requestWhenInUseAuthorization()requestAlwaysAuthorization())。系统会弹出带 Info.plist 描述的提示框。
      • .denied, .restricted: 用户拒绝了或受限,不能定位。最好给用户提示,引导他们去系统设置里开启。
      • .authorizedWhenInUse, .authorizedAlways: 已经有权限了,可以进行下一步。
    • 实现 locationManagerDidChangeAuthorization 代理方法。当用户响应了授权弹窗,或者之后在系统设置里修改了 App 的定位权限,这个方法会被调用。在这里重新调用 checkLocationAuthorization() 是个好习惯,可以确保你的 App 状态正确。

第二步:获取位置更新

拿到授权后,就可以让 CLLocationManager 开始工作,获取具体的位置信息了。

  1. 设置精度和启动更新

    在确认拥有权限后(比如在 checkLocationAuthorization 函数的 .authorizedWhenInUse.authorizedAlways case 里),你可以配置 CLLocationManager 并开始接收更新。

    // (继续在 LocationViewController 类中)
    
    func startReceivingLocationUpdates() {
        // 检查权限,避免无权限时启动
        let status = locationManager.authorizationStatus
        guard status == .authorizedWhenInUse || status == .authorizedAlways else {
            print("没有定位权限,无法开始获取位置。")
            return
        }
    
        // 设置期望的精度。根据需求选择,精度越高越耗电。
        locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters // 例如,精确到最近的10米
        // kCLLocationAccuracyBest: 尽可能高的精度 (GPS),最耗电
        // kCLLocationAccuracyKilometer: 精确到公里级别,省电
    
        // 设置距离筛选器 (可选)。只有当设备移动了超过这个距离(米)时,才会触发位置更新。设为 kCLDistanceFilterNone 表示任何移动都通知。
        // locationManager.distanceFilter = 50 // 移动超过 50 米才更新
    
        print("开始接收位置更新...")
        locationManager.startUpdatingLocation()
    }
    
    func stopReceivingLocationUpdates() {
        print("停止接收位置更新。")
        locationManager.stopUpdatingLocation()
    }
    
    // 在确认授权后调用
    func checkLocationAuthorization() {
        // ... (之前的 switch case)
        case .authorizedWhenInUse, .authorizedAlways:
             // ...
             print("权限已获取,准备开始定位...")
             // 在这里或者用户触发某个操作时调用 startReceivingLocationUpdates()
             startReceivingLocationUpdates() // 示例:检查完权限就立刻开始
        // ... (其他 case)
    }
    
    // (别忘了在适当的时候停止更新,比如视图消失时)
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        // 如果只是这个页面需要定位,离开时停止,节省电量
        // stopReceivingLocationUpdates()
        // 注意:如果需要后台定位,则不能在这里停止
    }
    
  2. 实现代理方法接收位置

    CLLocationManager 获取到新的位置数据时,会调用代理的 locationManager(_:didUpdateLocations:) 方法。你在这里处理收到的坐标。

    // (继续在 LocationViewController 类中,确保已遵循 CLLocationManagerDelegate)
    
    // MARK: - CLLocationManagerDelegate 方法
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        // locations 数组包含了自上次更新以来收到的所有位置点。
        // 通常我们只需要最新的那个。
        guard let currentLocation = locations.last else { return }
    
        let latitude = currentLocation.coordinate.latitude
        let longitude = currentLocation.coordinate.longitude
        let altitude = currentLocation.altitude // 海拔高度
        let speed = currentLocation.speed // 速度 (米/秒)
        let course = currentLocation.course // 方向 (0-359.9度,0表示正北)
        let timestamp = currentLocation.timestamp // 获取到位置的时间
    
        print("获取到新位置: 纬度 \(latitude), 经度 \(longitude) @ \(timestamp)")
        print("海拔: \(altitude) 米, 速度: \(speed) 米/秒, 方向: \(course) 度")
    
        // 在这里,你就可以用 `currentLocation` 做你想做的事了,比如:
        // 1. 更新地图显示 (见下一步)
        updateMap(with: currentLocation.coordinate)
    
        // 2. 把坐标存起来 (如果需要持久化)
        // storeLocation(latitude: latitude, longitude: longitude)
    
        // 3. 如果你只需要一次定位,可以在获取到满意的位置后停止更新
        // if currentLocation.horizontalAccuracy <= locationManager.desiredAccuracy {
        //     print("达到期望精度,停止更新。")
        //     stopReceivingLocationUpdates()
        // }
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        print("获取位置失败: \(error.localizedDescription)")
        // 在这里可以处理错误,比如提示用户检查网络或 GPS 信号
        // 如果错误是 CLError.denied,说明用户在运行时通过设置关闭了权限
        if let clErr = error as? CLError, clErr.code == .denied {
            print("用户在运行时关闭了定位权限。")
            // 可能需要停止尝试定位或再次引导用户去设置
            stopReceivingLocationUpdates()
            showEnableLocationServicesAlert()
        }
    }
    
    // Placeholder for map update function (实现见下一步)
    func updateMap(with coordinate: CLLocationCoordinate2D) {
        print("准备更新地图,中心点: \(coordinate.latitude), \(coordinate.longitude)")
        // ... 地图操作代码 ...
    }
    

    进阶技巧 :

    • 精度与电量 : desiredAccuracy 不是保证值,系统会尽力满足。精度越高越耗电。根据实际需要选择最低可用精度。
    • distanceFilter : 如果你的 App 不需要实时追踪用户的每一点移动(比如只是标记用户大概位置),设置一个合适的 distanceFilter (如 50-100米) 可以大大减少更新频率,显著省电。
    • 首次定位 : 刚启动定位时,获取到的第一个位置可能精度较低(比如来自 Wi-Fi 定位)。你可以检查 horizontalAccuracy 属性(水平精度,单位是米),忽略掉精度太差的点,或者等精度达到某个阈值后再使用。
    • 一次性定位 : 如果你只需要获取一次用户当前位置,可以在 didUpdateLocations 中获取到满意的位置后,立即调用 stopUpdatingLocation()。苹果也提供了 requestLocation() 方法,它会尝试获取一次定位然后自动停止,并通过 didUpdateLocationsdidFailWithError 返回结果,更适合一次性定位场景。

第三步:在地图上显示位置 (MapKit)

现在我们拿到了用户的坐标 (CLLocationCoordinate2D),该把它显示在地图上了。这里我们用苹果原生的 MapKit 框架。

  1. 添加 MapKit 视图

    你可以在 Storyboard 里拖一个 MKMapView 控件到你的 ViewController 上,并创建 Outlet 连接;或者纯代码创建。

    import MapKit // 别忘了导入 MapKit
    
    class LocationViewController: UIViewController, CLLocationManagerDelegate { // 已遵循 CLLocationManagerDelegate
    
        // ... (之前 locationManager 的代码) ...
    
        // 如果用 Storyboard, 创建一个 IBOutlet
        @IBOutlet weak var mapView: MKMapView!
    
        // 如果纯代码创建:
        // let mapView = MKMapView()
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            locationManager.delegate = self
            checkLocationAuthorization()
    
            // 如果是纯代码创建 mapView, 需要添加到视图并设置约束
            /*
            view.addSubview(mapView)
            mapView.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                mapView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
                mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
                mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
                mapView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
            ])
            */
    
            // 让地图显示用户的当前位置蓝点 (需要有 'When In Use' 或 'Always' 权限)
            mapView.showsUserLocation = true
    
            // 可选:设置地图类型 (标准, 卫星, 混合等)
            // mapView.mapType = .standard
        }
    
        // ... (checkLocationAuthorization, start/stop updates, delegate methods) ...
    
        // MARK: - MapKit 更新
    
        func updateMap(with coordinate: CLLocationCoordinate2D) {
            print("正在更新地图显示...")
    
            // 1. 创建一个区域,以用户位置为中心
            let regionRadius: CLLocationDistance = 1000 // 显示区域半径,单位是米 (例如 1 公里)
            let coordinateRegion = MKCoordinateRegion(
                center: coordinate,
                latitudinalMeters: regionRadius,
                longitudinalMeters: regionRadius
            )
    
            // 2. 让地图移动并缩放到这个区域
            mapView.setRegion(coordinateRegion, animated: true)
    
            // 3. (可选) 在用户位置添加一个大头针标记
            // 先移除旧的标记 (如果之前添加过)
            mapView.removeAnnotations(mapView.annotations)
    
            let annotation = MKPointAnnotation()
            annotation.coordinate = coordinate
            annotation.title = "我的位置"
            annotation.subtitle = "纬度: \(String(format: "%.4f", coordinate.latitude)), 经度: \(String(format: "%.4f", coordinate.longitude))"
            mapView.addAnnotation(annotation)
    
             // 如果你只想显示系统提供的蓝点,不需要手动添加大头针,可以省略第 3 步。
             // mapView.showsUserLocation = true 会自动处理蓝点。
             // 如果你想自定义蓝点的外观,需要实现 MKMapViewDelegate 的相关方法。
        }
    
    } // End of LocationViewController
    

    代码解释 :

    • 确保导入 MapKit
    • 获取到 MKMapView 实例(通过 Outlet 或代码创建)。
    • 设置 mapView.showsUserLocation = true。只要 App 有定位权限,地图就会自动显示代表用户当前位置的蓝色圆点,并且会自动更新。很多时候这一个属性就够用了!
    • locationManager(_:didUpdateLocations:) 代理方法中,当你获取到 currentLocation.coordinate 后,调用 updateMap(with:) 方法。
    • updateMap 方法做了几件事:
      • MKCoordinateRegion: 定义了地图要显示的中心点和范围。latitudinalMeterslongitudinalMeters 决定了地图南北和东西方向各显示多少米。
      • mapView.setRegion: 命令地图视图移动到指定的区域。animated: true 会让过渡更平滑。
      • (可选) MKPointAnnotation: 如果你想在地图上添加一个自定义的大头针(pin),可以创建一个 MKPointAnnotation 对象,设置它的 coordinate, title, subtitle,然后用 mapView.addAnnotation() 添加到地图上。添加前最好先 mapView.removeAnnotations() 清除旧的标记,避免重复。

    效果 :
    现在,当 App 启动并获得定位权限后,CLLocationManager 会开始获取位置,每次获取到新位置时,didUpdateLocations 被调用,接着 updateMap 被调用,地图就会自动移动到用户当前位置,并可能显示一个大头针。系统自带的 showsUserLocation 蓝点也会在那里。


现在你应该清楚如何从配置权限开始,一步步获取用户位置,并最终在 MKMapView 上展示出来了。记住,处理用户位置信息务必谨慎,始终把用户隐私放在第一位。