WidgetKit

WidgetKit

iOS 14之后,需要添加添加widget extension,使用timeline provider配置widget,它会告诉WidgetKit去跟新widget的内容,使用SwiftUI展示widget的内容。为了使窗口小部件支持用户可配置,可以向扩展中添加自定义SiriKit意向定义,WidgetKit自动提供自定义界面,让用户自定义widget。

一、创建Widget Extension

1、添加widget target

(1)添加步骤

File–>new–>Target在列表里面选择Widget Extension,在之后对话框中。

A screenshot showing Xcode’s new target sheet with Widget Extension selected

(2)如果你的Widget支持用户自定义记得勾选Configuration 复选框,如果不支持不勾选。
添加之后工程结构
在这里插入图片描述

2、添加具体配置

widget的body属性决定了widget是否包含用户配置属性,一般分为两种配置,

  • StaticConfiguration:不包含用户配置属性,例如股票app的widget展示市场信息,新闻app展示趋势标题。

  • IntentConfiguration:包含用户配置,需要在添加extension的时候勾选user- Configuration,并且需要提供以下信息

    • kind:是一个字符串,代表widget的identifer,描述widget代表的内容
    • Provider:遵守TimelineProvider的对象,该对象生成时间线告诉WidgetKit在何时去渲染widget,时间线包含开发者自定义的TimelineEntry ,这个timelineEntry表示开发者想让WidgetKit去更新自己widget内容的时间
    • Content Closure:一个closure包含SwiftUI的视图,从provider传递来TimelineEntry之后,WidgetKit唤醒Content Closure去渲染widget的内容。
    • Custom Intent:一个自定义的intent,他包含用户配置的属性列表,详细内容可参考Making a Configurable Widget.

使用modifiers提供其他配置详细信息,包括显示名称、描述和小部件支持的尺寸大小。 以下代码显示了一个小部件,它为游戏提供一般的、不可配置的状态:

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

Widget provider为widget提供了一个timeline,包含每一个entry的game status的详情,当到达每个timeline entry更新日期,widgetKit会唤醒contetn closure显示widget的内容,最后,modifiers指定显示在小部件库中的名称和描述,并允许用户选择小、中或大版本的小部件。

@mian:表示这个widget是widget extension的入口,例如上面代码@main表明 GameStatusWidget是widget extension的入口点,也表示了这个包含单个小部件,如果要支持多个小部件,参考Multiple Widgets in Your App Extension.

2、提供Timeline Entries

Timeline provider会生成一个由timeline entries组成的时间线,每个条目都指定更新小部件内容的日期和时间。示例中的游戏widget定义的timeline entry 包含了一个字符串,代表游戏的状态

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

开发时还需要提供一个预览snapshot,以便在widget库中显示自己的widget,也就是将getSnapshot(in:completion)方法中isPreview属性设置为true,这样WidgetKit就会在widget库中显示开发的widget,但是这里应该使用默认的数据,而不是从服务器请求数据,目的是快速创建snapShot。为了在小部件库中显示您的小部件,WidgetKit 会要求提供者提供预览快照。 通过检查传递给 getSnapshot(in:completion:) 方法的上下文参数的 isPreview 属性来识别此预览请求。 当 isPreview 为 true 时,WidgetKit 会在小部件库中显示您的小部件。 作为回应,您需要快速创建预览快照。 如果您的小部件需要花费时间从服务器生成或获取的资产或信息,请改用示例数据。在以下代码中,游戏widget设置snapShot的逻辑是,如果小部件的没有从服务器获取数据就展示一个空status

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:)从provider中定期请求timeline,timeline包含一个或多个timeline entries以及加载策略组成,这个加载策略定义了WidgetKit什么时候会去请求后续的timeline。下面的代码示例展示了game状态widget的provider生成的时间线,这个时间线包含来自服务器的当前游戏状态的的单条entry,以及在15分钟内请求一个新的timeline的加载策略

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

在上面的例子中,如果widget没有来自服务器的当前status,它可以存储completion的引用,向服务器执行异步请求以获取游戏状态,并在该请求完成时调用完成。下面我们来看看包含网络请求的widget,也就是保持widget跟随时间更新

保持widget跟随时间更新

(1)在预算内计划重新加载

budget

​ 为了电池寿命(小组件的加载会消耗资源和电池消耗),所以需要限制请求的频率和数据数量。为了管理系统负载,Widget使用budget(预算)来分配一天内widget的加载次数,budget的分配是动态的并且考虑了很多因素,包括widget对用户可见的频率和时间、widget重新加载的时间、widget的宿主app是否活跃,WidgetKit为用户添加的每个活跃widget维护不同的budget,例如用户添加了两个可配置的体育widget的实力,用于展示不同的组的信息,那么每个widget有它自己的budget。

系统怎么计算budget

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

但是,也有些情况不属于widget的budget

  • widget的宿主app在前台
  • widget的宿主app有一个活跃的音频和导航session
  • 系统区域更改
  • 动态类型或者辅助功能更改

系统外观更改或者系统区域更改等情况,不需要从app请求时间线重新加载,因为系统会自动更新widget。

系统自动更新widget的情况

  • widget位于用户很少访问的主屏幕页面上,widget会降低对widget的重新加载频率,之后用户查看页面时,WidgetKit会在widget可见时重新加载它
  • 位置组件,当位置发生很大变化时,Widget会重新加载

总之,如果widget可以预测自己重新加载的时间点,最好的方法是生成的timeline包含尽可能多的未来date,并且展示内容的entries也应该保持比较大的间隔。 WidgetKit 在重新加载小部件之前施加了最短的时间。您的时间线提供商应创建至少相隔约 5 分钟的时间线条目。 WidgetKit 可能会跨多个小部件合并重新加载,从而影响重新加载小部件的确切时间。

(2)生成可预测事件的时间表

可预测事件的时间表

许多类型的widget都有可预测的时间点,在这些时间点更新展示的内容是有意义的。例如,显示天气信息的小部件可能会在全天的每个小时更新一次温度;股票市场小部件会在开市时间频繁更新它展示的内容,但不会在周末更新。通过提前计划这些时间,WidgetKit 会在适当的时间到来时自动刷新您的小部件。

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

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

WidgetKit-Timeline-At-End@2x

当 WidgetKit 请求时间线时,provider会创建一个有四个条目的timeline。第一个条目代表当前时间,后面是每小时间隔的三个条目。将刷新策略设置为默认的 atEnd 时,WidgetKit 会在时间线条目中的最后一个日期之后请求新的时间线。当时间线中的每个日期到达时,WidgetKit 调用小部件的内容闭包并显示结果。在最后一个时间线条目过去后,WidgetKit 通过向提供者请求新的时间线来重复该过程。由于角色的健康已达到 100%,因此提供者以当前时间的单个条目和设置never为刷新策略。有了这个设置,WidgetKit 不会要求另一个时间线,直到应用程序使用 WidgetCenter 告诉 WidgetKit 请求一个新的时间线。

除了 atEnd 和 never refresh 策略之外,如果时间线可能在到达条目末尾之前或之后发生变化,则提供者可以完全指定不同的日期。例如,如果一条龙将在 2 小时后向角色发起挑战,provider将重新加载策略设置为 after(_😃,将日期传递到未来 2 小时。下图显示了 WidgetKit 在 2 小时标记处呈现小部件后如何请求新的小部件。

WidgetKit-Timeline-After-Date@2x

由于与龙的战斗,角色的治疗需要额外 2 小时才能达到 100%。新的时间线包含两个条目,一个是当前时间,另一个是未来 2 小时。时间线为刷新策略指定 atEnd,表示没有更多已知事件可能会改变时间线。 当 2 小时过去了,并且角色的健康状况为 100% 时,WidgetKit 会要求提供者提供新的时间表。因为角色的健康已经恢复,提供者生成与上图相同的最终时间线。当用户玩游戏并且角色的健康水平发生变化时,应用程序使用 WidgetCenter 让 WidgetKit 刷新时间线并更新小部件。 除了指定时间线结束之前的日期之外,提供者还可以指定时间线结束之后的日期。当您知道小部件的状态直到稍后才会更改时,这很有用。例如,股票市场小部件可以在周五市场收盘时创建一个时间线,并使用 afterDate() 刷新策略指定周一市场开盘的时间。由于股市周末休市,因此在开市前无需更新小部件。

如果widget在重新加载时向服务器发出请求,并在timeline entries中使用具有特定日期的 afterDate(),请提前计划。 WidgetKit 会尝试遵守您指定的日期,这可能会在多个设备大约同时重新加载您的小部件时导致服务器负载显着增加。

(3)时间线改变时通知WidgetKit
当widget的当前timeline受到影响时,宿主aoo可以通知 WidgetKit 请求新的时间线。在上面的游戏widget示例中,如果应用程序收到一个推送通知,表明队友给了角色治疗药水,应用程序可以告诉 WidgetKit 重新加载时间线并更新widget的内容。要重新加载特定类型的widget,需要app使用 WidgetCenter,如下所示:

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

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

如果开发的widget具有user-configurable属性,那么需要使用 WidgetCenter 验证是否存在适当设置的widget,从而避免不必要的重新加载。例如,当游戏收到有关角色接收治疗药水的推送通知时,它会在重新加载时间线之前验证widget是否正在显示该角色。

在以下代码中,应用程序调用 getCurrentConfigurations(_😃 来检索用户配置的小部件列表。然后它遍历生成的 WidgetInfo 对象,以找到一个意图配置为接收治疗药水的角色的对象。如果找到,应用程序会为该小部件的种类调用 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)
    }
}

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

WidgetCenter.shared.reloadAllTimelines()

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

(5)后台网络请求完成后更新

当widget extension处于活动状态时,例如提供快照或时间线时,它可以发起后台网络请求;例如,获取队友当前状态的游戏小部件,或获取带有图像缩略图的标题的新闻小部件。 发出异步的后台网络请求可以让您快速将控制权返回给系统,从而降低因响应时间过长而被终止的风险。该过程类似于应用程序处理此类请求的方式,WidgetKit 不是恢复宿主app,而是直接激活widget extension。 要处理网络请求的结果,请对小部件的配置使用 onBackgroundURLSessionEvents(matching:_😃 修饰符,并执行以下操作:

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

  • 使用 identifier 参数查找您在启动后台请求时使用的 URLSession 对象。 如果widget extension已终止,请使用标识符重新创建 URLSession。

调用 onBackgroundURLSessionEvents() 后,系统调用您提供给 URLSession 的 URLSessionDelegate 的 urlSession(_:downloadTask:didFinishDownloadingTo:) 方法。 当所有事件都被传递后,系统调用委托的 urlSessionDidFinishEvents(forBackgroundURLSession:) 方法。

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

3、展示展位widget

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

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

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

如果您在小部件扩展中启用数据保护功能,当数据保护权利指定以下值并且满足相关条件时,WidgetKit 会将您的小部件呈现为占位符:

NSFileProtectionComplete 或 NSFileProtectionCompleteUnlessOpen,并且设备被锁定。

NSFileProtectionCompleteUntilFirstUserAuthentication,并且用户还没有通过身份验证。

有关配置数据保护的更多信息,请参阅数据保护权利

4、展示widget内容

widget使用SwiftUI视图来定义展示的内容(通常是组合其他SwiftUI视图),如 Add Configuration Details 部分所示,widget的配置包含WidgetKit 调用以呈现小部件内容的闭包。当用户从窗口小部件库添加widget时,用户会从widget支持的系列中选择特定的类型(小型、中型或大型)。 小部件的内容闭包必须能够呈现小部件支持的美中类型,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()
        }
    }
}

对于small family,widget使用一个视图来显示游戏中轮到谁的简单摘要。 对于medium family,它表示上一回合的结果。 对于大large family,它会显示每个玩家的运行统计数据。 如果家族是未知类型,则显示默认视图,表示游戏状态不可用。

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

对于可配置的widget,provider遵循 IntentTimelineProvider协议,这个provider与 TimelineProvider 执行相同的功能,但它合并了用户对widget自定义的值。 这些自定义可以在intent timeline provider中使用,并且作为配置参数传递给 getSnapshot(for:in:completion:) 和 getTimeline(for:in:completion:) 。 通常将用户配置的值作为timeline entries的属性,以便widget的视图可以使用详细信息。

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

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

5、给widget增加动态内容

参考保持widget跟随时间更新的相关内容

6、响应用户的交互

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

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

  • 对于使用 WidgetFamily.systemMedium 或 WidgetFamily.systemLarge 的小部件,可以将一个或多个Link控件添加到widget的视图层次结构中。您可以同时使用 widgetURL 和 Link 控件。如果交互以链接控件为目标,系统将使用该控件中的 URL。对于小部件中其他任何位置的交互,系统使用在 widgetURL 视图修饰符中指定的 URL。

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

@ViewBuilder
var body: some View {
    ZStack {
        AvatarView(entry.character)
            .widgetURL(entry.character.url)
            .foregroundColor(.white)
    }
    .background(Color.gameBackground)
}
  • 如果widget显示字符列表,则列表中的每个项目都可以在Link控件中。每个Link控件指定它显示的特定字符的 URL。当widget收到交互时,系统会激活宿主aoo并将 URL 传递给 onOpenURL(perform:)、application(:open:options:) 或 application(:open:),具体取决于您的应用程序的生命周期使用。

  • 如果widget不使用 widgetURL 或 Link 控件,系统将激活宿主app并将 NSUserActivity 传递给 onContinueUserActivity(:perform:)、application(:continue:restorationHandler:) 或 application(_:continue:restorationHandler: ),UserActivity的 userInfo 字典包含有关用户与之交互的widget的详细信息。使用 WidgetCenter.UserInfoKey 中的键从 Swift 代码访问这些值。要从 Objective-C 访问 userInfo 值,请改用密钥 WGWidgetUserInfoKeyKind 和 WGWidgetUserInfoKeyFamily。

对于使用 IntentConfiguration 的widget,UserActivity的交互属性包含widget的 INIntent。IntentConfiguration 指的是使用自定义意图定义提供用户可配置选项的widget的内容。

7、在app extension中声明多个widget

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

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

@main
struct GameWidgets: WidgetBundle {
    @WidgetBundleBuilder
    var body: some Widget {
        GameStatusWidget()
        CharacterDetailWidget()
        LeaderboardWidget()
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Yootheme出品的组件. 相册工具提供了一个非常棒的方式来创建您网站上的相册图库。您可以在内置的目录中直接上传,删除图片,轻松地建立您的个人相册。在Beta5中 我们就已经加强了相册的外观显示: 图片墙 和幻灯片样式。图片墙允许您使用lightbox和spotlight效果。幻灯片样式则可以当您鼠标悬停时原尺寸图片,并能显示彼此相邻的图片。您只需要挑选您喜欢的展示图片的方式即可,其他一切都是在后台自动完成! 地图工具可让您创建多个位置和标记的地图。支持最新的谷歌地图API,您可以更改各种设置,以创建一个完全适合您的网站地图。 手风琴 手风琴小部件可以显示您创建的一组项目,使用流畅的滑动效果显示其内容。我们新增加了一个简单干净的显示风格,很容易使手风琴样式完全地符合您的网站风格。 媒体播放器 在BETA4时,我们已经推出了媒体播放器,它可以让你直接在你的网站上播放视频和音频文件。工具通过HTML5嵌入视频,可以广泛地支持移动设备,如iPhone,ipad或Android手机等。您也不必担心兼容旧浏览器的问题,因为它设置了回避不支持HTML5元素的功能。 集成ZOO Widgetkit和ZOO可以相互桥接!这是我们被要求添加的功能之一,我们很高兴现在提供给你。通过使用一个Joomla插件,可以是Widgetkit显示ZOO的任何内容,如手风琴,地图和幻灯片的内容。使用时,请确保您已安装最新版本的ZOO。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员的修养

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值