第8章:保存设置¶
每当你的应用程序关闭时,所有输入的数据,如你设置的任何评级或你记录的任何历史,都会丢失。大多数应用程序要想发挥作用,就必须在应用程序会话之间保持数据。数据持久化是将数据保存到永久存储的一种华丽的说法。
在本章中,你将探索如何使用AppStorage
和SceneStorage
来存储简单的数据。你将保存练习评级,如果你在练习中被叫走,你的应用程序将记住你在哪个练习中,并在那里开始,而不是在欢迎界面。
你还会了解到如何在Swift
字典中存储数据,并意识到字符串操作是很复杂的。
数据持久性¶
根据你要保存的数据类型,有不同的方法来持久化你的数据:
UserDefaults
:使用它来保存一个应用程序的用户偏好。这将是一个保存评级的好方法。Property List file
:一个macOS
和iOS
的设置文件,存储序列化的对象。串行化意味着将对象翻译成可以存储的格式。这将是一个存储历史数据的好格式,你将在下一章做这个。JSON file
:一种开放的标准文本文件,用于存储序列化的对象。你将在第二节中使用这种格式。Core Data
:一个具有macOS
和iOS
框架的对象图,用于存储对象。更多信息,请查看我们的Core Data
教程一书,https://bit.ly/39lo2k3。
将评级保存到UserDefaults
中¶
本节将学习的技能:AppStorage
; UserDefaults
.
UserDefaults
是一个类,它能够在一个属性列表(plist
)文件中存储和检索数据,该文件与你的应用程序的沙盒数据一起保存。它被称为defaults
,因为你应该只将UserDefaults
用于简单的应用程序范围的设置。你不应该存储诸如你的历史记录等数据,随着时间的推移,这些数据会越来越大。
➤ 继续上一章的最终项目或打开本章启动文件夹中的项目。
到目前为止,你已经在预览中使用了iPad
。请记住,使用iPhone
也一样可以测试你的应用程序。为了测试数据的持久性,你需要在模拟器中运行该应用,这样你就可以检查磁盘上的实际数据。
➤ 点击运行目标按钮,选择iPhone 12 Pro
。
AppStorage
¶
@AppStorage
是一个属性包装器,类似于@State
和@Binding
,它允许UserDefaults
和你的SwiftUI
视图之间的互动。
你设置了一个评级视图,允许用户对练习的难度从1
到5
进行评级。你要把这个评级保存到UserDefaults
中,这样你的评级就不会在你关闭应用程序时消失。
rating
的真实来源目前在ExerciseView.swift
中,在那里你为它设置了一个状态属性。
➤ 打开ExerciseView.swift
,将@State private var rating = 0
改为:
@AppStorage("rating") private var rating = 0
属性包装器@AppStorage
会把对rating
的任何修改保存到UserDefaults
中。你保存到UserDefaults
的每一个数据都需要一个唯一的键。使用@AppStorage
,你提供这个键是一个带引号的字符串,在这个例子中是rating
。
➤ 建立和运行,并选择一个练习。点选评级视图,为练习打分。UserDefaults
现在存储你的评分。
AppStorage
只允许几种类型:String
, Int
, Double
, Data
, Bool
和 URL
. 对于简单的数据,如用户可配置的应用程序设置,用AppStorage
将数据存储到UserDefaults
是非常容易的。
Note
尽管UserDefaults
存储在一个相当安全的目录中,但你不应该在那里保存敏感数据,如登录信息和其他认证代码。对于这些,你应该使用钥匙串:https://apple.co/3evbAkA。
➤ 在模拟器中停止该应用程序,从底部向上滑动。然后,在Xcode
中,再次运行该应用并进入相同的练习。你的评分在两次启动之间持续存在。
Note
当使用@AppStorage
和@SceneStorage
时,一定要确保在Xcode
中终止应用程序之前在模拟器或设备上退出应用程序。你的应用程序可能不会保存数据,直到系统通知它状态的改变。
你已经解决了数据持久性的问题,但引起了另一个问题。不幸的是,由于你对所有的评级只有一个评级键,你在UserDefaults
中只存储了一个值。当你进入另一个练习时,它的评分与第一个练习相同。如果你设置了一个新的评级,所有其他的练习都有相同的评级。
你真的需要存储一个评级数组,每个练习都有一个条目。例如,一个[1, 4, 3, 2]
的数组将存储练习1
到4
的单个评级值。 在解决这个问题之前,你会发现Xcode
是如何存储应用程序数据的。
数据目录¶
本节将学习的技能:应用包中的内容;数据目录;FileManager
;属性列表文件;Dictionary
。
当你在模拟器中运行你的应用程序时,Xcode
会创建一个沙盒目录,包含一组标准的子目录。沙盒是一种安全措施,意味着没有其他应用程序能够访问你的应用程序的文件。
反之,你的应用程序将只能读取iOS
允许的文件,而你将无法读取任何其他应用程序的文件。
应用程序Bundle
包¶
在你的应用程序沙盒内有两组目录。首先,你将检查应用包,然后是用户数据。
➤ 在Xcode
中,打开组列表底部的产品组。当您构建您的应用程序时,Xcode
将创建一个与项目名称相同的产品。
➤ 如果您的应用程序的名称是红色的,请构建该项目,以确保编译后的项目存在于磁盘上。
➤ 按住Control-click HIITFit.app
,并选择在Finder
中显示。
你在这里看到模拟器的应用程序的调试目录。
➤ 在Finder
中,Control-click HIITFit
并选择显示软件包内容。
该应用程序bundle
包包含:
- 当前模拟器的应用程序图标。
- 任何不在
Assets.xcassets
中的应用程序资产。在这个应用程序中,有四个锻炼视频。 HIITFit
可执行文件。- 来自
Assets.xcassets
的优化资产,保存在Assets.car
中。 - 设置文件 -
Info.plist
,PkgInfo
等。
应用程序bundle
包是只读的。一旦设备加载了你的应用程序,你就不能改变应用程序内的任何这些文件的内容。如果你有一些默认的数据包含在你的bundle
文件中,你的用户应该能够改变这些数据,你将需要在你的用户第一次安装后运行应用程序时将bundle
数据复制到用户数据目录中。
Note
这将是UserDefaults
的另一个好用途。当你运行应用程序时,存储一个布尔值--或字符串版本号--来标记该应用程序已经运行了。然后你可以检查这个标志或版本号,看看你的应用程序是否需要做任何内部更新。
在用Bundle.main.url(forResource:withExtension:)
加载你的视频文件时,你已经使用了bundle
文件。一般来说,你不需要查看磁盘上的bundle
文件,但是,如果你的应用程序由于某种原因无法加载bundle
文件,去查看应用程序中包含的实际文件并做一个理智的检查是很有用的。例如,很容易忘记在文件检查器中检查一个文件的目标成员。在这种情况下,该文件就不会被包含在应用程序包中。
用户数据目录¶
你最需要检查的文件和目录是你的应用程序在执行过程中创建和更新的那些文件。
FileManager
类¶
你使用FileManager
与文件系统对接。这允许你做所有你期望的文件操作,如检查、移动、删除和复制文件和目录。
➤ 打开HIITFitApp.swift
,在ContentView
中添加这个修改器:
.onAppear {
print(FileManager.default.urls(
for: .documentDirectory,
in: .userDomainMask))
}
你的应用程序将在每次视图最初出现时运行onAppear(perform:)
。
这里你使用共享文件管理器对象来列出指定目录的URL
。有许多重要的目录,你可以在https://apple.co/3pTE3U5的文档中找到这些目录。请记住,你的应用程序是沙盒的,每个应用程序将有自己的应用程序目录。
➤ 在模拟器中构建和运行。调试控制台将打印出指定域中的文档目录路径的URL
数组。这里的域,userDomainMask
,是用户的主目录。
Swift
他结果是一个URL
阵列,而不是一个单一的URL
,因为你可以要求一个域掩码阵列。这些域可以包括localDomainMask
,用于机器上每个人都可以使用的项目,networkDomainMask
用于网络上的项目,systemDomainMask
用于苹果系统文件。
➤ 突出显示从/Users..
到/Documents/
,然后控制点击选择。Choose Services ▸ Show in Finder
。
这将打开一个新的Finder
窗口,显示Simulator
的用户目录:
➤ 父目录 - 在这个例子中,47887...524CF -
包含该应用程序的沙盒用户目录。你会看到其他目录也是用UUID
命名的,这些目录属于你可能做过的其他应用。选择父目录并把它拖到你的收藏夹侧边栏,这样你就可以快速访问它。
在你的应用程序中,你可以访问这些目录中的一些:
documentDirectory
:Documents/.
应用程序的主要文档目录。libraryDirectory
:Library/.
用于存放不想暴露给用户的文件的目录。cachesDirectory
:Library/Caches/.
·暂时的缓存文件。如果你展开一个压缩文件并在你的应用程序中临时访问其内容,你可能会使用它。
iPhone
和iPad
的备份将保存文档和库,不包括Library/Caches
。
在一个属性列表文件内¶
@AppStorage
将你的rating
保存在一个UserDefaults
属性列表文件中。一个属性列表文件是一种XML
文件格式,用于存储结构化文本。所有的属性列表文件都包含一个Dictionary
或Array
类型的Root
,这个根包含一个带有数值的键的分层列表。
例如,你可以将HIITFit
的练习分布在一个数组中,而不是将它们存储在一个属性列表文件中,并在应用程序开始时将它们读入一个数组:
这样做的好处是,在未来的版本中,你可以添加应用内练习的购买,并在属性列表文件中跟踪购买的练习。
Xcode
将属性列表文件格式化为可读格式。这就是上面的属性列表文件的文本版本:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<dict>
<key>exerciseName</key>
<string>Squat</string>
<key>videoName</key>
<string>squat</string>
</dict>
<dict>
<key>exerciseName</key>
<string>Sun Salute</string>
<key>videoName</key>
<string>sun-salute</string>
</dict>
</array>
</plist>
这个文件的root
是一个数组,包含两个练习。每个练习都是Dictionary
类型,有练习属性的键值。
Swift Dive: Dictionary
¶
Dictionary
是一个哈希表,由一个可哈希的键和一个值组成。一个可散列的键是一个可以转化为数字的散列值的键,它允许使用该键在表中快速查找。
例如,你可以创建一个Dictionary
来保存练习的评级:
var ratings = ["burpee": 4]
这是一个[String : Integer]
类型的Dictionary
,其中burpee
是key
,4
是value
。
你可以用多个值进行初始化并添加新的值:
var ratings = ["step-up": 2, "sun-salute": 3]
ratings["squat"] = 5 // ratings now contains three items
最后一张图片来自Swift Playground
,它向你展示了字典与数组不同,没有保证顺序。游乐场显示的顺序与创建的顺序是不同的。
如果你以前没有使用过Swift Playgrounds
,它们对于测试代码片段是很有趣和有用的。你将在第24章"下载数据"中使用一个playground
。
你可以使用key
来检索value
:
let rating = ratings["squat"] // rating = 5
UserDefaults
属性列表文件¶
➤ 在Finder
中,打开你的应用程序的沙盒,找到Library/Preferences
。在该目录中,打开com.raywenderlich.HIITFit.plist
。这是UserDefaults
文件,你的评级被储存在这里。你的应用程序在你第一次存储rating
时自动创建了这个文件。
这个文件有一个条目:rating
,Number
值为1
,这是评级值,可以从1
到5
。
属性列表文件可以包含:
Dictionary
Array
String
Number
Boolean
Data
Date
有了所有这些类型,你可以看到直接存储到属性列表文件比@AppStorage
更灵活,它不支持字典或数组。你可以决定,为了存储你的评级数组,也许@AppStorage
终究不是办法。但是等等--你所要做的就是进行一些数据处理。你可以将你的整数等级存储为一个字符数组,也称为"字符串"。
你最初会把评分存储为一个"0000"
的字符串。当你需要,例如,第一个练习,你将读取字符串中的第一个字符。当你点击一个新的评级时,你将把新的评级存储回第一个字符。
这是可扩展的。如果你增加了更多的练习,你只需拥有一个更长的字符串。
Swift Dive: Strings
¶
本节中你将学习的技能:Unicode
; String
索引; nil
凝聚运算符; String
字符替换
字符串并不像它们看起来那么简单。为了支持对表情符号不断增长的需求,一个字符串是由扩展的字素群组成的。这些是一连串的Unicode
值,在平台支持的地方显示为单个字符。它们用于一些语言字符,也用于各种表情符号标签序列,例如肤色和旗帜。
威尔士旗使用七个标签序列来构建单字符🏴。在不支持标签序列的平台上,该旗帜将显示为黑色旗帜🏴。
一个String
是这些字符的集合,非常类似于一个Array
。字符串的每个元素都是一个Character
,类型为String.Element
。
就像数组一样,你可以使用for
循环来迭代一个字符串:
for character in "Hello World" {
print(character) // console shows each character on a new line
}
由于字符串的复杂性质,你不能直接对一个String
进行索引。但是,你可以使用索引进行下标操作。
let text = "Hello World"
let seventh = text[text.index(text.startIndex, offsetBy: 6)]
// seventh = "W"
text.index(_:offsetBy:)
返回一个String.Index
。然后你可以在方括号中使用这个特殊的索引,就像使用一个数组一样:text[specialIndex]
。
你很快就会看到,你也可以用String.insert(contentsOf:at:)
将一个String
插入到另一个String
的索引中,并用String.insert(_:at:)
将一个Character
插入到一个String
中。
Note
你可以做这么多的字符串操作,以至于你需要使用Use Your Loaf
的Swift String cheat sheet
,网址是https://bit.ly/3aGRjWp。
保存评级¶
现在你要存储ratings
,RatingView
是比ExerciseView
更好的真实来源。与其在ExerciseView
中存储ratings
,不如将当前练习的索引传递给RatingView
,然后它可以读写评分。
➤ 打开ExerciseView.swift
,并删除:
@AppStorage("rating") private var rating = 0
➤ 在body
的末尾,也就是显示编译错误的地方,将RatingView(rating: $rating)
改为:
RatingView(exerciseIndex: index)
你把当前练习的索引传递给评级视图。你会得到一个编译错误,直到你修复RatingView
。
➤ 打开RatingView.swift
并替换@Binding var rating: Int
改为:
let exerciseIndex: Int
@AppStorage("ratings") private var ratings = "0000"
@State private var rating = 0
在这里,你在本地持有rating
,并将ratings
设置为四个零的字符串。
预览持有它自己版本的@AppStorage
,这可能很难清除。
➤ 将RatingViewPreviews
替换为:
struct RatingView_Previews: PreviewProvider {
@AppStorage("ratings") static var ratings: String?
static var previews: some View {
ratings = nil
return RatingView(exerciseIndex: 0)
.previewLayout(.sizeThatFits)
}
}
要从Preview
UserDefaults
中删除一个键,你需要将其设置为nil
值。只有可选的类型可以持有nil
,所以你把ratings
定义为String?
,其中的?
标志着该属性是可选的。然后你可以设置@AppStorage ratings
为nil
值,确保你的Preview
不会加载之前的值。你将在下一章中再看一下选项。
你从Preview
传入练习索引,所以你的应用程序现在应该编译了。
从一个字符串中提取评级¶
➤ 在body
中,为Image
添加一个新的修改器:
// 1
.onAppear {
// 2
let index = ratings.index(
ratings.startIndex,
offsetBy: exerciseIndex)
// 3
let character = ratings[index]
// 4
rating = character.wholeNumberValue ?? 0
}
Swift
可以说是一种非常简洁的语言,在这段短短的代码中,有很多东西需要解读。
- 你的应用程序在每次视图最初出现时运行
onAppear(perform:)
。 ratings
被标记为@AppStorage
,所以它的值被存储在UserDefaults
属性列表文件中。你创建了一个String.Index
来使用exerciseIndex
对字符串进行索引。- 这里你使用
String.Index
从字符串中提取正确的字符。 - 将该字符转换为整数。如果该字符不是一个整数,
wholeNumberValue
的结果将是一个可选的值nil
。这两个问号被称为nil
凝聚运算符。如果wholeNumberValue
的结果是nil
,那么就使用问号后的值,在这种情况下,就是0。你将在下一章中学习更多关于选项的知识。
➤ 预览视图。你存储的评分目前是0000
,你正在预览练习零。
➤ 将@AppStorage("rating") private var ratings = "0000"
改为:
@AppStorage("ratings") private var ratings = "4000"
➤ Resume
预览,练习零的评级变为四。
在一个字符串中存储评级¶
你现在正从AppStorage
中读取评级。为了将评分存储到AppStorage
中,你将对字符串进行索引并替换该索引中的字符。
给RatingView
添加一个新方法:
func updateRating(index: Int) {
rating = index
let index = ratings.index(
ratings.startIndex,
offsetBy: exerciseIndex)
ratings.replaceSubrange(index...index, with: String(rating))
}
这里你用exerciseIndex
创建一个String.Index
,就像你之前做的那样。你用index...index
创建一个RangeExpression
,用新的等级替换这个范围。
Note
你可以在https://apple.co/3qNxD8R的官方文档中找到更多关于RangeExpressions
的信息。
➤ 将onTapGesture
动作rating = index
改为:
updateRating(index: index)
➤ 建立和运行并替换你所有练习的所有评级。现在每个练习都有其单独的评级。
➤ 在Finder
中,检查应用程序库目录中的com.raywenderlich.HIITFit.plist
。
你可以从属性列表文件中删除rating
,因为你不再需要它了。存储在上述属性列表文件中的评级是:
- Squat: 3
- Step Up: 1
- Burpee: 2
- Sun Salute: 4
思考可能的错误¶
你将在本节中学习的技能:自定义初始化器。
你应该总是考虑到你的代码可能失败的方式。如果你试图从一个数组中获取一个超出范围的值,你的应用程序就会崩溃。对于字符串也是如此。如果你试图访问一个超出范围的字符串索引,你的应用程序就会陷入困境。这是一个灾难性的错误,因为用户不可能输入正确长度的字符串,所以你的应用程序会一直失败。由于你控制了ratings
字符串,这不太可能发生,但错误还是会发生,最好是避免灾难性的错误。
你可以在初始化RatingView
时确保字符串有正确的长度。
自定义初始化器¶
➤ 为RatingView
添加一个新的初始化器:
// 1
init(exerciseIndex: Int) {
self.exerciseIndex = exerciseIndex
// 2
let desiredLength = Exercise.exercises.count
if ratings.count < desiredLength {
// 3
ratings = ratings.padding(
toLength: desiredLength,
withPad: "0",
startingAt: 0)
}
}
翻阅代码:
- 如果你不自己定义
init()
,Xcode
会创建一个默认的初始化器来设置所有必要的属性。然而,如果你创建一个自定义的初始化器,你必须自己初始化它们。在这里,exerciseIndex
是一个必要的属性,所以你必须把它作为一个参数接收,并存储到RatingView
实例中。 ratings
必须有与你的练习一样多的字符。- 如果
ratings
太短,那么你就用零来填充这个字符串。
为了测试这一点,在模拟器中,选择Device ▸ Erase All Contents and Settings…
来完全删除应用程序并清除缓存。
在RatingView
中,将@AppStorage("Rates") private var ratings = "4000"
改为:
@AppStorage("ratings") private var ratings = ""
当AppStorage
创建UserDefaults
时,它将创建一个比你的练习次数少的字符串。
➤ 构建和运行并进入一个练习。然后在Finder
中找到你的应用程序。擦除所有内容和设置,创建一个全新的应用程序沙盒,所以打开控制台中打印的路径。
➤ 打开Library ▸ Preferences ▸ com.raywenderlich.HIITFit.plist
。ratings
将用零来填充。
多个场景¶
你将在本节中学习的技能:多个iPad
窗口。
也许你的伙伴、狗或猫想在同一时间锻炼。或者你只是对HIITFit
非常兴奋,你想在iPad
上同时查看两个练习。在iPad
分割视图中,你可以打开第二个窗口,这样你就可以比较你的深蹲和Burpee
。
首先确保你的应用程序支持多窗口。
➤ 在项目导航器中选择顶部的HIITFit
组。
➤ 选择HIITFit
目标,常规选项卡,在部署信息下找到支持多窗口。
➤ 确保该选项被选中。如果不勾选,就不能让自己的应用程序或自己的应用程序和另一个应用程序的两个窗口并排在一起。
➤ 在你的iPad
设备上或在iPad
模拟器上建立和运行HIITFit
。
使用Command-Right Arrow.
将模拟器转到横向。打开应用程序后,从底部边缘轻轻向上滑动以显示Dock
。你会在Dock
中看到HIITFit
的图标。把它拖到屏幕的右侧或左侧边缘,然后放下。你可以通过拖动窗口之间的分隔线来调整窗口的大小。
你现在有两个session
开放。
做出评级反应¶
➤ 在每个窗口上,转到练习1蹲下并改变评级。你会注意到有一个问题,因为尽管评级是用AppStorage
存储在UserDefaults
中的,但这些窗口反映了两个不同的评级。当你在一个窗口中更新评级时,另一个窗口中的评级应该立即反应。
➤ 打开RatingView.swift
并查看代码。
通过AppStorage
,你可以为每个应用程序保存一个rating
值,无论有多少个窗口打开。你在onTapGesture(count:execute:)
中改变ratings
并更新rating
。第二个窗口持有它自己的rating
实例。当你改变一个窗口的评级时,第二个窗口应该对这一变化做出反应,并更新和重新绘制它的评级视图。
如果你用来自AppStorage
的ratings
字符串,而不是提取的整数rating
来显示一个视图,AppStorage
会自动使视图失效并重新绘制。然而,因为你要将字符串转换为整数,你需要在改变rating
时执行该代码。
你在onAppear(perform:)
中执行的代码将被添加到一个新的onChange(of:perform:)
修改器中,该修改器将在ratings
改变时运行。你将创建一个新的方法,并调用该方法两次,而不是重复代码。
➤ 在onAppear(执行:)
中,强调:
let index = ratings.index(
ratings.startIndex,
offsetBy: exerciseIndex)
let character = ratings[index]
rating = character.wholeNumberValue ?? 0
➤ 按住Control
键单击突出显示的代码,并选择Refactor ▸ Extract to Method
。
➤ 将提取的方法命名为convertRating()
。
Swift
注意新方法中的fileprivate
访问控制修饰符。这个修改器只允许在RatingView.swift
内部访问convertRating()
。
➤ 为Image
添加一个新的修改器:
.onChange(of: ratings) { _ in
convertRating()
}
这里你设置了一个反应式方法,每当ratings
发生变化时,就调用convertRating()
。如果你只使用一个窗口,你不会注意到这个效果,但现在多个窗口可以对另一个窗口中的属性变化做出反应。
➤ 用两个窗口并排构建并运行该应用程序。在两个窗口中返回练习 1,并在一个窗口中更改评级。当您更改评级时,另一个窗口中的评级视图应立即重绘。
应用程序、场景和视图¶
本节将学习的技能:scenes
;@SceneStorage
。
你在模拟器中打开了HIITFit
的两个独立会话。如果这个应用程序在macOS
上运行,用户会期望能够打开任意数量的HIITFit
窗口。现在你将看看SwiftUI
如何处理多个窗口。
➤ 打开HIITFitApp.swift
并检查代码:
@main
struct HIITFitApp: App {
var body: some Scene {
WindowGroup {
ContentView()
...
}
}
}
这个简单的代码控制你的应用程序的执行。@main
属性表示应用程序的入口点,并期望该结构符合App
协议。
HIITFitApp
定义了你的应用程序的基本层次结构,由以下部分组成:
HITTFitApp
:符合App
,它代表整个应用程序。WindowGroup
:符合Scene
协议。一个WindowGroup
呈现一个或多个窗口,它们都包含相同的视图层次。ContentView
:你在SwiftUI
应用程序中看到的所有东西都是一个View
。尽管SwiftUI
模板创建了ContentView
,但它是一个占位符名称,你可以重命名它。
WindowGroup
的行为因平台不同而不同。在macOS
和iPadOS
上,你可以打开一个以上的窗口或场景,但在iOS
、tvOS
和watchOS
上,你只能有一个窗口。
用SceneStorage
恢复场景状态¶
目前,当你退出并重新启动你的应用程序时,你总是从欢迎屏幕开始。这可能是你喜欢的行为,但使用@SceneStorage
,你可以持久化你的应用程序的每个场景的当前状态。
➤ 在iPad
模拟器中建立并运行你的应用程序,在两个窗口中,并在第二个窗口中进入练习3。在模拟器中通过从底部向上滑动来退出应用程序。然后在Xcode
中停止该应用。记住,你的应用程序可能不会保存数据,除非设备通知它状态已经改变。
➤ 重新运行该应用,由于该应用完全是用新的状态刷新的,因此该应用不记得你在其中一个窗口中做过练习3。
正如你可能猜到的,@SceneStorage
与@AppStorage
相似。@SceneStorage
属性不是按应用实例持久化,而是按场景持久化。
➤ 打开ContentView.swift
。
控制当前练习的属性是selectedTab
。
➤ 将@State private var selectedTab = 9
改为:
@SceneStorage("selectedTab") private var selectedTab = 9
➤ 构建并运行该应用程序。在第一个窗口中,转到练习1,在第二个窗口中,转到练习3。
➤ 在模拟器中通过从底部向上滑动来退出该应用程序。然后,在Xcode
中停止该应用。
➤ 再次构建和运行,这一次,应用程序会记住您正在查看练习 1 和练习 3,并直接进入那里。
Note
要在模拟器中重置SceneStorage
,你必须先清除缓存。在模拟器中,选择Device ▸ Erase All Content and Settings…
然后重新运行你的应用程序。
虽然你要到下一章才会意识到,引入SceneStorage
给你初始化HistoryStore
的方式带来了问题。目前你在ContentView.swift
中创建HistoryStore
,作为TabView
的环境对象修改器。SceneStorage
在存储selectedTab
时重新初始化TabView
,所以每次你改变标签,你就重新初始化HistoryStore
。如果你做一个练习,你的历史就不会被保存。你将在下一章中解决这个问题。
关键点¶
- 你有几种选择来存储数据。你应该使用
@AppStorage
和@SceneStorage
来存储轻量级的数据,并使用属性列表、JSON
或Core Data
来存储随时间增长的主要应用程序数据。 - 你的应用程序被沙盒化,所以其他应用程序不能访问其数据。你也不能从任何其他应用程序访问数据。你的应用程序的可执行文件被保存在只读的应用程序捆绑目录中,以及你的应用程序的所有资产。你可以使用
FileManager
访问你的应用程序的Documents
和Library
目录。 - 属性列表存储序列化的对象。如果你想在属性列表文件中存储自定义类型,你必须首先将它们转换为属性列表文件认可的数据类型,如
String
或Boolean
或Data
。 - 字符串操作可能相当复杂,但
Swift
提供了许多支持方法来提取字符串的一部分或在另一个字符串上附加一个字符串。 - 用
@SceneStorage
管理场景。iPad
和macOS
可以有多个场景,但在iPhone
上运行的应用程序只有一个。