用户接受位置权限提示后CurrentLocation为nil的iOS问题如何解决
问题根因
崩溃的核心原因有两个:
- 你当前的LocationManager封装只处理了权限状态的等待,完全没处理定位成功的异步过程:权限授权通过不代表定位已经拿到结果,此时CLLocationManager的location属性默认是nil,你用隐式解包的
CLLocation!去接当然会崩溃。第二次启动时因为系统已经有权限,且之前的定位缓存还在,所以location有值不会崩。 - 你用OperationQueue的实现逻辑有漏洞:权限通过就直接把队列放开,队列里的任务立刻执行,完全不等定位结果返回。
优化后的实现方案
第一步:改造LocationManager单例,支持定位结果回调
直接把定位请求、权限判断、结果返回都封装在单例内部,对外只暴露获取坐标的方法,不需要业务层处理权限和定位逻辑:
import Foundation import CoreLocation class LocationManager: NSObject, CLLocationManagerDelegate { static let sharedInstance = LocationManager() private var locationManager = CLLocationManager() // 存储所有待执行的定位完成回调 private var locationCallbacks: [(Result<CLLocation, Error>) -> Void] = [] // 标记是否正在定位中,避免重复发起请求 private var isLocating = false enum LocationError: Error { case permissionDenied case locateFailed } override init(){ super.init() locationManager.delegate = self // 可以根据你的业务需求调整定位精度 locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters } // 对外暴露的唯一方法:获取当前坐标 func fetchCurrentLocation(callback: @escaping (Result<CLLocation, Error>) -> Void) { let authStatus = CLLocationManager.authorizationStatus() // 先处理权限情况 switch authStatus { case .notDetermined: // 权限未申请,先存回调,再申请权限 locationCallbacks.append(callback) locationManager.requestWhenInUseAuthorization() case .authorizedWhenInUse, .authorizedAlways: // 权限已给,存回调,发起定位 locationCallbacks.append(callback) startLocation() case .denied, .restricted: // 权限被拒,直接返回错误 callback(.failure(LocationError.permissionDenied)) @unknown default: callback(.failure(LocationError.locateFailed)) } } private func startLocation() { guard !isLocating else { return } isLocating = true locationManager.startUpdatingLocation() } // 权限变更回调 func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { switch status { case .authorizedWhenInUse, .authorizedAlways: // 权限通过,发起定位 startLocation() case .denied, .restricted: // 权限被拒,回调所有等待的任务返回错误 isLocating = false locationCallbacks.forEach { $0(.failure(LocationError.permissionDenied)) } locationCallbacks.removeAll() default: break } } // 定位成功回调 func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let location = locations.last else { return } // 停止定位节省功耗 locationManager.stopUpdatingLocation() isLocating = false // 把定位结果回调给所有等待的业务方 locationCallbacks.forEach { $0(.success(location)) } locationCallbacks.removeAll() } // 定位失败回调 func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { locationManager.stopUpdatingLocation() isLocating = false locationCallbacks.forEach { $0(.failure(LocationError.locateFailed)) } locationCallbacks.removeAll() } }
第二步:业务层调用逻辑调整
你原来的业务代码直接替换成以下写法即可,不需要再处理权限判断、定位是否为空的逻辑:
LocationManager.sharedInstance.fetchCurrentLocation { [weak self] result in switch result { case .success(let currentLocation): // 拿到坐标直接发起请求,不会为空 let latitude = currentLocation.coordinate.latitude let longitude = currentLocation.coordinate.longitude // 拼接你的请求参数,发起API请求 SessionManager.manager.request(url, parameters: ["lat": latitude, "lon": longitude]).responseJSON { responseData in // 解析数据,刷新页面 callback() } case .failure(let error): // 这里可以处理权限被拒、定位失败的情况,比如默认返回热门城市的演出数据,或者弹提示引导用户开权限 print("定位失败:\(error)") } }
额外注意点
- 不要用sleep等待异步结果,sleep会卡住当前线程,完全起不到等待定位的作用,反而会让App卡住无响应
- 业务层不要直接访问
CLLocationManager.location属性,这个属性是系统缓存的定位结果,可能为空,也可能是很久之前的过期定位,必须通过didUpdateLocations回调拿最新的有效定位 - info.plist里一定要添加
NSLocationWhenInUseUsageDescription的权限描述,否则申请权限会直接崩溃
内容的提问来源于stack exchange,提问作者mrbenhill
相关产品推荐
相关产品推荐

