feat: 完善饮水 widget

This commit is contained in:
richarjiang
2025-09-09 14:26:16 +08:00
parent cacfde064f
commit e56ebe3636
13 changed files with 984 additions and 62 deletions

View 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

View 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)
}
}
}
}

View 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

View 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)
}
}
}

View 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

View 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)
}
}
}
}