iOS Widget 开发-9:可配置 Widget:使用 IntentConfiguration 实现参数选择

iOS Widget 支持通过"参数化"配置内容,让用户在添加 Widget 时根据个人偏好选择展示内容。这一功能通过 IntentConfiguration 实现,是打造个性化、可复用小组件的关键。

本篇文章介绍如何使用 IntentConfiguration(基于 .intentdefinition)为 Widget 提供可配置参数------以"选择蔬菜"为示例,讲解从创建 .intentdefinition 文件、生成代码、在 Widget 中使用到实现动态选项(Intents Extension)的完整流程,并给出调试与注意事项。

1. 概念回顾:IntentConfiguration 是什么

  • IntentConfiguration 是 WidgetKit 提供给第三方参数配置的经典方式,基于 Intents 框架。使用时,Widget 编辑界面会自动呈现意图中定义的参数选择器。
  • 主要三种 WidgetConfiguration:
    • StaticConfiguration:无配置项,固定展示内容。
    • IntentConfiguration:基于 .intentdefinition(Intents),通过 Intent 文件或 Intents extension 提供选项。
    • AppIntentConfiguration:iOS 16 引入的基于 AppIntents 的现代方式(更推荐在 iOS 16+ 环境使用)。

何时使用 IntentConfiguration:需要支持 iOS 14/15 的项目,或者已有 .intentdefinition 工作流,需要兼容旧系统。


2. 在 Xcode 中创建 .intentdefinition(静态选项)

以下以"选择蔬菜(SelectVegetableIntent)"为例:

在 Project Navigator 中,选择项目或 Widget 的 group,选择 File → New → File from Template

在模板列表选择 Resource 下的 SiriKit Intent Definition file.

点击 Next,命名为 VegetableCategories.intentdefinitionIntentDefinitions.intentdefinition,注意两个 target 都要勾选。

打开该文件,左下角点击 +,选择 New Intent,命名为 VegetableCategories(或你喜欢的名称)。

在这个文件里面,需要更改几个地方,将 Category 设置为 View,顺便将 Description 写一下,记得把 Intent is eligible for widgets 勾选上。

现在点击左下角+号创建一个 Enum, 命名为 Vegetable,并在 Cases 中添加一些类型。

现在选择刚才创建的 VegetableCategories intent,在 Parameters 中创建一个属性 vegetable,并选择类型为刚才创建的 enum。

到此,一个简单的 intentdefinition 文件就创建完了。

那么在项目中如何使用呢?

选中主工程的 target,在 General 下面的 Supported Intents 中添加刚才创建的 VegetableCategoriesIntent。

回到我们 Widget extension 中,修改代码如下:

swift 复制代码
struct VegetableCategoriesProvider: IntentTimelineProvider {
  func placeholder(in context: Context) -> SimpleEntry {
    SimpleEntry(date: Date(), configuration: VegetableCategoriesIntent())
  }

  func getSnapshot(for configuration: VegetableCategoriesIntent, in context: Context, completion: @escaping @Sendable (SimpleEntry) -> Void) {
    let simpleEntry = SimpleEntry(date: Date(), configuration: configuration)
    completion(simpleEntry)
  }

  func getTimeline(for configuration: VegetableCategoriesIntent, in context: Context, completion: @escaping @Sendable (Timeline<SimpleEntry>) -> Void) {

    let vegetableName = vegetableName(for: configuration)

    let currentDate = Date()
    let entry = SimpleEntry(date: currentDate, configuration: configuration, vegetableName: vegetableName)

    let timeline =  Timeline(entries: [entry], policy: .atEnd)
    completion(timeline)
  }
}

func vegetableName(for configuration: VegetableCategoriesIntent) -> String {
  switch configuration.vegetable {
  case .carrot:
    return "Carrot"
  case .broccoli:
    return "Broccoli"
  case .cucumber:
    return "Cucumber"
  case .celery:
    return "Celery"
  case .unknown:
    return "Unknown"
  }
}

struct SimpleEntry: TimelineEntry {
  let date: Date
  let configuration: VegetableCategoriesIntent
  var vegetableName: String?
}

struct SimpleWidgetEntryView : View {
    var entry: VegetableCategoriesProvider.Entry

    var body: some View {
      VStack {
        Text("Time:")
        Text(entry.date, style: .timer)
        
        Text("Favorite vegetable:")
        Text(entry.vegetableName ?? "未选择")
      }
    }
}

struct SimpleWidget: Widget {
    let kind: String = "SimpleWidget"

    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: VegetableCategoriesIntent.self, provider: VegetableCategoriesProvider()) { entry in
            SimpleWidgetEntryView(entry: entry)
                .containerBackground(.fill.tertiary, for: .widget)
        }
    }
}

顺利的话编译就通过了,上面代码中修改了以下 UI,将选择的 vegetable 显示出来了。


3. 动态创建 ConfigurationIntent(以 Country Widget 为例)

首先先看几个概念:

  • Configuration Intent:用于在 Widget 配置界面(或 Shortcuts UI)提供"参数选择"。
  • AppIntents:Apple 在较新 SDK 中提供的替代/补充方案,用于定义可供系统调用的动作与实体。
  • AppEntity:在 AppIntents 中表示一个"实体对象"(如联系人、地点或本例的国家),支持被系统识别、建议与持久化。
  • EntityQuery:为 AppEntity 提供加载/查找/建议项的方法(同步或异步)。
  • parameterSummary:用于在系统 UI 中以简洁的方式展示当前参数的摘要。Summary { \.$country } 会把参数投影(projected)值显示出来。

下面我们创建一个选择国家的 Widget,我们用 CountrySelectIntent(一个 Configuration Intent)作为 Widget 的配置参数,并通过 CountryEntry: AppEntity + CountryEntryQuery: EntityQuery 来提供建议列表与实体解析。Widget 的 TimelineProvider 读取用户选择的实体,渲染对应内容。

3.1 在 Intent 文件中定义实体与参数(CountrySelectIntent.swift

swift 复制代码
// CountrySelectIntent: 用于 Widget 配置的 Intent 定义
struct CountrySelectIntent: WidgetConfigurationIntent {
  static var title: LocalizedStringResource = "Select Country"
  static var description: IntentDescription = "Choose a country to display its information in the widget."

  // 使用 AppEntity 类型作为参数,可在系统 UI 中显示实体并支持建议列表
  @Parameter(title: "Country", description: "select a country")
  var country: CountryEntry

  // 在 UI 摘要处显示已选的 country(即 CountryEntry 的 displayRepresentation)
  static var parameterSummary: some ParameterSummary {
    Summary { \.$country }
  }
}

关键点说明:

  • @Parameter 的类型是 CountryEntry(实现了 AppEntity),不是普通的 String。这使得系统能展示"实体选择"界面,而不是只能输入文本。
  • parameterSummarySummary { \.$country } 表达式用来告诉系统在 Widget 编辑中如何显示用户当前的选择;\.$country 是参数的投影值(projected value),它会渲染为实体的 displayRepresentation

接着看 CountryEntryCountryEntryQuery

swift 复制代码
// CountryEntry: 一个轻量的 AppEntity,用来表示"国家"这一实体
struct CountryEntry: AppEntity, Identifiable {
  // 系统展示类型名称
  static var typeDisplayRepresentation: TypeDisplayRepresentation {
    TypeDisplayRepresentation(name: "Country")
  }

  // 默认的查询实现(当系统需要列出/解析此实体时会使用它)
  static var defaultQuery = CountryEntryQuery()

  // AppEntity 需要一个唯一 id,这里以 countryCode 作为 id
  var id: String { countryCode }
  let countryCode: String

  // 方便在运行时获取一个默认实体(eg. 用作回退值)
  static func defaultValue() -> CountryEntry {
    CountryEntry(countryCode: "US")
  }

  // 给调用方暴露一个友好的 name
  var countName: String {
    Self.names[countryCode] ?? countryCode
  }

  // 国家代码 -> 名称 映射(可扩展为更多国家或国际化)
  private static let names: [String: String] = [
    "US": "United States",
    "CA": "Canada",
    "GB": "United Kingdom",
    "FR": "France",
    "DE": "Germany"
  ]

  // 用于 AppIntents/UI 的显示;系统会把这个字符串展示在选择/摘要中
  var displayRepresentation: DisplayRepresentation {
    let name = Self.names[countryCode] ?? countryCode
    // 这里使用简单的 stringLiteral 表示:"United States (US)"
    return DisplayRepresentation(stringLiteral: "\(name) (\(countryCode))")
  }
}

// CountryEntryQuery: 实现 EntityQuery
struct CountryEntryQuery: EntityQuery {
  typealias EntityType = CountryEntry

  // 静态的示例数据(可替换为从网络或数据库加载的动态列表)
  var dataList: [CountryEntry] = [
    CountryEntry(countryCode: "US"),
    CountryEntry(countryCode: "CA"),
    CountryEntry(countryCode: "GB"),
    CountryEntry(countryCode: "FR"),
    CountryEntry(countryCode: "DE")
  ]

  // 系统使用的默认 query
  static var defaultQuery: CountryEntryQuery {
    return CountryEntryQuery()
  }

  // entities(for:): 给定一组标识符,返回对应实体。通常用于反解析已保存的配置。
  func entities(for identifiers: [String]) async throws -> [CountryEntry] {
    return identifiers.map { CountryEntry(countryCode: $0) }
  }

  // suggestedEntities(): 返回供 UI 显示的建议实体列表(可以是静态或动态的)
  func suggestedEntities() async throws -> [CountryEntry] {
    return dataList
  }
}

说明:

  • CountryEntrycountryCode 作为 id,并实现 displayRepresentation 返回可读文本(系统用于 UI 展示)。
  • CountryEntryQuerysuggestedEntities() 返回系统将用来显示在选择界面的建议项(本示例返回静态列表,但可以改为异步从网络加载)。
  • entities(for:) 用于把标识符数组还原为实体(系统在持久化/恢复配置时会调用)。

3.2 在 Widget 中使用 Configuration Intent

Widget 使用 AppIntentConfiguration(或 IntentConfiguration)来指定 intent 类型与 provider:

swift 复制代码
struct CountryProvider: AppIntentTimelineProvider {
  typealias Intent = CountrySelectIntent
  typealias Entry = CountryTimelineEntry

  func placeholder(in context: Context) -> CountryTimelineEntry {
    CountryTimelineEntry(date: Date(), configuration: CountrySelectIntent(), country: CountryEntry(countryCode: "US"))
  }

  func snapshot(for configuration: CountrySelectIntent, in context: Context) async -> CountryTimelineEntry {
    CountryTimelineEntry(date: Date(), configuration: CountrySelectIntent(), country: CountryEntry(countryCode: "US"))
  }

  func timeline(for configuration: CountrySelectIntent, in context: Context) async -> Timeline<CountryTimelineEntry> {
    let currentDate = Date()
    let entry = CountryTimelineEntry(date: currentDate, configuration: configuration, country: configuration.country)
    let timeline = Timeline(entries: [entry], policy: .atEnd)
    return timeline
  }
}

struct CountryTimelineEntry: TimelineEntry {
    let date: Date
    let configuration: CountrySelectIntent
    let country: CountryEntry
}

struct CountryView: View {
    var entry: CountryProvider.Entry

    var body: some View {
        VStack {
          Text("Country:")
          Text(entry.country.countName)
                .font(.headline)
        }
    }
}


struct CountryWidget: Widget {
    let kind: String = "CountryWidget"

  var body: some WidgetConfiguration {
    AppIntentConfiguration(kind: kind, intent: CountrySelectIntent.self, provider: CountryProvider()) { entry in
      CountryView(entry: entry)
    }
    .configurationDisplayName("Country Widget")
    .description("Displays information about the selected country.")
  }
}

关键点:

  • CountryProvider.timeline(for:in:)configuration 参数是 CountrySelectIntent,并且 configuration.countryCountryEntry 实例(由系统生成/反序列化),可以直接用于渲染。
  • placeholder / snapshot 中,建议提供合理值。

使用示例仅仅是为了使用 Intent,并未对数据进行本地缓存以及一些其他的优化。


4. 本地化、性能与安全注意事项

  • 本地化:在 .intentdefinition 中为每个 Item 设置本地化的 displayString,或在 Localizable.strings 中提供翻译。
  • 性能:Intents extension 运行时间短,避免同步等待网络请求;若确需网络,使用缓存(App Group)并优先返回缓存结果。
  • 权限与隐私:如果动态选项涉及用户隐私数据(联系人、日历等),在主 App 中请求并获取权限,Intents extension 需谨慎处理权限敏感操作。

5. AppIntents 的简要对比与迁移建议

  • iOS 16 引入了 AppIntents,它使用 Swift 原生 API 编写 Intent,替代了 .intentdefinition 的可视化工作流。
  • 优点:写 Swift 代码更直观,测试更方便。缺点:最低支持 iOS 16。
  • 迁移建议:如果你的应用仅面向 iOS 16+,优先考虑 AppIntentConfiguration;若需兼容 iOS 14/15 或已有大量 .intentdefinition,继续使用 Intents 文件。

6. 总结

  • 使用 IntentConfiguration 可以为 Widget 提供用户可配置的参数选择,适用于需要兼容较旧 iOS 版本或已有 .intentdefinition 的项目。
  • 静态选项简单直接:直接在 .intentdefinition 文件中添加 Items 并生成代码。
  • 动态选项需实现相应的 Intents 协议及回调,注意性能与缓存策略。
  • 推荐实践:把图片资源放在 Widget 的 Assets、把耗时网络操作迁移到 App 并通过 App Group 缓存结果、在 .intentdefinition 中做好本地化。

最后,希望能够帮助到有需要的朋友,如果觉得有帮助,还望点个赞,添加个关注,笔者也会不断地努力,写出更多更好用的文章。

相关推荐
报错小能手10 小时前
ios开发方向——swift错误处理:do/try/catch、Result、throws
开发语言·学习·ios·swift
小夏子_riotous12 小时前
openstack的使用——5. Swift服务的基本使用
linux·运维·开发语言·分布式·云计算·openstack·swift
开心就好202515 小时前
Flutter iOS应用混淆与安全配置详细文档指南
后端·ios
mCell16 小时前
MacOS 下实现 AI 操控电脑(Computer Use)的思考
macos·agent·swift
开心就好202517 小时前
苹果iOS应用开发上架与推广完整教程
后端·ios
用户693717500138417 小时前
XChat 为什么选择 Rust 语言开发
android·前端·ios
MonkeyKing17 小时前
Objective-C Runtime 完整机制:objc_class /cache/bits 源码解析
前端·ios
用户794572239541317 小时前
【DGCharts】iOS 图表渲染事实标准——8 种图表类型、高度可定制,3 行代码画出一条折线
swiftui·swift
秋雨梧桐叶落莳18 小时前
【iOS】 AutoLayout初步学习
学习·macos·ios·objective-c·cocoa·xcode
chaoguo12341 天前
Any metadata 的内存布局
swift·metadata·value witness table