HKWorkoutRouteBuilder 和 CLLocationManager 仅增量添加路由更新
HKWorkoutRouteBuilder and CLLocationManager only adding route updates in increments
我在 watchOS 上有一个功能性锻炼应用程序,用于跟踪室内和室外跑步。我正在尝试使用 HKWorkoutRouteBuilder 将户外 运行 路线添加到 HealthKit。实际户外跑步过程中的实际测试在地图上仅显示部分路线更新,如下图所示为一串小蓝点;
路由更新来自我在下面设置的CoreLocation class。
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 {
[=11=].coordinate.latitude
}
let longitudes = locations.map {
[=11=].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.distanceFilter
和 kCLDistanceFilterNone
更改为不同的值。对于 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.
}
}
}
从你的问题中不清楚这些是前台更新还是后台更新,但考虑到间隙似乎也有规律的频率,我怀疑你主要是在获取后台位置更新。
您已经将 activityType
设置为 .fitness
,我相信这会提供最高分辨率,但无论如何 iOS 仍会定期让位置管理器休眠以节省能源.
由于您正在收集您期望的或多或少恒定的移动场景,因此将 pausesLocationUpdatesAutomatically
设置为 false
也是一个好主意 - 明确控制fitness/run场景启动和停止的CLManager操作。这会消耗更多电量,但应该会为您提供更一致的更新。
您仍然可能会有一些间隙,最好的解决方案可能是根据您收到它们的时间戳在这些间隙之间插入路线位置。在一些人口稠密的城市地区,您可能还需要或想要平滑或以其他方式限制路线点,因为我发现获得穿过建筑物的路线位置而不是显示您所在的街道或人行道是“容易”的走着。我在天际线更开放、建筑物更低的地方几乎没有看到这个问题。
后来我发现间歇性位置更新的原因是我设置不正确的有点不相关的计时器。该计时器正在终止后台更新并影响定位服务。发布的代码确实有效。
我在 watchOS 上有一个功能性锻炼应用程序,用于跟踪室内和室外跑步。我正在尝试使用 HKWorkoutRouteBuilder 将户外 运行 路线添加到 HealthKit。实际户外跑步过程中的实际测试在地图上仅显示部分路线更新,如下图所示为一串小蓝点;
路由更新来自我在下面设置的CoreLocation class。
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 {
[=11=].coordinate.latitude
}
let longitudes = locations.map {
[=11=].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.distanceFilter
和 kCLDistanceFilterNone
更改为不同的值。对于 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.
}
}
}
从你的问题中不清楚这些是前台更新还是后台更新,但考虑到间隙似乎也有规律的频率,我怀疑你主要是在获取后台位置更新。
您已经将 activityType
设置为 .fitness
,我相信这会提供最高分辨率,但无论如何 iOS 仍会定期让位置管理器休眠以节省能源.
由于您正在收集您期望的或多或少恒定的移动场景,因此将 pausesLocationUpdatesAutomatically
设置为 false
也是一个好主意 - 明确控制fitness/run场景启动和停止的CLManager操作。这会消耗更多电量,但应该会为您提供更一致的更新。
您仍然可能会有一些间隙,最好的解决方案可能是根据您收到它们的时间戳在这些间隙之间插入路线位置。在一些人口稠密的城市地区,您可能还需要或想要平滑或以其他方式限制路线点,因为我发现获得穿过建筑物的路线位置而不是显示您所在的街道或人行道是“容易”的走着。我在天际线更开放、建筑物更低的地方几乎没有看到这个问题。
后来我发现间歇性位置更新的原因是我设置不正确的有点不相关的计时器。该计时器正在终止后台更新并影响定位服务。发布的代码确实有效。