第36天 项目 7 第一部分
开源操作系统 Linux 的创建者莱纳斯·托瓦兹(Linus Torvalds)曾被问及,对于想要构建 大型软件项目的开发者,他是否有什么建议。以下是他的回答:
“任何人 不应不应一开始就着手大型项目。你应从一个小型、简单的项目开始,并且绝不要期望它会发展壮大。如果抱有这样的期望,你就会过度设计,还可能会普遍认为这个项目在当前阶段的重要性远超其实际情况。更糟糕的是,你可能会被自己设想的庞大工作量吓倒。”
在编写本课程的过程中,已经有人给我发邮件问:“为什么在项目 1 中不用 X 来解决问题?”或者“在项目 4 中,用 Y 会比用 Z 好得多。”他们或许是对的,但如果我在项目 1 中就试图教给大家所有知识,你们会觉得难以承受且枯燥乏味,所以我们先构建了一个小型应用。之后在项目 2 中,我们又构建了第二个小型应用。接着是第三个、第四个,每个项目都在提升你的技能。
今天,你将开始项目 7,它目前无疑仍是一个小型应用。不过,在开发过程中,你将学会如何展示另一个屏幕、如何在不同屏幕间共享数据、如何加载和保存用户数据等等——这些功能能切实帮助你将 SwiftUI 技能提升到新的水平。
这并不意味着这个应用是完美的——正如你之后会了解到的,对于我们此处要实现的功能,UserDefaults 并非理想之选,像 SwiftData 这样更庞大、更复杂的工具会是更好的选择——但这也没关系。记住,我们的目标是先构建小型项目,再逐步推进,而不是一下子就投入到一个包罗万象的大型项目中。
如果你已准备就绪,那就开始吧!
今天你需要学习七个主题,从中你将了解 @Observable、sheet()、onDelete() 等内容。
- iExpense:介绍
- 结合类使用 @State
- 用 @Observable 共享 SwiftUI 状态
- 显示和隐藏视图
- 使用 onDelete() 删除项目
- 用 UserDefaults 存储用户设置
- 用 Codable 归档 Swift 对象
别忘了在网上分享你的进展——我们已经完成了课程的三分之一多,你做得很棒!
iExpense:介绍
作者:Paul Hudson 2023 年 1 月 31 日
接下来的两个项目将推动你的 SwiftUI 技能超越基础水平,我们会探索拥有多个屏幕、能加载和保存用户数据且用户界面更复杂的应用。
在本项目中,我们将构建 iExpense,这是一款费用追踪应用,能将个人费用与商务费用区分开。从核心功能来看,这款应用包含一个表单(你花了多少钱?)和一个列表(这些是你花费的金额),但要实现这两个功能,你需要学习如何:
- 展示和关闭第二个数据屏幕。
- 从列表中删除行。
- 保存和加载用户数据
……等等。
有很多工作要做,我们现在就开始:使用 App 模板创建一个新的 iOS 应用,将其命名为“iExpense”。我们将以这个应用作为主项目,但首先让我们仔细了解一下本项目所需的新技巧……
结合类使用 @State
作者:Paul Hudson 2023 年 10 月 29 日
SwiftUI 的 @State 属性包装器适用于当前视图本地的简单数据,但一旦你想共享数据,就需要采取一些重要的额外步骤。
我们通过代码来拆解这个问题——下面是一个用于存储用户姓名(名和姓)的结构体:
struct User {
var firstName = "比尔博"(Bilbo)
var lastName = "巴金斯"(Baggins)
}现在,我们可以在 SwiftUI 视图中使用它,只需创建一个 @State 属性,并将相关内容与 $user.firstName 和 $user.lastName 关联,代码如下:
struct ContentView: View {
@State private var user = User()
var body: some View {
VStack {
Text("你的名字是 \(user.firstName) \(user.lastName)。")
TextField("名", text: $user.firstName)
TextField("姓", text: $user.lastName)
}
}
}这样是可行的:SwiftUI 足够智能,能识别出一个对象包含了我们所有的数据,当任一值发生变化时,它会更新用户界面。在幕后,实际情况是,每当结构体内部的一个值发生变化时,整个结构体都会变化——就像我们每次输入名或姓的一个字符时,都会产生一个新的用户对象。这听起来可能有些浪费,但实际上速度非常快。
之前我们探讨过类和结构体的区别,其中有两个重要区别我提到过。第一,结构体始终有唯一的所有者,而多个对象可以指向同一个类的实例。第二,类中修改属性的方法不需要添加 mutating 关键字,因为你可以修改常量类的属性。
实际上,这意味着如果我们有两个 SwiftUI 视图,并且给它们都传递了同一个结构体,那么它们实际上各自拥有该结构体的一个唯一副本;如果其中一个视图修改了副本,另一个视图不会看到这种变化。另一方面,如果我们创建一个类的实例并将其传递给两个视图,那么这两个视图会共享变化。
对于 SwiftUI 开发者而言,这意味着如果我们想在多个视图之间共享数据——如果我们希望两个或更多视图指向同一份数据,以便当其中一个视图修改数据时,所有视图都能获取到这些变化——我们就需要使用类,而不是结构体。
因此,请将 User 结构体改为类。将以下代码:
struct User {改为:
class User {现在再次运行程序,看看会发生什么。
剧透一下:程序不再正常工作了。当然,我们仍然可以像之前一样在文本框中输入内容,但上方的文本视图不会发生变化。
当我们使用 @State 时,我们是在请求 SwiftUI 监视一个属性的变化。所以,如果你修改了一个字符串、切换了一个布尔值、向数组中添加了元素等等,该属性就会发生变化,SwiftUI 会重新调用视图的 body 属性。
当 User 是结构体时,每当我们修改该结构体的一个属性,Swift 实际上会创建一个新的结构体实例。@State 能够检测到这种变化,并自动重新加载我们的视图。现在 User 变成了类,这种行为就不再发生了:Swift 可以直接修改值。
还记得为什么结构体中修改属性的方法需要添加 mutating 关键字吗?这是因为如果我们将结构体的属性设为变量,但结构体本身是常量,那么我们就无法修改这些属性——当属性发生变化时,Swift 需要销毁并重新创建整个结构体,而对于常量结构体来说,这是不可能的。类则不需要 mutating 关键字,因为即使类的实例被标记为常量,Swift 仍然可以修改其变量属性。
我知道这些听起来非常理论化,但关键问题在于:现在 User 是类,属性本身没有发生变化,所以 @State 检测不到任何变化,也就无法重新加载视图。是的,类内部的值确实在变化,但 @State 不会监视这些内部值的变化,所以实际上发生的情况是,类内部的值在变化,但视图没有重新加载以反映这种变化。
我们只需一个小小的修改就能解决这个问题:我希望你在类的前面添加 @Observable 这一行代码。修改后的代码应如下所示:
@Observable
class User {现在我们的代码又能正常工作了。要理解其中的原因,让我们来探究一下 @Observable 到底有什么作用……
用 @Observable 共享 SwiftUI 状态
作者:Paul Hudson 2023 年 10 月 29 日
如果你将 @State 与结构体结合使用,当值发生变化时,SwiftUI 视图会自动更新,但如果你将 @State 与类结合使用,那么要让 SwiftUI 监视类内部内容的变化,就必须用 @Observable 标记该类。
为了更深入地理解这一机制,让我们仔细看看下面这段代码:
@Observable
class User {
var firstName = "比尔博"(Bilbo)
var lastName = "巴金斯"(Baggins)
}这是一个包含两个字符串变量的类,但它以 @Observable 开头。这会告诉 SwiftUI 监视该类内部的每个属性,当任一属性发生变化时,重新加载所有依赖于该属性的视图。这听起来可能有点像魔法,但并非如此——它只是隐藏了大量复杂的工作。
我想在这里深入讲解一下,让你了解实际情况,为此,我希望你在顶部靠近 import SwiftUI 的位置再添加一行 import 代码:
import Observation@Observable 这一行是一个宏,这是 Swift 用于悄悄重写我们的代码以添加额外功能的方式。现在我们已经导入了它所属的框架,Xcode 可以实现一个非常棒的功能:如果你在代码中右键点击 @Observable,选择“展开宏”(Expand Macro),就能看到具体重写了哪些内容——Xcode 会向你展示所有生成的隐藏代码。
我不会在这里写出完整的宏展开内容,因为内容太多,但我想指出其中三点:
- 我们的两个属性被标记为 @ObservationTracked,这意味着 Swift 和 SwiftUI 会监视它们的变化。
- 如果你右键点击 @ObservationTracked,也可以展开这个宏——没错,宏中还嵌套着宏。这个宏的作用是跟踪每个属性的读取和写入操作,以便 SwiftUI 只更新那些确实需要刷新的视图。
- 我们的类被设置为遵循 Observable 协议。这一点很重要,因为 SwiftUI 的某些部分会通过这个协议来识别“这个类可以被监视变化”。
这三点都很重要,但中间那一点承担了主要工作:iOS 会跟踪所有从 @Observed 对象中读取属性的 SwiftUI 视图,这样当某个属性发生变化时,它就能智能地更新所有依赖于该属性的视图,而不影响其他视图。
当使用结构体时,@State 属性包装器会保持值的存在,并监视其变化。另一方面,当使用类时,@State 仅用于保持对象的存在——所有监视变化和更新视图的工作都由 @Observable 负责。
显示和隐藏视图
作者:Paul Hudson 2023 年 10 月 29 日
在 SwiftUI 中,显示视图有多种方式,其中最基础的一种是“工作表”(sheet):在现有视图之上呈现一个新视图。在 iOS 上,这种方式会自动提供一种类似卡片的呈现效果——当前视图会略微向远处滑动,新视图则会在顶部以动画形式进入屏幕。
工作表的工作方式与警告框(alert)类似,我们不能通过 mySheet.present() 之类的代码直接呈现它们。相反,我们需要定义工作表应显示的条件,当这些条件为真或为假时,工作表会相应地呈现或关闭。
让我们从一个简单的例子开始,通过工作表从一个视图跳转到另一个视图。首先,我们创建要在工作表中显示的视图,代码如下:
struct SecondView: View {
var body: some View {
Text("第二个视图")
}
}这个视图没有任何特别之处——它并不知道自己将在工作表中显示,也不需要知道这一点。
接下来,我们创建初始视图,它将用于显示第二个视图。我们先把它写得简单一些,之后再进行扩展:
struct ContentView: View {
var body: some View {
Button("显示工作表") {
// 显示工作表
}
}
}要完成这个功能,需要四个步骤,我们将逐一处理。
第一步,我们需要一些状态来跟踪工作表是否正在显示。和警告框一样,我们可以用一个简单的布尔值来表示,现在就给 ContentView 添加这个属性:
@State private var showingSheet = false第二步,当我们点击按钮时,切换这个布尔值的状态,所以将“// 显示工作表”这行注释替换为以下代码:
showingSheet.toggle()第三步,我们需要在视图层级中的某个位置附加工作表。如果你还记得,我们通过 isPresented 并结合与状态属性的双向绑定来显示警告框,这里的做法几乎完全相同:使用 sheet(isPresented:)。
sheet() 和 alert() 一样,都是修饰符,所以现在请给我们的按钮添加这个修饰符:
.sheet(isPresented: $showingSheet) {
// 工作表的内容
}第四步,我们需要确定工作表中实际应包含什么内容。在这个例子中,我们很清楚自己想要什么:创建并显示 SecondView 的一个实例。用代码表示就是编写 SecondView(),然后……嗯,这样就可以了。
因此,完整的 ContentView 结构体应如下所示:
struct ContentView: View {
@State private var showingSheet = false
var body: some View {
Button("显示工作表") {
showingSheet.toggle()
}
.sheet(isPresented: $showingSheet) {
SecondView()
}
}
}现在运行程序,你会发现点击按钮后,第二个视图会从屏幕底部向上滑动进入视野,之后你可以向下拖动它来关闭。
当你像这样创建视图时,可以向其传递任何它需要的参数。例如,我们可以要求 SecondView 接收一个要显示的名称,代码如下:
struct SecondView: View {
let name: String
var body: some View {
Text("你好,\(name)!")
}
}现在,在工作表中仅使用 SecondView() 就不够了——我们需要传递一个字符串作为名称。例如,我们可以像这样传递我的 Twitter 用户名:
.sheet(isPresented: $showingSheet) {
SecondView(name: "@twostraws")
}现在工作表会显示“你好,@twostraws”。
Swift 在幕后为我们做了大量工作:一旦我们声明 SecondView 有一个 name 属性,Swift 就会确保我们的代码在所有 SecondView() 实例都改为 SecondView(name: "某个名称") 之前无法通过编译,这就避免了一系列可能出现的错误。
在继续之前,我还想演示一件事,那就是如何让一个视图自行关闭。是的,你已经知道用户可以向下滑动来关闭视图,但有时你可能希望通过编程方式关闭视图——例如,当用户点击某个按钮时,让视图消失。
要关闭另一个视图,我们需要用到另一个属性包装器——是的,我知道在 SwiftUI 中,解决问题的方法往往是使用另一个属性包装器。
不管怎样,这个新的属性包装器叫做 @Environment,它允许我们创建存储外部提供的值的属性。用户处于浅色模式还是深色模式?他们是否要求使用更小或更大的字体?他们所在的时区是什么?所有这些以及更多信息都是来自环境的值,在这个例子中,我们将请求环境来关闭我们的视图。
是的,我们需要请求环境来关闭视图,因为视图的呈现方式可能有很多种。所以,我们实际上是在说:“嘿,弄清楚我的视图是如何呈现的,然后以合适的方式关闭它。”
要尝试这个功能,请给 SecondView 添加以下属性,它会基于环境中的一个值创建一个名为 dismiss 的属性:
@Environment(\.dismiss) var dismiss现在,将 SecondView 中的文本视图替换为以下按钮:
Button("关闭") {
dismiss()
}这样一来,有了这个按钮,你就可以通过点击按钮来显示和隐藏工作表了。
使用 onDelete() 删除项目
作者:Paul Hudson 2023 年 10 月 29 日
SwiftUI 为我们提供了 onDelete() 修饰符,用于控制如何从集合中删除对象。实际上,它几乎专门用于 List 和 ForEach:我们创建一个列表,列表中的行通过 ForEach 来显示,然后将 onDelete() 附加到这个 ForEach 上,这样用户就可以删除他们不想要的行。
这是 SwiftUI 又一个为我们承担大量工作的场景,但正如你将看到的,它也有一些有趣的特殊之处。
首先,让我们构建一个示例来进行操作:一个显示数字的列表,每次点击按钮都会添加一个新数字。代码如下:
struct ContentView: View {
@State private var numbers = [Int]()
@State private var currentNumber = 1
var body: some View {
VStack {
List {
ForEach(numbers, id: \.self) {
Text("第 \($0) 行")
}
}
Button("添加数字") {
numbers.append(currentNumber)
currentNumber += 1
}
}
}
}你可能会认为不需要 ForEach——列表完全由动态行组成,所以我们可以这样写:
List(numbers, id: \.self) {
Text("第 \($0) 行")
}这样写确实也能工作,但这里有个特殊之处:onDelete() 修饰符只存在于 ForEach 中,所以如果我们希望用户能从列表中删除项目,就必须将项目放在 ForEach 内部。对于只有动态行的情况,这确实意味着要多写少量代码,但另一方面,这也使得创建只有部分行可删除的列表变得更容易。
为了让 onDelete() 正常工作,我们需要实现一个方法,该方法接收一个类型为 IndexSet 的参数。IndexSet 有点像整数集合,但它是有序的,它只是告诉我们 ForEach 中所有需要删除的项目的位置。
由于我们的 ForEach 完全是从一个数组创建的,所以实际上我们可以直接将这个索引集传递给我们的 numbers 数组——数组有一个专门的 remove(atOffsets:) 方法,该方法接受一个索引集作为参数。
因此,现在给 ContentView 添加以下方法:
func removeRows(at offsets: IndexSet) {
numbers.remove(atOffsets: offsets)
}最后,我们可以告诉 SwiftUI,当它想要从 ForEach 中删除数据时调用这个方法,只需将 ForEach 修改为以下代码:
ForEach(numbers, id: \.self) {
Text("第 \($0) 行")
}
.onDelete(perform: removeRows)现在运行应用,添加几个数字。准备好后,在列表中的任意一行上从右向左滑动,你会看到出现一个“删除”按钮。你可以点击这个按钮,也可以通过进一步滑动来使用 iOS 的滑动删除功能。
考虑到实现起来如此简单,我认为这个功能的效果非常好。但 SwiftUI 还有一个巧妙的设计:我们可以在导航栏中添加一个“编辑/完成”(Edit/Done)按钮,让用户更轻松地删除多行。
首先,将你的 VStack 包裹在 NavigationStack 中,然后给 VStack 添加以下修饰符:
.toolbar {
EditButton()
}这真的就是全部步骤了——运行应用后,你会发现可以添加一些数字,然后点击“编辑”(Edit)开始删除这些行。完成后,点击“完成”(Done)退出编辑模式。只需这么少的代码就能实现,真是不错!
用 UserDefaults 存储用户设置
作者:Paul Hudson 2023 年 10 月 29 日
大多数用户几乎都期望应用能够存储他们的数据,以便获得更个性化的体验,因此 iOS 为我们提供多种读写用户数据的方式也就不足为奇了。
存储少量数据的一种常用方式是使用 UserDefaults,它非常适合存储简单的用户偏好设置。对于“少量数据”并没有一个具体的数量标准,但你存储在 UserDefaults 中的所有数据都会在应用启动时自动加载——如果在其中存储过多数据,应用的启动速度会变慢。给你一个大致的参考,你应该尽量保证存储的数据不超过 512KB。
提示: 如果你在想“512KB?那到底是多少呢?”,我可以给你一个粗略的估计:大概相当于你在本书中读到的所有章节的文字总量。
UserDefaults 非常适合存储诸如用户上次启动应用的时间、他们上次阅读的新闻报道或其他被动收集的信息之类的数据。更棒的是,SwiftUI 通常可以将 UserDefaults 封装在一个简洁的属性包装器中,这个包装器叫做 @AppStorage——目前它仅支持部分功能,但确实非常实用。
闲话少说,让我们来看一些代码。下面是一个带有按钮的视图,按钮会显示点击次数,每次点击按钮,次数就会增加 1:
struct ContentView: View {
@State private var tapCount = 0
var body: some View {
Button("点击次数:\(tapCount)") {
tapCount += 1
}
}
}显然,这是一款“非常重要的应用”,我们希望保存用户的点击次数,这样当用户以后再次打开应用时,就能从上次停止的地方继续。
要实现这个功能,我们需要在按钮的动作闭包中写入 UserDefaults。因此,在 tapCount += 1 这行代码之后添加以下内容:
UserDefaults.standard.set(tapCount, forKey: "Tap")仅仅这一行代码中,你就能看到三个要点:
- 我们需要使用 UserDefaults.standard。这是与我们的应用关联的内置 UserDefaults 实例,但在更复杂的应用中,你可以创建自己的实例。例如,如果你想在多个应用扩展之间共享默认值,就可以创建自己的 UserDefaults 实例。
- 有一个专门的 set() 方法,它可以接受任何类型的数据——整数、布尔值、字符串等等。
- 我们给这些数据附加一个字符串名称,在这个例子中是键“Tap”。这个键是区分大小写的,就像普通的 Swift 字符串一样,而且非常重要——我们需要使用相同的键才能从 UserDefaults 中读取数据。
说到读取数据,我们不应再将 tapCount 的初始值设为 0,而是应该从 UserDefaults 中读取值,代码如下:
@State private var tapCount = UserDefaults.standard.integer(forKey: "Tap")注意,这里使用的键名与之前完全相同,这样才能确保读取到正确的整数值。
现在运行应用试试——你应该可以点击按钮几次,然后回到 Xcode,再次运行应用,就能看到点击次数保持在上次结束时的数值。
这段代码中有两点你无法直接看到,但都很重要。第一,如果我们没有设置“Tap”这个键会发生什么?第一次运行应用时就会出现这种情况,但正如你刚才看到的,程序仍然能正常工作——如果找不到指定的键,它会返回 0。
有时候,像 0 这样的默认值会很有用,但其他时候可能会造成混淆。例如,对于布尔值,如果 boolean(forKey:) 找不到你指定的键,就会返回 false,但这个 false 是你自己设置的值,还是因为根本没有这个值呢?
第二,iOS 需要一点时间才能将你的数据写入永久存储——也就是真正将这些变化保存到设备上。系统不会立即写入更新,因为你可能会连续进行多次修改,所以系统会等待一段时间,然后一次性写入所有变化。具体等待多长时间我们并不清楚,但几秒钟应该就足够了。
因此,如果你点击按钮后立即从 Xcode 重新启动应用,会发现最近一次的点击次数没有被保存。以前有办法强制立即写入更新,但现在已经没用了——即使用户在做出选择后立即开始终止你的应用,你的默认数据也会被立即写入,所以不会有任何数据丢失。
现在,我提到过 SwiftUI 提供了一个围绕 UserDefaults 的 @AppStorage 属性包装器,在像这样的简单场景中,它非常实用。它的作用是让我们可以完全忽略 UserDefaults,只需使用 @AppStorage 而不是 @State,代码如下:
struct ContentView: View {
@AppStorage("tapCount") private var tapCount = 0
var body: some View {
Button("点击次数:\(tapCount)") {
tapCount += 1
}
}
}同样,这里有三点我想指出:
- 我们通过 @AppStorage 属性包装器来访问 UserDefaults 系统。它的工作方式与 @State 类似:当值发生变化时,它会重新调用 body 属性,以便我们的用户界面反映新的数据。
- 我们附加了一个字符串名称,这是我们想要在 UserDefaults 中存储数据的键。我使用的是“tapCount”,但它可以是任何名称——不一定需要与属性名相同。
- 属性的其他部分按正常方式声明,包括提供默认值 0。如果 UserDefaults 中没有已保存的值,就会使用这个默认值。
显然,使用 @AppStorage 比直接使用 UserDefaults 更简单:它只需一行代码,而不是两行,而且还意味着我们不必每次都重复键名。不过,目前 @AppStorage 还无法轻松处理存储复杂对象(如 Swift 结构体)的情况——这可能是因为苹果希望我们记住,在 UserDefaults 中存储大量数据是不可取的!
重要提示: 当你向 App Store 提交应用时,苹果会要求你说明为什么要使用 UserDefaults 加载和保存数据。这一点也适用于 @AppStorage 属性包装器。这没什么好担心的,他们只是想确保开发者不会试图跨应用识别用户。
用 Codable 归档 Swift 对象
作者:Paul Hudson 2023 年 10 月 29 日
@AppStorage 非常适合存储整数、布尔值等简单设置,但当涉及到复杂数据(例如自定义 Swift 类型)时,我们需要多做一些工作。这时,我们就需要直接使用 UserDefaults 本身,而不是通过 @AppStorage 属性包装器。
下面是一个我们可以用来操作的简单 User 数据结构:
struct User {
let firstName: String
let lastName: String
}它包含两个字符串,但这两个字符串并没有什么特别之处——它们只是普通的文本。整数(普通数字)、布尔值(真或假)和 Double(普通数字,只是中间可能有一个小数点)也是如此。即使是由这些类型组成的数组和字典也很容易理解:一个字符串之后是另一个字符串,再之后是第三个,依此类推。
处理这类数据时,Swift 为我们提供了一个出色的协议,叫做 Codable:这个协议专门用于归档和反归档数据,说白了就是“将对象转换为纯文本,再将纯文本转换回对象”。
在未来的项目中,我们会更深入地学习 Codable,但目前我们会尽量简化:我们希望将一个自定义类型归档,以便将其存入 UserDefaults,然后在从 UserDefaults 中读取数据时进行反归档。
对于仅包含简单属性(字符串、整数、布尔值、字符串数组等)的类型,要支持归档和反归档,我们只需让该类型遵循 Codable 协议即可,代码如下:
struct User: Codable {
let firstName: String
let lastName: String
}Swift 会自动为我们生成一些代码,用于根据需要对 User 实例进行归档和反归档,但我们仍然需要告诉 Swift 何时进行归档,以及如何处理归档后的数据。
这一过程由一个名为 JSONEncoder 的新类型来完成。它的作用是接收一个遵循 Codable 协议的对象,并返回该对象的 JavaScript 对象表示法(JSON)格式数据——从名称上看,它似乎专门用于 JavaScript,但实际上我们都在使用它,因为它速度快且结构简单。
Codable 协议并不要求我们必须使用 JSON,实际上也有其他格式可供选择,但 JSON 是目前最常用的。在这个例子中,我们其实并不关心使用的是哪种格式,因为它只是要被存储在 UserDefaults 中。
要将我们的 user 数据转换为 JSON 数据,我们需要调用 JSONEncoder 的 encode() 方法。这个方法可能会抛出错误,因此应该使用 try 或 try? 来妥善处理错误。例如,如果我们有一个用于存储 User 实例的属性,代码如下:
@State private var user = User(firstName: "泰勒"(Taylor), lastName: "斯威夫特"(Swift))那么我们可以创建一个按钮,用于归档用户数据并将其保存到 UserDefaults 中,代码如下:
Button("保存用户") {
let encoder = JSONEncoder()
if let data = try? encoder.encode(user) {
UserDefaults.standard.set(data, forKey: "UserData")
}
}这里直接访问了 UserDefaults,而没有通过 @AppStorage,因为 @AppStorage 属性包装器在这种情况下无法使用。
这个 data 常量是一种新的数据类型,不巧的是,它也叫做 Data(数据)。它用于存储各种类型的数据,如字符串、图像、压缩文件等等。不过,在这个例子中,我们只关心它是一种可以直接写入 UserDefaults 的数据类型。
当我们需要反向操作时——也就是有了 JSON 数据,想要将其转换回 Swift 的 Codable 类型时——我们应该使用 JSONDecoder 而不是 JSONEncoder(),但操作过程非常相似。
到这里,我们的项目概述就结束了,现在请将你的项目重置到初始状态,为后续的开发做好准备。