ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

iOS地图定位Demo实战:MapKit与CoreLocation权限、回调与避坑指南

iOS地图定位Demo实战:MapKit与CoreLocation权限、回调与避坑指南 简介面向iOS开发者的地图定位示例项目基于Core Location与MapKit框架完整演示从定位权限申请、位置更新回调到地图上展示用户位置、切换追踪模式的全流程。工程以MMLocationManager为主模块包含TestViewController、AppDelegate、定位管理器封装等Objective-C类目录结构清晰便于逐段对照阅读。压缩包共23个文件主要类型包括5个m源文件、3个h头文件、plist权限配置、strings本地化文件、json数据以及Xcode工程配置pbxproj、xcworkspacedata等整体仅42KB体量小巧适合快速下载和编译运行。已有265人学习浏览。通过该Demo可直观掌握CLLocationManager的初始化与代理设置、requestWhenInUseAuthorization等权限请求方式、desiredAccuracy和distanceFilter的定位精度与过滤间距设定核心位置更新在didUpdateLocations中处理经纬度数据并能调用stopUpdatingLocation停止更新以节省电量同时涉及Info.plist中定位权限描述键的配置配合MapKit还能设置地图区域、显示用户当前位置方便在实际App中快速集成地图定位能力对初学iOS定位功能的开发者尤其友好。1. 拿到一个 iOS 地图定位 demo到底是拿来干吗的很多人在真机上跑 iOS 地图定位 demo 时会撞上一堆说不清的现象地图上没蓝点、回调里没数据、模拟器上死活不出位置。这套东西看着是苹果原生 API但坑比想象的多——权限键写错一个字母整个定位链就静默失效连个报错都没有。这个 demo 走的是 MapKit CoreLocation 原生路线不依赖第三方 key适合 iOS 新手入门、面试前突击定位流程以及在接高德、百度地图 SDK 之前先把系统定位机制吃透的人。30 分钟能跑通但前提是你知道每一步该看哪里。2. 权限链与 CLLocationManager 初始化授权这关的时序陷阱定位功能最先碰的不是坐标是权限。很多 demo 跑不起来不是代码逻辑错了是 Info.plist 里的权限描述没配或者配错。苹果的规则很直接你必须在 Info.plist 里声明用定位干什么否则系统连弹窗都不会弹直接在回调里给你一个 denied。这不是偶发是机制。2.1 Info.plist 权限描述没有它一切静默失败在 Xcode 里新建工程后默认的 Info.plist 里没有定位相关的 key。你需要手动加下面这段keyNSLocationWhenInUseUsageDescription/key string需要获取您的位置用于在应用中展示地图定位信息/string这段是用户授权弹窗里的正文说明。键名一定不能写错NSLocationWhenInUseUsageDescription和NSLocationAlwaysAndWhenInUseUsageDescription是两个完全不同的 key。前者代表「使用 App 期间允许定位」后者代表「始终允许定位」。如果只想在 app 在前台时定位写前者就够了如果要做后台轨迹记录才需要后者。系统对权限状态的管理是分级的代码里判断逻辑时要清楚每一档的含义状态含义你能做什么notDetermined用户还没看到弹窗调用 requestWhenInUseAuthorization 触发弹窗restricted家长控制或设备管理限制不能请求权限直接提示用户denied用户拒绝引导去设置里开权限authorizedWhenInUse使用期间允许可以 startUpdatingLocationauthorizedAlways始终允许可以后台定位也可以 startUpdatingLocation有个细节容易被忽略iOS 14 之后用户可以在弹窗里选择「精确位置」或「模糊位置」。模糊位置给的是一个圆心范围精度大概半径几百米到几公里。如果你的 demo 只展示用户在哪模糊位置也能用但如果你要算轨迹距离拿到的误差会让你怀疑人生。真机上测试时注意在设置里检查是不是被降级成了模糊位置。2.2 CLLocationManager 实例化参数怎么设、delegate 为什么不能丢CoreLocation 的核心入口是 CLLocationManager。初始化看起来简单但有一个非常经典的坑CLLocationManager 的 delegate 是 weak 属性如果你的 manager 对象没有被强持有系统随时可能把它释放掉然后所有回调都不触发也不报错。这就是「黑匣子」式问题最常见于把 manager 写在一个局部变量里的场景。import CoreLocation class LocationService: NSObject, CLLocationManagerDelegate { private let manager CLLocationManager() override init() { super.init() manager.delegate self manager.desiredAccuracy kCLLocationAccuracyBest manager.distanceFilter 10 // 位移超过 10 米才回调一次 manager.activityType .other } func start() { let status manager.authorizationStatus if status .notDetermined { manager.requestWhenInUseAuthorization() } else if status .authorizedWhenInUse || status .authorizedAlways { manager.startUpdatingLocation() } } }这里有几个参数值得逐个说desiredAccuracy设置的是系统定位精度的期望值不是保证值。kCLLocationAccuracyBest会优先用 GPS功耗最高如果只是地图展示用kCLLocationAccuracyNearestTenMeters就够了省电不少。我一般做室内外切换判断时用 Best做骑行、步行轨迹用 NearestTenMeters。distanceFilter是位移阈值单位是米。默认值是kCLDistanceFilterNone意思是只要有新定位包就回调频率很高。设成 10 米表示「位移超过 10 米才回调一次」可以大幅减少 UI 刷新次数。做 demo 时设不设无所谓但做真实项目时这是必调的参数。activityType会给系统一个定位用途的提示影响 GPS 行为。.other最通用.automotiveNavigation会在车辆高速移动时调整定位策略.fitness会优化步行场景。demo 用.other就好。还有一点LocationService 实例必须在 ViewController 里作为属性持有不能是局部变量。否则 delegate 是弱引用manager 会被释放回调全丢。这是一个新手必踩的坑。2.3 授权时序请求权限后别急着等第一个坐标很多人在 viewDidLoad 里写「requestWhenInUseAuthorization() 之后马上 startUpdatingLocation()」结果发现回调不来。实际上授权弹窗弹出后用户还没点击任何按钮时系统不会给你位置数据。必须等用户在弹窗上做出选择系统回调授权状态变更后你再执行 startUpdatingLocation才能拿到坐标。func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { switch manager.authorizationStatus { case .authorizedWhenInUse, .authorizedAlways: manager.startUpdatingLocation() // 拿到授权再开始定位 case .denied, .restricted: print(定位权限不可用请在设置中开启) case .notDetermined: break unknown default: break } }注意locationManagerDidChangeAuthorization是 iOS 14 之后的新回调。iOS 13 及以下对应的是didChangeAuthorizationStatus如果你要兼容老系统需要两个回调一起写。现在新工程基本可以只写新回调因为真机上 iOS 14 已经覆盖绝大多数用户。但做 SDK 级别的代码还是建议两个都接。提示用户拒绝后从后台切到前台或者从设置里重新打开权限系统会再次回调locationManagerDidChangeAuthorization所以这个分支也承担了「权限恢复后重新开始定位」的工作。时序上还有一点容易踩在notDetermined状态直接调startUpdatingLocation是没有意义的因为根本没权限。正确顺序是——先 request 弹窗等 callback 里确认状态是 authorized再 start。这个顺序错乱是「为什么我明明点了允许还是没坐标」的头号原因。3. 地图显示与定位回调把坐标变成地图上的那个点权限通了定位回调也触发了接下来才是地图部分。MKMapView 是苹果原生地图组件跟 CoreLocation 配合起来不需要额外配置但有两个容易混淆的概念showsUserLocation只是在地图上「画」用户位置它不负责「获取」用户位置。真正的定位数据来自 CLLocationManager这两者各干各的没有直接绑定。3.1 MKMapView 接入显示用户位置的两种方式在 UIKit 工程里MKMapView 可以拖到 storyboard也可以代码创建。一套最小可用的配置长这样import MapKit class MapViewController: UIViewController, MKMapViewDelegate { IBOutlet weak var mapView: MKMapView! override func viewDidLoad() { super.viewDidLoad() mapView.delegate self mapView.showsUserLocation true mapView.userTrackingMode .follow } }showsUserLocation true会在当前位置显示蓝色圆点但这个蓝点要出现前提是 CoreLocation 已经拿到了坐标。如果你只开了 showsUserLocation 而没启动 CLLocationManager地图上什么都不会显示。userTrackingMode有三个值.none不跟随用户拖动地图后蓝点可能会移出屏幕.follow让蓝点始终居中地图跟着移动.followWithHeading在 follow 基础上加了一个方向角地图会跟着手机方向旋转。demo 里用.follow最合适因为能看到蓝点又不至于像导航那样转来转去。MKMapViewDelegate 的回调里regionDidChangeAnimated是最常用的一个地图区域变化时触发。注意不要在它里面再次调用setRegion否则会触发无限循环。3.2 didUpdateLocations 回调速度、方向、精度怎么处理CLLocationManager 的定位结果通过 delegate 回调返回每次回调带一个数组你通常只需要最后一个。func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let loc locations.last else { return } guard loc.horizontalAccuracy 0 else { return } let lat loc.coordinate.latitude let lon loc.coordinate.longitude print(坐标: \(lat), \(lon)) print(速度: \(loc.speed) m/s, 方向: \(loc.course)°) let region MKCoordinateRegion( center: loc.coordinate, latitudinalMeters: 500, longitudinalMeters: 500 ) mapView.setRegion(region, animated: true) }有几个数据字段值得知道含义字段含义判读建议coordinate经纬度直接用于地图和业务逻辑horizontalAccuracy水平精度单位米负数无效0 是模拟器常见值100 基本是网络定位speed速度m/s静止时为 0步行约 1.4骑行约 4course航向角度0北90东180南270西timestamp定位时间注意判断是否太旧超过 5 秒就值得怀疑horizontalAccuracy是第一个要看的字段。如果它小于 0说明这次定位结果无效直接丢弃。如果它大于 100 米说明这次是 WiFi/基站定位或者刚从室内走出来位置可信度很低。我一般会设一个阈值小于 50 米才拿去画地图小于 20 米才去做导航计算。MKCoordinateRegion的latitudinalMeters和longitudinalMeters决定地图缩放范围。500 米适合步行、找附近地点开车导航用 1000 到 2000 米更合适。设置这个参数后地图会自动把中心点移到你传的坐标配合animated: true有平滑过渡效果。定位回调频率可能非常高如果直接把速度、方向、坐标全刷新到 UI 上容易出现掉帧。常见做法是做一个「最后一个有效点」缓存回调里先把坐标存起来等 UI 的定时器或 CADisplayLink 去读取而不是每次回调都直接刷新 UI。demo 里可以不做这个优化但要知道为什么真实项目要做。3.3 反向地理编码把坐标变成文字地址坐标是一串数字用户看不懂。大部分地图 demo 会在定位成功后尝试反编码把经纬度变成「城市 街道」这样的文字。import CoreLocation let geocoder CLGeocoder() func reverseGeocode(_ loc: CLLocation) { geocoder.reverseGeocodeLocation(loc) { placemarks, error in guard let placemark placemarks?.first, error nil else { return } let city placemark.locality ?? let street placemark.thoroughfare ?? let detail \(city) \(street) DispatchQueue.main.async { self.addressLabel.text detail } } }这里有两个实操要点。第一CLGeocoder 的 reverseGeocodeLocation 是网络请求苹果有限流策略定位回调越频繁越不能每次回调都触发反编码。常见做法是记住上一个反编码的坐标位移超过 100 米才再次发起请求。第二回调不一定在主线程UI 更新必须切回 main。placemark 里常用的字段locality是城市administrativeArea是省thoroughfare是街道名称name是具体地名或 POI。国内数据下有些字段可能为空拼接时做好空值兜底。4. 定位排查与避坑模拟器、坐标系、后台策略四个坑跑定位 demo 最耗时间的不是写代码是排查那些「系统不报错、行为不正常」的静默问题。这里把最常见的四个坑按现象拆开讲每一条都是实际调试中会遇到的情况。4.1 现象模拟器上一直拿不到定位回调真机上一切正常一换模拟器地图上没有蓝点didUpdateLocations 完全不触发。调试到心态崩溃还以为是自己代码的问题。原因在模拟器本身它没有 GPS 硬件必须由你手动指定模拟位置。如果 Xcode 的 Debug 菜单里没有选择任何模拟位置CoreLocation 根本不会产生回调代码再对也没用。解决步骤很简单在 Xcode 里选 Debug Simulate Location Apple或者任意一个预设位置蓝点就会出现在对应地点。也可以在模拟器菜单栏选 Features Location City Run。如果预设位置不够用后面会讲怎么自定义 GPX 轨迹。注意一点模拟器返回的 horizontalAccuracy 通常在几十米量级有时候甚至是 0不能按真机的精度标准去过滤数据。你在模拟器上验证逻辑流程但精度表现必须以真机为准。4.2 现象地图上标注点偏移几十米甚至几百米定位坐标看起来没问题放到地图上却偏了。这种偏移量不是固定方向也不是固定大小而且换个地图 SDK 偏移程度还不一样。原因基本可以确定是坐标系混用。CoreLocation 返回的是 WGS-84 坐标苹果自家 MapKit 也用 WGS-84所以原生组合没有偏移问题。但如果你在高德地图、腾讯地图上展示这个坐标问题就来了——国内地图强制使用 GCJ-02火星坐标系把 WGS-84 坐标直接甩给高德 SDK就会产生几十到几百米的偏移。解决方式是统一坐标系。用苹果 MapKit不用做任何转换用高德 SDK必须先把 WGS-84 转成 GCJ-02 再传。反过来如果从高德 SDK 拿到坐标要往苹果地图上放也要先转回去。不要在一个 demo 里前端用 MapKit、后端接高德数据坐标乱成一锅粥。注意中国区域内的 GPS 原始数据是经过偏移的国外坐标不受影响。这也是为什么很多定位 bug 只在国内容易复现。4.3 现象用户拒绝授权后app 毫无反应第一次授权弹窗被用户点了拒绝之后再进 app一切照常运行没有报错、没有提示但就是不定位。用户也完全不知道该怎么恢复。原因是拒绝授权后CLLocationManager 不会给你任何错误回调startUpdatingLocation静默无效。这不是 bug是设计如此。Apple 希望开发者通过权限状态主动引导用户。解决方式是在locationManagerDidChangeAuthorization的.denied分支加一个 UI 提示引导用户去系统设置里重新打开权限case .denied, .restricted: let alert UIAlertController( title: 需要定位权限, message: 请在 设置 隐私 定位服务 \(Bundle.main.displayName ?? 本应用) 中开启, preferredStyle: .alert ) alert.addAction(UIAlertAction(title: 去设置, style: .default) { _ in guard let url URL(string: UIApplication.openSettingsURLString) else { return } UIApplication.shared.open(url) }) present(alert, animated: true)UIApplication.openSettingsURLString会直接跳到当前 app 的系统设置页避免用户在里面乱翻。这是一个 10 行代码救回无数用户的操作。在网上的很多 demo 里这步经常被省略所以 DEMO 看着简单实际用起来缺个口子。4.4 现象加了后台定位配置却被审核拒了在 Capabilities 里勾了 Background Modes 的 Location UpdatesInfo.plist 里也写了权限描述但提交审核时还是被拒。或者更奇怪真机调试时后台跑了一会儿就停。原因通常是权限描述和后台模式不匹配。后台定位需要的是NSLocationAlwaysAndWhenInUseUsageDescription并且代码里要调requestAlwaysAuthorization而不只是 WhenInUse。如果 Info.plist 里只有 WhenInUse 的描述系统根本不认为你有后台定位资格App Store 审核也会以「用途说明不足」为由打回。解决方式分两种情况。如果 demo 不需要后台跟踪最简单的方法是关掉 Background Modes 里的 Location Updates只保留 WhenInUse审核就能过。如果确实需要后台轨迹记录要在 Info.plist 里补上NSLocationAlwaysAndWhenInUseUsageDescription并且代码中调用requestAlwaysAuthorization()。另外很多人把「显著位置变化」和「后台定位」混为一谈。startMonitoringSignificantLocationChanges不需要后台模式系统会在位置大幅变化时自动唤醒 app但它不保证持续追踪。如果你的 demo 被做成「锁屏后还在持续记录轨迹但系统频繁杀掉 app」检查一下是不是用了错误的策略组合。5. 进阶调试用 GPX 模拟轨迹与坐标系校准定位功能写到能跑只是第一步真正要验证轨迹、距离计算这类逻辑不可能每次都在马路上跑一圈。GPX 文件可以让你坐在工位上模拟一整条骑行路线比手动选点实用得多。5.1 用 GPX 文件模拟轨迹在 Xcode 里Debug Simulate Location Add GPX File to Workspace然后创建一个 .gpx 文件?xml version1.0? gpx version1.1 creatorXcode wpt lat31.2304 lon121.4737 name起点/name time2024-06-01T10:00:00Z/time /wpt wpt lat31.2310 lon121.4742 name途经点/name time2024-06-01T10:01:00Z/time /wpt wpt lat31.2321 lon121.4750 name终点/name time2024-06-01T10:02:00Z/time /wpt /gpx保存后回到模拟器在 Debug Simulate Location 里选中这个文件模拟器就会按照 wpt 的顺序移动。time字段决定每踩到一个点的时间节点差值越小移动速度越快。做骑行轨迹、配送路线演示时这个方式可以完全脱离真机完成流程验证省下至少一个下午的体力活。5.2 给 demo 留一个 WGS-84 与 GCJ-02 互转函数如果你的 demo 未来要接高德导航或腾讯地图坐标系转换函数迟早要用。在工具类里预留一个标准互转是按业界惯例的最优做法private let a 6378245.0 private let ee 0.006693421622965943 func outOfChina(lat: Double, lon: Double) - Bool { return lon 72.004 || lon 137.8347 || lat 0.8293 || lat 55.8271 } func transformLat(x: Double, y: Double) - Double { var ret -100.0 2.0 * x 3.0 * y 0.2 * y * y 0.1 * x * y 0.2 * sqrt(abs(x)) ret (20.0 * sin(6.0 * x * .pi) 20.0 * sin(2.0 * x * .pi)) * 2.0 / 3.0 ret (20.0 * sin(y * .pi) 40.0 * sin(y / 3.0 * .pi)) * 2.0 / 3.0 ret (160.0 * sin(y / 12.0 * .pi) 320.0 * sin(y * .pi / 30.0)) * 2.0 / 3.0 return ret } func transformLon(x: Double, y: Double) - Double { var ret 300.0 x 2.0 * y 0.1 * x * x 0.1 * x * y 0.1 * sqrt(abs(x)) ret (20.0 * sin(6.0 * x * .pi) 20.0 * sin(2.0 * x * .pi)) * 2.0 / 3.0 ret (20.0 * sin(x * .pi) 40.0 * sin(x / 3.0 * .pi)) * 2.0 / 3.0 ret (150.0 * sin(x / 12.0 * .pi) 300.0 * sin(x / 30.0 * .pi)) * 2.0 / 3.0 return ret } func wgs84ToGcj02(lat: Double, lon: Double) - (Double, Double) { if outOfChina(lat: lat, lon: lon) { return (lat, lon) } let dLat transformLat(x: lon - 105.0, y: lat - 35.0) let dLon transformLon(x: lon - 105.0, y: lat - 35.0) let radLat lat / 180.0 * .pi var magic sin(radLat) magic 1 - ee * magic * magic let sqrtMagic sqrt(magic) let dLatFinal (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * .pi) let dLonFinal (dLon * 180.0) / (a / sqrtMagic * cos(radLat) * .pi) return (lat dLatFinal, lon dLonFinal) }这套算法是国内地图开发里跑了很多年的一套逻辑outOfChina先做边界判断避免对非中国区域坐标做无意义的偏移。要注意的是只有接高德、腾讯地图时才需要调用wgs84ToGcj02纯苹果 MapKit 场景不要用转了反而偏。我后来每次跑定位 demo无论多急第一件事都是三个检查Info.plist 权限键有没有写对、CLLocationManager 有没有被强引用、模拟器位置有没有设置。这三件不查完后面排错就是浪费时间。从那以后我跑任何地图定位 demo 都会强制把这三个检查走一遍再去碰 UI。这个习惯救过我不少次因为定位的问题很少有显式报错黑匣子不响不按顺序查就只能瞎猜。希望帮到你。本文还有配套的精品资源点击获取
返回列表