SwiftUI有非常方便的骨架屏显示方法,这在需要获取网络内容的界面显示非常重要。显示骨架屏相比转圈的加载中界面更加缓解用户等待的焦虑感。

SwiftUI有一个非常方便的方式添加,那就是:

1
2
View
.redacted(reason: .placeholder)

下面的内容来自查看英文原文学习如何使用占位符编写骨架屏。

您是否曾经使用过需要一段时间才能加载的移动应用程序或网站?缓慢的连接速度并不令人愉快,是吗?当您无法判断内容是否正在加载或在此过程中是否失败时,情况会更糟。

幸运的是,有几种方法可以在某些时间比预期更长时通知用户。最现代的方法之一是使用经过编辑的占位符。这些是在 iOS 14 中引入到 SwiftUI 中的。

在本教程中,你将了解:

  • 如何在 SwiftUI 中利用占位符
  • 为什么加载状态如此重要
  • 隐藏私人用户信息的最佳做法
  • 如何创建小部件

占位符是一种更现代的方法,用于展示 UI 的预览。此设计模式通常用于文本字段,其中字段显示提示,帮助用户了解要输入的内容。

占位符的另一个优势是能够隐藏私人信息。金融应用通常会在应用进入后台时执行此操作。在 SwiftUI 中,显示占位符比创建单独的视图来隐藏敏感信息更容易。

因此,事不宜迟,是时候学习如何做到这一点了!

开始

通过单击教程顶部或底部的“下载材料”按钮下载初学者项目。

下载材料(蓝奏云盘)

在ZIP中,你会发现两个文件夹,finalstarter。打开初学者文件夹。该项目由一个应用组成,该应用显示带有应用名称“报价”的标题。

该项目包含一个包含励志名言的 JSON 文件。此文件位于 Supporting Files/quotes.json。每个报价都有一个 ID、日期和图标名称,以及报价本身。您可以在 Shared/Quote.swift 找到此数据的数据模型。此数据集中的引用来自 motivationping.com

本教程的目的是展示加载状态在软件中的重要性。它将展示如何在应用程序和iOS 14小部件中执行此操作。

请求报价

您需要做的第一件事是将报价加载到应用程序中。打开位于应用程序/报价视图模型.swift的视图模型。这是您将加载报价的地方。将以下属性添加到 的顶部:QuotesViewModel

1
2
@Published var isLoading = false
@Published var quotes: [Quote] = []

第一个属性确定是否正在加载内容。第二个是应用程序将显示的引号数组。由于应用程序将有一个小部件,因此最好共享加载逻辑。

打开共享/模型加载器.swift并将 的内容更改为以下内容:bundledQuotes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1
guard let url = Bundle.main
.url(forResource: "quotes", withExtension: "json")
else {
return []
}

// 2
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .secondsSince1970

do {
// 3
let data = try Data(contentsOf: url)
return try decoder.decode([Quote].self, from: data)
} catch {
print(error)
return []
}

下面是上面的代码正在做的事情:

  1. 这是 JSON 文件的路径。
  2. 这将创建一个用于解析引号的。JSONdecoder
  3. 解码器尝试从文件中读取数据,并将解码的引号数组返回到 。bundleQuotes

现在,您有一种方法可以读取捆绑的数据。返回到 QuotesViewModel.swift并将以下内容添加到类的末尾:

1
2
3
4
5
init() {
withAnimation {
self.quotes = ModelLoader.bundledQuotes
}
}

此初始值设定项负责通过调用新方法从磁盘加载引号。

由于没有用于显示引号的 UI,因此不会显示任何内容。不用担心!接下来你会谈到这一点。

打开“应用/报价视图”.swift并在以下下添加以下内容:body

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private func row(from quote: Quote) -> some View {
// 1
HStack(spacing: 12) {
// 2
Image(systemName: quote.iconName)
.resizable()
.aspectRatio(nil, contentMode: .fit)
.frame(width: 20)

// 3
VStack(alignment: .leading) {
Text(quote.content)
.font(
.system(
size: 17,
weight: .medium,
design: .rounded
)
)

Text(quote.createdDate, style: .date)
.font(
.system(
size: 15,
weight: .bold,
design: .rounded
)
)
.foregroundColor(.secondary)
}
}
}

浏览代码:

  1. 此行视图在左侧显示一个图标,在右侧显示文本。
  2. 捆绑的 JSON 文件中的报价数据包含 SF 符号。这将显示所需的符号。Image
  3. 报价及其日期一个接一个地显示。

现在,将以下内容添加到 :List``body

1
2
3
ForEach(viewModel.quotes) { quote in
row(from: quote)
}

这将循环遍历引号并将其加载到视图中。构建并运行。

报价视图。

太好了,您现在可以看到捆绑的报价!

显示进度

在理想情况下,信息会立即加载,不会发生错误。但是,对于蜂窝网络和复杂的服务器端代码,可能会出现一些问题,因此尝试使用户尽可能顺利地处理此类情况非常重要。

甚至本地信息也可能需要一些时间来加载。生成超过 100,000 个项目的数据库查询至少需要几秒钟。目前,报价加载没有延迟或问题。但是,在本教程中,您将添加人为延迟来模拟慢速网络连接。

打开引号视图模型.swift并在下面添加以下内容:init()

1
2
3
4
5
private func delay(interval: TimeInterval, block: @escaping () -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + interval) {
block()
}
}

此帮助程序方法在指定的时间段后使用大中央调度运行关闭。您将使用它来延迟加载过程的几个部分。

接下来,将 的内容替换为:init()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
isLoading = true
let simulatedRequestDelay = Double.random(in: 1..<3)

delay(interval: simulatedRequestDelay) {
withAnimation {
self.quotes = ModelLoader.bundledQuotes
}

let simulatedIngestionDelay = Double.random(in: 1..<3)

self.delay(interval: simulatedIngestionDelay) {
self.isLoading = false
}
}

这段代码增加了两个延迟。您可以更新此处的属性,以增加额外的进度层。两个随机数有助于模拟真实的工作场景。
构建并运行。

An empty loading screen.

现在您将看到一个空视图,直到加载报价。在生产应用程序中,这样的行为会让用户感到困惑。用户不清楚是否发生了什么,也不清楚是否有什么失败了。
最常用的 UI 模式之一是旋转器,用于传达数据正在加载的信息。在 iPhone X 推出之前,显示加载中的旋转器的最简单方法是.UIApplication.isNetworkActivityIndicatorVisible.UIApplication.isNetworkActivityIndicatorVisible.其他流行的模式包括加载条、模糊和占位符。
您要做的第一项改进是添加加载指示器。这在 UIKit 中称为 a,在 SwiftUI 中称为 a。UIActivityIndicatorViewProgressView 的视图模型已经为此进行了设置。 打开 QuotesView.swift,将其中的内容替换为:QuotesViewbody

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ZStack {
NavigationView {
List {
ForEach(viewModel.quotes) { quote in
row(from: quote)
}
}
.navigationTitle("Quotation")
}

if viewModel.quotes.isEmpty {
ProgressView()
}
}

这里的主要区别在于使用 a 将进度视图定位在 .如果发生错误,进度视图将停止,表示发生了意外情况。ZStack``List

通常情况下,不需要大量网络请求来检索视图所需的所有数据。以加载购物车内容的视图为例。此视图需要对每个产品图像或用户评论提出请求。当页面完全加载时,可能已经发出了数十个请求。

构建并运行。

使用居中微调器加载的视图。

这在很大程度上向用户显示正在发生的事情!ProgressView

已编辑的占位符

之前,您在加载报价时添加了两个延迟。第一个延迟模拟对网络的初始请求。第二个用于显示加载速度较慢的视图数据片段。这就是密文的用武之地。

在 中,紧跟在内侧的右大括号之后添加以下内容:QuotesView``ForEach``body

1
2
3
.redacted(
reason: viewModel.isLoading ? .placeholder : []
)

这将使 中的每一行在 是 时出现被编辑。List``isLoading``true

构建并运行。

充满占位符表单元格的视图。

在 SwiftUI 中隐藏视图的某些部分非常容易:编辑的修饰符将隐藏标签,直到加载完成。此修饰符可为您创建出色的占位符视图。

在某些情况下,自动占位符视图效果不佳,因为您可能希望始终显示某些视图。幸运的是,苹果想到了这一点!

将 in 更改为以下内容:Image``row(from:)

1
2
3
4
5
Image(systemName: quote.iconName)
.resizable()
.aspectRatio(nil, contentMode: .fit)
.frame(width: 20)
.unredacted()

构建并运行。

带有占位符文本的图标。

现在图标图像始终显示。

1
.unredacted()`完美互补。但在此示例中,如果报价需要额外的网络请求来获取其数据,则可能需要更长的时间。`.redacted()

隐藏用户数据

虽然大多数应用使用帐户来存储有关用户的信息,这些信息对他们有利,但信息是私密的,未经同意不应共享。

例如,跟踪您的投资的股票交易应用程序应该是安全的。它还应该深思熟虑地防止敏感信息被窥探。这些类型的应用程序使用的一个常见技巧是在关闭应用程序时隐藏您的信息。

这将是对你的应用的一个很好的补充。报价并不像您的财务记录那么重要,但暂时假装它们是。:]

要实现此效果,您将重用一些现有逻辑。

打开 QuotesViewModel.swift并在类的顶部添加一个新属性:

1
@Published var shouldConceal = false

之后,将以下三个新方法添加到类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private func beginObserving() {
// 1
let center = NotificationCenter.default
center.addObserver(
self,
selector: #selector(appMovedToBackground),
name: UIApplication.willResignActiveNotification,
object: nil
)
center.addObserver(
self,
selector: #selector(appMovedToForeground),
name: UIApplication.didBecomeActiveNotification,
object: nil
)
}

@objc private func appMovedToForeground() {
// 2
shouldConceal = false
}

@objc private func appMovedToBackground() {
// 3
shouldConceal = true
}

这是正在做的事情:

  1. 两个观察者将侦听应用状态的通知。NotificationCenter
  2. 当应用移动到前台时,它会显示引号。
  3. 当应用程序关闭时,它会隐藏引号。

在 : 顶部呼叫 :beginObserving``init()

1
beginObserving()

继续前进,在 的顶部添加以下计算属性:QuotesViewModel

1
2
3
var shouldHideContent: Bool {
return shouldConceal || isLoading
}

现在,您将使用此新属性以及现有属性来更新视图。如果为 ,视图将隐藏用户的内容。shouldConceal``isLoading``true

返回 QuotesView.swift,更改已编辑修饰符以使用新的计算属性:

1
2
3
.redacted(
reason: viewModel.shouldHideContent ? .placeholder : []
)

构建并运行。

关闭应用。

现在,当您的应用程序进入后台时,没有人可以看到您的宝贵报价!当您使应用程序再次进入前台时,您的报价将被恢复。:]

用于创建占位符视图适用于多种情况。它不仅看起来不错,而且易于使用和自定义。它也适用于模板视图和加载指示器。redacted

Apple创建编辑视图的目的是将其用于改进的主屏幕小部件。这就是您接下来要介绍的内容!

创建小部件

苹果在iOS 14中重新引入了小部件。以前,它们只显示在“今日视图”上。小部件的旧实现没有良好的加载状态;他们从空白开始,通常需要一段时间才能加载。

现在小部件位于主屏幕上的前端和中心,因此更需要更好的加载状态。该解决方案是适用于任何 .由于任何第三方应用程序都可以提供自己的小部件,因此此通用解决方案是完美的。View

此应用程序中的小部件每小时显示一个新报价。该设计将像主应用程序一样工作。在系统显示占位符的情况下,您仍希望显示该图标。

导航到 Widget/QuoteOfTheHour.swift并将 的实现更改为以下内容:getTimeline(in:completion:)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var entries: [QuoteEntry] = []
// 1
var quotes = ModelLoader.bundledQuotes

let calendar = Calendar.current
let currentDate = Date()

for hourOffset in 0..<24 {
// 2
guard let entryDate = calendar.date(
byAdding: .hour,
value: hourOffset,
to: currentDate)
else {
continue
}

// 3
guard let randomQuote = quotes.randomElement() else {
continue
}

// 4
if let index = quotes.firstIndex(of: randomQuote) {
quotes.remove(at: index)
}

entries.append(QuoteEntry(model: randomQuote, date: entryDate))
}

// 5
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)

在上面的代码中:

  1. 报价是从共享模型加载器中提取的。
  2. 在接下来的 24 小时内,每小时安排一个新报价。
  3. 报价是随机选择的。
  4. 所选报价将被删除,以便计划报价是唯一的。
  5. 时间线是使用 24 个选定的报价设置的。

现在有数据可以使用,下一步是设计小部件。

QuoteOfTheHour.swift 中的正文替换为以下内容:QuoteOfTheHourEntryView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
VStack(alignment: .leading) {
HStack {
Image(systemName: entry.model.iconName)
.resizable()
.aspectRatio(nil, contentMode: .fit)
.frame(width: 12)

Spacer()

Text(entry.model.createdDate, style: .date)
.font(
.system(
size: 12,
weight: .bold,
design: .rounded
)
)
.foregroundColor(.secondary)
.multilineTextAlignment(.trailing)
}

Text(entry.model.content)
.font(
.system(
size: 16,
weight: .medium,
design: .rounded
)
)

Spacer()
}
.padding(12)

这类似于主应用程序中使用的行,不同之处在于字体大小和图标较小。

构建并运行。关闭应用程序并将小部件添加到主屏幕,如下所示:

添加主屏幕小部件。

最后一件事是确保始终显示小部件中的图标。由于此应用程序使用SF符号作为图标,因此始终可以访问它们。操作系统从时间线加载小组件的报价和日期,因此在此期间将对其进行编辑。

与主应用程序一样,单个修饰符将确保图标显示。

将 中的 替换为以下内容:Image``body

1
2
3
4
5
Image(systemName: entry.model.iconName)
.resizable()
.aspectRatio(nil, contentMode: .fit)
.frame(width: 12)
.unredacted()

构建并运行。

部分编辑的小组件。

当系统加载小部件时,它将显示一个带有图标的占位符。

注意:小部件应立即在模拟器中加载。此屏幕截图的小组件中添加了已编辑的修饰符。

现在,该小部件在所有情况下看起来都很棒。

何去何从?

通过单击教程顶部或底部的“下载材料”按钮下载已完成的项目文件。

回顾一下您学到的内容:

  • 空白加载视图会导致混淆。
  • 简单的微调器在大多数情况下很有帮助。
  • 占位符甚至更好,看起来很棒。
  • 占位符不仅适用于加载,而且也非常适合隐藏数据。

有关更多信息,请参阅 Apple 有关修订的文档。此外,来自WWDC2020的小部件视频的构建 SwiftUI 视图值得一看。

要了解有关 SwiftUI 的更多信息,请查看有关该主题的其他 raywenderlich.com 教程

希望您喜欢本教程。如果您有任何问题或意见,请加入下面的讨论!