随着 iOS 17 和 iOS 18 的发布,Apple 对 Widget 系统做了一系列现代化改造:新的 Provider 协议、App Intents 配置、交互按钮、containerBackground 等。如果你的项目仍在使用 iOS 14 时代的 API,现在是将 App 迁移到现代 Widget 的最佳时机。
本篇将提供一套系统性的迁移方案,覆盖从 TimelineProvider 升级到容器适配的全过程。
1. 迁移概览:旧 vs 新
| 维度 | 旧 API(iOS 14-16) | 新 API(iOS 17+) |
|---|---|---|
| Provider | TimelineProvider(回调式) |
AppIntentTimelineProvider(async/await) |
| 配置方式 | .intentdefinition 文件 + IntentConfiguration |
Swift 代码 + AppIntentConfiguration |
| 背景设置 | .padding().background() |
.containerBackground() |
| Widget 交互 | 无(仅 .widgetURL()) |
Button(intent:) + Toggle |
| 控制中心 | 不支持 | ControlWidget(iOS 18+) |
| 数据库共享 | App Group + UserDefaults | 同上 + SwiftData(iOS 17+) |
| Intent 定义 | 可视化 XML 文件 | 纯 Swift @Parameter 属性包装器 |
2. 从 TimelineProvider 迁移到 AppIntentTimelineProvider
旧代码(iOS 14 风格)
swift
struct OldProvider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), value: "...")
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> Void) {
completion(SimpleEntry(date: Date(), value: "snapshot"))
}
func getTimeline(in context: Context, completion: @escaping (Timeline<SimpleEntry>) -> Void) {
var entries: [SimpleEntry] = []
// 构建 entries...
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
新代码(iOS 17+)
swift
struct NewProvider: AppIntentTimelineProvider {
typealias Intent = ConfigurationAppIntent
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), value: "...")
}
func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry {
SimpleEntry(date: Date(), value: "snapshot")
}
func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<SimpleEntry> {
var entries: [SimpleEntry] = []
// 构建 entries... 可以使用 await
let timeline = Timeline(entries: entries, policy: .atEnd)
return timeline
}
}
迁移要点
- 方法名简化:
getSnapshot→snapshot,getTimeline→timeline - 回调变为 async/await,代码更线性
- 多了
Intent关联类型和for configuration:参数 - 错误处理可以用 do-catch 替代回调中的 if-else
3. 从 IntentConfiguration 迁移到 AppIntentConfiguration
旧代码
swift
// .intentdefinition 文件定义 SelectCityIntent(INIntent 子类)
struct OldWidget: Widget {
var body: some WidgetConfiguration {
IntentConfiguration(
kind: "CityWidget",
intent: SelectCityIntent.self,
provider: OldIntentProvider()
) { entry in
CityWidgetView(entry: entry)
}
}
}
新代码
swift
// 纯 Swift 定义的 App Intent
struct SelectCityIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "选择城市"
@Parameter(title: "城市")
var city: CityEntity?
}
// CityEntity 实现 AppEntity
struct CityEntity: AppEntity {
let id: String
let name: String
// ... AppEntity 要求的方法
}
// CityEntityQuery 实现 EntityQuery
struct CityEntityQuery: EntityQuery {
func suggestedEntities() async throws -> [CityEntity] {
// 返回可选城市列表
}
func entities(for identifiers: [String]) async throws -> [CityEntity] {
// 根据 ID 反查实体
}
}
struct NewWidget: Widget {
var body: some WidgetConfiguration {
AppIntentConfiguration(
kind: "CityWidget",
intent: SelectCityIntent.self,
provider: NewProvider()
) { entry in
CityWidgetView(entry: entry)
}
}
}
迁移好处
- 纯代码管理,不再依赖不可读的 XML 文件
- 参数类型安全(编译时检查)
EntityQuery原生支持动态选项列表- 与 Shortcuts 和其他系统功能更好地集成
4. 背景设置的迁移
旧代码
swift
struct OldWidgetView: View {
var body: some View {
VStack { ... }
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
}
}
新代码(iOS 17+)
swift
struct OldWidgetView: View {
var body: some View {
VStack { ... }
}
}
// 在 Widget 配置中统一设置背景
struct NewWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(...) { entry in
OldWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
}
}
.containerBackground从 iOS 17 开始是强制要求,不添加会导致 Widget 无法正确渲染背景。如果仍需支持旧系统,使用可用性检查。
5. 条件编译:同时支持新旧系统
如果你的 App 还需要兼容 iOS 14-16,可以使用 @available 和条件编译:
swift
@main
struct MyWidgets: WidgetBundle {
var body: some Widget {
// iOS 14+ 的旧版本 Widget
LegacyWidget()
// iOS 17+ 的新版本 Widget(含交互按钮)
if #available(iOS 17.0, *) {
ModernWidget()
}
}
}
在 Widget 内部:
swift
struct MyWidget: Widget {
var body: some WidgetConfiguration {
if #available(iOS 17.0, *) {
AppIntentConfiguration(kind: "MyWidget", intent: ConfigIntent.self, provider: ModernProvider()) { entry in
MyWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
} else {
IntentConfiguration(kind: "MyWidget", intent: LegacyIntent.self, provider: LegacyProvider()) { entry in
MyWidgetView(entry: entry)
.padding()
.background(Color(.systemBackground))
}
}
}
}
6. 迁移步骤清单
阶段一:评估
- 确认 App 支持的最低 iOS 版本
- 列出所有已有的 Widget 和它们的配置方式
- 评估是否可以放弃 iOS 14/15 支持
阶段二:Provider 迁移
- 将
TimelineProvider改为AppIntentTimelineProvider - 将回调式方法改为 async/await
- 更新
placeholder/snapshot/timeline方法签名
阶段三:配置迁移
- 创建新的
WidgetConfigurationIntent结构体 - 将
.intentdefinition参数转换为@Parameter - 创建对应的
AppEntity和EntityQuery - 将
IntentConfiguration改为AppIntentConfiguration
阶段四:UI 适配
- 将
.padding().background()改为.containerBackground() - 添加
Button(intent:)交互(如适用) - 更新锁屏 Widget 的背景处理
阶段五:测试与发布
- 在所有支持的 iOS 版本上测试
- 验证 Widget 刷新、配置和交互功能
- 使用 Xcode 工具检查内存和性能
7. 常见迁移问题
Q:可以只迁移部分 Widget 吗?
A:可以。一个 WidgetBundle 中可以同时存在新旧两种 Widget。逐个迁移,降低风险。
Q:旧版 .intentdefinition 文件需要删除吗?
A:如果所有 Widget 都已迁移到新 API,可以删除。但若还有兼容旧系统的 Widget 在用,需要保留。
Q:迁移后 Widget 会丢失用户配置吗?
A:是的。由于 kind 和 Intent 的类型变了,旧的 Widget 实例会被系统视为不同的 Widget。建议主 App 中使用相同的 kind 值,并做好数据过渡。
Q:AppEntity 如何支持动态数据?
A:通过 EntityQuery.suggestedEntities() 返回动态列表,可以从本地数据库或 App Group 缓存中获取实时数据。
Q:如何处理依赖网络请求的参数列表?
A:在 EntityQuery 中异步加载(async throws),如果网络不可用,返回缓存或默认列表。
8. 现代 Widget 架构推荐
Widget Extension/
├── Widgets/
│ ├── MainWidget.swift // 主要展示型 Widget
│ ├── QuickActionWidget.swift // 交互型 Widget
│ └── ControlWidget.swift // iOS 18 控制中心组件
├── Intents/
│ ├── ConfigurationIntent.swift // WidgetConfigurationIntent
│ ├── ActionIntents.swift // 按钮操作的 AppIntent
│ └── Entities/ // AppEntity + EntityQuery
│ ├── CityEntity.swift
│ └── CityEntityQuery.swift
├── Providers/
│ ├── MainWidgetProvider.swift // AppIntentTimelineProvider
│ └── QuickActionProvider.swift
├── Models/
│ ├── WidgetEntry.swift // TimelineEntry 定义
│ └── WidgetState.swift // 状态枚举
├── Views/
│ ├── MainWidgetView.swift
│ ├── SmallWidgetView.swift
│ └── AccessoryWidgetView.swift
├── Shared/
│ ├── DataCache.swift // 缓存管理
│ └── NetworkService.swift // 网络请求
└── Bundle.swift // @main WidgetBundle
小结
- 迁移路径:
TimelineProvider→AppIntentTimelineProvider,IntentConfiguration→AppIntentConfiguration - 使用条件编译同时支持新旧系统,渐进式迁移降低风险
- iOS 17+ 必须使用
.containerBackground,必须支持Button(intent:)的交互能力 - 新架构更清晰:纯 Swift 代码、async/await、类型安全的 App Intents
上一篇 :iOS Widget 开发-19:Widget 调试与单元测试
系列完。感谢阅读本系列 20 篇 iOS Widget 开发博客。从基础概念到进阶实战,希望能帮助你在 Widget 开发中少走弯路。