一、引言
自 iPhone 14 Pro 引入灵动岛 (Dynamic Island) 以来,它成为了 iOS UI 中最有创意也最具操作性的一个部分。目前,很多 App 开发者都已经尝试通过 ActivityKit 和 WidgetKit 展示灵动岛内容,但如何在灵动岛中加载网络图片,依然是个常见且费脑筋的问题。
本文将全面分析灵动岛图片显示的机制、限制,并提供一套完整的解决方案,重点讲解使用 App Group 模式来实现主 App 与 Widget 之间的图片共享与通信。
二、灵动岛开发机制概览
灵动岛基于 ActivityKit 和 WidgetKit 实现:
- 通过 LiveActivity 开启一个实时活动
- 通过 Widget Extension 定义灵动岛的 UI
- WidgetKit 是静态 UI,不支持网络请求
- UI 分为 compact、expanded 和锁屏 banner 等几种样式
这也意味着,想要显示网络图片,必须由主应用进行下载,并与 Widget 共享资源。
三、网络图片的限制
WidgetKit/ActivityKit 不支持直接进行网络请求,原因包括:
- Widget 执行环境不允许同步/异步网络操作
- Timeline 是静态的,需要预先构造
- Activity 不支持 SwiftUI 中的 async 操作
因此,需要借助 App 主进程 完成下载、缓存,再与 Widget 共享。
四、推荐方案
前置下载 + 共享缓存
主 App 通过 URLSession 下载图片
保存至 App Group 共享文件夹
Widget 通过 Image(uiImage:) or Image(contentsOfFile:) 加载
通过 Activity 更新状态,进而触发 UI 重绘
五、App Group 设置详解
要实现主应用与 Widget Extension 的资源共享,必须使用 App Group。以下是设置步骤:
1. 开启 App Group 功能
打开 Xcode,选择你的 App target
点击“Signing & Capabilities”选项卡
点击左上角的“+ Capability”按钮,添加 App Groups
2. 添加一个共享 Group ID
例如:group.com.yourcompany.myapp.shared
❗️请确保 App 与 Widget Extension 都添加了相同的 Group ID
3. 访问共享文件夹
在代码中通过如下方式获取共享容器的目录:
let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.yourcompany.myapp.shared")
可以使用 containerURL 创建文件路径,进行图片保存与读取。
4. 权限校验
App 和 Widget 的 App Group ID 必须完全一致
App 和 Widget 都需要在 Capabilities 中启用 App Groups
若 ID 拼写有误,或配置不同步,文件共享将会失败
六、实战代码
1. 在 App 中下载图片并存入本地
func downloadImage(from url: URL, to filename: String) {
let task = URLSession.shared.dataTask(with: url) { data, _, _ in
guard let data = data else { return }
let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.example.shared")!
let fileURL = containerURL.appendingPathComponent(filename)
try? data.write(to: fileURL)
updateActivityState(with: filename)
}
task.resume()
}
2. 更新 Live Activity 状态
func updateActivityState(with imageFilename: String) {
let attributes = MyActivityAttributes(...)
let contentState = MyActivityAttributes.ContentState(imageName: imageFilename)
Task {
for activity in Activity<MyActivityAttributes>.activities {
await activity.update(using: contentState)
}
}
}
3. Widget 中显示图片
let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.example.shared")!
let imageURL = containerURL.appendingPathComponent(context.state.imageName)
if let uiImage = UIImage(contentsOfFile: imageURL.path) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fill)
} else {
Text("Loading...")
}
七、常见问题与注意点
- 图片必须已下载并保存在 AppGroup 目录
- Widget 重绘时间有延迟,不能即时刷新
- 图片类型应为 UIImage(contentsOfFile:)
- 选择简单图片格式(PNG/JPEG),避免解码性能问题
- App Group ID 必须在 Widget + App 同时配置
- ❗️由于 WidgetKit 会同时渲染 compact、expanded 和 lockscreen 三种 UI,每种都会尝试读取共享目录图片,容易出现并发访问文件的问题,导致图片加载失败或加载为空。
✅ 推荐缓存机制方案
为避免上述并发访问问题,建议主 App 在下载图片后添加缓存标志,并通过临时中间文件名控制写入流程,确保 Widget 读取时文件完整:
1. 下载并命名为临时文件,再原子重命名:
func safeSaveImage(_ data: Data, named filename: String) {
guard let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.example.shared") else { return }
let tempURL = containerURL.appendingPathComponent("temp_\(filename)")
let finalURL = containerURL.appendingPathComponent(filename)
do {
try data.write(to: tempURL, options: .atomic)
try FileManager.default.moveItem(at: tempURL, to: finalURL)
} catch {
print("Image cache save failed: \(error)")
}
}
2. Widget 端读取前先判断文件存在和大小,并引入缓存池:
class ImageCache {
static let shared = ImageCache()
private var cache: [String: UIImage] = [:]
func image(named filename: String, from containerURL: URL) -> UIImage? {
if let cached = cache[filename] {
return cached
}
let imageURL = containerURL.appendingPathComponent(filename)
guard FileManager.default.fileExists(atPath: imageURL.path),
let attrs = try? FileManager.default.attributesOfItem(atPath: imageURL.path),
let fileSize = attrs[.size] as? NSNumber,
fileSize.intValue > 0,
let image = UIImage(contentsOfFile: imageURL.path) else {
return nil
}
cache[filename] = image
return image
}
}
通过以上策略,可以大大降低由于并发读取未完成文件所导致的加载失败问题,并提升整体性能,避免多次反复从磁盘解码同一张图片。
八、总结
灵动岛的开发模型大量采用了 WidgetKit 的设计理念,并不是一个支持即时输入的 View 组件。因此,如果想要在灵动岛中加载网络图片,不得不借助主应用进行下载,通过 AppGroup 共享资源,完成 UI 更新。
这种设计在确保安全性与性能一致性的同时,也对我们的架构设计提出了更高要求,需要开发者采用更加灵活的实现策略。