阅读 1143

SwiftUI WidgetKit 官方文档翻译

SwiftUI WidgetKit 苹果官方文档中文翻译

  • 原文档参见 Apple Document
  • 使用 DeepL 机翻后人工校验
    • 其中 Widget = 小组件,部分手动更改,部分还是以“小组件”命名
    • 第二篇文章有些绕,但确实可以做到😂
  • GitHub地址

补充说明

  • Apple的官方文档的层级有很多层,现在暂时没有好的解决方案,如果有需要请Clone到本地使用全局搜索
  • 文件名以官方英文名开头(中文翻译结束).md 便于搜索
  • 烦请看到的大佬给小弟点个Star……
    • 正在找工作(._.) 有些Star有些底气😂

以下放出三篇文档(Article),其余API等详见GitHub仓库

1. 创建一个小部件扩展

添加并配置一个扩展,以在iOS主屏幕和macOS通知中心上显示你的应用程序的内容。

概述

Widget可以直观的显示相关内容,让用户快速进入您的应用了解更多细节。您的应用程序可以提供多种小部件,让用户专注于对他们最重要的信息。

用户可以添加同一小部件的多个副本,根据他们的独特需求和布局来定制每个小部件。如果您允许Widget的自定义功能,用户可以单独定制每个Widget。

Widget支持多种尺寸;您可以选择最适合您的应用程序内容的尺寸。然而由于可用空间有限,请让您的Widget专注于呈现人们最重视的信息。

将Widget添加到您的应用程序中仅仅需要少量的设置。Widget使用 SwiftUI 视图显示其内容。更多信息,请参阅SwiftUI。

在您的应用程序中添加一个小部件

小部件扩展模板可以方便您快速创建一个Widget。一个小部件扩展可以包含多种小部件。

例如,一个体育应用可能有一个显示球队信息的Widget和另一个显示比赛时间表的Widget。单个Widget扩展可以包含这两种Widget。虽然建议将所有的widget包含在一个widget扩展中,但如果有必要,您可以添加多个扩展。

  1. 在Xcode中打开您的应用程序项目,然后选择文件>新建>目标。
  2. 从应用程序扩展组中,选择Widget扩展,然后单击 "下一步"。
  3. 输入扩展名的名称。
  4. 如果小部件提供了用户可配置的属性,请选中 "包含配置意图 "「Include Configuration Intent checkbox」复选框。
  5. 单击 "完成"。

WidgetKit-Add-Widget-Extension@2x

添加配置信息

小部件扩展模板提供了一个符合小部件协议的初始小部件实现。

该Widget的body属性决定了该Widget是否具有用户可配置的属性

有两种配置可供选择。

  • StaticConfiguration: 对于一个没有用户可配置属性的Widget。「例如,显示一般市场信息的股票市场Widget,或显示趋势标题的新闻Widget。」

  • IntentConfiguration。对于一个具有用户可配置属性的Widget来说,你可以使用SiriKit自定义意图来定义属性。您使用 SiriKit 自定义意图来定义属性。「例如,一个天气Widget需要一个城市的邮政编码或邮政编码,或者一个包裹跟踪Widget需要一个跟踪号码。」

包含配置意图「Include Configuration Intent」复选框决定了Xcode使用哪种配置。当您选择这个复选框时,Xcode使用将使用默认设置进行配置;

否则,它使用静态配置。要初始化配置,请提供以下信息。

  • kind。识别Widget的字符串。这是您选择的标识符,应描述Widget所代表的内容。
  • Provider:符合TimelineProvider的对象。一个符合TimelineProvider的对象,它能产生一个时间线,告诉WidgetKit何时渲染Widget。时间线包含一个你定义的自定义TimelineEntry类型。时间线条目标识了你希望WidgetKit更新Widget内容的日期。在自定义类型中包含你的Widget的视图需要渲染的属性。
  • Placeholder。一个 SwiftUI 视图,WidgetKit 用来在第一次渲染Widget。占位符是您的Widget的通用表示,没有特定的配置或数据。
  • Content Closure(内容闭合)。一个包含SwiftUI视图的封闭。WidgetKit调用它来渲染Widget的内容,从提供者那里传递一个TimelineEntry参数。
  • Custom Intent(自定义配置)。一个定义用户可配置属性的自定义意图。有关添加自定义的更多信息,请参见制作可配置的Widget。

使用修饰符来提供额外的配置细节,包括显示名称(name)、描述(description)和Widget支持的系列(families)。

以下代码显示了一个通用的、不可配置的状态的游戏Widget。

@main // 声明为主要部件
struct GameStatusWidget: Widget { // 声明为Widget // 而不是 some view
    var body: some WidgetConfiguration { // widget配置项
        StaticConfiguration( // 静态配置 StaticConfiguration
            kind: "com.mygame.game-status", // 唯一标识符
            provider: GameStatusProvider(), // 时间线提供者
            placeholder: GameStatusPlaceholderView() // placeholder
        ) { entry in // 显示的内容
            GameStatusView(entry.gameStatus)
        }
        .configurationDisplayName("Game Status") // 显示的名称
        .description("Shows an overview of your game status") // 描述
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) // 支持的widget大小
    }
}
复制代码

在这个例子中,Widget使用GameStatusPlaceholder作为占位符视图,GameStatusView作为内容封闭。

占位符视图显示您Widget的通用表示,让用户对Widget显示的内容有一个大致的了解。不要在占位符视图中包含实际数据「例如,使用灰色框来表示文本行,或使用灰色圆圈来表示图像。」「骨架图」

Provider为Widget生成一个时间线,并在每个条目中包含游戏状态细节。当每个时间线条目的日期到来时,WidgetKit会调用内容封闭来显示小组件的内容。最后,修改器指定小部件图库中显示的名称和描述,并允许用户选择小部件的小、中、大版本。

请注意 @main 属性在此小组件上的用法。此属性表示 GameStatusWidget 是小组件扩展的入口点,意味着扩展包含一个小组件。要支持多个小组件,请参见在您的应用程序扩展中声明多个小组件。

Provider 时间线配置(Provide Timeline Entries)

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

struct GameStatusEntry: TimelineEntry {
    var date: Date
    var gameStatus: String
}
复制代码

为了在小组件图库中显示你的小组件,WidgetKit会要求提供者提供一个预览快照。你可以通过检查传递给 snapshot(for:with:completion:)方法的上下文参数的 isPreview 属性来识别这个预览请求。当isPreview为真时,WidgetKit会在小组件库中显示你的小组件。作为回应,你需要快速创建预览快照。如果你的widget需要花时间从服务器生成或获取的资产或信息,请使用样本数据代替。

在下面的代码中,当游戏状态部件还没有从服务器获取状态时,Provider通过显示一个空状态来实现snapshot方法。

struct GameStatusProvider: TimelineProvider {
    var hasFetchedGameStatus: Bool
    var gameStatusFromServer: String

    func snapshot(with context: Context, completion: @escaping (Entry) -> ()) {
        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调用timeline(for:with:completion:)向提供者请求一个常规的时间线。时间线由一个或多个时间线条目和一个重载策略组成,告知WidgetKit何时请求后续时间线。 下面的例子显示了游戏状态部件的Provider如何生成一个时间线,该时间线由一个单一条目组成,其中包含来自服务器的当前游戏状态,以及一个在15分钟内请求新时间线的重载策略。

struct GameStatusProvider: TimelineProvider {
    func timeline(with context: Context, completion: @escaping (Timeline<GameStatusEntry>) -> ()) {
        // 为 "现在 now"创建一个时间轴条目。
        let date = Date()
        let entry = GameStatusEntry(
            date: date,
            gameStatus: gameStatusFromServer
        )

        // 创建一个未来15分钟的日期。
        let nextUpdateDate = Calendar.current.date(byAdding: .minute, value: 15, to: date)!

        // 创建带有条目的时间线 & 带有日期的重载策略。
        // 以便下次更新。
        let timeline = Timeline(
            entries:[entry],
            policy: .after(nextUpdateDate)
        )

        // 调用completion将时间线传递给WidgetKit。
        completion(timeline)
    }
}
复制代码

在这个例子中,如果Widget没有来自服务器的当前状态,它可以存储对completion的引用也是说不再更新了?,向服务器执行异步请求以获取游戏状态,并在该请求完成时调用completion。 关于生成时间线的更多细节,包括在小组件中处理网络请求,请参见《让小组件保持最新状态》「Keeping a Widget Up To Date」。

在Widget中显示内容

Widget使用SwiftUI视图来定义它们的内容,通常是通过调用其他SwiftUI视图来实现。如上图所示,widget的配置包含WidgetKit调用的闭包来渲染widget的内容。 当用户从Widget库中添加你的Widget时,他们会从你的Widget支持的系列中选择特定的规格(小、中、大)。Widget的内容必须能够渲染Widget支持的每个规格。WidgetKit在SwiftUI环境中设置了相应的大小「Family」和附加属性,例如配色方案(亮色或暗色)。

在上图所示的游戏状态Widget的配置中,内容闭合使用GameStatusView来显示状态。由于widget支持所有三个widget规格,所以它使用widgetFamily来决定显示哪个特定的SwiftUI视图,如图所示。

struct GameStatusView : View {
    @Environment(\.widgetFamily) var family: WidgetFamily // 获取环境变量
    var gameStatus: GameStatus

    @ViewBuilder
    var body: some View {
        switch family {
        case .systemSmall: GameTurnSummary(gameStatus)
        case .systemMedium: GameStatusWithLastTurnResult(gameStatus)
        case .systemLarge: GameStatusWithStatistics(gameStatus)
        default: GameDetailsNotAvailable()
        }
    }
}
复制代码

对于规格为小的Widget来说,小部件使用的视图显示了一个简单的摘要,说明在游戏中轮到谁了。对于中等的,它显示的是状态,表示上一回合的结果。对于大户,因为有更多的空间,它可以显示每个玩家的运行统计。如果规格是未知类型,则显示默认视图,表示游戏状态不可用。

Note

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

对于可配置的widget,提供者符合IntentTimelineProvider。该提供者执行与TimelineProvider相同的功能,但它结合了用户在小组件上自定义的值。在传递给 snapshot(for:with:completion:)和 timeline(for:with:completion:)的配置参数中,这些自定义值可供 Intent Timeline 提供者使用。您通常将用户配置的值作为您的自定义时间线条目类型的属性包含在内,以供小组件的视图使用。

Important

小组件只显示只读信息,不支持互动元素,如滚动元素或开关。WidgetKit在渲染widget的内容时,会省略交互式元素。

当用户与你的widget交互时,WidgetKit会激活你的应用,并传递一个你指定的URL。当你的应用激活时,通过将用户带到相关位置来处理URL。

为Widget添加动态内容

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

在应用程序扩展中声明多个Widget

上面的GameStatusWidget示例使用@main属性为widget扩展指定了一个单一的入口点。要支持多个widget,请声明一个符合WidgetBundle的结构,该结构在其body属性中将多个widget分组。在这个widget bundle结构上添加@main属性来告诉WidgetKit你的扩展支持多个widget。 例如,如果游戏应用有第二个widget来显示角色的健康状况,第三个widget来显示排行榜,它就会像这里一样把它们分组。

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

2. 制作一个可配置的小部件

通过在您的项目中添加自定义SiriKit意图定义,让用户可以选择自定义他们的小部件。

概述

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

添加可配置的属性到您的widget中。

  1. 添加一个定义可配置属性的自定义意图定义到你的Xcode项目中。
  2. 在您的widget中使用IntentTimelineProvider来将用户的选择纳入您的时间线条目中。
  3. 如果属性依赖于动态数据,请实现一个Intents扩展。

如果您的应用程序已经支持 Siri 建议或 Siri 快捷方式,并且您有一个自定义意图,您可能已经完成了大部分工作。否则,请考虑利用您为小组件所做的工作来添加对 Siri 建议或 Siri 快捷方式的支持。有关如何充分利用意图的更多信息,请参阅 SiriKit。 下一节介绍如何给显示游戏中角色信息的小组件添加可配置属性。

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

在你的Xcode项目中,选择File > New File并选择SiriKit Intent Definition File。点击 "下一步 "并在提示时保存文件。Xcode创建一个新的.intenttdefinition文件,并将其添加到您的项目中。

WidgetKit-Add-Intent-Definition-File@2x

Xcode从意图定义「intent definition file」文件中生成代码。要在一个目标「target」中使用这些代码。

  • 将intent定义文件作为目标的一个成员。
  • 通过添加 intent 的类名到 target 属性的 Supported Intents 部分来指定要包含在 target 中的特定 intents。

如果您将 intent 定义文件包含在框架中,您还必须将其包含在包含应用程序的 target 中。在这种情况下,为了避免应用程序和框架中的类型重复,请在 "文件 "检查器的 "目标成员 "部分中为应用程序目标选择 "不生成类"。


要添加和配置一个自定义意图,让用户在游戏中选择一个角色。

  1. 在项目导航器中,选择意图文件。Xcode显示一个空的意图定义编辑器。
  2. 选择 Editor > New Intent 并选择 Custom Intents 下的 intent。
  3. 将自定义意图的名称改为SelectCharacter。请注意,"属性检查器 "的 "自定义类 "字段显示了您在代码中引用该意图时使用的类名。在本例中,它是SelectCharacterIntent。
  4. 将Category(类别)设置为View(视图),并选择 "Intent is eligible for widgets "「意图有资格成为小部件」复选框,以表明widgets可以使用该intent。
  5. 在 "Parameters "下,添加一个新参数,以字符串作为名称,这是小组件的可配置设置。
    WidgetKit-Configure-Custom-Intent@2x

添加一个参数后,为其配置细节。如果一个参数给用户提供了一个静态的选择列表,那么选择 "添加枚举 "菜单项来创建一个静态枚举。

例如,如果一个参数指定了一个角色的头像,而可能的头像列表是一个不变的集合,你可以使用一个静态枚举,在意图定义文件中指定可用的选择。

如果可能的头像列表可以变化,或者是动态生成的,可以使用带有动态选项的类型代替。

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

  1. 点击 "左下角 + 按钮",选择 "New Type"。Xcode在编辑器的Types部分添加一个新类型。

  2. 将该类型的名称改为GameCharacter。 添加一个新的name属性,并从类型弹出菜单中选择String。

  3. 选择 SelectCharacter intent。

  4. 在意图编辑器中,选择 "选项是动态提供的「Options are provided dynamically」 "复选框,以表明您的代码为该参数提供了一个动态项目列表。

WidgetKit-Add-Custom-Type@2x

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

Note

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


为您的项目添加一个Intent扩展

要动态地提供字符列表,你需要在你的应用程序中添加一个Intents扩展。当用户编辑widget时,WidgetKit会加载Intents扩展来提供动态信息。

添加一个 Intents 扩展。

  1. 选择File > New > Target 并选择 Intents Extension。

  2. 点击 "下一步"。

  3. 为你的Intents扩展名输入一个名字,并将Starting Point(起始点)设置为none。

  4. 单击 "Finish"。如果Xcode提示您激活新方案,请点击激活。

  5. 点击根对象,找到左侧边栏的Targets中刚新建的对象。在其"General"选项卡中,在 "Supported Intents"部分添加一个条目,并将 "Class Name"设置为"SelectCharacterIntent"。

  6. 在 "项目导航器 "中,选择您之前添加的自定义意图定义文件。

  7. 使用文件检查器「File inspector」将Intent文件添加到意图扩展目标中。

Important

在文件检查器中,验证包含的应用程序、小组件扩展名和意图扩展名是否都包含意图Intent Definition文件。

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

当用户使用提供动态值的自定义意图编辑小组件时,系统需要一个对象来提供这些值。它通过要求Intents扩展为意图提供一个处理程序来识别这个对象。

当Xcode创建Intents扩展时,它向您的项目添加了一个名为IntentHandler.swift的文件,其中包含一个名为IntentHandler的类。这个类包含一个返回处理程序的方法。您将扩展该处理程序,为小组件的自定义提供值。

基于自定义的意图定义文件,Xcode会生成一个协议,即SelectCharacterIntentHandling,该处理程序必须符合这个协议。将这个符合性添加到IntentHandler类的声明中。(要查看这个协议的细节,以及Xcode自动生成的其他类型,请选择SelectCharacterIntentHandling并选择Navigate > Jump to Definition)。

class IntentHandler: INExtension, SelectCharacterIntentHandling {
    ...
}
复制代码

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

构建您的项目,并使用fix-it来添加这个存根。 该方法包括一个您调用的完成处理程序,传递一个INObjectCollection。请注意GameCharacter类型;这是意图定义文件中的自定义类型。Xcode生成定义它的代码如下。

public class GameCharacter: INObject {
    @available(iOS 13.0, macOS 10.16, watchOS 6.0, *)
    @NSManaged public var name: String?
}
复制代码

请注意name属性,它也来自于你添加的自定义类型的意图定义文件。 为了实现provideCharacterOptionsCollection(for:with:)方法,widget使用一个存在于游戏项目中的结构。这个结构定义了一个可用角色的列表和它们的详细信息,如下所示。

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数组,为每个角色创建一个GameCharacter对象。为了简单起见,GameCharacter的标识是角色的名字。游戏角色数组被放入一个INObjectCollection中,处理程序将该集合传递给完成处理程序。

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

        // 迭代可用的字符
        // 每个都有一个GameCharacter。
        let characters: [GameCharacter] = CharacterDetail.availableCharacters.map { character in
            let gameCharacter = GameCharacter(
                identifier: character.name,
                display: character.name
            )
            gameCharacter.name = character.name
            return gameCharacter
        }

        // 用字符数组创建一个集合。
        let collection = INObjectCollection(items: characters)

        // 调用完成处理程序,传递集合。
        completion(collection, nil)
    }
}
复制代码

完成了意图定义文件的配置,并将意图扩展添加到应用程序中,用户就可以编辑小组件来选择要显示的特定字符。WidgetKit使用意图定义文件中的信息自动创建编辑小组件的用户界面。

一旦用户编辑小组件并选择了一个字符,下一步就是将该选择纳入小组件的显示中。

处理用户自定义的值

为了支持可配置的属性,小组件使用 IntentTimelineProvider 配置。例如,Character-detail小组件定义其配置如下。

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

SelectCharacterIntent参数决定了小组件的用户可定制属性。

配置使用 CharacterDetailProvider 来管理小组件的时间线事件。有关时间线提供者的更多信息,请参见 "保持 Widget 更新"。

在用户编辑小组件后,WidgetKit会在请求时间线条目时将用户自定义的值传递给提供者。您通常会在提供者生成的时间线条目中包含来自意图的相关细节。

在这个例子中,提供者使用一个辅助方法,使用意图中的角色名称来查找CharacterDetail,然后创建一个包含该角色细节的条目的时间线。

struct CharacterDetailProvider: IntentTimelineProvider {
    func timeline(for configuration: SelectCharacterIntent, with context: Context, completion: @escaping (Timeline<CharacterDetailEntry>) -> ()) {
        // 访问 intent 的自定义属性
        let characterDetail = lookupCharacterDetail(for: configuration.character.name)

        // 构造一个当前日期的时间线条目,并包含人物细节
        let entry = CharacterDetailEntry(date: Date(), detail: characterDetail)

        // 创建时间线并调用完成处理程序。配置为Never - 永不刷新
        // 策略表明包含的应用程序将使用WidgetCenter方法。
        // 当细节发生变化时,重新加载小组件的时间线
        let timeline = Timeline(entries: [entry], policy: .never)
        completion(timeline)
    }
}
复制代码

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

3. 保持组件状态为最新(Keeping a Widget Up To Date)

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

概述

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

为可预测的事件生成一个时间轴

许多小组件有可预测的时间点,在这些时间点上更新其内容是有意义的。例如,一个显示天气信息的小组件可以在一天中每小时更新一次温度。一个股票市场的小部件可以在开市时间频繁更新其内容,但在周末则完全不更新。通过提前规划这些时间,WidgetKit会在合适的时间到来时自动刷新你的widget。

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

下面是一个显示角色健康水平的游戏小部件的例子。

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

WidgetKit-Timeline-At-End@2x

当WidgetKit最初请求时间轴时,provider会创建一个有四个条目的时间轴。第一个条目代表当前的时间(Now),之后是每小时一次的三个条目。在刷新策略设置为默认的atEnd的情况下,WidgetKit会在时间线条目中的最后一个日期之后请求一个新的时间线。当时间线中的每个日期到达时,WidgetKit会调用小组件的内容闭包显示结果。在最后一个时间线条目过后,WidgetKit会重复这个过程,要求提供者提供一个新的时间线。由于角色的健康度已经达到了100%,提供者会以当前时间的单一条目和刷新策略设置为never来回应。在这种设置下,WidgetKit不会要求另一条时间线,直到应用程序使用WidgetCenter告诉WidgetKit请求新的时间线。

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

WidgetKit-Timeline-After-Date@2x

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

当2个小时过去后,角色的健康状况达到100%时,WidgetKit会要求provider提供新的时间线。因为角色的健康状况已经恢复,所以提供者会生成和上面第一张图一样的最终时间线。当用户玩游戏,角色的健康水平发生变化时,应用会使用WidgetCenter让WidgetKit刷新时间线,更新小部件。

除了指定时间线结束前的日期,提供者还可以指定时间线结束后的日期。当您知道小组件的状态在以后才会改变时,这很有用。例如,股市小组件可以在周五收市时创建一个时间线,并使用 afterDate() 刷新策略指定周一市场开盘的时间。因为股市在周末休市,所以在市场开盘前不需要更新小组件。


当时间线改变时通知WidgetKit

当某件事情影响到小组件的当前时间线时,您的应用可以告诉WidgetKit请求新的时间线。在上面的游戏小组件示例中,如果应用程序收到推送通知,表明队友给角色提供了治疗药水,应用程序可以告诉WidgetKit重新加载时间线并更新小组件的内容。要重载特定类型的widget,你的应用使用WidgetCenter,如这里所示。

WidgetCenter.shared.reloadTimelines(ofKind: "com.mygame.character-detail")
复制代码

kind参数包含与用于创建widget的WidgetConfiguration的值相同的字符串。

如果你的widget具有用户可配置的属性,那么通过使用WidgetCenter来验证是否存在具有适当设置的widget,从而避免不必要的重新加载。

例如,当游戏收到关于某个角色收到治疗药水的推送通知时,它会在重新加载时间线之前验证一个widget是否显示该角色。

在下面的代码中,应用程序调用getCurrentConfigurations(_:)来检索用户配置的小组件列表。然后,它遍历所产生的 WidgetInfo 对象,以找到一个具有接收治疗药水的角色所配置的意图的部件。如果它找到了一个,应用程序就会为该widget的种类调用reloadTimelines(ofKind:)。

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

    // 遍历WidgetInfo元素,找到符合的元素。
    // 来自推送通知的字符。
    if let widget = widgets.first(
        where: { widget in
            let intent = widget.configuration as? SelectCharacterIntent
            return intent?.character == characterThatReceivedHealingPotion
        }
    ) {
        WidgetCenter.shared.reloadTimelines(ofKind: widget.kind)
    }
}
复制代码

如果你的应用使用WidgetBundle来支持多个widget,你可以使用WidgetCenter来重新加载所有widget的时间线。例如,如果您的widget需要用户登录到一个账户,但他们已经登出,您可以通过调用重新加载所有widget。

WidgetCenter.shared.reloadAllTimelines()
复制代码

显示动态日期

因为你的widget扩展并不总是在运行,所以你不能直接更新widget的内容。取而代之的是,WidgetKit代表你渲染你的widget的视图并显示结果。然而,一些SwiftUI视图可以让你在你的widget可见时显示持续更新的内容。

在您的小组件中使用文本视图,您可以在屏幕上显示保持最新的日期和时间。

以下示例显示了可用的组合。

要显示自动更新的相对时间

let components = DateComponents(minute: 11, second: 14)
let futureDate = Calendar.current.date(byAdding: components, to: Date())!

Text(futureDate, style: .relative)
// Displays:
// 11 min, 14 sec

Text(futureDate, style: .offset)
// Displays:
// -11 minutes
复制代码

使用相对样式(.relative)显示当前日期和时间与指定日期之间的绝对差异,无论日期是在未来还是在过去。

偏移样式(.offset)显示当前日期和时间与指定日期之间的差异,用减号(-)前缀表示未来的日期,用加号(+)前缀表示过去的日期。

要继续显示自动更新的计时器。

let components = DateComponents(minute: 15)
let futureDate = Calendar.current.date(byAdding: components, to: Date())!

Text(futureDate, style: .timer)
// Displays:
// 15:00
复制代码

对于未来的日期,定时器显示为倒数时间,直到当前时间到达指定的日期和时间,并在日期过去后向上计数。

要显示绝对日期或时间。

// Absolute Date or Time
let components = DateComponents(year: 2020, month: 4, day: 1, hour: 9, minute: 41)
let aprilFirstDate = Calendar.current(components)!

Text(aprilFirstDate, style: .date)
Text("Date: \(aprilFirstDate, style: .date)")
Text("Time: \(aprilFirstDate, style: .time)")

// Displays:
// April 1, 2020
// Date: April 1, 2020
// Time: 9:41AM
复制代码

最后,显示两个日期之间的时间间隔。

let startComponents = DateComponents(hour: 9, minute: 30)
let startDate = Calendar.current.date(from: startComponents)!

let endComponents = DateComponents(hour: 14, minute: 45)
let endDate = Calendar.current.date(from: endComponents)!

Text(startDate ... endDate)
Text("The meeting will take place: \(startDate ... endDate)")

// Displays:
// 9:30AM-2:45PM
// The meeting will take place: 9:30AM-2:45PM
复制代码

使用地图视图显示位置

小组件通过使用SwiftUI MapView来显示地图。要在小组件中显示地图,请执行以下操作。

  1. UIWidgetWantsLocation键添加到你的widget扩展的Info.plist文件中,值为true。
  2. 在包含应用程序的 Info.plist 文件中包含使用目的字符串(usage-purpose)。
  3. 在包含应用程序中请求访问位置信息的权限。
  4. 有关请求位置授权和使用目的字符串的更多信息,请参阅请求位置服务的授权。

后台网络请求完成后更新

当您的小组件扩展处于活动状态时,例如提供快照或时间线时,它可以启动后台网络请求。

这个过程类似于应用程序如何处理这种类型的请求,这在 "在后台下载文件 "中描述。

WidgetKit没有恢复你的应用,而是直接激活你的widget的扩展。

要处理网络请求的结果,使用 onBackgroundURLSessionEvents(matching:_:) 修饰符到你的 widget的配置中,并执行以下操作。

  • 存储一个对完成参数的引用 您在处理完所有网络事件后调用完成处理程序。

  • 使用标识符参数找到您在发起后台请求时使用的 URLSession 对象。如果您的widget扩展被终止,请使用标识符重新创建URLSession。

    在调用onBackgroundURLSessionEvents()后,系统会调用你提供给URLSession的URLSessionDelegate的urlSession(_:downloadTask:didFinishDownloadingTo:)方法。当所有的事件都被交付后,系统会调用委托人的urlSessionDidFinishEvents(forBackgroundURLSession:)方法。 要在网络请求完成后刷新你的widget的时间线,从你的委托人的urlSessionDidFinishEvents的实现中调用WidgetCenter方法。一旦你处理完这些事件,就调用之前存储在onBackgroundURLSessionEvents()中的完成处理程序。

    如果您能看到这,那真是对我最大的鼓励。 感谢您的阅读,回见(._.)