|
...
|
...
|
@@ -10,19 +10,25 @@ import HealthKit |
|
|
|
import WidgetKit
|
|
|
|
import Combine
|
|
|
|
|
|
|
|
class WatchDataManager: ObservableObject {
|
|
|
|
@MainActor
|
|
|
|
final class WatchDataManager: ObservableObject {
|
|
|
|
static let share = WatchDataManager()
|
|
|
|
private let healthStore = HKHealthStore()
|
|
|
|
@Published var myLatestHRVData: HRVData?
|
|
|
|
@Published var otherLatestHRVData: HRVData?
|
|
|
|
|
|
|
|
@Published var latestHRVData: WatchHRVData?
|
|
|
|
@Published var activityTarget: ActivityTarget?
|
|
|
|
|
|
|
|
@Published var myTodayAvgHRV: Double?
|
|
|
|
@Published var myData: HFriendTodayData?
|
|
|
|
@Published var friendData: HFriendTodayData?
|
|
|
|
|
|
|
|
// @Published var myTodayAvgHRV: Double?
|
|
|
|
@Published var myLatestHeartRate: Double?
|
|
|
|
@Published var myTodayStepCount: Int?
|
|
|
|
@Published var myTodayActiveEnergy: Double?
|
|
|
|
@Published var myTodayExerciseTime: Double?
|
|
|
|
@Published var myTodayStandTime: Double?
|
|
|
|
@Published var myTodaySleepTime: TimeInterval?
|
|
|
|
@Published var otherTodayHealthInfo: TodayHealthInfo?
|
|
|
|
|
|
|
|
|
|
|
|
// 想读取的类型,比如心率、步数
|
|
|
|
private let readTypes: Set<HKObjectType> = [
|
|
|
|
HKObjectType.quantityType(forIdentifier: .heartRate)!,
|
|
...
|
...
|
@@ -33,7 +39,7 @@ class WatchDataManager: ObservableObject { |
|
|
|
HKObjectType.quantityType(forIdentifier: .activeEnergyBurned)!,
|
|
|
|
HKObjectType.quantityType(forIdentifier: .appleExerciseTime)!,
|
|
|
|
HKObjectType.quantityType(forIdentifier: .appleStandTime)!,
|
|
|
|
|
|
|
|
|
|
|
|
HKObjectType.quantityType(forIdentifier: .walkingHeartRateAverage)!,//步行心率
|
|
|
|
HKObjectType.quantityType(forIdentifier: .restingHeartRate)!,//静息心率
|
|
|
|
//睡眠心率(注)
|
|
...
|
...
|
@@ -42,9 +48,18 @@ class WatchDataManager: ObservableObject { |
|
|
|
,
|
|
|
|
HKObjectType.quantityType(forIdentifier: .appleSleepingWristTemperature)!,
|
|
|
|
HKObjectType.quantityType(forIdentifier: .respiratoryRate)!,
|
|
|
|
HKObjectType.categoryType(forIdentifier: .irregularHeartRhythmEvent)!,
|
|
|
|
]
|
|
|
|
|
|
|
|
func requestAuthForHealth() {
|
|
|
|
|
|
|
|
func requestAuthForHealth(completion: ((Bool) -> Void)?) {
|
|
|
|
guard HKHealthStore.isHealthDataAvailable() else {
|
|
|
|
print("❌ HealthKit is not available on this watch")
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
completion?(false)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let typesToShare: Set = [
|
|
|
|
HKObjectType.workoutType()
|
|
|
|
]
|
|
...
|
...
|
@@ -57,19 +72,49 @@ class WatchDataManager: ObservableObject { |
|
|
|
} else {
|
|
|
|
print("❌ HealthKit not authorized by user")
|
|
|
|
}
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
completion?(success && error == nil)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func logout(){
|
|
|
|
latestHRVData = nil
|
|
|
|
activityTarget = nil
|
|
|
|
friendData = nil
|
|
|
|
|
|
|
|
// myTodayAvgHRV = nil
|
|
|
|
myLatestHeartRate = nil
|
|
|
|
myTodayStepCount = nil
|
|
|
|
myTodayActiveEnergy = nil
|
|
|
|
myTodayExerciseTime = nil
|
|
|
|
myTodayStandTime = nil
|
|
|
|
myTodaySleepTime = nil
|
|
|
|
|
|
|
|
[
|
|
|
|
AppGroupConstants.Key.latestHealthData,
|
|
|
|
AppGroupConstants.Key.acitivyTarget,
|
|
|
|
AppGroupConstants.Key.myTodayStandTime,
|
|
|
|
AppGroupConstants.Key.myTodayExerciseTime,
|
|
|
|
AppGroupConstants.Key.myTodayActiveEnergy,
|
|
|
|
AppGroupConstants.Key.myTodayStepCount,
|
|
|
|
AppGroupConstants.Key.myLatestHeartRate
|
|
|
|
].forEach { key in
|
|
|
|
AppGroupConstants.defaults?.removeObject(forKey: key)
|
|
|
|
}
|
|
|
|
WatchWidgetReloader.reloadAllDynamicWidgets()
|
|
|
|
WatchHealthWidgetRefreshCoordinator.shared.refreshAll()
|
|
|
|
}
|
|
|
|
|
|
|
|
func fetchCoupleLatestHRV() {
|
|
|
|
fetchLatestHRV()
|
|
|
|
fetchOtherLatestHRV()
|
|
|
|
getLatestHealthData()
|
|
|
|
getAcitivtyTarget()
|
|
|
|
observeHealthData()
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private func fetchLatestHRV() {
|
|
|
|
guard let hrvType = HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN) else { return }
|
|
|
|
|
|
|
|
|
|
|
|
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
|
|
|
|
let query = HKSampleQuery(sampleType: hrvType,
|
|
|
|
predicate: nil,
|
|
...
|
...
|
@@ -80,52 +125,27 @@ class WatchDataManager: ObservableObject { |
|
|
|
print("查询最新HRV失败: \(error.localizedDescription)")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if let sample = samples?.first as? HKQuantitySample {
|
|
|
|
let value = sample.quantity.doubleValue(for: .secondUnit(with: .milli))
|
|
|
|
Task {
|
|
|
|
let result = try? await HealthService().getLatestHRV()
|
|
|
|
let myHrvBaseline = result?.userHrvBaseline
|
|
|
|
let data = HRVData(value: value,
|
|
|
|
hrvBaseline: myHrvBaseline,
|
|
|
|
date: sample.endDate)
|
|
|
|
await MainActor.run {
|
|
|
|
self.myLatestHRVData = data
|
|
|
|
}
|
|
|
|
self.saveHRVBaselineValue(myHrvBaseline)
|
|
|
|
}
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.saveHRVValue(value)
|
|
|
|
self.uploadHRV(value: value, date: sample.endDate)
|
|
|
|
WidgetCenter.shared.reloadAllTimelines()
|
|
|
|
WatchWidgetReloader.reloadHRV()
|
|
|
|
print("最新HRV: \(sample)")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
healthStore.execute(query)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func fetchOtherLatestHRV() {
|
|
|
|
Task {
|
|
|
|
let result = try? await HealthService().getLatestHRV()
|
|
|
|
await MainActor.run {
|
|
|
|
if let value = result?.pairUserHrv {
|
|
|
|
let data = HRVData(value: value,
|
|
|
|
hrvBaseline: result?.pairHrvBaseline,
|
|
|
|
date: nil)
|
|
|
|
self.otherLatestHRVData = data
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private func fetchTodayAvgHRV() {
|
|
|
|
guard let hrvType = HKObjectType.quantityType(forIdentifier: .heartRateVariabilitySDNN) else { return }
|
|
|
|
|
|
|
|
|
|
|
|
let startOfDay = Calendar.current.startOfDay(for: Date())
|
|
|
|
let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: Date(), options: .strictStartDate)
|
|
|
|
|
|
|
|
|
|
|
|
let query = HKSampleQuery(sampleType: hrvType,
|
|
|
|
predicate: predicate,
|
|
|
|
limit: HKObjectQueryNoLimit,
|
|
...
|
...
|
@@ -135,26 +155,26 @@ class WatchDataManager: ObservableObject { |
|
|
|
print("查询HRV失败: \(error.localizedDescription)")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
guard let samples = samples as? [HKQuantitySample], !samples.isEmpty else { return }
|
|
|
|
|
|
|
|
|
|
|
|
let total = samples.reduce(0.0) { $0 + $1.quantity.doubleValue(for: .secondUnit(with: .milli)) }
|
|
|
|
let average = total / Double(samples.count)
|
|
|
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.myTodayAvgHRV = average
|
|
|
|
}
|
|
|
|
|
|
|
|
// DispatchQueue.main.async {
|
|
|
|
// self.myTodayAvgHRV = average
|
|
|
|
// }
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
healthStore.execute(query)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private func fetchTodayLatestHeartRate() {
|
|
|
|
guard let heartRateType = HKObjectType.quantityType(forIdentifier: .heartRate) else { return }
|
|
|
|
|
|
|
|
|
|
|
|
let startOfDay = Calendar.current.startOfDay(for: Date())
|
|
|
|
let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: Date(), options: .strictStartDate)
|
|
|
|
|
|
|
|
|
|
|
|
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierEndDate, ascending: false)
|
|
|
|
let query = HKSampleQuery(sampleType: heartRateType,
|
|
|
|
predicate: predicate,
|
|
...
|
...
|
@@ -165,24 +185,32 @@ class WatchDataManager: ObservableObject { |
|
|
|
print("查询今日最新心率失败: \(error.localizedDescription)")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if let sample = samples?.first as? HKQuantitySample {
|
|
|
|
let value = sample.quantity.doubleValue(for: .count().unitDivided(by: .minute()))
|
|
|
|
AppGroupConstants.defaults?.set(value, forKey: AppGroupConstants.Key.myLatestHeartRate)
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.myLatestHeartRate = value
|
|
|
|
WatchWidgetReloader.reloadHeartRate()
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
AppGroupConstants.defaults?.removeObject(forKey: AppGroupConstants.Key.myLatestHeartRate)
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.myLatestHeartRate = nil
|
|
|
|
WatchWidgetReloader.reloadHeartRate()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
healthStore.execute(query)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private func fetchTodayStepCount() {
|
|
|
|
guard let stepType = HKObjectType.quantityType(forIdentifier: .stepCount) else { return }
|
|
|
|
|
|
|
|
|
|
|
|
let startOfDay = Calendar.current.startOfDay(for: Date())
|
|
|
|
let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: Date(), options: .strictStartDate)
|
|
|
|
|
|
|
|
|
|
|
|
let query = HKStatisticsQuery(quantityType: stepType,
|
|
|
|
quantitySamplePredicate: predicate,
|
|
|
|
options: .cumulativeSum) { [weak self] _, result, error in
|
|
...
|
...
|
@@ -191,26 +219,24 @@ class WatchDataManager: ObservableObject { |
|
|
|
print("查询步数失败: \(error.localizedDescription)")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if let sum = result?.sumQuantity() {
|
|
|
|
let value = Int(sum.doubleValue(for: .count()))
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.myTodayStepCount = value
|
|
|
|
self.saveStepCountValue(value)
|
|
|
|
WidgetCenter.shared.reloadAllTimelines()
|
|
|
|
}
|
|
|
|
|
|
|
|
let value = Int(result?.sumQuantity()?.doubleValue(for: .count()) ?? 0)
|
|
|
|
AppGroupConstants.defaults?.set(value, forKey: AppGroupConstants.Key.myTodayStepCount)
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.myTodayStepCount = value
|
|
|
|
WatchWidgetReloader.reloadSteps()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
healthStore.execute(query)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private func fetchTodayActiveEnergy() {
|
|
|
|
guard let energyType = HKObjectType.quantityType(forIdentifier: .activeEnergyBurned) else { return }
|
|
|
|
|
|
|
|
|
|
|
|
let startOfDay = Calendar.current.startOfDay(for: Date())
|
|
|
|
let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: Date(), options: .strictStartDate)
|
|
|
|
|
|
|
|
|
|
|
|
let query = HKStatisticsQuery(quantityType: energyType,
|
|
|
|
quantitySamplePredicate: predicate,
|
|
|
|
options: .cumulativeSum) { [weak self] _, result, error in
|
|
...
|
...
|
@@ -219,36 +245,89 @@ class WatchDataManager: ObservableObject { |
|
|
|
print("查询活动能量失败: \(error.localizedDescription)")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if let sum = result?.sumQuantity() {
|
|
|
|
let value = sum.doubleValue(for: .kilocalorie())
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.myTodayActiveEnergy = value
|
|
|
|
}
|
|
|
|
|
|
|
|
let value = result?.sumQuantity()?.doubleValue(for: .kilocalorie()) ?? 0
|
|
|
|
AppGroupConstants.defaults?.set(value, forKey: AppGroupConstants.Key.myTodayActiveEnergy)
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.myTodayActiveEnergy = value
|
|
|
|
WatchWidgetReloader.reloadActivity()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
healthStore.execute(query)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private func fetchTodayExerciseTime() {
|
|
|
|
guard let exerciseType = HKObjectType.quantityType(forIdentifier: .appleExerciseTime) else { return }
|
|
|
|
|
|
|
|
let startOfDay = Calendar.current.startOfDay(for: Date())
|
|
|
|
let predicate = HKQuery.predicateForSamples(withStart: startOfDay, end: Date(), options: .strictStartDate)
|
|
|
|
|
|
|
|
let query = HKStatisticsQuery(quantityType: exerciseType,
|
|
|
|
quantitySamplePredicate: predicate,
|
|
|
|
options: .cumulativeSum) { [weak self] _, result, error in
|
|
|
|
guard let self = self else { return }
|
|
|
|
if let error = error {
|
|
|
|
print("查询锻炼时间失败: \(error.localizedDescription)")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let value = result?.sumQuantity()?.doubleValue(for: .minute()) ?? 0
|
|
|
|
AppGroupConstants.defaults?.set(value, forKey: AppGroupConstants.Key.myTodayExerciseTime)
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.myTodayExerciseTime = value
|
|
|
|
WatchWidgetReloader.reloadActivity()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
healthStore.execute(query)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func fetchTodayStandTime() {
|
|
|
|
let calendar = Calendar.current
|
|
|
|
var start = calendar.dateComponents([.era, .year, .month, .day], from: Date())
|
|
|
|
var end = start
|
|
|
|
start.calendar = calendar
|
|
|
|
end.calendar = calendar
|
|
|
|
|
|
|
|
let predicate = HKQuery.predicate(forActivitySummariesBetweenStart: start, end: end)
|
|
|
|
|
|
|
|
let query = HKActivitySummaryQuery(predicate: predicate) { [weak self] _, summaries, error in
|
|
|
|
guard let self = self else { return }
|
|
|
|
if let error = error {
|
|
|
|
print("查询站立小时失败: \(error.localizedDescription)")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
let value = summaries?.last?.appleStandHours.doubleValue(for: .count()) ?? 0
|
|
|
|
AppGroupConstants.defaults?.set(value, forKey: AppGroupConstants.Key.myTodayStandTime)
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
self.myTodayStandTime = value
|
|
|
|
WatchWidgetReloader.reloadActivity()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
healthStore.execute(query)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func fetchLastNightSleepDuration() {
|
|
|
|
Task {
|
|
|
|
let calendar = Calendar.current
|
|
|
|
let today = Date()
|
|
|
|
|
|
|
|
|
|
|
|
// 定义昨天晚上的时间范围(从昨天15:00到今天15:00)
|
|
|
|
let yesterday = calendar.date(byAdding: .day, value: -1, to: today)!
|
|
|
|
let searchStartTime = calendar.date(bySettingHour: 15, minute: 0, second: 0, of: yesterday)!
|
|
|
|
let searchEndTime = calendar.date(bySettingHour: 15, minute: 0, second: 0, of: today)!
|
|
|
|
|
|
|
|
|
|
|
|
guard let sleepType = HKObjectType.categoryType(forIdentifier: .sleepAnalysis) else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let predicate = HKQuery.predicateForSamples(withStart: searchStartTime, end: searchEndTime, options: .strictStartDate)
|
|
|
|
let sortDescriptor = NSSortDescriptor(key: HKSampleSortIdentifierStartDate, ascending: true)
|
|
|
|
|
|
|
|
|
|
|
|
let samples = try? await withCheckedThrowingContinuation { (continuation: CheckedContinuation<[HKCategorySample], Error>) in
|
|
|
|
let query = HKSampleQuery(
|
|
|
|
sampleType: sleepType,
|
|
...
|
...
|
@@ -260,26 +339,26 @@ class WatchDataManager: ObservableObject { |
|
|
|
continuation.resume(throwing: error)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let categorySamples = samples?.compactMap { $0 as? HKCategorySample } ?? []
|
|
|
|
continuation.resume(returning: categorySamples)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
healthStore.execute(query)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
guard let samples,
|
|
|
|
!samples.isEmpty else { return }
|
|
|
|
|
|
|
|
|
|
|
|
// 按开始时间排序
|
|
|
|
let sortedSamples = samples.sorted { $0.startDate < $1.startDate }
|
|
|
|
|
|
|
|
|
|
|
|
// 转换为睡眠阶段数据
|
|
|
|
let sleepStages = sortedSamples.compactMap { sample -> SleepStage? in
|
|
|
|
guard let sleepValue = HKCategoryValueSleepAnalysis(rawValue: sample.value) else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return SleepStage(
|
|
|
|
stage: sleepValue,
|
|
|
|
startTime: sample.startDate,
|
|
...
|
...
|
@@ -287,14 +366,14 @@ class WatchDataManager: ObservableObject { |
|
|
|
duration: sample.endDate.timeIntervalSince(sample.startDate)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
guard !sleepStages.isEmpty else { return }
|
|
|
|
|
|
|
|
|
|
|
|
// 计算总体睡眠时间范围
|
|
|
|
let overallStartTime = sleepStages.first!.startTime
|
|
|
|
let overallEndTime = sleepStages.last!.endTime
|
|
|
|
let totalDuration = overallEndTime.timeIntervalSince(overallStartTime)
|
|
|
|
|
|
|
|
|
|
|
|
// 计算实际睡眠时间(排除清醒和在床上但未睡着的时间)
|
|
|
|
let actualSleepTime = sleepStages
|
|
|
|
.filter { stage in
|
|
...
|
...
|
@@ -308,10 +387,10 @@ class WatchDataManager: ObservableObject { |
|
|
|
}
|
|
|
|
}
|
|
|
|
.reduce(0) { $0 + $1.duration }
|
|
|
|
|
|
|
|
|
|
|
|
// 计算睡眠效率
|
|
|
|
let sleepEfficiency = totalDuration > 0 ? (actualSleepTime / totalDuration) * 100 : nil
|
|
|
|
|
|
|
|
|
|
|
|
let data = SleepData(
|
|
|
|
startTime: overallStartTime,
|
|
|
|
endTime: overallEndTime,
|
|
...
|
...
|
@@ -320,22 +399,24 @@ class WatchDataManager: ObservableObject { |
|
|
|
totalSleepTime: actualSleepTime,
|
|
|
|
sleepEfficiency: sleepEfficiency
|
|
|
|
)
|
|
|
|
|
|
|
|
self.myTodaySleepTime = data.totalSleepTime
|
|
|
|
|
|
|
|
await MainActor.run {
|
|
|
|
self.myTodaySleepTime = data.totalSleepTime
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 方法1: 获取最新的单个事件
|
|
|
|
func fetchLatestIrregularRhythmEvent() {
|
|
|
|
let irregularRhythmType = HKCategoryType.categoryType(
|
|
|
|
forIdentifier: .irregularHeartRhythmEvent
|
|
|
|
)!
|
|
|
|
|
|
|
|
|
|
|
|
let sortDescriptor = NSSortDescriptor(
|
|
|
|
key: HKSampleSortIdentifierStartDate,
|
|
|
|
ascending: false // 降序,最新的在前
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
let query = HKSampleQuery(
|
|
|
|
sampleType: irregularRhythmType,
|
|
|
|
predicate: nil,
|
|
...
|
...
|
@@ -345,18 +426,18 @@ class WatchDataManager: ObservableObject { |
|
|
|
if let error = error {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
guard let sample = samples?.first as? HKCategorySample else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
self.uploadIrregularRhythm(value: sample.value,
|
|
|
|
date: sample.startDate)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
healthStore.execute(query)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private func fetchLatestWristTemperature() {
|
|
|
|
guard let wristTempType = HKObjectType.quantityType(forIdentifier: .appleSleepingWristTemperature) else { return }
|
|
|
|
|
|
...
|
...
|
@@ -379,7 +460,7 @@ class WatchDataManager: ObservableObject { |
|
|
|
|
|
|
|
healthStore.execute(query)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private func fetchLatestRespiratoryRate() {
|
|
|
|
guard let respiratoryType = HKObjectType.quantityType(forIdentifier: .respiratoryRate) else { return }
|
|
|
|
|
|
...
|
...
|
@@ -406,114 +487,87 @@ class WatchDataManager: ObservableObject { |
|
|
|
|
|
|
|
extension WatchDataManager {
|
|
|
|
func observeHealthData() {
|
|
|
|
guard HKHealthStore.isHealthDataAvailable() else { return }
|
|
|
|
|
|
|
|
let hrvType = HKQuantityType.quantityType(forIdentifier: .heartRateVariabilitySDNN)!
|
|
|
|
|
|
|
|
// 设置 observer query
|
|
|
|
let observerQuery = HKObserverQuery(sampleType: hrvType, predicate: nil) { [weak self] _, completionHandler, error in
|
|
|
|
if let _ = error {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// 有新数据,启动 Anchored Query 获取
|
|
|
|
self?.fetchLatestHRV()
|
|
|
|
completionHandler()
|
|
|
|
}
|
|
|
|
|
|
|
|
healthStore.execute(observerQuery)
|
|
|
|
healthStore.enableBackgroundDelivery(for: hrvType, frequency: .immediate) { success, error in
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
let stepType = HKQuantityType.quantityType(forIdentifier: .stepCount)!
|
|
|
|
|
|
|
|
// 设置 observer query
|
|
|
|
let stepObserverQuery = HKObserverQuery(sampleType: stepType, predicate: nil) { [weak self] _, completionHandler, error in
|
|
|
|
if let _ = error {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// 有新数据,启动 Anchored Query 获取
|
|
|
|
self?.fetchTodayStepCount()
|
|
|
|
completionHandler()
|
|
|
|
WatchHealthObserverUploader.shared.startObserversIfNeeded()
|
|
|
|
}
|
|
|
|
|
|
|
|
private func saveHRVData(_ healthData: WatchHRVData?) {
|
|
|
|
if let healthData{
|
|
|
|
do{
|
|
|
|
let data = try JSONEncoder().encode( healthData)
|
|
|
|
AppGroupConstants.defaults?.set(data, forKey: AppGroupConstants.Key.latestHealthData)
|
|
|
|
}catch{}
|
|
|
|
}else{
|
|
|
|
AppGroupConstants.defaults?.removeObject(forKey: AppGroupConstants.Key.latestHealthData)
|
|
|
|
}
|
|
|
|
|
|
|
|
healthStore.execute(stepObserverQuery)
|
|
|
|
healthStore.enableBackgroundDelivery(for: stepType, frequency: .immediate) { success, error in
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
private func saveActivityTarget(_ target: ActivityTarget?) {
|
|
|
|
if let target{
|
|
|
|
do{
|
|
|
|
let data = try JSONEncoder().encode( target)
|
|
|
|
AppGroupConstants.defaults?.set(data, forKey: AppGroupConstants.Key.acitivyTarget)
|
|
|
|
}catch{}
|
|
|
|
}else{
|
|
|
|
AppGroupConstants.defaults?.removeObject(forKey: AppGroupConstants.Key.acitivyTarget)
|
|
|
|
}
|
|
|
|
|
|
|
|
//房颤
|
|
|
|
let irregularRhythmType = HKCategoryType.categoryType(
|
|
|
|
forIdentifier: .irregularHeartRhythmEvent
|
|
|
|
)!
|
|
|
|
|
|
|
|
// 设置 observer query
|
|
|
|
let irregularRhythmObserverQuery = HKObserverQuery(sampleType: irregularRhythmType, predicate: nil) { [weak self] _, completionHandler, error in
|
|
|
|
if let _ = error {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension WatchDataManager {
|
|
|
|
func uploadTodayActivityTarget() {
|
|
|
|
let calendar = Calendar.current
|
|
|
|
var start = calendar.dateComponents([.era, .year, .month, .day], from: Date())
|
|
|
|
var end = start
|
|
|
|
start.calendar = calendar
|
|
|
|
end.calendar = calendar
|
|
|
|
|
|
|
|
let predicate = HKQuery.predicate(forActivitySummariesBetweenStart: start, end: end)
|
|
|
|
let query = HKActivitySummaryQuery(predicate: predicate) { [weak self] _, summaries, error in
|
|
|
|
guard let self else { return }
|
|
|
|
if let error {
|
|
|
|
print("查询活动目标失败: \(error.localizedDescription)")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// 有新数据,启动 Anchored Query 获取
|
|
|
|
self?.fetchLatestIrregularRhythmEvent()
|
|
|
|
completionHandler()
|
|
|
|
}
|
|
|
|
|
|
|
|
healthStore.execute(irregularRhythmObserverQuery)
|
|
|
|
healthStore.enableBackgroundDelivery(for: irregularRhythmType, frequency: .immediate) { success, error in
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
//睡眠手腕温度
|
|
|
|
let wristTempType = HKQuantityType.quantityType(forIdentifier: .appleSleepingWristTemperature)!
|
|
|
|
|
|
|
|
let wristTempObserverQuery = HKObserverQuery(sampleType: wristTempType, predicate: nil) { [weak self] _, completionHandler, error in
|
|
|
|
if let _ = error {
|
|
|
|
guard let summary = summaries?.last else {
|
|
|
|
print("查询活动目标为空")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
self?.fetchLatestWristTemperature()
|
|
|
|
completionHandler()
|
|
|
|
|
|
|
|
let body = Self.activityTargetUploadBody(from: summary)
|
|
|
|
self.uploadActivityTarget(body: body)
|
|
|
|
}
|
|
|
|
|
|
|
|
healthStore.execute(wristTempObserverQuery)
|
|
|
|
healthStore.enableBackgroundDelivery(for: wristTempType, frequency: .immediate) { success, error in
|
|
|
|
healthStore.execute(query)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
//呼吸频率
|
|
|
|
let respiratoryType = HKQuantityType.quantityType(forIdentifier: .respiratoryRate)!
|
|
|
|
nonisolated private static func activityTargetUploadBody(from summary: HKActivitySummary) -> [String: Any] {
|
|
|
|
var body: [String: Any] = [
|
|
|
|
"move": Int(summary.activeEnergyBurnedGoal.doubleValue(for: .kilocalorie())),
|
|
|
|
"stand": Int(summary.appleStandHoursGoal.doubleValue(for: .count())),
|
|
|
|
"active_energy_burned": summary.activeEnergyBurned.doubleValue(for: .kilocalorie()),
|
|
|
|
"active_energy_burned_goal": summary.activeEnergyBurnedGoal.doubleValue(for: .kilocalorie()),
|
|
|
|
"apple_exercise_time": summary.appleExerciseTime.doubleValue(for: .minute()),
|
|
|
|
"apple_exercise_time_goal": summary.appleExerciseTimeGoal.doubleValue(for: .minute()),
|
|
|
|
"apple_stand_hours": summary.appleStandHours.doubleValue(for: .count()),
|
|
|
|
"apple_stand_hours_goal": summary.appleStandHoursGoal.doubleValue(for: .count())
|
|
|
|
]
|
|
|
|
|
|
|
|
let respiratoryObserverQuery = HKObserverQuery(sampleType: respiratoryType, predicate: nil) { [weak self] _, completionHandler, error in
|
|
|
|
if let _ = error {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
self?.fetchLatestRespiratoryRate()
|
|
|
|
completionHandler()
|
|
|
|
if #available(watchOS 7.0, *) {
|
|
|
|
body["activity_move_mode"] = summary.activityMoveMode.rawValue
|
|
|
|
body["apple_move_time"] = summary.appleMoveTime.doubleValue(for: .minute())
|
|
|
|
body["apple_move_time_goal"] = summary.appleMoveTimeGoal.doubleValue(for: .minute())
|
|
|
|
}
|
|
|
|
|
|
|
|
healthStore.execute(respiratoryObserverQuery)
|
|
|
|
healthStore.enableBackgroundDelivery(for: respiratoryType, frequency: .immediate) { success, error in
|
|
|
|
|
|
|
|
if #available(watchOS 9.0, *) {
|
|
|
|
body["exercise_time_goal"] = summary.exerciseTimeGoal?.doubleValue(for: .minute())
|
|
|
|
body["stand_hours_goal"] = summary.standHoursGoal?.doubleValue(for: .count())
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
private func saveHRVValue(_ value: Double) {
|
|
|
|
let defaults = AppGroupConstants.defaults
|
|
|
|
defaults?.set(value, forKey: AppGroupConstants.Key.latestHRV)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func saveHRVBaselineValue(_ value: Double?) {
|
|
|
|
let defaults = AppGroupConstants.defaults
|
|
|
|
defaults?.set(value, forKey: AppGroupConstants.Key.latestHRVBaseline)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func saveStepCountValue(_ value: Int) {
|
|
|
|
let defaults = AppGroupConstants.defaults
|
|
|
|
defaults?.set(value, forKey: AppGroupConstants.Key.latestStepCount)
|
|
|
|
|
|
|
|
return body
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension WatchDataManager {
|
|
|
|
private func uploadHRV(value: Double, date: Date) {
|
|
|
|
nonisolated private func uploadHRV(value: Double, date: Date) {
|
|
|
|
Task {
|
|
|
|
let _ = try? await HealthService().upload(data: [
|
|
|
|
"data_type": HealthDataType.hrv.rawValue,
|
|
...
|
...
|
@@ -522,8 +576,8 @@ extension WatchDataManager { |
|
|
|
])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func uploadIrregularRhythm(value: Int, date: Date) {
|
|
|
|
|
|
|
|
nonisolated private func uploadIrregularRhythm(value: Int, date: Date) {
|
|
|
|
Task {
|
|
|
|
let _ = try? await HealthService().upload(data: [
|
|
|
|
"data_type": HealthDataType.irregularHeartRhythm.rawValue,
|
|
...
|
...
|
@@ -532,8 +586,8 @@ extension WatchDataManager { |
|
|
|
])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func uploadSleepWristTemperature(value: Double, date: Date) {
|
|
|
|
|
|
|
|
nonisolated private func uploadSleepWristTemperature(value: Double, date: Date) {
|
|
|
|
Task {
|
|
|
|
let _ = try? await HealthService().upload(data: [
|
|
|
|
"data_type": HealthDataType.sleepingWristTemperature.rawValue,
|
|
...
|
...
|
@@ -542,8 +596,8 @@ extension WatchDataManager { |
|
|
|
])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func uploadRespiratoryRate(value: Double, date: Date) {
|
|
|
|
|
|
|
|
nonisolated private func uploadRespiratoryRate(value: Double, date: Date) {
|
|
|
|
Task {
|
|
|
|
let _ = try? await HealthService().upload(data: [
|
|
|
|
"data_type": HealthDataType.respiratoryRate.rawValue,
|
|
...
|
...
|
@@ -552,23 +606,85 @@ extension WatchDataManager { |
|
|
|
])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
nonisolated private func uploadActivityTarget(body: [String: Any]) {
|
|
|
|
Task {
|
|
|
|
#if DEBUG
|
|
|
|
let debugBody = body
|
|
|
|
.map { "\($0.key)=\($0.value)" }
|
|
|
|
.sorted()
|
|
|
|
.joined(separator: ", ")
|
|
|
|
print("watch upload activity target: {\(debugBody)}")
|
|
|
|
#endif
|
|
|
|
|
|
|
|
let _ = try? await HealthService().uploadActivityTarget(data: body)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
extension WatchDataManager {
|
|
|
|
func fetchOtherTodayData() {
|
|
|
|
func getAcitivtyTarget(){
|
|
|
|
Task{
|
|
|
|
let result = try? await HealthService().getActivityTarget()
|
|
|
|
await MainActor.run {
|
|
|
|
self.activityTarget = result
|
|
|
|
self.saveActivityTarget(result)
|
|
|
|
WatchWidgetReloader.reloadActivityTarget()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
func getLatestHealthData(){
|
|
|
|
Task {
|
|
|
|
let result = try? await HealthService().getTodayStatusInfo()
|
|
|
|
let result = try? await HealthService().getHealthData(type: WatchUserinfoManager.share.displayType)
|
|
|
|
await MainActor.run {
|
|
|
|
self.otherTodayHealthInfo = result
|
|
|
|
self.latestHRVData = result
|
|
|
|
self.saveHRVData(result)
|
|
|
|
WatchWidgetReloader.reloadHRV()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func getMyLatestData(){
|
|
|
|
Task{
|
|
|
|
let result = try? await HealthService().getMyData()
|
|
|
|
await MainActor.run {
|
|
|
|
self.myData = result
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func getFriendLatestData(){
|
|
|
|
Task{
|
|
|
|
let result = try? await HealthService().getFriendData()
|
|
|
|
await MainActor.run {
|
|
|
|
self.friendData = result
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func fetchMyTodayData() {
|
|
|
|
fetchTodayAvgHRV()
|
|
|
|
// fetchTodayAvgHRV()
|
|
|
|
fetchTodayLatestHeartRate()
|
|
|
|
fetchTodayStepCount()
|
|
|
|
fetchTodayActiveEnergy()
|
|
|
|
fetchTodayExerciseTime()
|
|
|
|
fetchTodayStandTime()
|
|
|
|
fetchLastNightSleepDuration()
|
|
|
|
WatchHealthWidgetRefreshCoordinator.shared.refreshActivityTargetData()
|
|
|
|
}
|
|
|
|
|
|
|
|
func refreshHeartRateWidgetData() {
|
|
|
|
fetchTodayLatestHeartRate()
|
|
|
|
}
|
|
|
|
|
|
|
|
func refreshStepWidgetData() {
|
|
|
|
fetchTodayStepCount()
|
|
|
|
}
|
|
|
|
|
|
|
|
func refreshActivityWidgetData() {
|
|
|
|
fetchTodayActiveEnergy()
|
|
|
|
fetchTodayExerciseTime()
|
|
|
|
fetchTodayStandTime()
|
|
|
|
}
|
|
|
|
} |
...
|
...
|
|