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 中做好本地化。

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

相关推荐
非专业程序员Ping4 小时前
Vibe Coding 实战!花了两天时间,让 AI 写了一个富文本渲染引擎!
ios·ai·swift·claude·vibecoding
m0_495562786 小时前
Swift的逃逸闭包
服务器·php·swift
00后程序员张6 小时前
HTTP抓包工具推荐,Fiddler配置方法、代理设置与使用教程详解(开发者必学网络调试技巧)
网络·http·ios·小程序·fiddler·uni-app·webview
m0_495562787 小时前
Swift-static和class
java·服务器·swift
2501_940094027 小时前
iphone Delta模拟器如何从夸克网盘导入游戏ROM 附游戏资源下载
游戏·ios·iphone
2501_9400940215 小时前
iPhone Delta模拟器游戏资源包合集中文游戏ROM+BIOS+Delta皮肤附游戏导入教程
游戏·ios·iphone
2501_9159184115 小时前
HTTP和HTTPS工作原理、安全漏洞及防护措施全面解析
android·http·ios·小程序·https·uni-app·iphone
白玉cfc15 小时前
【iOS】UICollectionView
macos·ios·cocoa
2501_9160074716 小时前
如何在 Windows 电脑上调试 iOS 设备上的 Safari?完整方案与实战经验分享
android·windows·ios·小程序·uni-app·iphone·safari