更新 shenyan-app: HealthKit+WeatherKit+冰箱贴+pare+三区布局
- HealthKitBridge.m: 自定义原生模块,心率/步数/睡眠采集 - WeatherKitBridge.swift/m: 天气模块,当前+每日+每小时预报 - pare.py: 身体数据陡度监控,阈值告警→inbox+冰箱贴 - cyberboss: 5-20分钟随机唤醒,pare→decide→push - 冰箱贴: 纸质感卡片,黑字多级透明度,独立输入框 - 首页三区无视觉布局: 左门/中记录/右冰箱 - 端口3003→3004(VS Code占用) - 聊天屏锚定底部滚动 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
88
shenyan-app/ios/ShenYanApp/HealthKitBridge.m
Normal file
88
shenyan-app/ios/ShenYanApp/HealthKitBridge.m
Normal file
@@ -0,0 +1,88 @@
|
||||
#import <React/RCTBridgeModule.h>
|
||||
#import <HealthKit/HealthKit.h>
|
||||
|
||||
@interface HealthKitBridge : NSObject <RCTBridgeModule>
|
||||
@end
|
||||
|
||||
@implementation HealthKitBridge
|
||||
{
|
||||
HKHealthStore *_store;
|
||||
}
|
||||
|
||||
RCT_EXPORT_MODULE();
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_store = [[HKHealthStore alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (BOOL)requiresMainQueueSetup { return NO; }
|
||||
|
||||
RCT_EXPORT_METHOD(requestAuthorization:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject)
|
||||
{
|
||||
NSSet *readTypes = [NSSet setWithArray:@[
|
||||
[HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierHeartRate],
|
||||
[HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount],
|
||||
[HKObjectType categoryTypeForIdentifier:HKCategoryTypeIdentifierSleepAnalysis],
|
||||
]];
|
||||
[_store requestAuthorizationToShareTypes:nil readTypes:readTypes completion:^(BOOL success, NSError *error) {
|
||||
if (success) resolve(@YES);
|
||||
else reject(@"healthkit", error.localizedDescription, error);
|
||||
}];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(getLatestHeartRate:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject)
|
||||
{
|
||||
HKQuantityType *type = [HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierHeartRate];
|
||||
NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:HKSampleSortIdentifierEndDate ascending:NO];
|
||||
HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType:type predicate:nil limit:1 sortDescriptors:@[sort]
|
||||
resultsHandler:^(HKSampleQuery *q, NSArray *results, NSError *error) {
|
||||
if (error) { reject(@"healthkit", error.localizedDescription, error); return; }
|
||||
if (results.count == 0) { resolve(@{}); return; }
|
||||
HKQuantitySample *sample = results[0];
|
||||
double bpm = [sample.quantity doubleValueForUnit:[HKUnit unitFromString:@"count/min"]];
|
||||
resolve(@{@"bpm": @(bpm), @"time": @([sample.endDate timeIntervalSince1970] * 1000)});
|
||||
}];
|
||||
[_store executeQuery:query];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(getTodaySteps:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject)
|
||||
{
|
||||
HKQuantityType *type = [HKObjectType quantityTypeForIdentifier:HKQuantityTypeIdentifierStepCount];
|
||||
NSCalendar *cal = [NSCalendar currentCalendar];
|
||||
NSDate *start = [cal startOfDayForDate:[NSDate date]];
|
||||
NSDate *end = [NSDate date];
|
||||
NSPredicate *pred = [HKQuery predicateForSamplesWithStartDate:start endDate:end options:HKQueryOptionStrictStartDate];
|
||||
HKStatisticsQuery *query = [[HKStatisticsQuery alloc] initWithQuantityType:type
|
||||
quantitySamplePredicate:pred options:HKStatisticsOptionCumulativeSum
|
||||
completionHandler:^(HKStatisticsQuery *q, HKStatistics *result, NSError *error) {
|
||||
if (error) { reject(@"healthkit", error.localizedDescription, error); return; }
|
||||
double steps = [result.sumQuantity doubleValueForUnit:[HKUnit countUnit]];
|
||||
resolve(@{@"steps": @(steps)});
|
||||
}];
|
||||
[_store executeQuery:query];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(getLastSleep:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject)
|
||||
{
|
||||
HKCategoryType *type = [HKObjectType categoryTypeForIdentifier:HKCategoryTypeIdentifierSleepAnalysis];
|
||||
NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:HKSampleSortIdentifierEndDate ascending:NO];
|
||||
HKSampleQuery *query = [[HKSampleQuery alloc] initWithSampleType:type predicate:nil limit:1 sortDescriptors:@[sort]
|
||||
resultsHandler:^(HKSampleQuery *q, NSArray *results, NSError *error) {
|
||||
if (error) { reject(@"healthkit", error.localizedDescription, error); return; }
|
||||
if (results.count == 0) { resolve(@{}); return; }
|
||||
HKCategorySample *sample = results[0];
|
||||
double hours = [sample.endDate timeIntervalSinceDate:sample.startDate] / 3600.0;
|
||||
resolve(@{@"hours": @(hours), @"start": @([sample.startDate timeIntervalSince1970] * 1000), @"end": @([sample.endDate timeIntervalSince1970] * 1000)});
|
||||
}];
|
||||
[_store executeQuery:query];
|
||||
}
|
||||
|
||||
@end
|
||||
11
shenyan-app/ios/ShenYanApp/WeatherKitBridge.m
Normal file
11
shenyan-app/ios/ShenYanApp/WeatherKitBridge.m
Normal file
@@ -0,0 +1,11 @@
|
||||
#import <React/RCTBridgeModule.h>
|
||||
|
||||
@interface RCT_EXTERN_MODULE(WeatherKitBridge, NSObject)
|
||||
|
||||
RCT_EXTERN_METHOD(getWeather:(double)lat lon:(double)lon resolve:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
||||
|
||||
RCT_EXTERN_METHOD(getDailyForecast:(double)lat lon:(double)lon resolve:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
||||
|
||||
RCT_EXTERN_METHOD(getHourlyForecast:(double)lat lon:(double)lon resolve:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
||||
|
||||
@end
|
||||
80
shenyan-app/ios/ShenYanApp/WeatherKitBridge.swift
Normal file
80
shenyan-app/ios/ShenYanApp/WeatherKitBridge.swift
Normal file
@@ -0,0 +1,80 @@
|
||||
import Foundation
|
||||
import WeatherKit
|
||||
import CoreLocation
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
@objc(WeatherKitBridge)
|
||||
class WeatherKitBridge: NSObject {
|
||||
let service = WeatherService.shared
|
||||
|
||||
@objc
|
||||
static func requiresMainQueueSetup() -> Bool { return false }
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
@objc
|
||||
func getWeather(_ lat: Double, lon: Double, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
||||
let location = CLLocation(latitude: lat, longitude: lon)
|
||||
Task {
|
||||
do {
|
||||
let weather = try await service.weather(for: location)
|
||||
let current = weather.currentWeather
|
||||
let result: [String: Any] = [
|
||||
"temperature": current.temperature.value,
|
||||
"humidity": current.humidity * 100,
|
||||
"condition": current.condition.rawValue,
|
||||
"pressure": current.pressure.value,
|
||||
"uvIndex": current.uvIndex.value,
|
||||
"windSpeed": current.wind.speed.value as Any,
|
||||
"isDaylight": current.isDaylight,
|
||||
"visibility": current.visibility.value as Any
|
||||
]
|
||||
resolve(result)
|
||||
} catch {
|
||||
reject("weatherkit", error.localizedDescription, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
@objc
|
||||
func getDailyForecast(_ lat: Double, lon: Double, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
||||
let location = CLLocation(latitude: lat, longitude: lon)
|
||||
Task {
|
||||
do {
|
||||
let weather = try await service.weather(for: location)
|
||||
let daily = weather.dailyForecast.prefix(3).map { day in
|
||||
return [
|
||||
"high": day.highTemperature.value,
|
||||
"low": day.lowTemperature.value,
|
||||
"condition": day.condition.rawValue,
|
||||
"precipitationChance": day.precipitationChance * 100
|
||||
] as [String : Any]
|
||||
}
|
||||
resolve(daily)
|
||||
} catch {
|
||||
reject("weatherkit", error.localizedDescription, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
func getHourlyForecast(_ lat: Double, lon: Double, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
||||
let location = CLLocation(latitude: lat, longitude: lon)
|
||||
Task {
|
||||
do {
|
||||
let weather = try await service.weather(for: location)
|
||||
let hourly = weather.hourlyForecast.prefix(6).map { hour in
|
||||
return [
|
||||
"temperature": hour.temperature.value,
|
||||
"condition": hour.condition.rawValue,
|
||||
"precipitationChance": hour.precipitationChance * 100,
|
||||
"date": hour.date.timeIntervalSince1970 * 1000
|
||||
] as [String : Any]
|
||||
}
|
||||
resolve(hourly)
|
||||
} catch {
|
||||
reject("weatherkit", error.localizedDescription, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user