文章

创建小组件扩展

添加并配置扩展以在主屏幕、今天视图或通知中心显示你的 App 的内容。

最新英文文章

Creating a Widget Extension

概览

小组件一目了然地显示相关内容,让用户可以快速访问你的 App 以获取更多详情。你的 App 可以提供各种各样的小组件,让用户能够关注对他们来说最重要的信息。他们可以添加同一小组件的多个副本,并能根据他们独特的需求和布局调整每个副本。如果你在小组件中包含自定意图,用户还可以分别对每个小组件进行个性化设定。小组件支持多种尺寸;选择最适合你的 App 内容的尺寸。由于空间有限,请确保你的小组件呈现用户最看重的信息。

只需少量设置,以及几项有关你用户界面的配置和风格的决策,即可向你的 App 中添加小组件。小组件使用 SwiftUI 视图显示其内容。有关更多信息,请参阅“SwiftUI”。

向你的 App 中添加小组件目标

“Widget Extension”(小组件扩展) 模板为你创建小组件提供了一个起点。单个小组件扩展可以包含多个小组件。例如,某个体育 App 可能有一个用于显示团队信息的小组件,还有一个用于显示赛事安排的小组件。单个小组件扩展可以包含这两个小组件。

  1. 在 Xcode 中打开你的 App 项目,并选取“File”(文件) >“New”(新建) >“Target”(目标)。

  2. 从“Application Extension”(App 扩展) 组中,选择“Widget Extension”(小组件扩展),然后点按“Next”(下一步)。

  3. 输入你的扩展的名称。

  4. 如果小组件提供用户可配置的属性,请选中“Include Configuration Intent”(包含配置意图) 复选框。

  5. 点按“Finish”(完成)。

截屏显示了 Xcode 的新建目标表单,其中“Widget Extension”(小组件扩展) 处于选中状态

尽管你的 App 可以包含多个扩展,但你通常会将所有小组件都包含在单个小组件扩展中。例如,如果你的一些小组件使用位置信息,而另外一些小组件不使用,应将使用位置信息的小组件放在一个单独的扩展中。这样可让系统仅对使用位置信息的扩展中的小组件提示用户提供使用位置信息的授权。

添加配置详情

小组件扩展模板提供了一个遵从 Widget 协议的初始小组件实现。这个小组件的 body 属性用于确定小组件是否有用户可配置的属性。提供以下两种类型的配置:

  • StaticConfiguration:适用于不含用户可配置属性的小组件。例如,用于显示一般市场信息的股市小组件,或用于显示热门新闻标题的新闻小组件。

  • IntentConfiguration:适用于含有用户可配置属性的小组件。使用 SiriKit 自定意图定义属性。例如,需要城市邮政编码的天气小组件,或需要物流跟踪编号的包裹物流跟踪小组件。

“Include Configuration Intent”(包含配置意图) 复选框用于确定 Xcode 使用哪个配置。当你选中这个复选框时,Xcode 会使用意图配置,否则将使用静态配置。为初始化配置,请提供以下信息:

  • 种类:一个用于标识小组件的字符串。这是你选择的一个标识符,应描述小组件所呈现的内容。

  • 提供程序:一个符合 TimelineProvider 的对象,它将生成时间线来告诉 WidgetKit 何时呈现小组件。时间线包含你定义的自定 TimelineEntry 类型。时间线条目用于标识你希望 WidgetKit 更新小组件内容的日期。包含你的小组件视图需要在自定类型中呈现的属性。

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

  • 自定意图:自定意图用于定义用户可配置的属性。有关添加自定的更多信息,请参阅“制作可配置小组件”。

使用修饰符提供其他配置详情,包括显示名称、描述和小组件支持的系列。以下代码显示了一个小组件,该小组件用于提供游戏的通用、不可配置的状态:


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

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

注意 @main 属性在这个小组件中的用法。这个属性指示 GameStatusWidget 是小组件扩展的入口点,暗示该扩展包含单个小组件。要支持多个小组件,请参阅“在你的 App 扩展中声明多个小组件”。

提供时间线条目

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


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

要在小组件库中显示你的小组件,WidgetKit 会要求提供程序提供预览快照。可以通过检查传递给 getSnapshot(in:completion:) 方法的 context 参数的 isPreview 属性来识别这一预览请求。当 isPreview 为 true 时,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)
    }
}

在这个示例中,如果小组件没有来自服务器的当前状态,它可以存储对 completion 的引用,向服务器发起异步请求以获取游戏状态,并在该请求完成时调用 completion。

有关生成时间线的更多信息,包括处理小组件中的网络请求,请参阅“让小组件保持最新状态”。

显示占位符小组件

当 WidgetKit 首次显示你的小组件时,它会将小组件的视图呈现为一个占位符。占位符视图显示小组件的通用表示,让用户可以大致了解小组件显示的内容。WidgetKit 调用 placeholder(in:) 来请求表示小组件占位符配置的条目。例如,游戏状态小组件将按如下所示实现这一方法:


struct GameStatusProvider: TimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        GameStatusEntry(date: Date(), gameStatus: "—")
    }
}

为了将小组件的视图呈现为占位符,WidgetKit 使用 redacted(reason:) 视图修饰符并指定 placeholder 作为 reason。为防止小组件视图层次结构中的视图自动呈现为占位符,请使用 unredacted() 视图修饰符。

如果你在小组件扩展中启用“Data Protection”(数据保护) 功能,则当数据保护授权指定以下值且关联的条件得到满足时,WidgetKit 会将你的小组件呈现为占位符:

  • NSFileProtectionCompleteNSFileProtectionCompleteUnlessOpen,且设备已锁定。

  • NSFileProtectionCompleteUntilFirstUserAuthentication,且用户还没有进行身份验证。

有关配置数据保护的更多信息,请参阅“数据保护授权”。

在你的小组件中显示内容

小组件使用一个 SwiftUI 视图定义其内容,通常通过组合其他 SwiftUI 视图实现。如“添加配置详情”部分中所示,小组件的配置包含 WidgetKit 为呈现小组件内容而调用的闭包。

当用户从小组件库中添加你的小组件时,他们会从小组件支持的系列中选择特定的系列 (小尺寸、中尺寸或大尺寸)。小组件的内容闭包必须能够呈现小组件支持的每个系列。WidgetKit 在 SwiftUI 环境中设置相应的系列和其他属性,例如配色方案 (浅色或深色)。

在上面显示的游戏状态小组件配置中,内容闭包使用 GameStatusView 来显示状态。小组件支持全部三个小组件系列,而它会使用 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()
        }
    }
}

对于小尺寸系列,小组件所用的视图显示游戏中当前轮到的玩家的简单摘要。对于中尺寸系列,它显示状态以指示上一轮的结果。对于大尺寸系列,由于有更多可用空间,它显示每个玩家的运行统计数据。如果系列是未知类型,则显示默认视图,表示游戏状态不可用。

对于可配置的小组件,提供程序符合 IntentTimelineProvider。这个提供程序与 TimelineProvider 执行相同的功能,但它包含用户在小组件上自定的值。意图时间线提供程序可以在传递给 getSnapshot(for:in:completion:)getTimeline(for:in:completion:)configuration 参数中使用这些自定项。你通常会将用户配置的值作为自定时间线条目类型的属性包含在内,以便详情可在小组件的视图中显示。

有关 WidgetKit 支持的视图列表,请参阅“SwiftUI 视图”。

向你的小组件中添加动态内容

尽管小组件的显示基于视图的快照,但你可以使用各种可在小组件显示时继续更新的 SwiftUI 视图。有关提供动态内容的更多信息,请参阅“让小组件保持最新状态”。

响应用户互动

当用户与你的小组件进行互动时,系统将启动你的 App 来处理请求。当系统激活你的 App 时,导航到与小组件内容对应的详细信息。你的小组件可以指定一个 URL 来告知 App 要显示的内容。要在你的小组件中配置自定 URL:

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

  • 对于使用 WidgetFamily.systemMediumWidgetFamily.systemLarge 的小组件,向小组件的视图层次结构中添加一个或多个 Link 控件。你可以同时使用 widgetURLLink 控件。如果互动的目标为某个 Link 控件,则系统将使用该控件中的 URL。对于小组件中任何其他位置的互动,系统将使用在 widgetURL 视图修饰符中指定的 URL。

例如,用于显示游戏中单个角色的详情的小组件可以使用 widgetURL 打开 App 以显示该角色的详情。


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

如果小组件显示角色列表,则列表中的每一项都可以在 Link 控件中。每个 Link 控件都会指定它所显示的特定角色的 URL。

当小组件收到互动时,系统会激活包含 App 并将 URL 传递给 onOpenURL(perform:)application(_:open:options:)application(_:open:),具体取决于你的 App 使用的生命周期。

如果小组件没有使用 widgetURLLink 控件,系统将激活包含 App 并将 NSUserActivity 传递给 onContinueUserActivity(_:perform:)application(_:continue:restorationHandler:)application(_:continue:restorationHandler:)。用户活动的 userInfo 字典包含与用户互动的小组件的详细信息。使用 WidgetCenter.UserInfoKey 中的密钥访问 Swift 代码中的这些值。要访问 Objective-C 中的 userInfo 值,请改用 WGWidgetUserInfoKeyKindWGWidgetUserInfoKeyFamily 密钥。

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

在你的 App 扩展中声明多个小组件

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

例如,如果游戏 App 有第二个小组件用于显示角色生命值,第三个小组件用于显示排行榜,则它会将这些小组件组合在一起,如下所示:


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

另请参阅

小组件创建