iOS 灵动岛(Dynamic Island)如何加载网络图片

一、引言

自 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 不支持直接进行网络请求,原因包括:

  1. Widget 执行环境不允许同步/异步网络操作
  2. Timeline 是静态的,需要预先构造
  3. 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 更新。

这种设计在确保安全性与性能一致性的同时,也对我们的架构设计提出了更高要求,需要开发者采用更加灵活的实现策略。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容