大多数浏览器和
Developer App 均支持流媒体播放。
-
为 Apple Watch 上的智能叠放构建小组件
请跟随我们使用最新版的 SwiftUI 和 WidgetKit API 为 watchOS 10 上的智能叠放构建小组件。了解创建小组件的技巧、技术以及最佳实践以在 Apple Watch 上展示相关信息。
章节
- 0:58 - Get started
- 2:03 - Configure the widget
- 3:51 - Set up the timeline
- 10:16 - Build widget views
- 16:27 - Finish building timeline
- 19:58 - Provide relevant intents
资源
相关视频
WWDC23
-
下载
♪ ♪
Calvin:大家好 我是 Calvin Gaisford watchOS 团队的一名工程师 在本次代码讲座中 我们将一起为 Apple Watch 中的 新智能叠放构建小组件 我们将为你介绍 使用 AppIntent 配置 构建小组件的整个过程 在该过程中 我们将用到最新的 WidgetKit 和 SwiftUI 更新
我们会使用 Backyard Birds App 来编译代码 Backyard Birds App 可让你创建并管理 能够吸引鸟访客的后院 该小组件可以显示后院状态 如果有鸟来访 它便会显示鸟 以及后院的状态 该小组件还会为智能叠放 提供相关日期 以便在最相关的日子里 优先显示我们的小组件 如果你想和我们一起编程 请下载与本次讲座有关的 示例代码 并打开 Backyard Birds Xcode 项目 我们已将小组件扩展添加至项目中 该项目生成了 BackyardVisitorsWidget 等文件 接下来 我们会 将重点放在升级该文件上 以下是我们将要介绍的内容 首先 我们会来了解一下 定义小组件的小组件结构 我们还会快速介绍一下 小组件的配置意图 接着 我们会自定义 TimelineEntry 结构 来保存小组件视图所需的数据 并编译我们自己的时间线 在时间线有了 用于启用预览的数据后 我们会构建小组件视图 构建完视图后 我们会返回以完成时间线的构建 最后 我们会了解一下 RelevantIntentManager 并为小组件 在 watchOS 智能叠放上 优先显示的日期设置意图 首先 我们从小组件配置开始 了解一下代码中的小组件结构
小组件结构是 定义小组件配置的地方 watchOS 新推出了 AppIntentConfiguration 框架 我们会在小组件中使用 AppIntentConfiguration 当我们创建 WidgetExtension 时 configuration intent、provider 和 view 也已经创建好了 接下来 我们会 在 Backyard Birds 小组件中 逐一了解并实现这些内容 这个小组件定义看上去不错 所以我们来继续了解一下 WidgetConfigurationIntent 我们的小组件会使用 AppIntentConfiguration 来实现两个目的 首先 小组件能在 watchOS 小组件资源库中 提供一系列预配置的小组件 在 Backyard Birds 示例中 我们将为 App 中的 每个后院提供配置 其次 使用 WidgetConfigurationIntent 指定与小组件最相关的日期 智能叠放将使用该信息 来优先显示其中的小组件
接下来 我们来看看小组件中的 ConfigurationAppIntent Backyard Birds 中的每个后院 都有一个唯一的 ID 我已经添加了一个 名为 backyardID 的参数 用于创建一系列小组件意图 backyardID 会识别 其各自后院的意图 对于我们正在构建的 小组件而言 我们只需要这一个参数 想要进一步了解有关 App Intents 以及如何进一步使用 WidgetConfigurationIntent 的信息 请查看这些 关于 Apple Intents 的讲座 至此 我们已经定义了小组件结构 以及可用于保存后院 ID 的 WidgetConfigurationIntent 接下来 我们来看看小组件时间线 并了解一下 TimelineEntry 结构 TimelineEntry 结构可以保存 小组件视图在特定日期进行显示时 所需的所有数据 回到 BackyardVisitorsWidget 文件 找到生成的 SimpleEntry 结构
该文件在生成时已经添加了 date 和 configuration 属性 因此 我们需要定义 小组件视图所需要的 其他额外属性 我们的小组件会显示后院状态 包括其名字、食物和水的状态 如果有鸟来访 小组件就会 显示来访的鸟及其名字 如果没有鸟来访 它就会显示 院子中有多少鸟来过 为了显示院子信息 我们会使用 Backyard Birds App 中的结构 该结构可以保存特定时间内 关于后院的所有信息 这点非常重要 因为 TimelineEntry 可能会包含未来的日期 我们在 TimelineEntry 中 添加 backyard 属性
现在 我们根据 刚刚添加的 backyard 属性 再来添加一些计算属性 首先 我们添加一个 bird 属性 以供小组件视图确认 是否有鸟来访并进行显示
现在 我们再添加两个属性 来让视图显示更多的院子信息
视图中的 waterDuration 和 foodDuration 用于显示水和食物还能用多久 这些属性通过 TimelineEntry 中的 日期属性计算得到
TimelineEntry 中还有一个 名为 relevance 的属性 如果该属性得到实现 其便可以告知 watchOS 智能叠放 哪些时间线条目最为重要 我们将 relevance 属性 添加到 TimelineEntry 中
在这里 我们来查看一下 TimelineEntry 日期中 后院是否有鸟来访
如果有鸟来访 我们就会返回 TimelineEntryRelevance 结构
TimelineEntryRelevance 结构 有两个参数: score 和 duration Score 可以在同一时间线中 将某一条目置于优先地位 我们会将 score 设置为 10 来将条目按照访问数量 从高到低进行排序 该值是任意的 并且可以使用一系列值 来对时间线中的条目进行排序 Duration 用于告知智能叠放 该相关条目的有效时长 我们将 duration 设置为 持续到 visitor.endDate 如果没有鸟访客 我们就会返回 score 为 0 的 relevance 结构
这样 智能叠放就会知道 哪些时间线条目是最重要的 根据当时发生的其他事情 我们的小组件在智能叠放中 就可能会被置于优先地位
现在 我们的 TimelineEntry 看上去就很不错了 并且还包含了小组件视图 用于正确显示的所有内容 接下来 我们来继续构建 TimelineProvider 在 TimelineProvider 中 我们需要实现 4 个函数: placeholder、 snapshot、 timeline 和 recommendations Placeholder 函数 会在小组件第一次显示时启用 并会快速返回 为了占用一个 backyard 我们对 TimelineEntry 进行了更新 所以我们还需要再提供一个 为了修正这一点 我们会从 App 的 数据模型中添加一个随机 backyard
就是这样 我们继续 Snapshot 函数会在小组件 处于短暂情况下时使用 由于该函数需要快速返回 所以我们可以使用示例数据 只要其获取数据的时间 不超过几秒就可以 接着 我们会重复 placeholder 函数中的操作 并传入一个随机 yard
这样看上去还行 但我们还可以做得更好 Snapshot 函数中会传入 一个 configuration 意图 该意图包含我们此前添加的 backyardID 属性 由于我们的数据都是本地的 所以我们可以快速查找 并返回正确的 backyard 而不会使用随机 backyard 接下来 我们从配置中的 backyardID 获取配置的 backyard
现在 我们来确认一下 backyard 并看看能否从中获取 visitorEvent
我们会返回一个 配置了访问日期的条目 如果没有鸟访问 我们就会返回一个 配置了 currentDate 的 yard
这样便可以为用户 提供更好的预览效果 因为其会显示配置的 backyard 在我们深入探讨 timeline 函数之前 我想先打开 Xcode 画布预览 接下来 我们会处理最后剩下的 SimpleEntry 并赋予其 backyard 以便我们可以查看预览
现在 我们打开画布
Xcode 的新功能 可用于预览小组件时间线 画布显示了一个矩形小组件预览 其底部显示的是 一系列构成小组件时间线的 TimelineEntries 画布预览使用的是 我们在添加小组件时 生成的默认视图 在完成 timelineProvider 之前 我们先来构建视图 以便我们在构建过程中 可以更好地将时间线可视化 找到 BackyardBirdsWidgetEntryView 我们来为 widgetFamily 添加一个 environment 属性 以便为每个 family 编译特定的视图
我们用 switch 语句替换 body 从而可以为每个 accessoryWidget family 实现一个视图
现在 我们为 accessoryRectangular 创建一个例子 其视图以 entry 为参数 我们将在下方实现该视图
矩形视图是唯一的 因为它是 watchOS 智能叠放中 显示的小组件视图 在矩形视图中 我们会遵循常用的模式 即视图左侧显示一张图像 右侧显示三行文本 接下来 我们来到文件底部 并创建 RectangleBackyardView
该视图会使用 我们此前修饰过的 TimelineEntry 来包含 backyard 数据 在继续之前 我们先将画布视图 切换为智能叠放的矩形视图
这样 我们就可以在 构建小组件过程中对其进行可视化 现在 我们将一张图像和三行文本 放入视图中的 HStack 中
我们来看看预览 效果好像不太对 我们将文本行 放入其自己的 VStack 中
现在看上去就差不多了 接下来 我们将条目中的 真实数据放入该视图 首先 我们使用 Backyard Birds App 中 可用于显示鸟的 ComposedBird 视图
由于 bird 是可选的 所以我们需要对其进行解包 我们将 ComposedBird 视图 和 VStack 放入 if-let 检查中 来看看条目中是否有 bird
如果没有 bird 我们就在 yard 中 放置一个喷泉图像 以及显示没有鸟的文本
现在 我们可以浏览时间线 并看看显示鸟和三行文本的条目 以及显示没有鸟的条目
接下来 我们首先针对有鸟的情况 来填写细节信息 第一行 我们会显示鸟的名字 第二行 显示后院的名字 最后第三行 我们会添加 关于院子中食物和水的信息
而对于没有鸟的情况 我们会显示院子名字、 食物和水的信息 以及院子的鸟访客数量
我们来看看条目的效果怎么样
看上去很棒 但我们再来稍微调整一下布局 首先 我们来 更新 ComposedBird 我们将视图设置为 scaledToFit 和 widgetAccentable 这样 当该视图在 调色的表盘上使用时 就会进行着色 对于鸟的名字 我们会添加 标题字体 并令其可缩放 同时 我们也会将其设置为 widgetAccentable 以使其和表盘一同着色 此外 我们还会使用 foregroundStyle 来使文本采用鸟翅膀的颜色
我们将缩放比例 添加到其他两个视图中 以防止名字过长
我们将最后一行的 foregroundStyle 设置为 secondary
最后 我们将叠放设置为 alignment: .leading 以让三个文本视图对齐
我们的视图看上去棒极了 没有鸟来访时 我们就将 所有这些相同的更新应用到 else 语句的视图中
现在 我们的小组件看上去更棒了
需要注意的是 我们的小组件在 显示鸟或院子时会具有不同的间距 接下来 我们在鸟视图和图像视图中 添加一个框架来使这些内容相匹配
并在 VStacks 中添加框架 来使它们适当对齐
此外 我们还需要 添加一个选项来完成 我们在 watchOS 智能叠放中的 小组件 SwiftUI 中新增了 containerBackground 接下来 我们使用 backyard 的 gradient 来替代 containerBackground 我们将 containerBackground 的 位置设置为 widget
由于 containerBackground 是由系统选择性使用的 所以 在这里其只会出现在 watchOS 智能叠放中 而不会出现在表盘上
现在 我们的视图就可以应用于 watchOS 智能叠放了 这个视图看起来很棒 所以 现在我们再回到 TimelineProvider 中 完成时间线的构建
小组件可以在 timeline 函数中 生成时间线条目集合 该集合包含用于 显示小组件视图的数据 该函数是小组件的核心函数 现在 该函数正在使用 随机 backyard 数据生成 5 个条目 接下来 我们使用包含鸟访问的时间线 来替代这些条目 函数顶部是一个 TimelineEntries 数组 我们会使用该组数来构建时间线 首先 我们删除生成的时间线代码
现在 我们使用 backyardID 从 ConfigurationAppIntent 中 获取配置的 yard
backyard 结构中有一个属性 该属性包含院子所有的 visitorEvents 我们在检索到的 yard 中 对 visitorEvents 进行遍历 并为每个 event 创建一个包含 visitorEvent 中 startDate 的 TimelineEntry 并将其传入配置的 backyard 中
我们对时间线预览进行了更新 让我们来看看它是怎样变化的 现在 当我们选择时间线条目时 可以看到出现的鸟 这正是我们想要达到的效果 但是 每个条目都只有一个访客 鸟离开时 我们也需要添加条目 接下来 我们来创建第二个条目 并使用 visitorEvent 的 endDate 在这里 我们会使用同一个 backyard 并将 entry 添加到 entries 数组中
现在 我们再来看看时间线
这样 我们便有了鸟访问 以及鸟离开的条目 我们的小组件时间线看起来棒极了 而且这个新的时间线预览也非常棒 这也会让接下来的小组件 和时间线构建更加简单
最后 我们来实现 timelineProvider 的 recommendations 函数 在这里 我们需要返回包含 WidgetConfigurationIntent 的 AppIntentRecommendations 数组 其中 WidgetConfigurationIntent 保存了 backyardID 我们会删除默认实现
并创建一个要返回的 recommendations 数组
接下来 我们希望为 App 中 每个 backyard 都创建一个 recommendation 所以 我们对所有的 backyard 进行遍历
我们会为每个 backyard 创建一个 ConfigurationAppIntent 并设置 backyardID
最后 我们使用 ConfigurationIntent 创建 AppIntentRecommendation 并将其添加到数组中 接着 我们将 backyard 的 name 作为 description
现在 当用户选择 Backyard Birds 小组件时 recommendations 函数就会为 小组件资源库中的每个 backyard 提供小组件配置列表 恭喜! 你现在已经在 watchOS 上 构建了一个小组件 该小组件可以显示表盘复杂功能 和 watchOS 智能叠放 此前 我们在实现 TimelineEntry 中的 relevance 属性时 已经简要讨论了相关性 但关于这一点 我们还可以实现更多内容 Backyard Birds App 中的 每个院子 都会记录鸟可使用的水和食物 我们的新小组件也将显示这些信息 我们会在水和食物不足的时间段内 向系统提供一系列的相关意图 在这段时间内 我们的小组件会被置于优先地位 以便用户知道他们的院子需要关注
回到代码中 我们来创建一个新函数 用于为所有可能的小组件 构建相关意图 然后用这些意图来更新 RelevantIntentManager 我们会创建一个名为 updateBackyardRelevantIntents 的新函数
在该函数中 我们需要 一个 relevantIntents 数组
接着 我们使用该数组更新 RelevantIntentManager
为了填充 relevantIntents 数组 我们会遍历该 App 中所有的后院 接下来 我们会为后院 创建 configurationIntent 并将 backyardID 设置为当前后院 接着 我们会根据日期创建一个 RelevantContext 在本例中 我们会使用 后院未来食物不足的日期 以及未来没有食物的日期
最后 我们来创建 一个 relevantIntent 我们会使用小组件的 configurationIntent、 小组件的类型 以及刚刚创建的 relevantDateContext 并将其附加到数组中
现在 我们来对后院中的 lowWater 和 emptyWater 数据 进行同样的操作
看上去还不错 现在 RelevantIntentManager 就拥有了每个小组件配置 具有更高相关性的日期范围 接下来 我们将该功能 添加到关键组件中 以便适时更新 relevantIntents 首先 我们回到 timelineProvider 中的 timeline 函数 我们会在返回时间线之前 调用该函数
这样 在我们更新小组件时间线时 relevantIntents 也能 同时保持最新状态 我们回到 Backyard Birds App 现在 Backyard Birds App 就包含了每个院子的详细视图 并为用户提供了 用于补充食物和水的页面 由于食物和水的供应 可能会发生变化 所以这也是 更新 relevantIntents 的理想位置 在 BackyardContentTab 中 当 Refill 按钮被轻按时 我们会使用 updateBackyardRelevantIntents 函数 添加一个 Task 由于我们知道食物和水 刚刚得到更新 所以我们还应该调用 WidgetKit 并重新加载小组件的时间线
现在 当用户补充 院子的水和食物时 我们的相关意图就会得到更新 小组件的时间线也会重新加载
现在 我们已经为 watchOS 智能叠放 构建了一个小组件 并使用日期意图更新了 RelevantIntentManager 以在与小组件最相关时 将其置于优先地位 感谢你的观看 我们期待看到 你为 watchOS 智能叠放构建的小组件 想要进一步了解有关小组件、 智能叠放以及 App Intents 的信息 请查看这些讲座 敢于冒险 永不停下编程的脚步
-
-
4:15 - TimelineEntry
struct SimpleEntry: TimelineEntry { var date: Date var configuration: ConfigurationAppIntent var backyard: Backyard var bird: Bird? { return backyard.visitorEventForDate(date: date)?.bird } var waterDuration: Duration { return Duration.seconds(abs(self.date.distance(to: self.backyard.waterRefillDate))) } var foodDuration: Duration { return Duration.seconds(abs(self.date.distance(to: self.backyard.foodRefillDate))) } var relevance: TimelineEntryRelevance? { if let visitor = backyard.visitorEventForDate(date: date) { return TimelineEntryRelevance(score: 10, duration: visitor.endDate.timeIntervalSince(date)) } return TimelineEntryRelevance(score: 0) } }
-
7:50 - placeholder function
func placeholder(in context: Context) -> SimpleEntry { return SimpleEntry(date: Date(), configuration: ConfigurationAppIntent(), backyard: Backyard.anyBackyard(modelContext: modelContext)) }
-
8:15 - snapshot function
func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry { if let backyard = Backyard.backyardForID(modelContext: modelContext, backyardID: configuration.backyardID) { if let event = backyard.visitorEvents.first { return SimpleEntry(date: event.startDate, configuration: configuration, backyard: backyard) } else { return SimpleEntry(date: Date(), configuration: configuration, backyard: backyard) } } let yard = Backyard.anyBackyard(modelContext: modelContext) return SimpleEntry(date: Date(), configuration: ConfigurationAppIntent(), backyard: yard) }
-
10:26 - Widget Entry View
struct BackyardBirdsWidgetEntryView: View { @Environment(\.widgetFamily) private var family var entry: SimpleEntry var body: some View { switch family { case .accessoryRectangular: RectangularBackyardView(entry: entry) default: Text(entry.date, style: .time) } } }
-
11:23 - Backyard Rectangular View
struct RectangularBackyardView: View { var entry: SimpleEntry var body: some View { HStack { if let bird = entry.bird { ComposedBird(bird: bird) .scaledToFit() .widgetAccentable() .frame(width: 50, height: 50) VStack(alignment: .leading) { Text(bird.speciesName) .font(.headline) .foregroundStyle(bird.colors.wing.color) .widgetAccentable() .minimumScaleFactor(0.75) Text(entry.backyard.name) .minimumScaleFactor(0.75) HStack { Image(systemName: "drop.fill") Text(entry.waterDuration, format: remainingHoursFormatter) Image(systemName: "fork.knife") Text(entry.foodDuration, format: remainingHoursFormatter) } .imageScale(.small) .minimumScaleFactor(0.75) .foregroundStyle(.secondary) } .frame(maxWidth: .infinity, alignment: .leading) } else { Image(.fountainFill) .foregroundStyle(entry.backyard.backgroundColor) .imageScale(.large) .scaledToFit() .widgetAccentable() .frame(width: 50, height: 50) VStack(alignment: .leading) { Text(entry.backyard.name) .font(.headline) .foregroundStyle(entry.backyard.backgroundColor) .widgetAccentable() .minimumScaleFactor(0.75) HStack { Image(systemName: "drop.fill") Text(entry.waterDuration, format: remainingHoursFormatter) Image(systemName: "fork.knife") Text(entry.foodDuration, format: remainingHoursFormatter) } .imageScale(.small) .minimumScaleFactor(0.75) Text("\(entry.backyard.historicalEvents.count) visitors") .minimumScaleFactor(0.75) .foregroundStyle(.secondary) } .frame(maxWidth: .infinity, alignment: .leading) } } .containerBackground(entry.backyard.backgroundColor.gradient, for: .widget) } }
-
16:30 - Timeline Function
func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<SimpleEntry> { var entries: [SimpleEntry] = [] if let backyard = Backyard.backyardForID(modelContext: modelContext, backyardID: configuration.backyardID) { for event in backyard.visitorEvents { let entry = SimpleEntry(date: event.startDate, configuration: configuration, backyard: backyard) entries.append(entry) let afterEntry = SimpleEntry(date: event.endDate, configuration: configuration, backyard: backyard) entries.append(afterEntry) } } return Timeline(entries: entries, policy: .atEnd) }
-
18:35 - Recommendations Function
func recommendations() -> [AppIntentRecommendation<ConfigurationAppIntent>] { var recs = [AppIntentRecommendation<ConfigurationAppIntent>]() for backyard in Backyard.allBackyards(modelContext: modelContext) { let configIntent = ConfigurationAppIntent() configIntent.backyardID = backyard.id.uuidString let gardenRecommendation = AppIntentRecommendation(intent: configIntent, description: backyard.name) recs.append(gardenRecommendation) } return recs }
-
20:47 - Relevant Intents Function
func updateBackyardRelevantIntents() async { let modelContext = ModelContext(DataGeneration.container) var relevantIntents = [RelevantIntent]() for backyard in Backyard.allBackyards(modelContext: modelContext) { let configIntent = ConfigurationAppIntent() configIntent.backyardID = backyard.id.uuidString let relevantFoodDateContext = RelevantContext.date(from: backyard.lowSuppliesDate(for: .food), to: backyard.expectedEmptyDate(for: .food)) let relevantFoodIntent = RelevantIntent(configIntent, widgetKind: "BackyardVisitorsWidget", relevance: relevantFoodDateContext) relevantIntents.append(relevantFoodIntent) let relevantWaterDateContext = RelevantContext.date(from: backyard.lowSuppliesDate(for: .water), to: backyard.expectedEmptyDate(for: .water)) let relevantWaterIntent = RelevantIntent(configIntent, widgetKind: "BackyardVisitorsWidget", relevance: relevantWaterDateContext) relevantIntents.append(relevantWaterIntent) } do { try await RelevantIntentManager.shared.updateRelevantIntents(relevantIntents) } catch { } }
-
23:00 - Update Relevant Intents
Task { await updateBackyardRelevantIntents() WidgetCenter.shared.reloadTimelines(ofKind: "BackyardVisitorsWidget") }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。