学计算机的那个

不是我觉到、悟到,你给不了我,给了也拿不住;只有我觉到、悟到,才有可能做到,能做到的才是我的.

0%

【译】创建扩展小组件

添加和配置一个扩展,以显示您的应用程序的内容在主屏幕,今日视图,或通知中心。

概述

小组件显示相关的、可浏览的内容,让用户快速访问你的应用程序获得更多细节。你的应用程序可以提供各种小组件,让用户专注于对他们来说最重要的信息。他们可以添加同一个小组件的多个副本,并根据各自的独特需求和布局进行定制。如果在小组件中包含自定义意图(custom intent),用户还可以对每个小组件单独进行个性化设置。小部件支持多种大小;选择最适合应用内容的大小。由于空间有限,请确保您的小组件显示人们最重视的信息。

向应用程序中添加小组件只需要最少的设置,以及关于用户界面的配置和风格的一些决策。小组件使用SwiftUI视图显示其内容。更多信息请参见SwiftUI

添加一个小组件目标到您的应用程序

小组件扩展模板提供了创建小组件的起点。一个小组件扩展可以包含多个小组件。例如,一个体育应用程序可能有一个显示球队信息的小组件,而另一个显示比赛日程。一个小组件扩展可以包含两个小组件。

  1. Xcode中打开你的应用项目,选择File > New > Target
  2. 从应用程序扩展组中选择小组件扩展,然后单击下一步。
  3. 输入您的扩展名。
  4. 如果小组件提供用户可配置的属性,请选中Include Configuration Intent复选框。
  5. 单击Finish。

通常,你将所有的小组件包含在一个小组件扩展中,尽管你的应用程序可以包含多个扩展。例如,如果您的一些小组件使用位置信息,而其他小组件不使用,那么将使用位置信息的小组件保留在单独的扩展中。这允许系统提示用户授权只对使用位置信息的扩展的小组件使用位置信息。

添加配置详情

小组件扩展模板提供了符合widget协议的初始小组件实现。此小组件的主体属性确定该小组件是否具有用户可配置的属性。有两种配置:

StaticConfiguration(静态配置)
对于没有用户可配置属性的小组件。例如,显示一般市场信息的股票市场小组件,或显示趋势标题的新闻小组件。

IntentConfiguration(意向配置)
对于具有用户可配置属性的小组件。使用SiriKit自定义意图(custom intent)来定义属性。例如,一个天气小组件需要一个城市的邮政编码或邮政编码(zip or postal code),或者一个包裹跟踪(package-tracking)小组件需要一个跟踪号。

包含配置意图复选框决定了Xcode使用哪种配置。当你选中这个复选框时,Xcode会使用一个意向配置;否则,它使用静态配置。要初始化一个IntentConfiguration,向它的初始化器提供以下信息:

kind (种类)
标识小组件的字符串。这是您选择的标识符,并且应该描述小组件所代表的内容。

provider(供应商)
一个符合TimelineProvider并生成时间轴的对象,告诉WidgetKit何时呈现小组件。时间轴包含您定义的自定义TimelineEntry类型。当您希望WidgetKit更新小部件的内容时,时间轴条目(entry)标识日期。包含小组件视图需要以自定义类型呈现的属性。

intent(意图)
自定义用户可配置属性的自定义意图。有关添加自定义的详细信息,请参见制作可配置小组件。

content(内容)
包含SwiftUI视图的闭包。WidgetKit调用它来呈现小组件的内容,并从提供程序传递一个TimelineEntry参数。

使用修饰符(modifiers)提供额外的配置细节,包括显示名称、描述和小组件支持的系列(families)。以下代码显示了一个小组件,它为游戏提供了一般的、不可配置的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@main
struct GameStatusWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(
kind: "com.mygame.game-status",
provider: GameStatusProvider(),
) { entry in
GameStatusView(entry.gameStatus)
}
.configurationDisplayName("Game Status")
.description("Shows an overview of your game status")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge])
}
}

小组件的提供者为小组件生成时间轴,并在每个条目(entry)中包含游戏状态细节。当每个时间轴条目(entry)的日期到达时,WidgetKit调用内容闭包来显示小组件的内容。最后,修饰符指定(specify)小组件库(gallery)中显示的名称和描述,并允许用户选择小组件的小、中或大版本。

要让应用程序的小组件出现在小组件库中,用户必须在应用程序安装后启动包含该小组件的应用程序至少一次。

注意这个小组件上@main属性的用法。这个属性表明GameStatusWidget是小组件扩展的入口点,这意味着扩展包含单个小组件。要支持多个小组件,请参阅下面的部分在您的应用程序扩展中声明多个小组件。

提供时间轴条目

时间轴供应商生成由时间轴条目组成的时间轴,每个条目指定更新小组件内容的日期和时间。游戏状态小组件可以定义它的时间轴条目,包括一个表示游戏状态的字符串,如下所示:

1
2
3
4
struct GameStatusEntry: TimelineEntry {
var date: Date
var gameStatus: String
}

要在小组件库中显示小组件,WidgetKit要求提供者提供预览快照。通过检查传递给getSnapshot(in:completion:)方法的上下文参数的isPreview属性来识别这个预览请求。当isPreviewtrue时,WidgetKit在小组件库中显示您的小组件。因此,您需要快速创建预览快照。如果您的小组件需要从服务器生成或获取需要时间的资产(assets)或信息,请使用示例数据。

在以下代码中,游戏状态小组件的提供者通过显示一个空状态来实现snapshot方法,如果它还没有完成从其服务器获取状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct GameStatusProvider: TimelineProvider {
var hasFetchedGameStatus: Bool
var gameStatusFromServer: String

func getSnapshot(in context: Context, completion: @escaping (Entry) -> Void) {
let date = Date()
let entry: GameStatusEntry

if context.isPreview && !hasFetchedGameStatus {
entry = GameStatusEntry(date: date, gameStatus: "—")
} else {
entry = GameStatusEntry(date: date, gameStatus: gameStatusFromServer)
}
completion(entry)
}

在请求初始快照之后,WidgetKit调用getTimeline(in:completion:)从提供程序请求一个常规的时间轴。时间轴由一个或多个时间轴条目和一个重新加载策略组成,该策略通知WidgetKit何时请求后续的时间轴。

下面的例子展示了游戏状态小组件的提供者如何生成一个时间轴,它包含来自服务器的包含当前游戏状态的单个条目,以及一个在15分钟内请求新时间轴的重新加载策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct GameStatusProvider: TimelineProvider {
func getTimeline(in context: Context, completion: @escaping (Timeline<GameStatusEntry>) -> Void) {
// Create a timeline entry for "now."
let date = Date()
let entry = GameStatusEntry(
date: date,
gameStatus: gameStatusFromServer
)

// Create a date that's 15 minutes in the future.
let nextUpdateDate = Calendar.current.date(byAdding: .minute, value: 15, to: date)!

// Create the timeline with the entry and a reload policy with the date
// for the next update.
let timeline = Timeline(
entries:[entry],
policy: .after(nextUpdateDate)
)

// Call the completion to pass the timeline to WidgetKit.
completion(timeline)
}
}

在本例中,如果小部件没有来自服务器的当前状态,它可以存储对完成的引用,向服务器执行异步请求以获取游戏状态,并在请求完成时调用完成。

有关生成时间线(包括在小部件中处理网络请求)的更多信息,请参见保持小部件最新

显示占位符小组件并隐藏敏感数据

占位符视图是没有特定内容的通用可视化表示。当WidgetKit呈现您的小部件时,它可能需要将您的内容呈现为占位符;例如,当您在后台加载数据时。它使用 redacted(reason:)视图修饰符生成占位符,并将reason设置为占位符。该设置自动呈现小部件的视图,其方式适合作为占位符使用。若要选择不将呈现为占位符,并控制小部件的哪些视图显示为审查(redacted)以隐藏敏感信息,请在redacted(reason:)回调中使用unredacted()视图修饰符。

除了首次出现外,当您为小部件扩展启用Data Protection功能时,WidgetKit还将小部件视图呈现为占位符。WidgetKit使用您应用的编辑(redactions)来呈现小部件的视图,或者如果您将数据保护授权设置为:

  • NSFileProtectionComplete and the device is locked
  • NSFileProtectionCompleteUnlessOpen and the device is locked
  • NSFileProtectionCompleteUntilFirstUserAuthentication and the user has not yet authenticated

用户控制小部件在某些表示上下文中是否显示编辑(redacted)视图或占位符,例如在iPhone锁定屏幕或watchOS中的表面上。

watchOS中,设备在使用过程中很少被锁定,因为Apple Watch通常是在用户佩戴时解锁的。但是,用户可以通过选择“设置”来配置是否在“始终打开”期间显示敏感数据(Settings > Display & Brightness > Always On > Hide Sensitive Complications)。如果用户启用隐藏敏感并发症,WidgetKit将呈现您配置的编校(redactions)或返回占位符。他们可以选择为所有或个别并发症(individual complications)显示经过修订的内容。类似地,在iOS中,设备也可能支持Always On,用户可以配置在Always On期间是否显示敏感数据。在不支持Always On的iOS设备上,用户可以通过选择设置>面部ID和密码来控制是否在锁屏上显示敏感数据,并在ALLOW access WHEN LOCKED部分取消对锁屏小部件的数据访问

有关配置数据保护的详细信息,请参阅数据保护授权

在小部件中显示内容

小部件使用SwiftUI视图定义其内容,通常是通过组合其他SwiftUI视图。如Add Configuration Details部分所示,小部件的配置包含一个闭包,WidgetKit调用该闭包来呈现小部件的内容。

当用户从小部件库中添加您的小部件时,他们从小部件支持的小部件中选择特定的家族—例如,小型或中型。小部件的内容闭包必须能够呈现小部件支持的每个族。WidgetKitSwiftUI环境中设置相应的族和附加属性,例如配色方案(亮或暗)。

在上图所示的游戏状态小部件配置中,内容闭包使用GameStatusView来显示状态。因为小部件支持锁定屏幕上的大小和附件小部件,它使用widgetFamily来决定要显示哪个特定的SwiftUI视图,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct GameStatusView : View {
@Environment(\.widgetFamily) var family: WidgetFamily
var gameStatus: GameStatus
var selectedCharacter: CharacterDetail

@ViewBuilder
var body: some View {
switch family {
case .systemSmall: GameTurnSummary(gameStatus)
case .systemMedium: GameStatusWithLastTurnResult(gameStatus)
case .systemLarge: GameStatusWithStatistics(gameStatus)
case .systemExtraLarge: GameStatusWithStatisticsExtraLarge(gameStatus)
case .accessoryCircular: HealthLevelCircular(selectedCharacter)
case .accessoryRectangular: HealthLevelRectangular(selectedCharacter)
case .accessoryInline: HealthLevelInline(selectedCharacter)
default: GameDetailsNotAvailable()
}
}
}

对于小家庭(small family),小部件使用一个视图来显示它在游戏中的回合的简单摘要。对于medium,它显示状态,表示上一轮的结果。对于大号和特大号,因为有更多的空间可用,它会显示每个玩家的运行统计数据。附件小部件比主屏幕上显示的小部件小得多。因此,它们会显示圆形附件小部件当前所选字符的健康水平。因为矩形和内联附件小部件允许更多的文本,所以它们会显示角色的生命值级别和剩余的治疗时间。如果家族是未知类型,则显示默认视图,这表示游戏状态不可用

视图用@ViewBuilder声明它的主体,因为它使用的视图类型是不同的。

对于可配置的小部件,提供程序遵循IntentTimelineProvider。该提供程序执行与TimelineProvider相同的功能,但它合并了用户在小部件上自定义的值。在传递给getSnapshot(for:in:completion:)getTimeline(for:in:completion:)的配置参数中,意图时间轴提供程序可以使用这些自定义。您通常将用户配置的值作为自定义时间轴条目类型的属性包含进来,因此小部件的视图可以使用详细信息

小部件显示只读信息,不支持滚动元素或开关等交互元素。在呈现小部件的内容时,WidgetKit会忽略交互元素。

有关WidgetKit支持的视图列表,请参阅SwiftUI views

向小部件添加动态内容

尽管小部件的显示基于视图的快照,但您可以使用各种SwiftUI视图,它们在小部件可见时继续更新。有关提供动态内容的更多信息,请参见保持小部件更新

响应用户交互

当用户与您的小部件交互时,系统启动您的应用程序来处理请求。当系统激活你的应用程序时,导航到与小部件内容对应的细节。小部件可以指定一个URL来通知应用程序要显示什么内容。要在小部件中配置自定义URLs:

  • 对于所有小部件,在小部件的视图层次结构中添加widgetURL(_:)视图修饰符。如果小部件的视图层次结构包含多个widgetURL修饰符,则行为未定义。

  • 对于使用WidgetFamily.systemMedium, WidgetFamily.systemLargeWidgetFamily.systemextrallarge的小部件。将一个或多个Link控件添加到小部件的视图层次结构中。可以同时使用widgetURLLink控件。如果交互以Link控件为目标,则系统使用该控件中的URL。对于小部件中其他任何地方的交互,系统使用widgetURL视图修饰符中指定的URL

例如,显示游戏中单个角色细节的小部件可以使用widgetURL打开应用,显示该角色的细节。

1
2
3
4
5
6
7
8
9
@ViewBuilder
var body: some View {
ZStack {
AvatarView(entry.character)
.widgetURL(entry.character.url)
.foregroundColor(.white)
}
.background(Color.gameBackground)
}

如果小部件显示一个字符列表,则列表中的每个项都可以位于一个Link控件中。每个Link控件为它所显示的特定字符指定URL

当小部件接收到交互时,系统激活包含该应用程序的应用程序,并将URL传递给onOpenURL(perform:)application(_:open:options:)application(_:open:),这取决于应用程序使用的生命周期。

如果小部件不使用widgetURLLink控件,系统将激活包含该应用的应用程序,并将NSUserActivity传递给onContinueUserActivity(_:perform:)application(_:continue:restorationHandler:)application(_:continue:restorationHandler:)。用户活动的userInfo字典包含与用户交互的小部件的详细信息。通过WidgetCenter.UserInfoKey中的键获取Swift代码种的值。要从Objective-C访问userInfo值,请使用键WGWidgetUserInfoKeyKindWGWidgetUserInfoKeyFamily来代替。

对于使用IntentConfiguration的小部件,用户活动的交互属性包含小部件的INIntent

在你的应用程序扩展中声明多个小部件

上面的GameStatusWidget示例使用@main属性为小部件扩展指定一个入口点。要支持多个小部件,声明一个符合WidgetBundle的结构,该结构在其body属性中将多个小部件分组在一起。在这个小部件包结构上添加@main属性,以告诉WidgetKit您的扩展支持多个小部件。

例如,如果游戏应用有第二个显示角色生命值的小部件和第三个显示排行榜的小部件,它就会将它们组合在一起,如下所示:

1
2
3
4
5
6
7
8
9
@main
struct GameWidgets: WidgetBundle {
@WidgetBundleBuilder
var body: some Widget {
GameStatusWidget()
CharacterDetailWidget()
LeaderboardWidget()
}
}

在Xcode中预览widget

Xcode允许你查看小部件的预览,而无需在模拟器或测试设备上运行应用程序。下面的示例显示了使用了 Building Widgets Using WidgetKit and SwiftUIsample code的表情符号管理员小部件的预览代码。注意它是如何使用widgetFamily环境值来避免为每个小部件手动指定名称的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Environment(\.widgetFamily) var family

Group {
EmojiRangerWidgetEntryView(entry: SimpleEntry(date: Date(), relevance: nil, character: .panda))
.previewContext(WidgetPreviewContext(family: .accessoryCircular))
.previewDisplayName("\(family)")
EmojiRangerWidgetEntryView(entry: SimpleEntry(date: Date(), relevance: nil, character: .panda))
.previewContext(WidgetPreviewContext(family: .accessoryRectangular))
.previewDisplayName("\(family)")
EmojiRangerWidgetEntryView(entry: SimpleEntry(date: Date(), relevance: nil, character: .panda))
.previewContext(WidgetPreviewContext(family: .accessoryInline))
.previewDisplayName("\(family)")
EmojiRangerWidgetEntryView(entry: SimpleEntry(date: Date(), relevance: nil, character: .panda))
.previewContext(WidgetPreviewContext(family: .systemSmall))
.previewDisplayName("\(family)")
EmojiRangerWidgetEntryView(entry: SimpleEntry(date: Date(), relevance: nil, character: .panda))
.previewContext(WidgetPreviewContext(family: .systemMedium))
.previewDisplayName("\(family)")
}

参考

Creating a Widget Extension