feat(health): 新增日照时长监测卡片与 HealthKit 集成
- iOS 端集成 HealthKit 日照时间 (TimeInDaylight) 数据获取接口 - 新增 SunlightCard 组件,支持查看今日数据及最近30天历史趋势图表 - 更新统计页和自定义设置页,支持开启/关闭日照卡片 - 优化 HealthDataCard 组件,支持自定义图标组件和副标题显示 - 更新多语言文件及应用版本号至 1.1.6
This commit is contained in:
@@ -30,6 +30,14 @@ RCT_EXTERN_METHOD(getAppleStandTime:(NSDictionary *)options
|
||||
resolver:(RCTPromiseResolveBlock)resolver
|
||||
rejecter:(RCTPromiseRejectBlock)rejecter)
|
||||
|
||||
RCT_EXTERN_METHOD(getTimeInDaylight:(NSDictionary *)options
|
||||
resolver:(RCTPromiseResolveBlock)resolver
|
||||
rejecter:(RCTPromiseRejectBlock)rejecter)
|
||||
|
||||
RCT_EXTERN_METHOD(getTimeInDaylightSamples:(NSDictionary *)options
|
||||
resolver:(RCTPromiseResolveBlock)resolver
|
||||
rejecter:(RCTPromiseRejectBlock)rejecter)
|
||||
|
||||
RCT_EXTERN_METHOD(getActivitySummary:(NSDictionary *)options
|
||||
resolver:(RCTPromiseResolveBlock)resolver
|
||||
rejecter:(RCTPromiseRejectBlock)rejecter)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.1.5</string>
|
||||
<string>1.1.6</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
Reference in New Issue
Block a user