HKWorkoutRouteBuilder 和 CLLocationManager 仅增量添加路线更新

Posted

技术标签:

【中文标题】HKWorkoutRouteBuilder 和 CLLocationManager 仅增量添加路线更新【英文标题】:HKWorkoutRouteBuilder and CLLocationManager only adding route updates in increments 【发布时间】:2021-04-26 17:57:43 【问题描述】:

我在 watchOS 上有一个功能锻炼应用程序,用于跟踪室内和室外跑步。我正在尝试使用 HKWorkoutRouteBuilder 将户外跑步路线添加到 HealthKit。实际户外跑步期间的实际测试仅在地图上显示部分路线更新,如下图所示的一小部分蓝点;

路线更新来自我在以下课程中设置的 CoreLocation。

class LocationManager: NSObject, CLLocationManagerDelegate 

    public var globalLocationManager: CLLocationManager?
    private var routeBuilder: HKWorkoutRouteBuilder?

    public func setUpLocationManager() 
        globalLocationManager = CLLocationManager()
        globalLocationManager?.delegate = self
        globalLocationManager?.desiredAccuracy = kCLLocationAccuracyBest
        // Update every 13.5 meters in order to achieve updates no faster than once every 3sec.
        // This assumes runner is running at no faster than 6min/mile - 3.7min/km
        globalLocationManager?.distanceFilter = 13.5
        // Can use `kCLDistanceFilterNone` ???? which will give more updates but still only at wide intervals.

        globalLocationManager?.activityType = .fitness
        /*
        from the docs
        ...if your app needs to receive location events while in the background,
        it must include the UIBackgroundModes key (with the location value) in its Info.plist file.
        */

        routeBuilder = HKWorkoutRouteBuilder(healthStore: healthStore, device: .local())

        globalLocationManager?.startUpdatingLocation()
        globalLocationManager?.allowsBackgroundLocationUpdates = true
    

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) 
        // Filter the raw data, excluding anything greater than 50m accuracy
        let filteredLocations = locations.filter  isAccurateTo -> Bool in
            isAccurateTo.horizontalAccuracy <= 50
        

        guard !filteredLocations.isEmpty else  return 

        routeBuilder?.insertRouteData(filteredLocations, completion:  success, error in
            if error != nil 
                // throw alert due to error in saving route.
                print("Error in \(#function) \(error?.localizedDescription ?? "Error in Route Builder")")
            
        )
    

    // Called in class WorkoutController when workout session ends.
    public func addRoute(to workout: HKWorkout) 
        routeBuilder?.finishRoute(with: workout, metadata: nil, completion:  workoutRoute, error in
            if workoutRoute == nil 
                fatalError("error saving workout route")
            
        )
    


然后使用 HKAnchoredObjectQuery 将路线添加到 SwiftUI 地图中;

public func getRouteFrom(workout: HKWorkout) 
        let mapDisplayAreaPadding = 1.3

        let runningObjectQuery = HKQuery.predicateForObjects(from: workout)

        let routeQuery = HKAnchoredObjectQuery(type: HKSeriesType.workoutRoute(), predicate: runningObjectQuery, anchor: nil, limit: HKObjectQueryNoLimit)  (query, samples, deletedObjects, anchor, error) in

            guard error == nil else 
                fatalError("The initial query failed.")
            
            // Make sure you have some route samples
            guard samples!.count > 0 else 
                return
            
            let route = samples?.first as! HKWorkoutRoute

            // Create the route query from HealthKit.
            let query = HKWorkoutRouteQuery(route: route)  (query, locationsOrNil, done, errorOrNil) in
                // This block may be called multiple times.
                if let error = errorOrNil 
                    print("Error \(error.localizedDescription)")
                    return
                

                guard let locations = locationsOrNil else 
                    fatalError("*** NIL found in locations ***")
                

                let latitudes = locations.map 
                    $0.coordinate.latitude
                
                let longitudes = locations.map 
                    $0.coordinate.longitude
                

                // Outline map region to display
                guard let maxLat = latitudes.max() else  fatalError("Unable to get maxLat") 
                guard let minLat = latitudes.min() else  return 
                guard let maxLong = longitudes.max() else  return 
                guard let minLong = longitudes.min() else  return 

                if done 
                    let mapCenter = CLLocationCoordinate2D(latitude: (minLat + maxLat) / 2, longitude: (minLong + maxLong) / 2)
                    let mapSpan = MKCoordinateSpan(latitudeDelta: (maxLat - minLat) * mapDisplayAreaPadding,
                                                  longitudeDelta: (maxLong - minLong) * mapDisplayAreaPadding)

                    DispatchQueue.main.async 
                        // Push to main thread to drop dots on the map.
                        // Without this a warning will occur.
                        self.region = MKCoordinateRegion(center: mapCenter, span: mapSpan)
                        locations.forEach  (location) in
                            self.overlayRoute(at: location)
                        
                    
                
                // stop the query by calling:
                // store.stop(query)
            
            healthStore.execute(query)
        

        routeQuery.updateHandler =  (query, samples, deleted, anchor, error) in
            guard error == nil else 
                // Handle any errors here.
                fatalError("The update failed.")
            
            // Process updates or additions here.
        
        healthStore.execute(routeQuery)
    

我无法确定为什么地图注释只能以突发形式显示。我已将CLLocationManager.distanceFilterkCLDistanceFilterNone 更改为不同的值。对于 CoreLocation 授权,我使用了 whileInUse 和 Always 授权,更新频率没有变化。似乎更新是按时间间隔进行的,而不是经过的距离,但我不能完全确定。让应用在屏幕上和后台处于活动状态似乎对位置更新没有影响。

非常感谢任何见解。

以下是要读取的 HealthKit 类型和要共享设置代码的类型:

public func setupWorkoutSession() 
        let authorizationStatus = healthStore.authorizationStatus(for: HKWorkoutType.workoutType())
        let typesToShare: Set = [HKQuantityType.workoutType(), HKSeriesType.workoutRoute()]

        let typesToRead: Set = [
            HKQuantityType.quantityType(forIdentifier: .heartRate)!,
            HKQuantityType.quantityType(forIdentifier: .activeEnergyBurned)!,
            HKQuantityType.quantityType(forIdentifier: .distanceWalkingRunning)!,
            HKSeriesType.workoutType(),
            HKSeriesType.workoutRoute()
        ]

        if authorizationStatus == .sharingDenied 
            showAlertView()
            return
        

        // Request authorization for those quantity types.
        healthStore.requestAuthorization(toShare: typesToShare, read: typesToRead)  (success, error) in
            if success 
                self.beginWorkout()
             else if error != nil 
                // Handle errors.
            
        
    

【问题讨论】:

【参考方案1】:

从您的问题中不清楚这些是前台更新还是后台更新,但考虑到似乎也经常出现的差距,我怀疑您主要是在获取后台位置更新。

您已经将activityType 设置为.fitness,我相信它可以提供最高分辨率,但无论如何,ios 仍会定期休眠位置管理器以保存能量。

由于您正在收集您所期望的或多或少恒定的移动场景,将pausesLocationUpdatesAutomatically 设置为false 也是一个好主意 - 明确控制 CLManager 的操作与开始和健身/跑步场景的停止。这会消耗更多电量,但应该会为您提供更一致的更新。

您仍然可能有一些差距,并且可能最好的解决方案是根据您收到它们的时间戳在这些差距之间插入路线位置。在一些密集的城市地区,您可能还需要或想要平滑或以其他方式限制路线点,因为我发现获得穿过建筑物的路线位置而不是显示您所在的街道或人行道是“容易的”走着。在天际线更开阔、建筑物更低的地方,我几乎没有看到过这个问题。

【讨论】:

感谢您的宝贵时间,但这不是答案。诚然,我没有在问题中提到位置更新来自 Apple Watch 并且 .pausesLocationUpdatesAutomatically 在 watchOS 上不可用。赏金仍然开放。 @DanO'Leary 这只是为了确认您是否给予了以下许可?? “特别是对于路线数据,您必须请求读取和共享 HKWorkout 和 HKWorkoutRoute 样本的权限。”这在 Apple 文档之一中有所提及。我相信你一定有,因为你已经得到了一些数据。 感谢您的澄清,是的,我正在阅读和分享 HealthKit 中的路线示例。 另外,您是否将 HKSeriesType.workoutType() 和 HKSeriesType.workoutRoute() 添加到示例对象中? 我添加了显示我如何设置 HealthKit 授权的代码。 @DevanshiModha【参考方案2】:

我后来发现间歇性位置更新的原因是由于我设置不正确的一些无关的计时器。该计时器正在杀死后台更新并影响位置服务。发布的代码确实有效。

【讨论】:

以上是关于HKWorkoutRouteBuilder 和 CLLocationManager 仅增量添加路线更新的主要内容,如果未能解决你的问题,请参考以下文章

如何在 HealthKit 中获得使用 Cordova 编写锻炼路线的许可

G++和GCC和C,C++有啥区别的

c语言和VC.C++和VC++的的详细区别

c语言和c++有啥关系

C语言和C++有啥区别?

OC和C的区别?