import SwiftUI /// 从加密 Vault 异步加载并降采样显示原图的通用组件。 /// /// 替代「在 body 里 `try? FileVault.shared.loadImage(...)` 同步读盘 + 全量解码」的旧写法, /// 解决两个真实问题: /// 1. **OOM**:全分辨率位图(4000×3000 ≈ 48MB)进内存,翻几页就触发 jetsam。这里按 `maxPixel` /// 降采样,缩略图几百 KB,全屏图几 MB。 /// 2. **主线程卡顿**:读盘 + JPEG 解码在主线程会掉帧。这里放到后台线程,主线程只拿结果绘制。 /// /// 区分「加载中」与「读取失败」两态:加载中显示中性占位,只有真正失败才显示「原图无法读取」, /// 不会一打开就闪一下吓人的错误文案。`content` 拿到 `UIImage`(而非 `Image`), /// 方便需要 `image.size` 的调用方(如证据高亮 overlay)按真实宽高比定位。 struct VaultImage: View { let relativePath: String /// 降采样目标最大边(像素)。缩略图给 ~400,全屏查看器给 ~2000。 var maxPixel: CGFloat = 1024 @ViewBuilder var content: (UIImage) -> Content /// 占位回调,`isLoading == true` 表示仍在加载,`false` 表示加载完成但失败。 @ViewBuilder var placeholder: (_ isLoading: Bool) -> Placeholder @State private var image: UIImage? @State private var loading = true var body: some View { Group { if let image { content(image) } else { placeholder(loading) } } // id 变了(TabView 翻到新页 / 行复用换 asset)就重新加载;同一身份重渲染不会重复读盘。 .task(id: relativePath) { loading = true let path = relativePath let mp = maxPixel let loaded = await Task.detached(priority: .userInitiated) { try? FileVault.shared.loadDownsampledImage(relativePath: path, maxPixelSize: mp) }.value guard !Task.isCancelled else { return } image = loaded loading = false } } }