iOS Widget 开发-20:从旧版 API 迁移到 iOS 17+ 现代 Widget

随着 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
    }
}

迁移要点

  1. 方法名简化:getSnapshotsnapshotgetTimelinetimeline
  2. 回调变为 async/await,代码更线性
  3. 多了 Intent 关联类型和 for configuration: 参数
  4. 错误处理可以用 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
  • 创建对应的 AppEntityEntityQuery
  • 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

小结

  • 迁移路径: TimelineProviderAppIntentTimelineProviderIntentConfigurationAppIntentConfiguration
  • 使用条件编译同时支持新旧系统,渐进式迁移降低风险
  • iOS 17+ 必须使用 .containerBackground,必须支持 Button(intent:) 的交互能力
  • 新架构更清晰:纯 Swift 代码、async/await、类型安全的 App Intents

上一篇iOS Widget 开发-19:Widget 调试与单元测试

系列完。感谢阅读本系列 20 篇 iOS Widget 开发博客。从基础概念到进阶实战,希望能帮助你在 Widget 开发中少走弯路。

相关推荐
Daniel_Coder1 小时前
iOS Widget 开发-19:Widget 调试与单元测试
ios·单元测试·swift·widget·widgetcenter
我是谁的程序员15 小时前
Mac 上生成 AppStoreInfo.plist 文件,App Store 上架
后端·ios
sweet丶16 小时前
微信Matrix 卡顿监控原理梳理与图解
ios
2501_9160074718 小时前
iOS开发中抓取HTTPS请求的完整解决方法与步骤详解
android·网络协议·ios·小程序·https·uni-app·iphone
ZZH_AI项目交付1 天前
我把 AI 最容易改坏真实 App 的地方,整理成了 skills
人工智能·ios·app
00后程序员张1 天前
Windows 下怎么生成 AppStoreInfo.plist?不依赖 Xcode 的方法
ide·macos·ios·小程序·uni-app·iphone·xcode
原鸣清1 天前
iOS 自定义 Markdown 渲染实践:从成品库到可魔改 Demo
ios
Daniel_Coder1 天前
iOS Widget 开发-18:Widget 的 SwiftUI 视图适配与设计
ios·swiftui·swift·widget·widgetcenter
Daniel_Coder1 天前
iOS Widget 开发-17:Widget 错误处理与空状态设计
ios·swift·widget·widgetcenter