在 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. 方法名简化为
snapshot`
接收一个 configuration
参数,包含了用户的所有配置
是一个 async
方法,直接 return
结果,语法更简洁 |
| 时间线方法 | func getTimeline( in context: Context, completion: @escaping (Timeline<Entry>) -> () )
| func timeline( for configuration: MyConfigIntent, in context: Context ) async -> Timeline<Entry>
|
| | 1. 方法名以 get
开头
不接收用户配置参数
异步操作通过闭包 completion
返回结果 | 1. 方法名简化为 timeline
接收 configuration
参数,可根据用户选择生成不同时间线
是一个 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 Extension
Embed 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
” 框。这代表这些文件既是为原始应用程序目标构建的,现在也是为小组件构建的。
为什么不添加所有文件: