```
feat: 添加拍药盒功能和语音直达入口 - 实现拍药盒扫描流程,支持本地OCR识别药品信息 - 在日记页面添加拍药盒和记症状的三选一入口 - 优化按钮点击区域,确保符合苹果HIG最小命中区标准 - 添加用药记录到时间线的独立分类显示 - 实现长按+号语音直达功能,支持语音意图分类跳转 - 更新项目配置文件,启用代码分析和死代码剥离选项 - 增加多项本地化字符串支持新功能 ```
This commit is contained in:
44
康康/Persistence/WidgetSnapshot.swift
Normal file
44
康康/Persistence/WidgetSnapshot.swift
Normal file
@@ -0,0 +1,44 @@
|
||||
import Foundation
|
||||
|
||||
/// 主 App → 桌面 Widget 的数据快照(经 App Group UserDefaults 传递)。
|
||||
///
|
||||
/// 为什么不让 Widget 直接读 SwiftData:store 在 App 沙盒且开了文件保护,
|
||||
/// extension 进程独立、锁屏时不可读;快照是「最后一次看到的值」,锁屏也能显示。
|
||||
///
|
||||
/// ⚠️ 同步契约:`KangkangWidget` extension 里有本结构的独立拷贝
|
||||
/// (extension 不引主 App 代码,避免 Xcode target membership 配置成本)。
|
||||
/// 改字段时两边一起改:KangkangWidget/PinnedIndicatorsWidget.swift。
|
||||
struct WidgetSnapshot: Codable, Equatable {
|
||||
struct Item: Codable, Equatable {
|
||||
var name: String // "收缩压"
|
||||
var value: String // "128"
|
||||
var unit: String // "mmHg"
|
||||
var statusRaw: String // IndicatorStatus.rawValue: high|low|normal
|
||||
var capturedAt: Date
|
||||
}
|
||||
|
||||
var updatedAt: Date
|
||||
var items: [Item]
|
||||
|
||||
// MARK: - App Group 存取
|
||||
|
||||
/// App Group ID。两个 target 的 App Groups capability 都要勾这一个。
|
||||
static let appGroupID = "group.com.xuhuayong.kangkang"
|
||||
static let storeKey = "kk.widget.snapshot.v1"
|
||||
|
||||
/// App Group 未配置(capability 没加)时返回 nil → 调用方静默跳过,App 照常运行。
|
||||
static var sharedDefaults: UserDefaults? {
|
||||
UserDefaults(suiteName: appGroupID)
|
||||
}
|
||||
|
||||
func save(to defaults: UserDefaults? = WidgetSnapshot.sharedDefaults) {
|
||||
guard let defaults, let data = try? JSONEncoder().encode(self) else { return }
|
||||
defaults.set(data, forKey: Self.storeKey)
|
||||
}
|
||||
|
||||
static func load(from defaults: UserDefaults? = WidgetSnapshot.sharedDefaults) -> WidgetSnapshot? {
|
||||
guard let defaults,
|
||||
let data = defaults.data(forKey: storeKey) else { return nil }
|
||||
return try? JSONDecoder().decode(WidgetSnapshot.self, from: data)
|
||||
}
|
||||
}
|
||||
42
康康/Persistence/WidgetSnapshotRefresher.swift
Normal file
42
康康/Persistence/WidgetSnapshotRefresher.swift
Normal file
@@ -0,0 +1,42 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
import WidgetKit
|
||||
|
||||
/// 把 pinned 指标的最新值写进 App Group 快照,并请求 WidgetKit 刷新。
|
||||
/// 调用时机:App 进后台 / 启动完成(RootView)。读库很轻(只取 pinned),无 AI、无网络。
|
||||
/// App Group capability 未配置时整体静默 no-op,不影响主 App。
|
||||
enum WidgetSnapshotRefresher {
|
||||
|
||||
/// 每个系列(seriesKey,无则按 name)只取最新一条,最多 6 条。
|
||||
@MainActor
|
||||
static func refresh(in ctx: ModelContext) {
|
||||
let pinnedPredicate = #Predicate<Indicator> { $0.pinned == true }
|
||||
var descriptor = FetchDescriptor<Indicator>(
|
||||
predicate: pinnedPredicate,
|
||||
sortBy: [SortDescriptor(\.capturedAt, order: .reverse)]
|
||||
)
|
||||
descriptor.fetchLimit = 200 // pinned 总量不大,设上限只是兜底
|
||||
guard let pinned = try? ctx.fetch(descriptor) else { return }
|
||||
|
||||
var seenSeries = Set<String>()
|
||||
var items: [WidgetSnapshot.Item] = []
|
||||
for ind in pinned { // 已按 capturedAt 降序,首见即该系列最新
|
||||
let key = ind.seriesKey ?? ind.name
|
||||
guard seenSeries.insert(key).inserted else { continue }
|
||||
items.append(.init(
|
||||
name: ind.name,
|
||||
value: ind.value,
|
||||
unit: ind.unit,
|
||||
statusRaw: ind.statusRaw,
|
||||
capturedAt: ind.capturedAt
|
||||
))
|
||||
if items.count >= 6 { break }
|
||||
}
|
||||
|
||||
let snapshot = WidgetSnapshot(updatedAt: .now, items: items)
|
||||
// 内容没变就不写、不刷新,省 WidgetKit 的刷新预算。
|
||||
if let old = WidgetSnapshot.load(), old.items == snapshot.items { return }
|
||||
snapshot.save()
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user