Showing
5 changed files
with
199 additions
and
26 deletions
| @@ -41,13 +41,7 @@ final class HealthDataReader { | @@ -41,13 +41,7 @@ final class HealthDataReader { | ||
| 41 | startDate: startDate, | 41 | startDate: startDate, |
| 42 | endDate: endDate | 42 | endDate: endDate |
| 43 | ) | 43 | ) |
| 44 | - async let stand = fetchDailyCumulativeSamples( | ||
| 45 | - identifier: .appleStandTime, | ||
| 46 | - dataType: .stand, | ||
| 47 | - unit: .second(), | ||
| 48 | - startDate: startDate, | ||
| 49 | - endDate: endDate | ||
| 50 | - ) | 44 | + async let stand = fetchStandData(startDate: startDate, endDate: endDate) |
| 51 | async let steps = fetchDailyCumulativeSamples( | 45 | async let steps = fetchDailyCumulativeSamples( |
| 52 | identifier: .stepCount, | 46 | identifier: .stepCount, |
| 53 | dataType: .steps, | 47 | dataType: .steps, |
| @@ -225,14 +219,7 @@ final class HealthDataReader { | @@ -225,14 +219,7 @@ final class HealthDataReader { | ||
| 225 | } | 219 | } |
| 226 | 220 | ||
| 227 | func fetchStandData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] { | 221 | func fetchStandData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] { |
| 228 | - try await fetchDailyCumulativeSamples(identifier: .appleStandTime, dataType: .stand, unit: .second(), startDate: startDate, endDate: endDate) | ||
| 229 | -// try await fetchQuantitySamples( | ||
| 230 | -// identifier: .appleStandTime, | ||
| 231 | -// dataType: .stand, | ||
| 232 | -// unit: .minute(), | ||
| 233 | -// startDate: startDate, | ||
| 234 | -// endDate: endDate | ||
| 235 | -// ) | 222 | + try await fetchDailyStandHours(startDate: startDate, endDate: endDate) |
| 236 | } | 223 | } |
| 237 | 224 | ||
| 238 | func fetchStepCountData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] { | 225 | func fetchStepCountData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] { |
| @@ -296,7 +283,16 @@ final class HealthDataReader { | @@ -296,7 +283,16 @@ final class HealthDataReader { | ||
| 296 | continuation.resume( | 283 | continuation.resume( |
| 297 | returning: NativeActivityTarget( | 284 | returning: NativeActivityTarget( |
| 298 | move: Int(summary.activeEnergyBurnedGoal.doubleValue(for: .kilocalorie())), | 285 | move: Int(summary.activeEnergyBurnedGoal.doubleValue(for: .kilocalorie())), |
| 299 | - stand: Int(summary.appleStandHoursGoal.doubleValue(for: .count())) | 286 | + stand: Int(summary.appleStandHoursGoal.doubleValue(for: .count())), |
| 287 | + activityMoveMode: Self.activityMoveModeValue(from: summary), | ||
| 288 | + activeEnergyBurned: summary.activeEnergyBurned.doubleValue(for: .kilocalorie()), | ||
| 289 | + activeEnergyBurnedGoal: summary.activeEnergyBurnedGoal.doubleValue(for: .kilocalorie()), | ||
| 290 | + appleMoveTime: Self.appleMoveTimeValue(from: summary), | ||
| 291 | + appleMoveTimeGoal: Self.appleMoveTimeGoalValue(from: summary), | ||
| 292 | + appleExerciseTime: summary.appleExerciseTime.doubleValue(for: .minute()), | ||
| 293 | + exerciseTimeGoal: Self.exerciseTimeGoalValue(from: summary), | ||
| 294 | + appleStandHours: summary.appleStandHours.doubleValue(for: .count()), | ||
| 295 | + standHoursGoal: Self.standHoursGoalValue(from: summary) | ||
| 300 | ) | 296 | ) |
| 301 | ) | 297 | ) |
| 302 | } | 298 | } |
| @@ -304,6 +300,41 @@ final class HealthDataReader { | @@ -304,6 +300,41 @@ final class HealthDataReader { | ||
| 304 | } | 300 | } |
| 305 | } | 301 | } |
| 306 | 302 | ||
| 303 | + private static func activityMoveModeValue(from summary: HKActivitySummary) -> Int? { | ||
| 304 | + if #available(iOS 14.0, *) { | ||
| 305 | + return summary.activityMoveMode.rawValue | ||
| 306 | + } | ||
| 307 | + return nil | ||
| 308 | + } | ||
| 309 | + | ||
| 310 | + private static func appleMoveTimeValue(from summary: HKActivitySummary) -> Double? { | ||
| 311 | + if #available(iOS 14.0, *) { | ||
| 312 | + return summary.appleMoveTime.doubleValue(for: .minute()) | ||
| 313 | + } | ||
| 314 | + return nil | ||
| 315 | + } | ||
| 316 | + | ||
| 317 | + private static func appleMoveTimeGoalValue(from summary: HKActivitySummary) -> Double? { | ||
| 318 | + if #available(iOS 14.0, *) { | ||
| 319 | + return summary.appleMoveTimeGoal.doubleValue(for: .minute()) | ||
| 320 | + } | ||
| 321 | + return nil | ||
| 322 | + } | ||
| 323 | + | ||
| 324 | + private static func exerciseTimeGoalValue(from summary: HKActivitySummary) -> Double? { | ||
| 325 | + if #available(iOS 16.0, *) { | ||
| 326 | + return summary.exerciseTimeGoal?.doubleValue(for: .minute()) | ||
| 327 | + } | ||
| 328 | + return nil | ||
| 329 | + } | ||
| 330 | + | ||
| 331 | + private static func standHoursGoalValue(from summary: HKActivitySummary) -> Double? { | ||
| 332 | + if #available(iOS 16.0, *) { | ||
| 333 | + return summary.standHoursGoal?.doubleValue(for: .count()) | ||
| 334 | + } | ||
| 335 | + return nil | ||
| 336 | + } | ||
| 337 | + | ||
| 307 | private func fetchHeartRateFamily(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] { | 338 | private func fetchHeartRateFamily(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] { |
| 308 | async let heartRate = fetchQuantitySamples( | 339 | async let heartRate = fetchQuantitySamples( |
| 309 | identifier: .heartRate, | 340 | identifier: .heartRate, |
| @@ -416,6 +447,41 @@ final class HealthDataReader { | @@ -416,6 +447,41 @@ final class HealthDataReader { | ||
| 416 | } | 447 | } |
| 417 | } | 448 | } |
| 418 | 449 | ||
| 450 | + private func fetchDailyStandHours(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] { | ||
| 451 | + let calendar = Calendar.current | ||
| 452 | + var start = calendar.dateComponents([.era, .year, .month, .day], from: startDate) | ||
| 453 | + var end = calendar.dateComponents([.era, .year, .month, .day], from: endDate) | ||
| 454 | + start.calendar = calendar | ||
| 455 | + end.calendar = calendar | ||
| 456 | + | ||
| 457 | + let predicate = HKQuery.predicate(forActivitySummariesBetweenStart: start, end: end) | ||
| 458 | + | ||
| 459 | + return try await withCheckedThrowingContinuation { continuation in | ||
| 460 | + let query = HKActivitySummaryQuery(predicate: predicate) { _, summaries, error in | ||
| 461 | + if let error { | ||
| 462 | + continuation.resume(throwing: error) | ||
| 463 | + return | ||
| 464 | + } | ||
| 465 | + | ||
| 466 | + let points = (summaries ?? []) | ||
| 467 | + .compactMap { summary -> NativeHealthDataPoint? in | ||
| 468 | + guard let date = calendar.date(from: summary.dateComponents(for: calendar)) else { | ||
| 469 | + return nil | ||
| 470 | + } | ||
| 471 | + return NativeHealthDataPoint( | ||
| 472 | + dataType: .stand, | ||
| 473 | + time: date.timeIntervalSince1970, | ||
| 474 | + value: summary.appleStandHours.doubleValue(for: .count()) | ||
| 475 | + ) | ||
| 476 | + } | ||
| 477 | + .sorted { $0.time < $1.time } | ||
| 478 | + | ||
| 479 | + continuation.resume(returning: points) | ||
| 480 | + } | ||
| 481 | + healthStore.execute(query) | ||
| 482 | + } | ||
| 483 | + } | ||
| 484 | + | ||
| 419 | private func fetchSleepIntervals(startDate: Date, endDate: Date) async throws -> [NativeSleepInterval] { | 485 | private func fetchSleepIntervals(startDate: Date, endDate: Date) async throws -> [NativeSleepInterval] { |
| 420 | guard let type = NativeHealthTypeCatalog.category(.sleepAnalysis) else { | 486 | guard let type = NativeHealthTypeCatalog.category(.sleepAnalysis) else { |
| 421 | throw NativeHealthKitError.invalidType("sleepAnalysis") | 487 | throw NativeHealthKitError.invalidType("sleepAnalysis") |
| @@ -52,6 +52,63 @@ struct NativeSleepInterval: Codable { | @@ -52,6 +52,63 @@ struct NativeSleepInterval: Codable { | ||
| 52 | struct NativeActivityTarget { | 52 | struct NativeActivityTarget { |
| 53 | let move: Int? | 53 | let move: Int? |
| 54 | let stand: Int? | 54 | let stand: Int? |
| 55 | + let activityMoveMode: Int? | ||
| 56 | + // 千卡 | ||
| 57 | + let activeEnergyBurned: Double? | ||
| 58 | + let activeEnergyBurnedGoal: Double? | ||
| 59 | + // 公里 | ||
| 60 | + let appleMoveTime: Double? | ||
| 61 | + let appleMoveTimeGoal: Double? | ||
| 62 | + // 分钟 | ||
| 63 | + let appleExerciseTime: Double? | ||
| 64 | + let exerciseTimeGoal: Double? | ||
| 65 | + //小时 | ||
| 66 | + let appleStandHours: Double? | ||
| 67 | + let standHoursGoal: Double? | ||
| 68 | +} | ||
| 69 | + | ||
| 70 | +extension NativeActivityTarget { | ||
| 71 | + var uploadBody: [String: Any] { | ||
| 72 | + var body: [String: Any] = [:] | ||
| 73 | + for child in Mirror(reflecting: self).children { | ||
| 74 | + guard let label = child.label, let value = Self.unwrapOptional(child.value) else { | ||
| 75 | + continue | ||
| 76 | + } | ||
| 77 | + body[label.camelCaseToSnakeCase] = value | ||
| 78 | + } | ||
| 79 | + return body | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + var uploadBodyForDebug: String { | ||
| 83 | + let pairs = uploadBody | ||
| 84 | + .map { "\($0.key)=\($0.value)" } | ||
| 85 | + .sorted() | ||
| 86 | + .joined(separator: ", ") | ||
| 87 | + return "{\(pairs)}" | ||
| 88 | + } | ||
| 89 | + | ||
| 90 | + private static func unwrapOptional(_ value: Any) -> Any? { | ||
| 91 | + let mirror = Mirror(reflecting: value) | ||
| 92 | + guard mirror.displayStyle == .optional else { | ||
| 93 | + return value | ||
| 94 | + } | ||
| 95 | + return mirror.children.first?.value | ||
| 96 | + } | ||
| 97 | +} | ||
| 98 | + | ||
| 99 | +private extension String { | ||
| 100 | + var camelCaseToSnakeCase: String { | ||
| 101 | + unicodeScalars.reduce(into: "") { result, scalar in | ||
| 102 | + if CharacterSet.uppercaseLetters.contains(scalar) { | ||
| 103 | + if !result.isEmpty { | ||
| 104 | + result.append("_") | ||
| 105 | + } | ||
| 106 | + result.append(String(scalar).lowercased()) | ||
| 107 | + } else { | ||
| 108 | + result.append(String(scalar)) | ||
| 109 | + } | ||
| 110 | + } | ||
| 111 | + } | ||
| 55 | } | 112 | } |
| 56 | 113 | ||
| 57 | enum NativeSleepStage { | 114 | enum NativeSleepStage { |
| @@ -269,7 +269,7 @@ private extension NativeHealthDataUploader { | @@ -269,7 +269,7 @@ private extension NativeHealthDataUploader { | ||
| 269 | debugLog("[activityTarget] no target data") | 269 | debugLog("[activityTarget] no target data") |
| 270 | return UploadTaskResult(success: true, uploadedCount: 0, errorMessage: nil) | 270 | return UploadTaskResult(success: true, uploadedCount: 0, errorMessage: nil) |
| 271 | } | 271 | } |
| 272 | - debugLog("[activityTarget] uploading, move=\(target.move.map(String.init) ?? "<nil>"), stand=\(target.stand.map(String.init) ?? "<nil>")") | 272 | + debugLog("[activityTarget] uploading, body=\(target.uploadBodyForDebug)") |
| 273 | try await uploadActivityTarget(target) | 273 | try await uploadActivityTarget(target) |
| 274 | debugLog("[activityTarget] upload success") | 274 | debugLog("[activityTarget] upload success") |
| 275 | return UploadTaskResult(success: true, uploadedCount: 1, errorMessage: nil) | 275 | return UploadTaskResult(success: true, uploadedCount: 1, errorMessage: nil) |
| @@ -338,7 +338,7 @@ private extension NativeHealthDataUploader { | @@ -338,7 +338,7 @@ private extension NativeHealthDataUploader { | ||
| 338 | "value": $0.value, | 338 | "value": $0.value, |
| 339 | ] as [String: Any] | 339 | ] as [String: Any] |
| 340 | } | 340 | } |
| 341 | - try await request(path: "/client/doublefeel/health/data_upload/common/", method: "POST", body: ["data_list": list]) | 341 | + try await request(path: "/client/doublefeel/health/v2/data_upload/common/", method: "POST", body: ["data_list": list]) |
| 342 | } | 342 | } |
| 343 | 343 | ||
| 344 | func uploadSleep(_ data: [NativeSleepInterval]) async throws { | 344 | func uploadSleep(_ data: [NativeSleepInterval]) async throws { |
| @@ -349,18 +349,15 @@ private extension NativeHealthDataUploader { | @@ -349,18 +349,15 @@ private extension NativeHealthDataUploader { | ||
| 349 | "to_time": $0.toTime, | 349 | "to_time": $0.toTime, |
| 350 | ] as [String: Any] | 350 | ] as [String: Any] |
| 351 | } | 351 | } |
| 352 | - try await request(path: "/client/doublefeel/health/data_upload/sleep/", method: "POST", body: ["data_list": list]) | 352 | + try await request(path: "/client/doublefeel/health/v2/data_upload/sleep/", method: "POST", body: ["data_list": list]) |
| 353 | } | 353 | } |
| 354 | 354 | ||
| 355 | func uploadActivityTarget(_ target: NativeActivityTarget) async throws { | 355 | func uploadActivityTarget(_ target: NativeActivityTarget) async throws { |
| 356 | - var body: [String: Any] = [:] | ||
| 357 | - body["move"] = target.move | ||
| 358 | - body["stand"] = target.stand | ||
| 359 | - try await request(path: "/client/doublefeel/health/activity_target/", method: "POST", body: body) | 356 | + try await request(path: "/client/doublefeel/health/v2/activity_target/", method: "POST", body: target.uploadBody) |
| 360 | } | 357 | } |
| 361 | 358 | ||
| 362 | func fetchLastUploadTime() async throws -> NativeHealthUploadTimeList { | 359 | func fetchLastUploadTime() async throws -> NativeHealthUploadTimeList { |
| 363 | - let data = try await request(path: "/client/doublefeel/health/data_upload/common/", method: "GET") | 360 | + let data = try await request(path: "/client/doublefeel/health/v2/data_upload/common/", method: "GET") |
| 364 | return try NativeHealthUploadTimeList.decode(from: data) | 361 | return try NativeHealthUploadTimeList.decode(from: data) |
| 365 | } | 362 | } |
| 366 | 363 |
| @@ -215,22 +215,66 @@ struct HealthSleepUploadDataPoint: Hashable { | @@ -215,22 +215,66 @@ struct HealthSleepUploadDataPoint: Hashable { | ||
| 215 | struct HealthActivityTargetData: Hashable { | 215 | struct HealthActivityTargetData: Hashable { |
| 216 | var move: Int64? = nil | 216 | var move: Int64? = nil |
| 217 | var stand: Int64? = nil | 217 | var stand: Int64? = nil |
| 218 | + var activityMoveMode: Int64? = nil | ||
| 219 | + var activeEnergyBurned: Double? = nil | ||
| 220 | + var activeEnergyBurnedGoal: Double? = nil | ||
| 221 | + var appleMoveTime: Double? = nil | ||
| 222 | + var appleMoveTimeGoal: Double? = nil | ||
| 223 | + var appleExerciseTime: Double? = nil | ||
| 224 | + var appleExerciseTimeGoal: Double? = nil | ||
| 225 | + var exerciseTimeGoal: Double? = nil | ||
| 226 | + var appleStandHours: Double? = nil | ||
| 227 | + var appleStandHoursGoal: Double? = nil | ||
| 228 | + var standHoursGoal: Double? = nil | ||
| 218 | 229 | ||
| 219 | 230 | ||
| 220 | // swift-format-ignore: AlwaysUseLowerCamelCase | 231 | // swift-format-ignore: AlwaysUseLowerCamelCase |
| 221 | static func fromList(_ pigeonVar_list: [Any?]) -> HealthActivityTargetData? { | 232 | static func fromList(_ pigeonVar_list: [Any?]) -> HealthActivityTargetData? { |
| 222 | let move: Int64? = nilOrValue(pigeonVar_list[0]) | 233 | let move: Int64? = nilOrValue(pigeonVar_list[0]) |
| 223 | let stand: Int64? = nilOrValue(pigeonVar_list[1]) | 234 | let stand: Int64? = nilOrValue(pigeonVar_list[1]) |
| 235 | + let activityMoveMode: Int64? = nilOrValue(pigeonVar_list[2]) | ||
| 236 | + let activeEnergyBurned: Double? = nilOrValue(pigeonVar_list[3]) | ||
| 237 | + let activeEnergyBurnedGoal: Double? = nilOrValue(pigeonVar_list[4]) | ||
| 238 | + let appleMoveTime: Double? = nilOrValue(pigeonVar_list[5]) | ||
| 239 | + let appleMoveTimeGoal: Double? = nilOrValue(pigeonVar_list[6]) | ||
| 240 | + let appleExerciseTime: Double? = nilOrValue(pigeonVar_list[7]) | ||
| 241 | + let appleExerciseTimeGoal: Double? = nilOrValue(pigeonVar_list[8]) | ||
| 242 | + let exerciseTimeGoal: Double? = nilOrValue(pigeonVar_list[9]) | ||
| 243 | + let appleStandHours: Double? = nilOrValue(pigeonVar_list[10]) | ||
| 244 | + let appleStandHoursGoal: Double? = nilOrValue(pigeonVar_list[11]) | ||
| 245 | + let standHoursGoal: Double? = nilOrValue(pigeonVar_list[12]) | ||
| 224 | 246 | ||
| 225 | return HealthActivityTargetData( | 247 | return HealthActivityTargetData( |
| 226 | move: move, | 248 | move: move, |
| 227 | - stand: stand | 249 | + stand: stand, |
| 250 | + activityMoveMode: activityMoveMode, | ||
| 251 | + activeEnergyBurned: activeEnergyBurned, | ||
| 252 | + activeEnergyBurnedGoal: activeEnergyBurnedGoal, | ||
| 253 | + appleMoveTime: appleMoveTime, | ||
| 254 | + appleMoveTimeGoal: appleMoveTimeGoal, | ||
| 255 | + appleExerciseTime: appleExerciseTime, | ||
| 256 | + appleExerciseTimeGoal: appleExerciseTimeGoal, | ||
| 257 | + exerciseTimeGoal: exerciseTimeGoal, | ||
| 258 | + appleStandHours: appleStandHours, | ||
| 259 | + appleStandHoursGoal: appleStandHoursGoal, | ||
| 260 | + standHoursGoal: standHoursGoal | ||
| 228 | ) | 261 | ) |
| 229 | } | 262 | } |
| 230 | func toList() -> [Any?] { | 263 | func toList() -> [Any?] { |
| 231 | return [ | 264 | return [ |
| 232 | move, | 265 | move, |
| 233 | stand, | 266 | stand, |
| 267 | + activityMoveMode, | ||
| 268 | + activeEnergyBurned, | ||
| 269 | + activeEnergyBurnedGoal, | ||
| 270 | + appleMoveTime, | ||
| 271 | + appleMoveTimeGoal, | ||
| 272 | + appleExerciseTime, | ||
| 273 | + appleExerciseTimeGoal, | ||
| 274 | + exerciseTimeGoal, | ||
| 275 | + appleStandHours, | ||
| 276 | + appleStandHoursGoal, | ||
| 277 | + standHoursGoal, | ||
| 234 | ] | 278 | ] |
| 235 | } | 279 | } |
| 236 | static func == (lhs: HealthActivityTargetData, rhs: HealthActivityTargetData) -> Bool { | 280 | static func == (lhs: HealthActivityTargetData, rhs: HealthActivityTargetData) -> Bool { |
| @@ -161,7 +161,16 @@ final class HealthKitHostApiImpl: HealthKitHostApi { | @@ -161,7 +161,16 @@ final class HealthKitHostApiImpl: HealthKitHostApi { | ||
| 161 | completion(.success(target.map { | 161 | completion(.success(target.map { |
| 162 | HealthActivityTargetData( | 162 | HealthActivityTargetData( |
| 163 | move: $0.move.map(Int64.init), | 163 | move: $0.move.map(Int64.init), |
| 164 | - stand: $0.stand.map(Int64.init) | 164 | + stand: $0.stand.map(Int64.init), |
| 165 | + activityMoveMode: $0.activityMoveMode.map(Int64.init), | ||
| 166 | + activeEnergyBurned: $0.activeEnergyBurned, | ||
| 167 | + activeEnergyBurnedGoal: $0.activeEnergyBurnedGoal, | ||
| 168 | + appleMoveTime: $0.appleMoveTime, | ||
| 169 | + appleMoveTimeGoal: $0.appleMoveTimeGoal, | ||
| 170 | + appleExerciseTime: $0.appleExerciseTime, | ||
| 171 | + exerciseTimeGoal: $0.exerciseTimeGoal, | ||
| 172 | + appleStandHours: $0.appleStandHours, | ||
| 173 | + standHoursGoal: $0.standHoursGoal | ||
| 165 | ) | 174 | ) |
| 166 | })) | 175 | })) |
| 167 | } catch { | 176 | } catch { |
-
Please register or login to post a comment