feat: 完善饮水 widget
This commit is contained in:
39
ios/digitalpilates/AppGroupUserDefaults.m
Normal file
39
ios/digitalpilates/AppGroupUserDefaults.m
Normal file
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// AppGroupUserDefaults.m
|
||||
// digitalpilates
|
||||
//
|
||||
// Objective-C bridge file for AppGroupUserDefaults Swift module
|
||||
//
|
||||
|
||||
#import <React/RCTBridgeModule.h>
|
||||
|
||||
@interface RCT_EXTERN_MODULE(AppGroupUserDefaults, NSObject)
|
||||
|
||||
RCT_EXTERN_METHOD(setString:(NSString *)groupId
|
||||
key:(NSString *)key
|
||||
value:(NSString *)value
|
||||
resolver:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject)
|
||||
|
||||
RCT_EXTERN_METHOD(getString:(NSString *)groupId
|
||||
key:(NSString *)key
|
||||
resolver:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject)
|
||||
|
||||
RCT_EXTERN_METHOD(setNumber:(NSString *)groupId
|
||||
key:(NSString *)key
|
||||
value:(NSNumber *)value
|
||||
resolver:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject)
|
||||
|
||||
RCT_EXTERN_METHOD(getNumber:(NSString *)groupId
|
||||
key:(NSString *)key
|
||||
resolver:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject)
|
||||
|
||||
RCT_EXTERN_METHOD(removeKey:(NSString *)groupId
|
||||
key:(NSString *)key
|
||||
resolver:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject)
|
||||
|
||||
@end
|
||||
111
ios/digitalpilates/AppGroupUserDefaults.swift
Normal file
111
ios/digitalpilates/AppGroupUserDefaults.swift
Normal file
@@ -0,0 +1,111 @@
|
||||
//
|
||||
// AppGroupUserDefaults.swift
|
||||
// digitalpilates
|
||||
//
|
||||
// Native module for accessing App Group UserDefaults
|
||||
// Allows sharing data between main app and widget
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import React
|
||||
|
||||
@objc(AppGroupUserDefaults)
|
||||
class AppGroupUserDefaults: NSObject, RCTBridgeModule {
|
||||
|
||||
static func moduleName() -> String! {
|
||||
return "AppGroupUserDefaults"
|
||||
}
|
||||
|
||||
static func requiresMainQueueSetup() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: - String Methods
|
||||
|
||||
@objc
|
||||
func setString(_ groupId: String, key: String, value: String, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
guard let userDefaults = UserDefaults(suiteName: groupId) else {
|
||||
rejecter("ERROR", "Failed to create UserDefaults with suite name: \(groupId)", nil)
|
||||
return
|
||||
}
|
||||
|
||||
userDefaults.set(value, forKey: key)
|
||||
userDefaults.synchronize()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
resolver(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
func getString(_ groupId: String, key: String, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
guard let userDefaults = UserDefaults(suiteName: groupId) else {
|
||||
rejecter("ERROR", "Failed to create UserDefaults with suite name: \(groupId)", nil)
|
||||
return
|
||||
}
|
||||
|
||||
let value = userDefaults.string(forKey: key)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
resolver(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Number Methods
|
||||
|
||||
@objc
|
||||
func setNumber(_ groupId: String, key: String, value: NSNumber, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
guard let userDefaults = UserDefaults(suiteName: groupId) else {
|
||||
rejecter("ERROR", "Failed to create UserDefaults with suite name: \(groupId)", nil)
|
||||
return
|
||||
}
|
||||
|
||||
userDefaults.set(value, forKey: key)
|
||||
userDefaults.synchronize()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
resolver(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
func getNumber(_ groupId: String, key: String, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
guard let userDefaults = UserDefaults(suiteName: groupId) else {
|
||||
rejecter("ERROR", "Failed to create UserDefaults with suite name: \(groupId)", nil)
|
||||
return
|
||||
}
|
||||
|
||||
let value = userDefaults.object(forKey: key) as? NSNumber ?? NSNumber(value: 0)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
resolver(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Remove Key Method
|
||||
|
||||
@objc
|
||||
func removeKey(_ groupId: String, key: String, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
guard let userDefaults = UserDefaults(suiteName: groupId) else {
|
||||
rejecter("ERROR", "Failed to create UserDefaults with suite name: \(groupId)", nil)
|
||||
return
|
||||
}
|
||||
|
||||
userDefaults.removeObject(forKey: key)
|
||||
userDefaults.synchronize()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
resolver(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
ios/digitalpilates/WaterRecordManager.m
Normal file
16
ios/digitalpilates/WaterRecordManager.m
Normal file
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// WaterRecordManager.m
|
||||
// digitalpilates
|
||||
//
|
||||
// Objective-C bridge file for WaterRecordManager Swift module
|
||||
//
|
||||
|
||||
#import <React/RCTBridgeModule.h>
|
||||
|
||||
@interface RCT_EXTERN_MODULE(WaterRecordManager, NSObject)
|
||||
|
||||
RCT_EXTERN_METHOD(addWaterRecord:(int)amount
|
||||
resolver:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject)
|
||||
|
||||
@end
|
||||
81
ios/digitalpilates/WaterRecordManager.swift
Normal file
81
ios/digitalpilates/WaterRecordManager.swift
Normal file
@@ -0,0 +1,81 @@
|
||||
//
|
||||
// WaterRecordManager.swift
|
||||
// digitalpilates
|
||||
//
|
||||
// Native module for managing water records through React Native bridge
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import React
|
||||
import WidgetKit
|
||||
|
||||
@objc(WaterRecordManager)
|
||||
class WaterRecordManager: NSObject, RCTBridgeModule {
|
||||
|
||||
static func moduleName() -> String! {
|
||||
return "WaterRecordManager"
|
||||
}
|
||||
|
||||
static func requiresMainQueueSetup() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@objc
|
||||
func addWaterRecord(_ amount: Int, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
|
||||
|
||||
// 更新 App Group UserDefaults(用于 Widget 显示)
|
||||
guard let sharedDefaults = UserDefaults(suiteName: "group.com.anonymous.digitalpilates") else {
|
||||
rejecter("USERDEFAULTS_ERROR", "Failed to access App Group UserDefaults", nil)
|
||||
return
|
||||
}
|
||||
|
||||
let currentIntake = sharedDefaults.object(forKey: "widget_current_water_intake") as? Int ?? 0
|
||||
let newIntake = currentIntake + amount
|
||||
sharedDefaults.set(newIntake, forKey: "widget_current_water_intake")
|
||||
|
||||
// 创建 ISO8601 格式的日期字符串
|
||||
let formatter = ISO8601DateFormatter()
|
||||
let isoString = formatter.string(from: Date())
|
||||
sharedDefaults.set(isoString, forKey: "widget_last_sync_time")
|
||||
sharedDefaults.synchronize()
|
||||
|
||||
print("Water record added via bridge: +\(amount)ml, New total: \(newIntake)ml")
|
||||
|
||||
// 通过 React Native bridge 调用 TypeScript 代码来创建记录
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let bridge = self?.bridge else {
|
||||
rejecter("BRIDGE_ERROR", "React Native bridge is not available", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 调用 TypeScript 中的 Redux action 来创建喝水记录
|
||||
let eventData = [
|
||||
"amount": amount,
|
||||
"recordedAt": isoString,
|
||||
"source": "auto" // 标记为来自 Widget/自动添加
|
||||
]
|
||||
|
||||
// 发送事件给 TypeScript 代码
|
||||
self?.sendEvent(withName: "WaterRecordAdded", body: eventData)
|
||||
|
||||
// 刷新 Widget
|
||||
if #available(iOS 14.0, *) {
|
||||
WidgetCenter.shared.reloadTimelines(ofKind: "WaterWidget")
|
||||
}
|
||||
|
||||
resolver(["success": true, "amount": amount, "newTotal": newIntake])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Event Emitter
|
||||
|
||||
override func supportedEvents() -> [String]! {
|
||||
return ["WaterRecordAdded"]
|
||||
}
|
||||
|
||||
private func sendEvent(withName name: String, body: Any) {
|
||||
if let bridge = self.bridge {
|
||||
bridge.eventDispatcher().sendAppEvent(withName: name, body: body)
|
||||
}
|
||||
}
|
||||
}
|
||||
18
ios/digitalpilates/WidgetManager.m
Normal file
18
ios/digitalpilates/WidgetManager.m
Normal file
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// WidgetManager.m
|
||||
// digitalpilates
|
||||
//
|
||||
// Objective-C bridge file for WidgetManager Swift module
|
||||
//
|
||||
|
||||
#import <React/RCTBridgeModule.h>
|
||||
|
||||
@interface RCT_EXTERN_MODULE(WidgetManager, NSObject)
|
||||
|
||||
RCT_EXTERN_METHOD(reloadTimelines:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject)
|
||||
|
||||
RCT_EXTERN_METHOD(reloadAllTimelines:(RCTPromiseResolveBlock)resolve
|
||||
rejecter:(RCTPromiseRejectBlock)reject)
|
||||
|
||||
@end
|
||||
46
ios/digitalpilates/WidgetManager.swift
Normal file
46
ios/digitalpilates/WidgetManager.swift
Normal file
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// WidgetManager.swift
|
||||
// digitalpilates
|
||||
//
|
||||
// Native module for managing widget refresh
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import React
|
||||
import WidgetKit
|
||||
|
||||
@objc(WidgetManager)
|
||||
class WidgetManager: NSObject, RCTBridgeModule {
|
||||
|
||||
static func moduleName() -> String! {
|
||||
return "WidgetManager"
|
||||
}
|
||||
|
||||
static func requiresMainQueueSetup() -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@objc
|
||||
func reloadTimelines(_ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
|
||||
DispatchQueue.main.async {
|
||||
if #available(iOS 14.0, *) {
|
||||
WidgetCenter.shared.reloadTimelines(ofKind: "WaterWidget")
|
||||
resolver(nil)
|
||||
} else {
|
||||
rejecter("UNSUPPORTED", "WidgetKit is only available on iOS 14.0 and later", nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
func reloadAllTimelines(_ resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
|
||||
DispatchQueue.main.async {
|
||||
if #available(iOS 14.0, *) {
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
resolver(nil)
|
||||
} else {
|
||||
rejecter("UNSUPPORTED", "WidgetKit is only available on iOS 14.0 and later", nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user