iOS14 新特性 “Meet Widget”

3,262 阅读7分钟

Meet WidgetKit

Widgets 可以显示你 App 相关的内容,使用户可以快速访问您的应用以获取更多详细的信息;一个 iOS App 可以提供多种样式的 Widget ,使用户可以专注于那些对自己最有价值的信息;我们可以添加同一 Widget 的多个副本,从而根据其独特的需求和布局定制每个 Widget;如果 Widget 中有自定义的功能,则用户可以分别个性化 Widget;Widget 支持多种尺寸,你可以根据实际情况选择适合自己的尺寸,在屏幕可用空间有限的情况下,Widget 呈现的信息将是用户最关心的。

在你的应用中添加 Widget

将 Widget 添加到 App 中需要进行少量的设置,并且将使用 SwiftUI 来展示他的内容。

  • 打开你的 Xcode 工程, 并且选择 File > New > Target.
  • 在 Application Extension group 中选择 Widget Extension
  • 输入 Widget 的名字
  • 如果 Widget 提供了用户可配置的属性,请选中“ Include Configuration Intent ”复选框。
  • 点击完成

image

添加详细配置信息

Widget extension 模板提供了符合 Widget 协议的初始化实现。Widget 体里面的属性确定 了 Widget 是否具有用户可配置的属性。

有两种配置:

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

  • IntentConfiguration:用于具有用户可配置属性的 Widget。例如,需要一个城市的邮政编码的天气 Widget,或者需要一个跟踪号的包裹跟踪 Widget。

Include Configuration Intent 复选框决定了 Xcode 使用哪种配置。当您选中此复选框时,Xcode 将使用 intent configuration ;否则,它使用静态配置。要初始化配置,请提供以下信息:

  • Kind:标识 Widget 的字符串。这是您选择的标识符,并且应描述 Widget 所代表的内容。
  • Provider:符合 TimelineProvider 的对象,该对象生成一个时间线,告诉 WidgetKit 何时渲染。时间线包含自定义的 TimelineEntry 类型。TimelineEntry 标识您希望 WidgetKit 更新 Widget 内容的日期,包括 Widget 视图需要渲染自定义类型的属性。
  • Placeholder View:WidgetKit 使用一个 SwiftUI 视图来首次渲染。占位符是 Widget 的通用表示形式,没有特定的配置或数据。
  • Content Closure:包含 SwiftUI 视图的关闭。 WidgetKit 调用此方法来渲染 Widget 内容,并从 provider 传递 TimelineEntry 参数。
  • Custom Intent:定义用户可配置属性。

以下代码显示了一个 Widget,它为游戏提供了常规的,不可配置的状态:

@main
struct GameStatusWidget: Widget {
    var body: some WidgetConfiguration {
        StaticConfiguration(
            kind: "com.mygame.game-status",
            provider: GameStatusProvider(),
            placeholder: GameStatusPlaceholderView()
        ) { entry in
            GameStatusView(entry.gameStatus)
        }
        .configurationDisplayName("Game Status")
        .description("Shows an overview of your game status")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}

在此示例中,Widget 将 GameStatusPlaceholder 用于placeholder view (这里简称占位符视图),并将 GameStatusView 用于 content closure。占位符视图显示您 Widget 的一般表示形式,使用户可以大致了解 Widget 的显示内容。不要在占位符视图中包含实际数据。例如,使用灰色框表示文本行,或使用灰色圆圈表示图像。

Provider 为 Widget 生成 timeline,并在每个条目中包含游戏状态详细信息, 每个 timeline 条目的日期到达时,WidgetKit 都会调用 content closure 以显示 widget 的内容。最后,修饰符指定 Widget 库中显示的名称和描述,并允许用户选择小,中或大版本的 Widget。

请注意此 Widget 上 @main 属性的用法。此属性指示 GameStatusWidget 是窗口小部件扩展的入口点,这意味着该扩展包含单个 Widget, 要支持多个小部件,请参阅在App Extension中声明多个小部件。

Provide Timeline Entries

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

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

为了在 Widget 库中显示,WidgetKit 要求提供者提供预览快照。

您可以通过检查传递给 snapshot(for:with:completion :) 方法的 context 的 isPreview 属性来标识此预览请求。当 isPreview 为 true 时,Widget 将在 WidgetKit 库中显示。作为响应,您需要快速创建预览快照。如果您的 Widget 需要花费时间才能从服务器生成或从服务器获取的资源或信息,可以使用如下示例代码:

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

请求初始 snapshot 后,WidgetKit调用时间轴(for:with:completion :) 来向 provider 请求常规时间轴。时间轴由一个或多个时间轴条目以及一个重载策略组成,该重载策略通知 WidgetKit 何时请求后续时间轴。

以下示例显示了游戏状态 widget 的 provider 如何生成时间线,该时间线由服务器上具有当前游戏状态的单个条目以及重载策略组成,以在15分钟内请求新的时间线:

struct GameStatusProvider: TimelineProvider {
    func timeline(with context: Context, completion: @escaping (Timeline<GameStatusEntry>) -> ()) {
        // 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 不具有服务器的当前状态,则它可以存储完成的引用,向服务器执行异步请求以获取游戏状态,并在该请求完成时调用完成。

在 Widget 中显示内容

Widget 通常通过组合使用 SwiftUI 视图定义内容。

当用户从 Widget 库中添加 Widget 时,他们从 Widget 支持的类型中选择特定的系列(小,中或大),Widget 的 content closure 必须能够渲染其支持的每个类型, WidgetKit 在 SwiftUI environment 中设置相应的系列和其他属性,例如配色方案(浅色或深色)。

在上面显示的游戏状态 Widget 的配置中,content closure 使用 GameStatusView 来显示状态。因为 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 仅显示只读信息,不支持交互元素,例如滚动元素或开关。在呈现 Widget 的内容时,WidgetKit 会忽略交互式元素。

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

在应用中申明多个 Widgets

例如,如果游戏应用程序具有第二个用于显示角色健康状况的小部件,而第三个用于显示排行榜,则将它们分组在一起,如下所示:

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

结尾

iOS 用户终于不必再像过去那样进入应用程序内获取天气、新闻资讯、日期等信息,可直接通过在主界面上添加不同应用、不同尺寸的组件,关键信息就可直接在主屏幕上一目了然,有点致敬安卓的影子。

好了,今天的讲解就到这里,感兴趣的朋友可以关注我的技术公众号,每周都有优质技术文章推送,微信扫一扫下方二维码即可关注:

image