feat(health): 新增日照时长监测卡片与 HealthKit 集成

- iOS 端集成 HealthKit 日照时间 (TimeInDaylight) 数据获取接口
- 新增 SunlightCard 组件,支持查看今日数据及最近30天历史趋势图表
- 更新统计页和自定义设置页,支持开启/关闭日照卡片
- 优化 HealthDataCard 组件,支持自定义图标组件和副标题显示
- 更新多语言文件及应用版本号至 1.1.6
This commit is contained in:
richarjiang
2025-12-19 17:38:16 +08:00
parent e51aca2fdb
commit 17664c679d
15 changed files with 851 additions and 7 deletions

View File

@@ -78,6 +78,13 @@ class HealthKitManager: RCTEventEmitter {
return nil
}
}
static var timeInDaylight: HKQuantityType? {
if #available(iOS 17.0, *) {
return HKObjectType.quantityType(forIdentifier: .timeInDaylight)
} else {
return nil
}
}
static var all: Set<HKObjectType> {
var types: Set<HKObjectType> = [activitySummary, workout, dateOfBirth]
@@ -95,6 +102,7 @@ class HealthKitManager: RCTEventEmitter {
if let bodyMass = bodyMass { types.insert(bodyMass) }
if let menstrualFlow = menstrualFlow { types.insert(menstrualFlow) }
if let appleSleepingWristTemperature = appleSleepingWristTemperature { types.insert(appleSleepingWristTemperature) }
if let timeInDaylight = timeInDaylight { types.insert(timeInDaylight) }
return types
}
@@ -623,6 +631,151 @@ class HealthKitManager: RCTEventEmitter {
healthStore.execute(query)
}
@objc
func getTimeInDaylight(
_ options: NSDictionary,
resolver: @escaping RCTPromiseResolveBlock,
rejecter: @escaping RCTPromiseRejectBlock
) {
guard HKHealthStore.isHealthDataAvailable() else {
rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil)
return
}
guard let daylightType = ReadTypes.timeInDaylight else {
rejecter("TYPE_NOT_AVAILABLE", "Time in daylight type is not available", nil)
return
}
let startDate: Date
if let startString = options["startDate"] as? String, let d = parseDate(from: startString) {
startDate = d
} else {
startDate = Calendar.current.startOfDay(for: Date())
}
let endDate: Date
if let endString = options["endDate"] as? String, let d = parseDate(from: endString) {
endDate = d
} else {
endDate = Date()
}
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
let query = HKStatisticsQuery(quantityType: daylightType,
quantitySamplePredicate: predicate,
options: .cumulativeSum) { [weak self] (query, statistics, error) in
DispatchQueue.main.async {
if let error = error {
rejecter("QUERY_ERROR", "Failed to query time in daylight: \(error.localizedDescription)", error)
return
}
guard let statistics = statistics else {
resolver([
"totalValue": 0,
"startDate": self?.dateToISOString(startDate) ?? "",
"endDate": self?.dateToISOString(endDate) ?? ""
])
return
}
let totalValue = statistics.sumQuantity()?.doubleValue(for: HKUnit.minute()) ?? 0
let result: [String: Any] = [
"totalValue": totalValue,
"startDate": self?.dateToISOString(startDate) ?? "",
"endDate": self?.dateToISOString(endDate) ?? ""
]
resolver(result)
}
}
healthStore.execute(query)
}
@objc
func getTimeInDaylightSamples(
_ options: NSDictionary,
resolver: @escaping RCTPromiseResolveBlock,
rejecter: @escaping RCTPromiseRejectBlock
) {
guard HKHealthStore.isHealthDataAvailable() else {
rejecter("HEALTHKIT_NOT_AVAILABLE", "HealthKit is not available on this device", nil)
return
}
guard let daylightType = ReadTypes.timeInDaylight else {
rejecter("TYPE_NOT_AVAILABLE", "Time in daylight type is not available", nil)
return
}
let startDate: Date
if let startString = options["startDate"] as? String, let d = parseDate(from: startString) {
startDate = d
} else {
startDate = Calendar.current.startOfDay(for: Date())
}
let endDate: Date
if let endString = options["endDate"] as? String, let d = parseDate(from: endString) {
endDate = d
} else {
endDate = Date()
}
let predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: .strictStartDate)
var interval = DateComponents()
interval.day = 1
let anchorDate = Calendar.current.startOfDay(for: startDate)
let query = HKStatisticsCollectionQuery(quantityType: daylightType,
quantitySamplePredicate: predicate,
options: .cumulativeSum,
anchorDate: anchorDate,
intervalComponents: interval)
query.initialResultsHandler = { [weak self] (_, results, error) in
DispatchQueue.main.async {
if let error = error {
rejecter("QUERY_ERROR", "Failed to query time in daylight samples: \(error.localizedDescription)", error)
return
}
guard let results = results else {
resolver([
"data": [],
"count": 0,
"startDate": self?.dateToISOString(startDate) ?? "",
"endDate": self?.dateToISOString(endDate) ?? ""
])
return
}
var data: [[String: Any]] = []
results.enumerateStatistics(from: startDate, to: endDate) { statistics, _ in
let value = statistics.sumQuantity()?.doubleValue(for: HKUnit.minute()) ?? 0
data.append([
"date": self?.dateToISOString(statistics.startDate) ?? "",
"value": value
])
}
let result: [String: Any] = [
"data": data,
"count": data.count,
"startDate": self?.dateToISOString(startDate) ?? "",
"endDate": self?.dateToISOString(endDate) ?? ""
]
resolver(result)
}
}
healthStore.execute(query)
}
@objc
func getActivitySummary(
_ options: NSDictionary,