在 iOS 开发早期,构建应用程序很简单,构建的所有内容都构建在一个编译的二进制文件中,所有代码和功能包含在一个地方。如今情况不再如此,我们不仅发布一个应用程序,还发布许多单独的组件( iMessage 应用程序,自定义键盘,Safari 操作扩展,或者是连接到 Siri 的东西)。
现在苹果的组件,从类型上划分其实主要有这几种:
| 类型 | 最常见小组件
Home/Lock Screen Widget | 实时活动/灵动岛
Live Activity / Dynamic Island | 控制中心控件
Control Center Control |
| --- | --- | --- | --- |
| 核心目的 | 提供应用核心信息的“概览” | 追踪一个“正在进行”的短期事件。用于在锁屏和灵动岛上,显示一个具有开始和结束状态、内容会实时更新的短期任务 | 提供一个快捷的“开关”或“动作”。专门用于在控制中心创建新的开关、按钮或滑块 |
| 显示位置 | 主屏幕、锁屏、待机模式 | 锁屏、灵动岛 | 控制中心 |
| 核心框架 | WidgetKit | ActivityKit 框架管理活动的生命周期 | 使用名为 Controls 的独立框架 |
| UI 技术 | WidgetKit (SwiftUI) | WidgetKit (SwiftUI) | Controls 框架自定义 |
| 交互技术 | AppIntents | AppIntents | Controls 框架自定义 |
| 实现方式 | Widget | 需要定义一个遵循 ActivityAttributes 的属性结构体,然后使用 Activity 的 request 方法来启动一个实时活动 | 需要创建一个遵循 ControlWidget 协议的结构体 |
| 总结 | 在它里面根据实现方式的不同,可以分成:静态组件 & 可配置交互组件 | 虽然在 UI 和交互上与 WidgetKit 技术相通,但目的和管理方式完全不同。小组件是为了提供一目了然的概览信息,而实时活动是为了追踪一个“正在进行中”的事件。 | 是一种功能专一的“小组件”,使用场景和技术栈都和主屏幕小组件完全独立。可以把它看作是苹果开放给开发者的一种“系统级开关”的接口。 |
上面提到的能够放置在主屏幕、锁屏和待机模式上的最常见小组件,按实现方式也可以分成两种,它们都属于 WidgetKit 框架。基本差异对比:
| 静态小组件 Static Widget | 可配置/交互式小组件 Configurable Widget | |
|---|---|---|
| 支持系统版本 | iOS 14+ | iOS 17+ |
| 主要用途 | 显示通用信息,用户无法进行任何个性化设置 | 用户可以通过系统提供的编辑界面,自定义小组件的外观和内容。支持交互式按钮 |
| 交互能力 | 不支持。点击小组件的任何位置只会打开主 App | 支持。可以使用 Button(intent: ...) 来创建可交互的按钮,在不打开 App 的情况下执行一个 AppIntent 动作 |
| 用户配置定义 | 无。不需要定义任何配置 | 通过创建一个遵循 WidgetConfigurationIntent 协议的 struct 来定义。使用 @Parameter 属性包装器来定义每一个可配置的选项 |
| 配置方式 | 编译时固定 | 运行时动态配置 |
两者的 Widget 配置结构对比:
| StaticConfiguration | AppIntentConfiguration | |
|---|---|---|
| 关键配置 | StaticConfiguration |
AppIntentConfiguration |
| 配置创建 | `StaticConfiguration( |
kind: kind,
provider: Provider()
) { entry in
MyWidgetEntryView(entry: entry)
}|AppIntentConfiguration(
kind: kind,
intent: MyConfigIntent.self,
provider: Provider()
) { entry in
MyWidgetEntryView(entry: entry)
}| | **Intent要求** | 无 | 需要定义继承自WidgetConfigurationIntent` 的 AppIntent |
| 配置UI | 无 | 配置界面是自动生成的 |
数据提供者协议对比:
| StaticConfiguration | AppIntentConfiguration | |
|---|---|---|
| 协议名称 | TimelineProvider |
AppIntentTimelineProvider |
| 一个通用的数据提供者,不关心用户配置 | 一个专门为可配置组件设计的数据提供者,它能接收用户当前的配置意图 | |
| 泛型参数 | TimelineProvider<Entry> |
AppIntentTimelineProvider<Entry, ConfigurationAppIntent> |
| 配置参数 | 无 | 需要传入 AppIntent 类型 |
有差异的方法实现对比:
| StaticConfiguration | AppIntentConfiguration | |
|---|---|---|
| 快照方法 | `func getSnapshot( |
in context: Context,
completion: @escaping (Entry) -> ()
)|func snapshot(
for configuration: MyConfigIntent,
in context: Context
) async -> Entry| | | 1. 方法名以get开头 2. 不接收用户配置参数 3. 异步操作通过闭包completion返回结果 | 1. 方法名简化为snapshot2. 接收一个configuration参数,包含了用户的所有配置 3. 是一个async方法,直接return结果,语法更简洁 | | **时间线方法** |func getTimeline(
in context: Context,
completion: @escaping (Timeline<Entry>) -> ()
)|func timeline(
for configuration: MyConfigIntent,
in context: Context
) async -> Timeline<Entry>| | | 1. 方法名以get开头 2. 不接收用户配置参数 3. 异步操作通过闭包completion返回结果 | 1. 方法名简化为timeline2. 接收configuration参数,可根据用户选择生成不同时间线 3. 是一个async方法,直接return` 结果 |
StaticConfiguration 示例结构
struct MyWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(
kind: "MyWidget",
provider: MyTimelineProvider()
) { entry in
MyWidgetView(entry: entry)
}
}
}
struct MyTimelineProvider: TimelineProvider {
func placeholder(in context: Context) -> MyEntry { }
func getSnapshot(in context: Context, completion: @escaping (MyEntry) -> Void) { }
func getTimeline(in context: Context, completion: @escaping (Timeline<MyEntry>) -> Void) { }
}
AppIntentConfiguration 示例结构
struct MyWidget: Widget {
var body: some WidgetConfiguration {
AppIntentConfiguration(
kind: "MyWidget",
intent: MyConfigIntent.self,
provider: MyTimelineProvider()
) { entry in
MyWidgetView(entry: entry)
}
}
}
struct MyTimelineProvider: AppIntentTimelineProvider {
func placeholder(in context: Context) -> MyEntry { }
func snapshot(for configuration: MyConfigIntent, in context: Context, completion: @escaping (MyEntry) -> Void) { }
func timeline(for configuration: MyConfigIntent, in context: Context, completion: @escaping (Timeline<MyEntry>) -> Void) { }
}
由于 AppIntent 方式是未来的超集,所以未来完全可以只学习并使用这一种方式来构建所有的小组件,这几乎没有任何负面影响。
首先 AppIntentConfiguration 可以完美地实现 StaticConfiguration 的所有效果。如何做到?只需提供一个不包含任何 @Parameter 属性的 AppIntent 即可。当 WidgetKit 系统检测到提供给 AppIntentConfiguration 的意图(Intent)里没有任何可供用户配置的 @Parameter 时,它就足够智能,根本不会在长按菜单中显示“编辑小组件”的选项。从体验来看它和用 StaticConfiguration 创建的小组件是完全一样的。
// **示例:用 AppIntent 方式做一个“静态”小组件**
import AppIntents
import WidgetKit
// 1. 创建一个“空的”意图,没有任何 @Parameter
struct EmptyIntent: WidgetConfigurationIntent {
// 标题和描述是必需的,但因为没有可配置项,用户永远不会看到这个界面
static var title: LocalizedStringResource = "My Simple Widget Intent"
static var description = IntentDescription("This intent has no configurable options.")
}
// 2. 在小组件主体中,依然使用 AppIntentConfiguration
@main
struct MySimpleWidget: Widget {
let kind: String = "MySimpleWidget"
var body: some WidgetConfiguration {
// 传入这个“空的”意图
AppIntentConfiguration(
kind: kind,
intent: EmptyIntent.self, // <-- 使用空意图
provider: Provider() // Provider 需遵循 AppIntentTimelineProvider
) { entry in
MyWidgetEntryView(entry: entry)
}
.configurationDisplayName("我的简单小组件")
.description("这是一个简单的信息展示小组件。")
}
}
AppIntentTimelineProvider)、一种配置 (AppIntentConfiguration) 和一种数据流模式。这让你能把精力更集中在核心功能开发上。AppIntent 方式,你需要做的仅仅是在你的 EmptyIntent 里增加一个 @Parameter 属性。而如果你当初用的是 StaticConfiguration,你将需要重构整个小组件的配置,把 StaticConfiguration 换成 AppIntentConfiguration,TimelineProvider 换成 AppIntentTimelineProvider,工作量要大得多。Button(intent: ...) 这个功能也需要 AppIntent 的支持。你的基础架构已经准备就绪,添加交互会非常自然。在创建小组件时,实际上是在构建项目中一个完全独立的部分 target,这意味着:
首先我们需要创建一个小组件目标 widget target,该 target 将成为未来所有小组件的 host 主体,所有的小组件都在这个 target 中定义
File 菜单并选择 New > Target ,然后选择 Widget ExtensionEmbed in Application 设置,这是将两个目标绑定到一个应用程序的关键Include Live Activity 和 Include Configuration App 两项<aside> 💡 如果这是您第一次使用 scheme 方案,请仔细阅读以下内容以避免混淆! 我们现在在多个独立的 targets 之间工作,不同的程序必须单独运行才能发挥作用。Xcode 会根据 schemes 决定应该运行哪一个,当前激活的方案就是下次按 Cmd+R 时,会运行的程序。这里 Xcode 询问是否激活新的小组件 scheme ,这很有帮助。这意味着我们可以继续构建小部件,并且之后每次按 Cmd+R 时,都会部署并运行新的小部件代码。因此,请立即按激活。 将来,如果希望返回到以前的方案,可以通过转到 “Product” 菜单并选择 “Scheme > UltimatePortfolio” 来执行此操作。
</aside>
往小组件添加编译文件:
现在小组件 target 文件夹,看起来像一个迷你应用:它有两个 Swift 代码文件;一个资产目录;还有一个 Info.plist 文件。小组件是单独的二进制文件,默认情况下它只有两个 Swift 文件的代码,是不够的。除了直接在该 target 中写代码,我们还可以复用之前在主应用中的一些代码,所以我们要告诉 Xcode 哪些其他 Swift 文件应该编译到小组件中。
添加方法是选择需要的文件,打开右侧文件检查器面板,然后选中 “Target Membership” 中的 “PortfolioWidgetExtension” 框。这代表这些文件既是为原始应用程序目标构建的,现在也是为小组件构建的。
为什么不添加所有文件: