WidgetKit框架详细解析(二) —— 一个基于WidgetKit和SwiftUI的简单示例(一)

版本记录

版本号 时间
V1.0 2020.11.20 星期五

前言

WidgetKit是iOS14的新的SDK,接下来几篇我们就一起看一下这个专题。感兴趣的可以看下面几篇文章。
1. WidgetKit框架详细解析(一) —— 基本概览(一)

开始

首先看下主要内容:

在本教程中,您将向一个大型SwiftUI应用添加小部件(widget),重用其视图以显示该应用存储库中的条目。内容来自翻译

下面就看下写作环境

Swift 5, iOS 14, Xcode 12

接着就是正文了

在今年的WWDC Platforms State of the Union中看到新的主屏幕小部件(widgets)后,我就知道必须为自己喜欢的应用制作一个!在主屏幕上看到它真是太好了。我并不孤单。每个人都在做!苹果公司知道自己是赢家,并提供了一个由三部分组成的代码,以使所有人开始使用。

已经发布了几本指导手册,那么本教程有什么不同?好吧,我决定将一个小部件添加到由一组开发人员编写的相当大的SwiftUI应用程序中,但没人是我。有大量的代码可供筛选,以查找构建窗口小部件所需的内容。而且所有这些都没有考虑到小部件的编写。因此,请跟随我,向我展示如何做到。

注意:您将需要Xcode 12 beta。您还需要运行iOS 14的iOS设备。Catalina可以。如果您有运行Big Sur BetaMac [partition],则可以尝试在其中运行代码,以防它无法在Catalina上运行。

最重要的是,目前这是一个真正的bleeding-edge APIWWDC演示中出现的内容不是Xcode 12 beta 1的一部分。您可能会遇到一些不稳定的情况。就是说,Widgets很酷,很有趣!

在打开启动程序项目之前,请打开Terminalcdstarter / emitron-iOS-development文件夹,然后运行以下命令:

scripts/generate_secrets.sh

您正在生成运行项目所需的一些机密文件。

现在,在starter / emitron-iOS-development文件夹中打开Emitron项目。这需要一些时间才能获取一些软件包,因此,这里有一些有关项目的信息,您可以在等待的同时进行。

Emitronraywenderlich.com应用程序。如果您是订阅者(subscriber),则一定已将其安装在iPhoneiPad上。它可以让您流式传输视频,并且,如果您具有专业订阅,则可以下载视频以进行离线播放。

该项目是开源的。您可以在其GitHub存储库GitHub repository,中阅读有关它的信息,当然,欢迎您为它的改进做出贡献。

您下载的starter版进行了一些修改:

  • 设置是最新的,iOS Deployment Target14.0
  • Downloads / DownloadService.swift中,注释了两个在Xcode beta 1中引起错误的promise语句。下载服务是针对专业订阅的,本教程不需要它。
  • Guardpost / Guardpost.swift中,将authSession?.prefersEphemeralWebBrowserSession设置为false,从而避免每次构建和运行应用程序时都需要输入登录详细信息。您仍然必须点击Sign in,提示您使用raywenderlich.com登录。点击Continue。首次构建和运行时,您可能仍必须输入电子邮件和密码,但是在随后的构建和运行中,点击Continue会跳过登录表单。

到目前为止,Xcode已经安装了所有软件包。在模拟器中构建并运行。

忽略有关HashableSwiftyJSON的警告。 滚动和播放在Xcode beta 1中效果不佳,但是您不会在本教程中对此进行修复。 如果滚动“太多”,则该应用程序将崩溃。 也不是你的问题。


WidgetKit

本教程全部关于向Emitron添加闪亮的新小部件。


Adding a Widget Extension

首先添加带有File ▸ New ▸ Target…widget extension

Create a new target

搜索widget,选择Widget Extension并点击Next

将其命名为EmitronWidget,并确保未选中Include Configuration Intent

Don’t select Include Configuration Intent

有两种窗口小部件配置(widget configurations)StaticIntent。 具有IntentConfiguration的小部件使用Siri Intents来使用户自定义小部件参数。

单击Finish并同意激活方案(activate-scheme)对话框:

1. Running Your Widget

小部件模板(widget template)提供了许多您只需自定义的样板代码。它可以直接使用,因此,您可以立即设置所有内容,以确保在准备测试代码时一切都能顺利运行。

注意:Xcode 12 beta 1模拟器不会在widget gallery中显示您的widget。因此,在将来的某个Beta版本之前,您必须在iOS 14设备上构建并运行。如果您没有可用的设备,则可以运行Widget scheme而不是主Emitron scheme,该Widget将出现在模拟器的主屏幕上。

在项目导航器中,选择顶级Emitron文件夹对targets进行签名。更改bundle identifier,并为每个target的每个版本设置team

注意:对于widget,您可能会遇到一个明显的Xcode bug,该bug将三个版本中的两个的签名标识设置为Distribution。如果看到此错误,请打开Build Settings,搜索distribution并将签名标识更改为Apple Development

最后一个陷阱:确保小部件的bundle ID前缀与应用程序的ID匹配。这意味着您将需要在“ ios”“ EmitronWidget”之间插入dev以获取your.prefix.emitron.ios.dev.EmitronWidget

OK,现在连接您的iOS设备,选择Emitron scheme和您的设备,然后构建并运行。登录,然后关闭应用程序,然后在主窗口的空白区域上按,直到图标开始抖动。

点击右上角的+按钮,然后向下滚动以找到raywenderlich

Scroll down in widget gallery

选择它可以查看三种尺寸的快照:

Snapshots of the three widget sizes

点击Add Widget以在屏幕上查看您的小部件:

Your widget on the home screen

点击widget以重新打开Emitron

您的小部件widget起作用了! 现在,您只需要使其显示来自Emitron的信息即可。


Defining Your Widget

使您的小部件显示应用程序为每个教程显示的一些信息是很有意义的。

Card view in the Emitron app

此视图在UI / Shared / Content List / CardView.swift中定义。 我的第一个想法是将窗口widget target添加到此文件中。 但这需要添加越来越多的文件,以容纳Emitron中所有复杂的连接。

您真正需要的只是Text视图。 这些图片很可爱,但是您需要包括持久性基础结构以防止它们消失。

您将复制相关Text视图的布局。 它们使用几个实用程序扩展,因此找到这些文件并将EmitronWidgetExtension target添加到其中:

Add the widget target to these files

注意:确保注意到图像顶部Assets

CardView显示ContentListDisplayable对象的属性。 这是Displayable / ContentDisplayable.swift中定义的协议:

protocol ContentListDisplayable: Ownable {
  var id: Int { get }
  var name: String { get }
  var cardViewSubtitle: String { get }
  var descriptionPlainText: String { get }
  var releasedAt: Date { get }
  var duration: Int { get }
  var releasedAtDateTimeString: String { get }
  var parentName: String? { get }
  var contentType: ContentType { get }
  var cardArtworkUrl: URL? { get }
  var ordinal: Int? { get }
  var technologyTripleString: String { get }
  var contentSummaryMetadataString: String { get }
  var contributorString: String { get }
  // Probably only populated for screencasts
  var videoIdentifier: Int? { get }
}

您的widget仅需要namecardViewSubtitledescriptionPlainTextreleasedAtDateTimeString。 因此,您将为这些属性创建一个结构。

1. Creating a TimelineEntry

创建一个新的名为WidgetContent.swiftSwift文件,并确保其targetsemitronEmitronWidgetExtension

Create WidgetContent with targets emitron and widget

它应该在EmitronWidget组中。

现在,将此代码添加到新文件中:

import WidgetKit

struct WidgetContent: TimelineEntry {
  var date = Date()
  let name: String
  let cardViewSubtitle: String
  let descriptionPlainText: String
  let releasedAtDateTimeString: String
}

要在窗口小部件中使用WidgetContent,它必须符合TimelineEntry。 唯一必需的属性是date,您可以将其初始化为当前日期。

2. Creating an Entry View

接下来,创建一个视图以显示四个String属性。 创建一个新的SwiftUI View文件,并将其命名为EntryView.swift。 确保其target仅是EmitronWidgetExtension,并且也应位于EmitronWidget组中:

Create EntryView with only the widget as target

现在,用以下代码替换struct EntryView的内容:

let model: WidgetContent

var body: some View {
  VStack(alignment: .leading) {
    Text(model.name)
      .font(.uiTitle4)
      .lineLimit(2)
      .fixedSize(horizontal: false, vertical: true)
      .padding([.trailing], 15)
      .foregroundColor(.titleText)
    
    Text(model.cardViewSubtitle)
      .font(.uiCaption)
      .lineLimit(nil)
      .foregroundColor(.contentText)
    
    Text(model.descriptionPlainText)
      .font(.uiCaption)
      .fixedSize(horizontal: false, vertical: true)
      .lineLimit(2)
      .lineSpacing(3)
      .foregroundColor(.contentText)
    
    Text(model.releasedAtDateTimeString)
      .font(.uiCaption)
      .lineLimit(1)
      .foregroundColor(.contentText)
  }
  .background(Color.cardBackground)
  .padding()
  .cornerRadius(6)
}

您实质上是从CardView复制Text视图并添加填充间距。

完全删除EntryView_Previews

3. Creating Your Widget

现在开始定义窗口widget。 打开EmitronWidget.swift并在该行中双击SimpleEntry

public typealias Entry = SimpleEntry

选择Editor ▸ Edit All in Scope,并将名称更改为WidgetContent。 这将导致一些错误,您将在接下来的几个步骤中进行修复。 首先删除声明:

struct WidgetContent: TimelineEntry {
  public let date: Date
}

现在,此声明是多余的,并且与WidgetContent.swift中的声明冲突。

4. Creating a Snapshot Entry

provider的一种方法提供了一个快照条目,以显示在widget gallery中。 为此,您将使用特定的WidgetContent对象。

import语句的下面,添加此全局对象:

let snapshotEntry = WidgetContent(
  name: "iOS Concurrency with GCD and Operations",
  cardViewSubtitle: "iOS & Swift",
  descriptionPlainText: """
    Learn how to add concurrency to your apps! \
    Keep your app's UI responsive to give your \
    users a great user experience.
    """,
  releasedAtDateTimeString: "Jun 23 2020 • Video Course (3 hrs, 21 mins)")

这是我们的并发视频课程的更新,该课程在WWDC第2天发布。

现在,将snapshot(with:completion:)的第一行替换为:

let entry = snapshotEntry

当您在gallery中查看此小部件时,它将显示此条目。

5. Creating a Temporary Timeline

小部件需要一个TimelineProvider才能为其提供TimelineEntry类型的条目。 它会在条目的date属性指定的时间显示每个条目。

最重要的provider方法是timeline(with:completion:)。 它已经有一些代码来构造时间轴,但是您没有足够的条目。 因此,注释掉最后两行以外的所有内容,并添加以下行:

let entries = [snapshotEntry]

您正在创建一个仅包含snapshotEntryentries数组。

6. Creating a Placeholder View

小部件在等待实际时间轴条目时显示其PlaceholderView。 您还将为此使用snapshotEntry

以此替换Text视图:

EntryView(model: snapshotEntry)

WWDC代码中还显示了一个特殊的修饰符,该修饰符使视图的内容模糊不清,以表明这是一个占位符,而不是真实的东西。 是这样的:

.isPlaceholder(true)

WWDC视频中看起来很酷,但是在Xcode 12 beta 1中没有编译。有关更多信息,请参阅Apple开发者论坛中的此项 this entry

7. Defining Your Widget

最后,您可以将所有这些部分放在一起。

首先,删除EmitronWidgetEntryView。 您将改用EntryView

现在,将EmitronWidget的内部替换为以下内容:

private let kind: String = "EmitronWidget"

public var body: some WidgetConfiguration {
  StaticConfiguration(
    kind: kind, 
    provider: Provider(), 
    placeholder: PlaceholderView()
  ) { entry in
    EntryView(model: entry)
  }
  .configurationDisplayName("RW Tutorials")
  .description("See the latest video tutorials.")
}

这三个字符串是您想要的:kind描述您的窗口小部件,最后两个字符串显示在库中每个窗口小部件上方的尺寸。

在您的设备上构建并运行,登录,然后关闭该应用以查看您的小部件。

如果仍然显示时间,请将其删除并重新添加。

Widget gallery with snapshot entry

这是中等大小的小部件现在的样子:

The medium size widget on the home screen

只有中等大小的小部件看起来不错,因此请修改您的小部件以仅提供该大小。 在.description下面添加此修饰符:

.supportedFamilies([.systemMedium])

接下来,您将直接从应用程序的存储库中为时间线提供真实的条目!


Providing Timeline Entries

该应用程序将在Data / ContentRepositories / ContentRepository.swift中创建的contents中显示ContentListDisplayable对象的数组。 要与您的小部件widget共享此信息,您将创建一个应用程序组。 然后,在ContentRepository.swift中,将文件写入此应用程序组,并在EmitronWidget.swift中读取该文件。

1. Creating an App Group

在项目页面上,选择emitron target。 在Signing & Capabilities选项卡中,单击+ Capability,然后将App Group拖到窗口中。 将其命名为group.your.prefix.emitron.contents;确保适当替换your.prefix

现在,选择EmitronWidgetExtension target并添加App Group功能。 滚动浏览App Group以查找并选择group.your.prefix.emitron.contents

2. Writing the Contents File

ContentRepository.swift的顶部,在import Combine语句的下面,添加以下代码:

import Foundation

extension FileManager {
  static func sharedContainerURL() -> URL {
    return FileManager.default.containerURL(
      forSecurityApplicationGroupIdentifier: "group.your.prefix.emitron.contents"
    )!
  }
}

这只是获取应用程序组容器的URL的一些标准代码。 确保替换您的应用标识符前缀。

现在,在var contents下面,添加此辅助方法:

  func writeContents() {
    let widgetContents = contents.map {
      WidgetContent(name: $0.name, cardViewSubtitle: $0.cardViewSubtitle,
      descriptionPlainText: $0.descriptionPlainText, 
      releasedAtDateTimeString: $0.releasedAtDateTimeString)
    }
    let archiveURL = FileManager.sharedContainerURL()
      .appendingPathComponent("contents.json")
    print(">>> \(archiveURL)")
    let encoder = JSONEncoder()
    if let dataToSave = try? encoder.encode(widgetContents) {
      do {
        try dataToSave.write(to: archiveURL)
      } catch {
        print("Error: Can't write contents")
        return
      }
    }
  }

在这里,您将创建一个WidgetContent对象的数组,每个对象用于存储库中的每个项目。 您将它们分别转换为JSON并将其保存到app group的容器中。

let archiveURL行设置一个断点。

设置contents后,您将调用此方法。 将此didSet闭包添加到contents中:

didSet {
  writeContents()
}

如果Xcode在警告WidgetContent。 跳转到WidgetContent的定义,使其符合Codable

struct WidgetContent: Codable, TimelineEntry {

现在,在模拟器中构建并运行该应用程序。 在断点处,widgetContents具有20个值。

继续执行程序并在应用程序中向下滚动。 在断点处,widgetContents现在具有40个值。 因此,您可以控制与小部件共享多少项。

停止应用程序,禁用断点,然后从调试控制台复制URL文件夹路径并在Finder中定位。 看一下contents.json

接下来,转到并设置widget以读取此文件。

3. Reading the Contents File

EmitronWidget.swift中,添加相同的FileManager代码:

extension FileManager {
  static func sharedContainerURL() -> URL {
    return FileManager.default.containerURL(
      forSecurityApplicationGroupIdentifier: "group.your.prefix.emitron.contents"
    )!
  }
}

确保更新您的前缀。

将此帮助程序方法添加到Provider

func readContents() -> [Entry] {
  var contents: [WidgetContent] = []
  let archiveURL = 
    FileManager.sharedContainerURL()
      .appendingPathComponent("contents.json")
  print(">>> \(archiveURL)")

  let decoder = JSONDecoder()
  if let codeData = try? Data(contentsOf: archiveURL) {
    do {
      contents = try decoder.decode([WidgetContent].self, from: codeData)
    } catch {
      print("Error: Can't decode contents")
    }
  }
  return contents
}

这将读取您保存到应用程序组容器中的文件。

取消注释timeline(with:completion:)中的代码,然后替换此行:

var entries: [WidgetContent] = []

使用下面

var entries = readContents()

接下来,修改注释和for循环以将日期添加到条目中:

// Generate a timeline by setting entry dates interval seconds apart,
// starting from the current date.
let currentDate = Date()
let interval = 5
for index in 0 ..< entries.count {
  entries[index].date = Calendar.current.date(byAdding: .second,
    value: index * interval, to: currentDate)!
}

删除for循环下面的let entry行。

之后的那一行设置时间轴运行并指定刷新策略。 在这种情况下,时间轴将在用完所有当前条目后刷新。

在您的设备上构建并运行,登录并加载列表。 然后关闭该应用程序,添加您的小部件并观看它每5秒更新一次。

Widget updating entry every 5 seconds

我可以整天看这个。

如果您没有滚动列表,则该widget将在20个项目后用完所有条目。如果等待那么长时间,您会在刷新时看到它暂停。

注意:这是Beta版软件。如果未获得预期的结果,请尝试从设备中删除该应用,然后重新启动设备。另外,请记住,小部件并不是要以秒为单位测量时间间隔。在教程设置中,非常短的间隔只是更加方便。但是结果是,时间轴刷新的等待时间感觉很长!最后一条警告:不要让5秒小部件在设备上运行,因为它会耗尽电池电量。


Enabling User Customization

我为时间轴间隔选择了5秒,因此无需等待很长时间即可看到更新。如果您想要更短或更长的间隔,只需更改代码中的值即可。或者...创建一个intent,让您可以通过在主屏幕上直接编辑小部件来设置时间间隔!

注意:使用intent更改时间间隔时,直到widget刷新其时间轴,您才会看到效果。

1. Adding an Intent

首先,添加您的intent:创建一个新文件(Command-N),搜索intent,选择SiriKit Intent Definition File并将其命名为TimelineInterval。确保其target同时是emitronEmitronWidgetExtension

intent侧边栏的左下角,单击+,然后选择New Intent

Add new intent

intent命名为TimelineInterval。 如图所示,使用Category View设置Custom Intent

Custom intent with category view

并添加一个名为Integer类型的interval的参数,其默认值,最小值和最大值(如所示)和Type Field。 或设置您自己的值和/或使用步进器。

Add interval parameter

2. Reconfiguring Your Widget

EmitronWidget.swift中,将小部件重新配置为IntentConfiguration

Provider协议更改为IntentTimelineProvider

struct Provider: IntentTimelineProvider {

snapshot(with:completion:)定义为:

public func snapshot(
  for configuration: TimelineIntervalIntent, 
  with context: Context, 
  completion: @escaping (Entry) -> Void
) {

现在,将timeline(with:completion:)的定义更改为:

public func timeline(
  for configuration: TimelineIntervalIntent, 
  with context: Context, 
  completion: @escaping (Timeline<Entry>) -> Void
) {

timeline(for:with:completion)中,更改interval以使用配置参数:

let interval = configuration.interval as! Int

最后,在EmitronWidget中,将StaticConfiguration(kind:provider:placeholder :)更改为此:

IntentConfiguration(
  kind: kind, 
  intent: TimelineIntervalIntent.self, 
  provider: Provider(), 
  placeholder: PlaceholderView()
) { entry in

在您的设备上构建并运行,登录并加载列表。 关闭应用程序,添加小部件,然后长按该小部件。 它会翻转以显示Edit Widget按钮。

Edit widget button

点击此按钮更改间隔值

Change the interval value
Setting a new interval

本教程向您展示了如何利用大型应用程序中的代码来创建一个小部件,以显示该应用程序自己的存储库中的项目。 以下是一些您可以添加到Emitron小部件中的想法:

  • 为大型窗口小部件设计一个视图,该视图显示两个或更多条目。 查看AppleEmojiRangers示例应用程序,以了解如何为小部件系列修改EntryView
  • 将一个widgetURL添加到EntryView,以便点按该小部件可在该项目的详细信息视图中打开Emitron
  • 添加intent以使用户可以从小部件设置应用程序的过滤器。

后记

本篇主要讲述了基于WidgetKitSwiftUI的简单示例,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容