SwiftUI学习100天(Day80 - 项目 16,第二部分)

原创链接:https://www.hackingwithswift.com/100/swiftui
以下内容仅供学习参考:

今天你将以 Swift Result
类型的形式处理一个棘手的概念,但为了平衡事情,我们还将涵盖两个更简单的概念,希望你今天不会发现太多工作。
Swift 的Result
类型旨在解决当你知道事情 A 可能为真或事情 B 可能为真时的问题,但在任何给定时间只有一个可以为真。如果你将它们想象成布尔属性,那么每个属性都有两种状态(真和假),但它们加起来有四种状态:
A是假的,B是假的
A为真,B为假
A为假,B为真
A为真,B为真
如果你确定选项 1 和 4 永远不可能——A 或 B 必须为真但它们不能都为真——那么你可以立即将逻辑的复杂性减半。
美国作家厄休拉·K·勒金曾说过:“唯一使生活成为可能的是永久的、无法忍受的不确定性;不知道接下来会发生什么。” 优秀软件的情况恰恰相反:我们可以执行的确定性越强,我们可以应用的约束越多,我们的代码就越安全,Swift 编译器可以为我们做的工作就越多。
因此,尽管Result
需要你考虑将转义闭包作为参数传入,但回报更智能、更简单、更安全——完全值得。
今天你要完成三个主题,你将在其中学习Result
、objectWillChange
和图像插值。

手动发布 ObservableObject 变化
符合ObservableObject
协议的类可以使用 SwiftUI 的@Published
属性包装器来自动宣布对属性的更改,以便使用该对象的任何视图都可以重新调用其body
属性并与其数据保持同步。这在很多时候都非常有效,但有时你想要更多的控制,SwiftUI 的解决方案被称为objectWillChange
.
每个符合ObservableObject
的类都会
自动获得一个名为objectWillChange
. 这是一个publisher,这意味着它与属性包装器@Published
做同样的工作
:它通知任何正在观察该对象的视图一些重要的东西已经改变。顾名思义,应该在我们进行更改之前立即触发此发布者,这允许 SwiftUI 检查我们的 UI 状态并为动画更改做准备。
为了证明这一点,我们将构建一个自我更新 10 次的ObservableObject
类。我们将使用一个名为 DispatchQueue.main.asyncAfter()
的方法
,它允许我们在选择的延迟后运行附加的闭包,这意味着我们可以说“1 秒后执行此工作”而不是“现在执行此工作”。
在此测试用例中,我们将在asyncAfter()
从 1 到 10 的循环中使用,因此我们将整数值递增 10。该整数将被包装使用@Published
以便将更改公告发送到正在观看它的任何视图。
在你的代码中的某处添加此类:
要使用它,我们只需要在 ContentView
中添加一个
,然后在我们的主体中显示该值,如下所示:@StateObject
属性
当你运行该代码时,你会看到该值一直向上计数,直到达到 10,这正是你所期望的。
现在,如果你删除@Published
属性包装器,你将看到 UI 不再发生变化。在幕后,所有asyncAfter()
工作仍在进行,但不会再导致 UI 刷新,因为不会发送任何更改通知。
我们可以通过使用我之前提到的objectWillChange
属性手动发送更改通知来解决这个问题。这让我们可以随时发送更改通知,而不是依赖@Published
自动执行。
尝试将value
属性更改为此:
现在你将再次获得旧行为 - 用户界面将像以前一样计数到 10。除了这一次,我们有机会在该观察者willSet
中添加额外的功能
。也许你想记录一些东西,也许你想调用另一个方法,或者你想把整数限制在value
里面,
这样它就不会超出范围——现在一切都在我们的控制之下。



理解 Swift 的Result类型
Swift 提供了一种特殊类型Result
,它允许我们将成功的值或某种错误类型封装在一个单独的数据片段中。因此,就像一个可选项可能包含一个字符串或可能什么都不包含一样,例如,Result
可能包含一个字符串或可能包含一个错误。起初使用它的语法有点奇怪,但它确实在我们的项目中扮演着重要的角色。
要查看Result
实际效果,我们可以从编写一个方法开始,该方法从服务器下载一组数据读数,如下所示:
该代码工作得很好,但它没有给我们很大的灵活性——如果我们想把工作藏在某个地方并在它运行时做其他事情怎么办?如果我们想在未来的某个时候读取它的结果,也许完全在其他地方处理任何错误怎么办?或者如果我们只是因为不再需要它而想取消它怎么办?
好吧,我们可以通过使用 Result
获得所有这些
,并且实际上可以通过你之前遇到过的 API 获得它:Task
。我们可以将上面的代码重写为:
我们之前使用过Task
来启动工作,但在这里我们给Task
对象起了一个名字fetchTask
——这给了我们额外的灵活性来传递它,或者在需要时取消它。并注意我们的Task
闭包现在如何返回一个值?该值存储在我们的Task
实例中,以便我们将来准备好时可以读取它。
更重要的是,Task
如果抛出网络获取失败,或者数据解码失败,这可能会引发错误,这就是Result
进来的地方:我们任务的结果可能是一个字符串,上面写着“找到 10000 个读数”,但它也可能包含一个错误。找出答案的唯一方法是查看内部——它与可选项非常相似。
要从 一个Task
中读取结果
,请像这样读取其result
属性:
注意到我们没有使用try
来
读出Result
吗?那是因为Result
将它保存在自身内部——可能会抛出一个错误,但除非我们愿意,否则我们现在不必担心它。
如果你查看result
的类型,你会发现它是一个Result<String, Error>
– 如果它成功,它将包含一个字符串,但它也可能失败并包含一个错误。
你可以直接从 if 中读取成功值Result
,但你需要确保并适当地处理错误,如下所示:
或者,你可以在switch
中用
Result
,
编写代码来检查成功和失败情况。这些情况中的每一个都有它们的值(成功的字符串和失败的错误),因此 Swift 让我们使用特制的匹配来读取这些值case
:
无论你如何处理它,Result
的优点
是它可以让我们将某些工作的全部成功或失败存储在一个值中,将其传递到我们需要的任何地方,并且仅在我们准备好时才读取错误。



在 SwiftUI 中控制图像插值
如果你创建一个 SwiftUI Image
视图,将其内容拉伸到大于其原始大小,会发生什么情况?默认情况下,我们得到图像插值,这是 iOS 如此平滑地混合像素的地方,你甚至可能根本没有意识到它们已经被拉伸了。这当然会带来性能成本,但大多数时候不值得担心。
但是,图像插值在一个地方会导致问题,那就是当你处理精确像素时。例如,GitHub 上这个项目的文件包含一个名为 example@3x.png 的小卡通外星人图像——它取自https://kenney.nl/assets/platformer-art-deluxe上的 Kenney Platform Art Deluxe 捆绑包和在公共领域下可用。
继续将该图形添加到你的资产目录,然后将你的ContentView
结构更改为:
这会在黑色背景下呈现外星人角色,使其更容易看到,并且由于它的大小可调整,SwiftUI 会将其拉伸以填充所有可用空间。
仔细观察颜色的边缘:它们看起来参差不齐,但也很模糊。锯齿状部分来自原始图像,因为它的大小仅为 66x92 像素,但模糊部分是 SwiftUI 在像素被拉伸时试图混合像素以使拉伸不那么明显的地方。
通常这种混合效果很好,但在这里很难,因为源图片很小(因此需要大量混合才能以我们想要的大小显示),而且因为图像有很多纯色所以混合像素很突出很明显。
对于这种情况,SwiftUI 为我们提供了interpolation()
修改器,让我们可以控制像素混合的应用方式。这有多个层次,但实际上我们只关心一个:.none
。这将完全关闭图像插值,因此它们不会混合像素,而是会按比例放大并带有锐利的边缘。
因此,将你的图像修改为:
现在你会看到外星人角色保留了它的像素化外观,这不仅在复古游戏中特别受欢迎,而且对于线条艺术也很重要,因为线条艺术在模糊时看起来会很糟糕。


