美文网首页
SwiftUI-App extensions

SwiftUI-App extensions

作者: xiaofu666 | 来源:发表于2023-03-02 10:53 被阅读0次

将应用程序的基本功能扩展到系统的其他部分,例如添加小部件。

使用SwiftUIWidgetKit将小部件添加到应用程序中。小部件可快速访问应用程序的相关内容。定义符合Widget协议的结构,并声明小部件的视图层次结构。像配置其他SwiftUI视图一样配置小部件内的视图,使用视图修饰符,包括一些特定于小部件的修饰符。

创建小部件

使用WidgetKit和SwiftUI构建小部件

protocol Widget
要显示在主屏幕或通知中心的小部件的配置和内容。
protocol WidgetBundle
用于从单个小部件扩展中公开多个小部件的容器。
struct LimitedAvailabilityConfiguration
类型擦除的小部件配置。

给小部件贴标签

func widgetLabel<S>(S) -> some View
返回一个文本标签,该标签在配件系列小部件的主SwiftUI视图之外显示其他内容。
func widgetLabel(LocalizedStringKey) -> some View
返回一个本地化的文本标签,该标签在配件系列小部件的主SwiftUI视图之外显示其他内容。
func widgetLabel<Label>(label: () -> Label) -> some View
创建一个标签,用于在配件系列小部件的主SwiftUI视图之外显示其他内容。

控制重音组

func widgetAccentable(Bool) -> some View
将视图及其所有子视图添加到重音组中。

管理动态岛的安置

func dynamicIsland(verticalPlacement: DynamicIslandExpandedRegionVerticalPlacement) -> some View
指定动态岛中显示的扩展实时活动视图的垂直位置。

创建小部件扩展

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

小部件显示相关、一目了然的内容,让用户快速访问您的应用程序以获取更多详细信息。您的应用程序可以提供各种小部件,让用户专注于对他们来说最重要的信息。他们可以添加同一小部件的多个副本,并根据他们独特的需求和布局定制每个副本。如果您在小部件中包含自定义意图,用户还可以单独个性化每个小部件。小部件支持多种尺寸;选择最适合您应用程序内容的尺寸。由于空间有限,请确保您的小部件呈现人们最看重的信息。

为您的应用程序添加小部件目标
小部件扩展模板为创建小部件提供了一个起点。单个小部件扩展可以包含多个小部件。例如,体育应用程序可能有一个显示团队信息的小部件,另一个显示比赛时间表的小部件。单个小部件扩展可以包含两个小部件。

  1. Xcode中打开您的应用程序项目,然后选择文件>新建>目标。
  2. 从应用程序扩展组中,选择小部件扩展,然后单击下一步。
  3. 输入您的分机号名称。
  4. 如果小部件提供用户可配置的属性,请选中“包括配置意图”复选框。
  5. 点击完成。
image.png

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

添加配置详细信息

小部件扩展模板提供了符合Widget协议的初始小部件实现。此小部件的body属性决定了小部件是否具有用户可配置的属性。有两种配置可供选择:

  1. StaticConfiguration
    对于没有用户可配置属性的小部件。例如,显示一般市场信息的股市小部件,或显示趋势头条新闻的新闻小部件。
  2. IntentConfiguration
    对于具有用户可配置属性的小部件。使用SiriKit自定义意图来定义属性。例如,需要城市邮政编码的天气小部件,或需要跟踪号码的包裹跟踪小部件。

包括配置意图复选框决定了Xcode使用的配置。当您选中此复选框时,Xcode将使用意图配置;否则,它将使用静态配置。要初始化Intent,请向其初始化器提供以下信息:

kind
标识小部件的字符串。这是您选择的标识符,应该描述小部件代表什么。
provider
一个符合Timeline并生成时间线的对象,告诉WidgetKit何时渲染小部件。时间线包含您定义的自定义Timeline类型。时间线条目标识您希望WidgetKit更新小部件内容的日期。包括小部件视图需要在自定义类型中渲染的属性。
intent
定义用户可配置属性的自定义意图。有关添加自定义的更多信息,请参阅制作可配置小部件。
content
包含SwiftUI视图的闭包。WidgetKit调用此来呈现小部件的内容,从提供程序传递Timeline参数。

使用修饰符提供额外的配置详细信息,包括显示名称、描述和小部件支持的系列。以下代码显示了一个小部件,该小部件为游戏提供了通用的、不可配置的状态:

@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])
    }
}

小部件的提供商为小部件生成时间线,并在每个条目中包含游戏状态详细信息。当每个时间线条目的日期到达时,WidgetKit会调用content闭包来显示小部件的内容。最后,修饰符指定小部件库中显示的名称和描述,并允许用户选择小部件的小型、中型或大型版本。

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

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

提供时间线条目

时间线提供程序生成一个由时间线条目组成的时间线,每个条目指定更新小部件内容的日期和时间。游戏状态小部件可能会定义其时间线条目,以包含表示游戏状态的字符串,如下所示:

struct GameStatusEntry: TimelineEntry {
    var date: Date
    var gameStatus: String
}

要在小部件库中显示您的小部件,WidgetKit要求提供商提供预览快照。通过检查传递给getSnapshot(in:completion:)方法的context参数的is属性来识别此预览请求。当is为真时,WidgetKit会在小部件库中显示您的小部件。作为回应,您需要快速创建预览快照。如果您的小部件需要需要时间从服务器生成或获取的资产或信息,请改用示例数据。

在以下代码中,如果尚未从服务器获取状态,游戏状态小部件的提供程序通过显示空状态来实现快照方法:

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分钟内请求新的时间线:

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设置为placeholder。此设置以适合用作占位符的方式自动呈现小部件的视图。要选择退出作为占位符的渲染,并控制哪些小部件的视图显示已编辑以隐藏敏感信息,请在redacted(reason:)回调中使用theunredactedunredacted()视图修饰符。

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

  • NSFileProtectionComplete设备被锁定了
  • NSFileProtectionCompleteUnlessOpen设备被锁定了
  • NSFileProtectionCompleteUntilFirstUserAuthentication用户尚未进行身份验证

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

watchOS中,设备在使用期间很少被锁定,因为Apple Watch通常在用户佩戴时解锁。但是,用户可以通过选择设置>显示和亮度>始终打开>隐藏敏感复杂功能来配置是否在始终打开期间显示敏感数据。如果用户启用了隐藏敏感复杂功能,WidgetKit将呈现您配置的编辑或返回占位符。他们可以选择显示所有或单个并发症的编辑内容。同样,在iOS中,设备也可能支持“始终打开”,用户可以配置是否在“始终打开”期间显示敏感数据。在不支持“始终打开”的iOS设备上,用户通过选择“设置”>“面容 ID和密码”以及停用锁定屏幕小部件的数据访问来控制是否在锁定屏幕上显示敏感数据。

在小部件中显示内容

小部件使用SwiftUI视图定义其内容,通常通过编写其他SwiftUI视图。如添加配置详细信息部分所示,小部件的配置包含WidgetKit调用以呈现小部件内容的闭包。

当用户从小部件库中添加您的小部件时,他们会从小部件支持的系列中选择特定的系列(例如,小型或中型)。小部件的内容闭包必须能够渲染小部件支持的每个家庭。WidgetKitSwiftUI环境中设置相应的系列和其他属性,例如配色方案(浅色或深色)。

在上面显示的游戏状态小部件的配置中,内容关闭使用Game来显示状态。由于小部件支持锁定屏幕上的大小和配件小部件,因此它使用widget来决定要显示的特定SwiftUI视图,如下所示:

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()
        }
    }
}

对于小家庭,小部件使用一个视图,显示游戏中轮到谁的简单摘要。对于媒体,它显示状态,表示最后一圈的结果。对于大型和超大型,由于有更多的可用空间,它显示每个玩家的运行统计信息。配件小部件比主屏幕上显示的小部件小得多。因此,它们显示了循环配件小部件当前所选字符的健康级别。由于矩形和内联配件小部件允许更多文本,它们显示角色的健康水平和剩余的愈合时间。如果家庭是未知类型,它会显示默认视图,这表明游戏状态不可用。

笔记
视图使用@View声明其主体,因为它使用的视图类型各不相同。

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

重要
小部件提供只读信息,不支持滚动元素或开关等交互式元素。WidgetKit在渲染小部件的内容时省略了交互式元素。

将动态内容添加到您的小部件中

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

响应用户互动

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

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

  • 对于使用Widget.systemWidget.systemWidget.system的小部件,请将一个或多个Link控件添加到小部件的视图层次结构中。您可以使用widgetLink控件。如果交互针对Link控件,系统将使用该控件中的URL。对于小部件中其他任何地方的交互,系统使用widgetURL视图修饰符中指定的URL

例如,在游戏中显示单个角色详细信息的小部件可以使用widget打开该应用程序以显示该角色的详细信息。

@ViewBuilder
var body: some View {
    ZStack {
        AvatarView(entry.character)
            .widgetURL(entry.character.url)
            .foregroundColor(.white)
    }
    .background(Color.gameBackground)
}

如果小部件显示字符列表,则列表中的每个项目都可以在Link控件中。每个Link控件指定其显示的特定字符的URL。

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

如果小部件不使用widgetLink控件,系统会激活包含的应用程序,并将NSUser传递给onActivity(_:perform:)application(_:continue:restorationHandler:)application(_:continue:restorationHandler:)用户活动的user字典包含有关用户交互的小部件的详细信息。使用Widget.User中的键从Swift代码中访问这些值。要从Objective-C访问user值,请改用WGWidgetWGWidget键。

对于使用Intent的小部件,用户活动的interaction属性包含小部件的INIntent

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

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

例如,如果游戏应用程序有第二个小部件来显示角色健康状况,第三个小部件来显示排行榜,它将按如下所示将它们分组:

@main
struct GameWidgets: WidgetBundle {
    @WidgetBundleBuilder
    var body: some Widget {
        GameStatusWidget()
        CharacterDetailWidget()
        LeaderboardWidget()
    }
}

在Xcode中预览小部件

Xcode允许您查看小部件的预览,而无需在模拟器或测试设备上运行应用程序。以下示例显示了使用WidgetKit和SwiftUI示例代码项目构建小部件的表情符号游侠小部件的预览代码。注意它如何使用widgetFamily环境值来避免手动为每个小部件指定名称。

@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)")
}

使小部件保持最新状态

计划小部件的时间线,使用动态视图显示及时的相关信息,并在事情发生变化时更新时间线。

小部件使用SwiftUI视图来显示其内容。WidgetKit代表您在一个单独的进程中呈现视图。因此,即使小部件在屏幕上,您的小部件扩展也不会持续激活。尽管您的小部件并不总是处于活动状态,但有几种方法可以使其内容保持最新状态。

计划在预算内重新加载

重新加载小部件会消耗系统资源,并因额外的网络和处理而导致电池耗尽。为了减少这种性能影响并保持全天电池寿命,请将您请求的更新频率和数量限制在必要时。
为了管理系统负载,WidgetKit使用预算在一天中分发小部件重新加载。预算分配是动态的,并考虑了许多因素,包括:

  • 用户可以看到小部件的频率和时间。
  • 小部件的最后一次重新加载时间。
  • 小部件包含的应用程序是否处于活动状态。

WidgetKit为用户添加到设备中的每个活动小部件维护不同的预算。例如,如果用户添加了两个可配置的体育小部件实例,显示两个不同团队的信息,则每个小部件都有自己的预算。

小部件的预算适用于24小时。WidgetKit根据用户的日常使用模式调整24小时窗口,这意味着每日预算不一定在午夜重置。对于用户经常查看的小部件,每日预算通常包括40到70次刷新。这个速率大致相当于每15到60分钟重新加载一次小部件,但由于涉及许多因素,这些间隔通常会有所不同。

笔记
系统需要几天时间来了解用户的行为。在此学习期间,您的小部件可能会收到比正常情况下更多的重新加载。

WidgetKit不将重新加载计入小部件预算的情况包括:

  • 小部件包含的应用程序位于前台。
  • 小部件包含的应用程序具有活跃的音频或导航会话。
  • 系统区域设置更改。
  • 动态类型或辅助功能设置更改。

对于系统外观更改或系统区域设置更改等情况,请不要从应用程序请求重新加载时间线。系统会自动更新您的小部件。

虽然您的小部件时间线提供程序驱动您的重新加载计划,但WidgetKit有时会重新加载您的小部件,以帮助保持其内容的新鲜。一些常见的场景包括:

  • 如果小部件位于用户很少访问的主屏幕页面上,WidgetKit可能会减少该小部件重新加载的频率。稍后,当用户查看页面时,WidgetKit可能会在小部件可见时重新加载小部件。
  • 对于使用位置服务的小部件,WidgetKit会在发生重大位置更改后重新加载它们。

如果您的小部件可以预测它应该重新加载的时间点,最好的方法是为尽可能多的未来日期生成一个时间表。对于您显示的内容,尽可能保持时间线中的条目间隔。WidgetKit在重新加载小部件之前施加了最短的时间。您的时间表提供商应创建至少相隔约5分钟的时间线条目。WidgetKit可能会在多个小部件之间合并重新加载,影响小部件重新加载的确切时间。

为可预测事件生成时间线

许多小部件都有可预测的时间点,更新其内容是有意义的。例如,显示天气信息的小部件可能会全天每小时更新温度。股市小部件可以在开放市场时间经常更新其内容,但在周末根本无法更新。通过提前计划这些时间,WidgetKit会在适当时间到达时自动刷新您的小部件。

当您定义小部件时,您实现了自定义TimelineProviderWidgetKit从您的提供商那里获取时间线,并使用它来跟踪何时更新您的小部件。时间线是Timeline对象的数组。时间线中的每个条目都有一个日期和时间,以及小部件显示其视图所需的其他信息。除了时间线条目外,时间线还指定了一个刷新策略,告诉WidgetKit何时请求新的时间线。

以下是显示角色健康水平的游戏小部件示例。当健康水平低于100%时,角色以每小时25%的速度恢复。例如,当角色的健康水平为25%时,需要3个小时才能完全恢复到100%。下图显示了WidgetKit如何从提供商请求时间线,在时间线条目中指定的每个时间渲染小部件。

image.png

WidgetKit最初请求时间线时,提供商会创建一个包含四个条目的时间表。第一个条目代表当前时间,然后是每小时三个条目。将刷新策略设置为默认的atWidgetKit会在时间线条目的最后日期之后请求新的时间线。当时间线中的每个日期到达时,WidgetKit会调用小部件的内容闭包并显示结果。最后一个时间线条目通过后,WidgetKit通过向提供商询问新的时间线来重复该过程。由于角色的健康状况已达到100%,提供商会响应当前时间的单个条目,刷新策略设置为never。使用此设置,在应用程序使用Widget告诉WidgetKit请求新的时间线之前,WidgetKit不会要求另一个时间线。

除了atnever刷新策略外,如果时间线在到达条目结束之前或之后可能会发生变化,提供商可以指定完全不同的日期。例如,如果龙将在2小时内出现,向角色挑战战斗,提供者会将重新加载策略设置为after(_:)在未来2小时内传递一个日期。下图显示了WidgetKit在2小时标记渲染小部件后如何请求一个新的小部件。

image.png

由于与龙的战斗,角色的治疗将需要额外的2个小时才能达到100%。新的时间表由两个条目组成,一个是当前时间,第二个条目是未来2小时。时间线指定了刷新策略的End,表示没有更多已知事件可能会改变时间线。

当2小时过去,角色的健康状况达到100%时,WidgetKit会要求提供商提供新的时间表。由于角色的健康状况已恢复,提供商生成与上面第一张图表相同的最终时间表。当用户玩游戏并且角色的健康级别发生变化时,该应用程序使用Widget让WidgetKit刷新时间线并更新小部件。

除了在时间线结束前指定日期外,提供商还可以在时间线结束后指定日期。当您知道小部件的状态要到以后才会改变时,这很有用。例如,股市小部件可以在周五市场收盘时创建一个时间表,并使用afterDate()刷新策略指定周一开盘时间。由于股市周末关闭,在市场开盘之前无需更新小部件。

重要
如果您的小部件在重新加载并使用afterDate()向服务器发出请求,请提前计划,并在时间线条目中使用特定日期。WidgetKit试图尊重您指定的日期,当多个设备大约在同一时间重新加载您的小部件时,这可能会导致服务器负载显著增加。

当时间线发生变化时通知WidgetKit

当有东西影响小部件的当前时间线时,您的应用程序可以告诉WidgetKit请求新的时间线。在上面的游戏小部件示例中,如果应用程序收到推送通知,表明队友已给角色服用治疗药水,该应用程序可以告诉WidgetKit重新加载时间线并更新小部件的内容。要重新加载特定类型的小部件,您的应用程序使用Widget,如下所示:

WidgetCenter.shared.reloadTimelines(ofKind: "com.mygame.character-detail")

kind参数包含与用于创建小部件Widget的值相同的字符串。

如果您的小部件具有用户可配置的属性,请使用WidgetCenter验证是否存在具有适当设置的小部件,以避免不必要的重新加载。例如,当游戏收到关于角色收到治疗药水的推送通知时,它会在重新加载时间线之前验证小部件是否显示该角色。

在以下代码中,应用程序调用getConfigurations(_:)来检索用户配置的小部件列表。然后,它迭代生成的Widget对象,以找到一个具有配置为接受治疗药水的字符的intent。如果它找到一个,该应用程序会为该小部件的kind调用reloadTimelines(ofKind:)

WidgetCenter.shared.getCurrentConfigurations { result in
    guard case .success(let widgets) = result else { return }

    // Iterate over the WidgetInfo elements to find one that matches
    // the character from the push notification.
    if let widget = widgets.first(
        where: { widget in
            let intent = widget.configuration as? SelectCharacterIntent
            return intent?.character == characterThatReceivedHealingPotion
        }
    ) {
        WidgetCenter.shared.reloadTimelines(ofKind: widget.kind)
    }
}

如果您的应用程序使用Widget来支持多个小部件,您可以使用Widget重新加载所有小部件的时间线。例如,如果您的小部件要求用户登录帐户,但它们已注销,您可以通过调用以下方式重新加载所有小部件:

WidgetCenter.shared.reloadAllTimelines()

显示动态日期

即使您的小部件不会持续运行,它也可以显示WidgetKit实时更新的基于时间的信息。例如,它可能会显示一个倒数计时计时器,即使您的小部件扩展没有运行,它也会继续倒计时。有关更多信息,请参阅在小部件中显示动态日期。

后台网络请求完成后更新

当您的小部件扩展处于活动状态时,例如在提供snapshottimeline时,它可以启动后台网络请求。例如,获取队友当前状态的游戏小部件,或带有图像缩略图获取标题的新闻小部件。发出异步后台网络请求可以让您快速将控制权返回到系统,从而降低因响应时间过长而被终止的风险。
该过程类似于应用程序处理此类请求的方式,这在后台下载文件中进行了描述。WidgetKit不会恢复您的应用程序,而是直接激活小部件的扩展。要处理网络请求的结果,请使用onBackgroundEvents(matching:_:)修饰符进行小部件的配置,并执行以下操作:

  • 存储对completion参数的引用。您在处理所有网络事件后调用完成处理程序。
  • 使用标识identifier参数查找您在发起后台请求时使用的URLSession对象。如果您的小部件扩展被终止,请使用标识符重新创建URLSession

调用onBackgroundEvents()后,系统调用您提供给URLSession的urlSession(_:downloadTask:didTo:)方法。当所有事件都已交付后,系统会调用委托的urlEvents(forURLSession:)方法。

要在网络请求完成后刷新小部件的时间线,请从委托的url实现中调用Widget方法。完成处理事件后,调用您之前存储在onBackgroundEvents()中的completion程序。

制作可配置的小部件

通过向您的项目添加自定义SiriKit意图定义,为用户提供自定义小部件的选项。

为了使用户能够轻松访问最相关的信息,小部件可以提供可定制的属性。例如,用户可以为股票报价小部件选择特定股票,或为包裹交付小部件输入跟踪编号。小部件通过使用自定义意图定义来定义可自定义属性,这与Siri建议和Siri快捷方式用于自定义这些交互的机制相同。

要将可配置属性添加到您的小部件中:

  1. 添加自定义意图定义,为您的Xcode项目定义可配置属性。
  2. 在小部件中使用Intent将用户的选择合并到您的时间线条目中。
  3. 如果属性依赖于动态数据,请实现意图扩展。

如果您的应用程序已经支持Siri建议或Siri快捷方式,并且您有自定义意图,那么您可能已经完成了大部分工作。否则,请考虑利用您为小部件所做的工作来添加对Siri建议或Siri快捷方式的支持。

以下部分展示了如何将可配置属性添加到显示游戏中角色信息的小部件中。

为您的项目添加自定义意图定义

在Xcode项目中,选择文件>新建文件,然后选择SiriKit意图定义文件。单击下一步,并在出现提示时保存文件。Xcode创建一个新的.intentdefinition文件,并将其添加到您的项目中。

image.png

Xcode从意图定义文件生成代码。要在目标中使用此代码:

  • 将意图定义文件作为目标的成员。
  • 通过将意图的类名添加到目标属性的“支持的意图”部分,指示要包含在目标中的具体意图。

如果您在框架中包含意图定义文件,您还必须将其包含在包含应用程序的目标中。在这种情况下,为了避免在应用程序和框架中重复类型,请在文件检查器的目标成员部分中为应用程序目标选择无生成类。
要添加和配置允许用户在游戏中选择角色的自定义意图:

  1. 在项目导航器中,选择意图文件。Xcode显示一个空的意图定义编辑器。
  2. 选择编辑器>新意图,然后在自定义意图下选择意图。
  3. 将自定义意图的名称更改为Select。请注意,属性检查器的自定义类字段显示您在代码中引用意图时使用的类名。在这种情况下,它是Select。
  4. 将类别设置为查看,然后选择“意图符合小部件的条件”复选框,以指示小部件可以使用意图。
  5. 在参数下,添加一个带有名称character的新参数,这是小部件的可配置设置。
image.png

添加参数后,为它配置详细信息。如果参数为用户提供静态选择列表,请选择添加枚举菜单项以创建静态枚举。例如,如果参数指定了字符的头像,并且可能的头像列表是一个不变的常量集,您可以使用静态枚举来指定意图定义文件中的可用选项。如果可能的头像列表可以变化或动态生成,请改用带有动态选项的类型。

在本例中,character属性依赖于应用程序中可用的字符动态列表。要提供动态数据,请创建一个新类型:

  1. 从类型弹出式菜单中,选择添加类型。Xcode在编辑器的类型部分添加了一种新类型。
  2. 将类型名称更改为Game。
  3. 添加一个新的name属性,然后从类型弹出式菜单中选择字符串。
  4. 选择Select意图。
  5. 在意图编辑器中,选择“选项是动态提供的”复选框,以指示您的代码为此参数提供了动态项目列表。
image.png

Game类型描述了用户可以选择的角色。在下一节中,您将添加代码以动态提供字符列表。

笔记
意图中参数的顺序决定了用户编辑小部件时它们出现的顺序。您可以通过拖动来重新排列列表中的项目。

为您的项目添加意图扩展

为了动态提供字符列表,您将向应用程序添加Intents扩展。当用户编辑小部件时,WidgetKit会加载Intents扩展以提供动态信息。要添加意图扩展:

  1. 选择文件>新建>目标,然后选择意图扩展名。
  2. 点击下一步。
  3. 为您的Intents扩展输入一个名称,并将起点设置为None。
  4. 点击完成。如果Xcode提示您激活新方案,请单击激活。
  5. 在新目标属性的常规选项卡中,在“支持的意图”部分中添加一个条目,并将类名称设置为Select。
  6. 在项目导航器中,选择您之前添加的自定义意图定义文件。
  7. 使用文件检查器将定义文件添加到意图扩展目标中。

重要
在文件检查器中,验证包含的应用程序、小部件扩展和意图扩展是否都包含意图定义文件。

实现一个意图处理程序来提供动态值

当用户编辑具有提供动态值的自定义意图的小部件时,系统需要一个对象来提供这些值。它通过要求Intents扩展为Intents提供处理程序来识别此对象。当Xcode创建Intents扩展时,它会向您的项目添加一个名为Intent.swift的文件,其中包含一个名为Intent的类。该类包含一个返回处理程序的方法。您将扩展此处理程序,以提供小部件自定义的值。

基于自定义意图定义文件,Xcode生成一个协议Select,处理程序必须符合该协议。将此一致性添加到Intent类的声明中。(要查看此协议的详细信息以及Xcode自动生成的其他类型,请选择Select,然后选择导航>跳转到定义。)

class IntentHandler: INExtension, SelectCharacterIntentHandling {
    ...
}

当处理程序提供动态选项时,它必须实现一个名为provide[Type]OptionalCollection(for:with:)的方法,其中[Type]是来自意图定义文件的类型名称。如果缺少此方法,Xcode将报告构建错误,提供修复程序以添加协议存根。构建您的项目,并使用修复程序添加此存根。

此方法包括您调用的完成处理程序,传递一个INObjectCollection<GameCharacter>。注意Game类型;这是意图定义文件中的自定义类型。Xcode生成代码来定义它如下:

public class GameCharacter: INObject {
    @available(iOS 13.0, macOS 11.0, watchOS 6.0, *)
    @NSManaged public var name: String?
}

请注意name属性,它也来自您添加的自定义类型的意图定义文件。
要实现provideCharacterCollection(for:with:)方法,小部件使用游戏项目中存在的结构。此结构定义了可用字符及其详细信息的列表,如下所示:

struct CharacterDetail {
    let name: String
    let avatar: String
    let healthLevel: Double
    let heroType: String

    static let availableCharacters = [
        CharacterDetail(name: "Power Panda", avatar: "🐼", healthLevel: 0.14, heroType: "Forest Dweller"),
        CharacterDetail(name: "Unipony", avatar: "🦄", healthLevel: 0.67, heroType: "Free Rangers"),
        CharacterDetail(name: "Spouty", avatar: "🐳", healthLevel: 0.83, heroType: "Deep Sea Goer")
    ]
}

在意图处理程序中,代码迭代availableCharacters数组,为每个字符创建一个Game对象。为了简单起见,Game的身份是角色的名字。游戏角色数组被放入INObject中,处理程序将集合传递给完成处理程序。

class IntentHandler: INExtension, SelectCharacterIntentHandling {
    func provideCharacterOptionsCollection(for intent: SelectCharacterIntent, with completion: @escaping (INObjectCollection<GameCharacter>?, Error?) -> Void) {

        // Iterate the available characters, creating
        // a GameCharacter for each one.
        let characters: [GameCharacter] = CharacterDetail.availableCharacters.map { character in
            let gameCharacter = GameCharacter(
                identifier: character.name,
                display: character.name
            )
            gameCharacter.name = character.name
            return gameCharacter
        }

        // Create a collection with the array of characters.
        let collection = INObjectCollection(items: characters)

        // Call the completion handler, passing the collection.
        completion(collection, nil)
    }
}

完成意图定义文件的配置,并将意图扩展添加到应用程序中,用户可以编辑小部件以选择要显示的特定字符。WidgetKit使用意图定义文件中的信息自动创建用于编辑小部件的用户界面。
一旦用户编辑了小部件并选择了一个字符,下一步就是将该选择纳入小部件的显示中。

处理用户自定义值

为了支持可配置的属性,小部件使用Intent配置。例如,字符详细信息小部件定义其配置如下:

struct CharacterDetailWidget: Widget {
    var body: some WidgetConfiguration {
        IntentConfiguration(
            kind: "com.mygame.character-detail",
            intent: SelectCharacterIntent.self,
            provider: CharacterDetailProvider(),
        ) { entry in
            CharacterDetailView(entry: entry)
        }
        .configurationDisplayName("Character Details")
        .description("Displays a character's health and other details")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}

Select参数决定了小部件的用户可自定义属性。该配置使用Character来管理小部件的时间线事件。有关时间线提供商的更多信息,请参阅保持小部件最新。

用户编辑小部件后,WidgetKit在请求时间线条目时将用户自定义值传递给提供商。您通常在提供商生成的时间线条目中包含来自意图的相关详细信息。在本例中,提供程序使用助手方法使用意图中的字符名称查找CharacterDetail,然后创建一个包含字符详细信息的条目的时间线:

struct CharacterDetailProvider: IntentTimelineProvider {
    func getTimeline(for configuration: SelectCharacterIntent, in context: Context, completion: @escaping (Timeline<CharacterDetailEntry>) -> Void) {
        // Access the customized properties of the intent.
        let characterDetail = lookupCharacterDetail(for: configuration.character.name)

        // Construct a timeline entry for the current date, and include the character details.
        let entry = CharacterDetailEntry(date: Date(), detail: characterDetail)

        // Create the timeline and call the completion handler. The .never reload 
        // policy indicates that the containing app will use WidgetCenter methods 
        // to reload the widget's timeline when the details change.
        let timeline = Timeline(entries: [entry], policy: .never)
        completion(timeline)
    }
}

当您在时间线条目中包含用户自定义值时,您的小部件可以显示适当的内容。

在Apple Watch上提供预配置的复杂功能

watchOS 9iOS 16开始,您可以使用WidgetKit实现在Apple Watch上显示为复杂功能的配件系列小部件。与iOSmacOS中的小部件一样,watch复杂功能使用自定义意图来显示用户可配置的数据,在watchOS中实现可配置小部件的工作原理与iOSmacOS相同。然而,watchOS没有提供用于配置复杂功能的专用用户界面。要在手表复杂功能中显示与用户最相关的数据,您可以创建预配置的复杂功能,并在可用复杂功能列表中向用户推荐它们。在您的Timeline代码中,实现recommendations()并返回您使用自定义意图创建的Intent对象。

当您的应用程序收到与您推荐的小部件配置相关的新数据时,请调用 invalidateConfigurationRecommendations(),使现已过时的建议无效。这告诉WidgetKit获取新的推荐的预配置。当您使推荐的配置无效时,请确保在recommendations()回调中返回更新的Intent对象。

相关文章

网友评论

      本文标题:SwiftUI-App extensions

      本文链接:https://www.haomeiwen.com/subject/vvklldtx.html