SwiftUI学习100天(Day47 - 里程碑:项目7-9)

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

你现在已经掌握了一些重量级技能,所以在我们继续之前,是时候回顾一下所涵盖的内容,详细介绍一些主题,并迎接新的挑战。

恭喜你完成了另外三个项目!在我们的绘图技术项目之后,你可能会感到疲倦,但今天和明天应该是一个很好的节奏变化——今天是巩固的一天,明天会有所不同。
今天的挑战很有趣,老实说,如果你有时间的话,它有很大的潜力可以开发成更大的应用程序。像今天这样的日子很重要,因为它们给了你一个完全在你掌握之中的想法,并给了你时间和范围来执行它。希望你能充分利用这一点——正如宇航员梅杰米森曾经说过的那样,“我喜欢将想法视为潜在的能量:它们真的很棒,但除非我们冒险将它们付诸行动,否则什么都不会发生。”
所以,今天是行动日:你前面有很多编码,如果你想进一步推进项目,还有更多编码的潜力。让我们开始吧!
今天你有三个主题要完成,其中之一是你的挑战。

你学到了什么
希望你觉得这些项目开始对你有所帮助,不仅可以进一步提高你的 SwiftUI 技能,还可以教你一些更高级的 Swift。此外,当然,你还有两个新构建的 SwiftUI 项目——你可以继续自定义所有你想要的项目,将它们放在 GitHub 上,或者将它们转换成更适合你口味的其他项目。
以下是我们在过去三个项目中涵盖的所有新事物的快速回顾:
为什么
@State
只适用于结构。如何使用
@ObservedObject
类工作。@Published
如何
让我们宣布正在观察的任何 SwiftUI 视图的属性更改。使用sheet()
修饰符和dismiss
环境键呈现和关闭视图。使用
onDelete(perform:)
启用滑动删除。添加
EditButton
到导航栏项目,让用户更轻松地编辑列表数据。使用
UserDefaults
读取和写入数据
。使用
Codable
归档和取消归档数据
,包括处理存储在层次结构中的数据。使用
Identifiable
协议来确保所有项目都可以在我们的用户界面中被唯一标识。如何使用
GeometryReader
使内容适合屏幕。ScrollView
用于在可滚动
区域中布置自定义视图。使用
NavigationLink
将新视图推送到导航堆栈
。使用 Swift 的泛型系统编写处理不同类型数据的方法。
创建自定义路径和形状。
使用
InsettableShape
创建可以插入并描边边框的形状
。用于
CGAffineTransform
创建旋转和平移。使用
ImagePaint
制作创意边框和填充
。启用 Metal 以使用
drawingGroup()
.修改混合模式和饱和度。
使用animatableData
和AnimatablePair
对形状进行动画处理
。
我希望你会很认同这些,而且范围很广——我们已经从硬核语言特性到面向用户的视图,甚至更进一步到 Swift 绘图系统的创造性使用。有些人会更喜欢纯语言的东西,而另一些人会更喜欢编码的更有创意的一面,这没关系——我们的学习方式都不一样!



关键点
虽然我们在前三个项目中涵盖了很多内容,但我想更详细地介绍三件具体的事情。别担心——绘画不是其中之一!
类与结构:有什么区别,为什么重要?
Swift 为我们提供了两种创建我们自己的复杂数据类型的方法,理解我们为什么要使用它们以及为任何给定任务选择哪种方法很重要。
类和结构的根本区别在于,一个是值类型,另一个是引用类型。这些是我们如何处理数据的标准编程术语:数据只是一个简单的值,例如“Hello”或 5,还是仅仅是一个路标,上面写着“我的数据存储在 RAM 中的这个位置”。
一旦你理解了这种差异,结构和类就变成了两种截然不同的东西,但是当你学习这些差异时,你会觉得它们根本没有什么不同。可以这样想:当我们创建一个包含结构的变量时,该数据实际上存储在变量中。相比之下,当我们使用一个类时,数据被放在内存中的某个地方,并且变量持有一个长数字来标识该内存的位置。
这就是这个名字的由来:“引用类型”被存储为对某处内存的引用,有点像路标。它不是一个直接指向我家的变量,而是指向一个指向我家的路标——有一个额外的间接层。这就是为什么如果你将两个或多个变量指向一个类的同一个实例,它们可以修改相同的数据:你只有几个路标都指向同一个房子。
这也是为什么引用类型和值类型在用作常量时表现不同的原因。如果我们创建一个类的常量实例,我们所做的就是创建一个常量路标——我们说过“这个路标总是指向门牌号 24601,不能指向不同的房子。” 然而,这并不能阻止我们改造房子:也许我们想加一层楼,或者改变厨房,或者甚至可能完全拆除房子并建造新的。如果你想让这些东西固定——如果你想让实际的房子本身保持不变——那么你需要为你的类使用常量属性。
因此,我们可以在拥有可变数据 (let myHouse = House()
) 的同时制作一个不变的路标 ( var numberOfFloors = 3
)。但我们也可以反过来:我们可以制作一个可变路标(var myHouse = House
)的同时让他
具有常量数据 ( let numberOfFloors = 3
),并且它的行为非常不同:我们可以移动路标,使其指向不同的房屋,但我们不能改造房子自己。
现在想想所有这些与 Swift、SwiftUI 甚至 UIKit 有何关系。如果你的应用程序中有三个屏幕,所有这些屏幕都共享相同的数据,那么确保数据在后台(所有变量包含相同的值)和给用户展示的界面(我们所有的列表/文本视图 / etc 显示相同的值)保持同步是很重要的。
SwiftUI 提供了诸如@State
和之类的包装器,@ObservedObject
以确保我们的视图在数据更改时保持更新,但是这些包装器无法与 UIKit 一起使用——你需要自己响应更改,然后更新用户界面以反映这些更改。
这产生了一个问题:
视图 A 可以创建一个类的实例。
视图 A 可以将其传递给视图 B,以便他们共享它。
然后视图 B 可以更改数据并更新其 UI。
视图 A 不会知道数据已更改,并且会显示旧的 UI。
因此,UIKit 开发人员通常将结构用于数据,因为这意味着每个视图都有自己的数据副本,并且不会意外更改。更有趣的是,所有 UIKit 的视图类型都是使用类构建的,这意味着 UIKit 开发人员将他们的视图构建为类并使用结构来存储他们的数据——这与 SwiftUI 完全相反。
明智地使用 UserDefaults
UserDefaults
让我们轻松存储少量数据——它会自动附加到我们的应用程序,这意味着它可以在我们的应用程序启动后立即加载。虽然它非常有用(而且你会严重依赖它!),但它确实有两个缺点:
你应该只在那里存储少量数据——任何超过 512KB 的数据都是可怕的。
你只能轻松存储某些类型的数据;其他一切都必须首先使用
Codable
来获取一些二进制数据。
UserDefaults
支持的类型列表
简短而精确:字符串、数字、日期、URL 和二进制数据,以及这些类型的数组和字典。不包括 URL(实际上只是花哨的字符串),所有这些都是可以存储在 plist 文件中的相同类型——属性列表的缩写。
这不是巧合:UserDefaults
实际上就像我们的 Info.plist 文件一样使用属性列表写出它的数据。事实上,记住这个链接真的可以帮助你充分利用UserDefaults
——如果我们的 Info.plist 文件包含 100,000 个数据条目,把 100,000 个项目放在UserDefaults
,那会很奇怪。
因此,按照其设计目的使用UserDefaults
系统——正如 Apple 自己的文档所说,它被称为用户默认值,“因为它们通常用于确定应用程序在启动时的默认状态或默认情况下的行为方式。”
何时使用泛型
我们使用泛型来创建一种解码方法,该方法能够从应用程序包中获取任何 JSON 文件并加载到我们选择的Codable
类型中。但是——这是一个很大的但是!– 我们首先将方法编写为非泛型:如果你还记得的话,它最初解码了一组宇航员,然后升级为加载任何Codable
类型。
那不是我在浪费你的时间,而是向你介绍了一种思考泛型和协议的明智方法。在这个项目中,我们需要解码来自 astronauts.json 中的Astronaut
实例数组,因此我们编写了一个方法来精确地做到这一点——没有协议也没有泛型,只是一个简单的扩展Bundle
来帮助保持代码的组织性。这模仿了我们大脑的思维方式:我们可以理解像宇航员这样的具体事物,并且可以很容易地描述它们。
但是,对于协议和泛型,事情并不是那么简单——我们现在有一系列可能的类型可以使用,除了遵循相同的协议之外,它们可能完全无关。例如,整数和字符串符合 Swift 的内置Comparable
协议,这就是为什么 Swift 知道如何对它们的数组进行排序,但除此之外它们是完全不同的东西。
也许令人困惑的是,我们无法比较两个可比较的对象,事实上,即使试图从方法中返回Comparable
也行不通。如果你不相信我,请尝试一下:
这不会编译,并且有充分的理由:Comparable
它本身并不意味着什么。正如我所说,字符串和整数都符合Comparable
协议,但这意味着你可以将一个整数与另一个整数进行比较,而不是你可以将任何Comparable
类型与另一种类型进行比较——这没有任何意义。
这就是通用约束如此有用的原因:它们让我们说“这可以是任何类型的对象,只要……”然后提供一些限制。而且——也许违反直觉——添加限制通常会启用更多功能。如你所见,当我们说我们的解码方法可以用于任何类型时,这意味着我们不能使用JSONDecoder
;在我们明确添加Codable
限制之前,Swift 无法知道它可以安全地将 JSON 解码为该类型。
所以,用好泛型的关键是一开始不要使用它们,当你确实需要它们时添加限制,这样你就可以获得尽可能多的功能。



挑战
在我们继续下一批项目之前,你需要完成一个新的挑战。这意味着使用你在前三个项目中获得的技能,自己从头开始构建一个完整的应用程序。
这次你的目标是为那些想要跟踪他们做了多少事情的人开发一个习惯跟踪应用程序。这可能是学习一门语言、练习一种乐器、锻炼身体等等——他们可以决定添加哪些活动,并根据需要进行跟踪。
至少,这意味着应该有一个他们想要跟踪的所有活动的列表,以及一个用于添加新活动的表格——一个标题和描述就足够了。
对于更大的挑战,点击其中一项活动应该会显示带有描述的详细信息屏幕。对于艰巨的挑战——请参阅下面的提示!– 使该详细信息屏幕包含他们完成的次数,以及一个增加完成次数的按钮。
如果你想让该应用程序真正有用,请使用Codable
并使用UserDefaults
加载和保存你的所有数据。
因此,此应用程序分为三个级别,你可以根据自己有多少时间以及想要推动自己走多远来选择想要走多远。不过,我确实建议你至少尝试每个级别——你获得的每一点练习都有助于巩固你的学习!

提示:
从你的数据开始:定义一个包含单个活动的结构,以及一个包含一系列活动的类。
该类将需要符合
ObservableObject
并给属性使用@Published
。你的主要列表和表单都应该能够读取共享活动对象。
确保你的活动符合
Identifiable
以避免出现问题。使用
sheet()
显示你的添加表单
,使用NavigationLink
显示你的活动详细信息视图(如果你添加了一个)
。
制作增加完成计数的按钮会给你带来挑战,因为你需要修改传入的活动。如果你遇到困难,最简单的方法是:
让你的结构符合
Equatable
. 你在这里不需要任何特别的东西——只需在Equatable
之后添加
Codable
和
Identifiable
。
将选定的活动和
ObservableObject
课程都传递到你的详细信息视图中。当点击增量按钮时,复制现有活动并将其完成计数加 1。
使用
firstIndex(of:)
查找上一个活动在类数组中的位置,然后将其更改为你的新活动 - 类似data.activities[index] = newActivity
的东西
会起作用。(这需要Equatable
步骤 1 的一致性!)
这是一个真正有用的应用程序,特别是如果它专门针对特定兴趣 - 如果目标是练习乐器,那么你可以想象一个更高级的应用程序会建议不同的练习,或者如果目标是锻炼,那么它可能会建议新的练习让事情变得混乱。
事实上,这个挑战只是一个小应用程序,但我希望它至少能让你思考。祝你好运!



注意:如果你没有在分配给他们的那一天完成挑战,请不要担心——在未来的日子里,你会发现你有一些空闲时间,所以挑战是你可以在未来返回的。