跳转至

第8章:保存设置

每当你的应用程序关闭时,所有输入的数据,如你设置的任何评级或你记录的任何历史,都会丢失。大多数应用程序要想发挥作用,就必须在应用程序会话之间保持数据。数据持久化是将数据保存到永久存储的一种华丽的说法。

在本章中,你将探索如何使用AppStorageSceneStorage来存储简单的数据。你将保存练习评级,如果你在练习中被叫走,你的应用程序将记住你在哪个练习中,并在那里开始,而不是在欢迎界面。

你还会了解到如何在Swift字典中存储数据,并意识到字符串操作是很复杂的。

数据持久性

根据你要保存的数据类型,有不同的方法来持久化你的数据:

  • UserDefaults:使用它来保存一个应用程序的用户偏好。这将是一个保存评级的好方法。
  • Property List file:一个macOSiOS的设置文件,存储序列化的对象。串行化意味着将对象翻译成可以存储的格式。这将是一个存储历史数据的好格式,你将在下一章做这个。
  • JSON file:一种开放的标准文本文件,用于存储序列化的对象。你将在第二节中使用这种格式。
  • Core Data:一个具有macOSiOS框架的对象图,用于存储对象。更多信息,请查看我们的Core Data教程一书,https://bit.ly/39lo2k3

将评级保存到UserDefaults

本节将学习的技能:AppStorage; UserDefaults.

UserDefaults是一个类,它能够在一个属性列表(plist)文件中存储和检索数据,该文件与你的应用程序的沙盒数据一起保存。它被称为defaults,因为你应该只将UserDefaults用于简单的应用程序范围的设置。你不应该存储诸如你的历史记录等数据,随着时间的推移,这些数据会越来越大。

➤ 继续上一章的最终项目或打开本章启动文件夹中的项目。

到目前为止,你已经在预览中使用了iPad。请记住,使用iPhone也一样可以测试你的应用程序。为了测试数据的持久性,你需要在模拟器中运行该应用,这样你就可以检查磁盘上的实际数据。

➤ 点击运行目标按钮,选择iPhone 12 Pro

AppStorage

@AppStorage是一个属性包装器,类似于@State@Binding,它允许UserDefaults和你的SwiftUI视图之间的互动。

你设置了一个评级视图,允许用户对练习的难度从15进行评级。你要把这个评级保存到UserDefaults中,这样你的评级就不会在你关闭应用程序时消失。

rating的真实来源目前在ExerciseView.swift中,在那里你为它设置了一个状态属性。

➤ 打开ExerciseView.swift,将@State private var rating = 0改为:

@AppStorage("rating") private var rating = 0

属性包装器@AppStorage会把对rating的任何修改保存到UserDefaults中。你保存到UserDefaults的每一个数据都需要一个唯一的键。使用@AppStorage,你提供这个键是一个带引号的字符串,在这个例子中是rating

➤ 建立和运行,并选择一个练习。点选评级视图,为练习打分。UserDefaults现在存储你的评分。

Rating the exercise

AppStorage只允许几种类型:String, Int, Double, Data, BoolURL. 对于简单的数据,如用户可配置的应用程序设置,用AppStorage将数据存储到UserDefaults是非常容易的。

Note

尽管UserDefaults存储在一个相当安全的目录中,但你不应该在那里保存敏感数据,如登录信息和其他认证代码。对于这些,你应该使用钥匙串:https://apple.co/3evbAkA

➤ 在模拟器中停止该应用程序,从底部向上滑动。然后,在Xcode中,再次运行该应用并进入相同的练习。你的评分在两次启动之间持续存在。

Note

当使用@AppStorage@SceneStorage时,一定要确保在Xcode中终止应用程序之前在模拟器或设备上退出应用程序。你的应用程序可能不会保存数据,直到系统通知它状态的改变。

你已经解决了数据持久性的问题,但引起了另一个问题。不幸的是,由于你对所有的评级只有一个评级键,你在UserDefaults中只存储了一个值。当你进入另一个练习时,它的评分与第一个练习相同。如果你设置了一个新的评级,所有其他的练习都有相同的评级。

你真的需要存储一个评级数组,每个练习都有一个条目。例如,一个[1, 4, 3, 2]的数组将存储练习14的单个评级值。 在解决这个问题之前,你会发现Xcode是如何存储应用程序数据的。

数据目录

本节将学习的技能:应用包中的内容;数据目录;FileManager;属性列表文件;Dictionary

当你在模拟器中运行你的应用程序时,Xcode会创建一个沙盒目录,包含一组标准的子目录。沙盒是一种安全措施,意味着没有其他应用程序能够访问你的应用程序的文件。

反之,你的应用程序将只能读取iOS允许的文件,而你将无法读取任何其他应用程序的文件。

App sandbox and directories

应用程序Bundle

在你的应用程序沙盒内有两组目录。首先,你将检查应用包,然后是用户数据。

➤ 在Xcode中,打开组列表底部的产品组。当您构建您的应用程序时,Xcode将创建一个与项目名称相同的产品。

➤ 如果您的应用程序的名称是红色的,请构建该项目,以确保编译后的项目存在于磁盘上。

➤ 按住Control-click HIITFit.app,并选择在Finder中显示。

App in Finder

你在这里看到模拟器的应用程序的调试目录。

➤ 在Finder中,Control-click HIITFit并选择显示软件包内容。

App bundle contents

该应用程序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,是用户的主目录。

Documents directory path

Swift

他结果是一个URL阵列,而不是一个单一的URL,因为你可以要求一个域掩码阵列。这些域可以包括localDomainMask,用于机器上每个人都可以使用的项目,networkDomainMask用于网络上的项目,systemDomainMask用于苹果系统文件。

➤ 突出显示从/Users../Documents/,然后控制点击选择。Choose Services ▸ Show in Finder

Show in Finder

这将打开一个新的Finder窗口,显示Simulator的用户目录:

Simulator directories

➤ 父目录 - 在这个例子中,47887...524CF - 包含该应用程序的沙盒用户目录。你会看到其他目录也是用UUID命名的,这些目录属于你可能做过的其他应用。选择父目录并把它拖到你的收藏夹侧边栏,这样你就可以快速访问它。

在你的应用程序中,你可以访问这些目录中的一些:

  • documentDirectoryDocuments/.应用程序的主要文档目录。
  • libraryDirectoryLibrary/.用于存放不想暴露给用户的文件的目录。
  • cachesDirectoryLibrary/Caches/.·暂时的缓存文件。如果你展开一个压缩文件并在你的应用程序中临时访问其内容,你可能会使用它。

iPhoneiPad的备份将保存文档和库,不包括Library/Caches

在一个属性列表文件内

@AppStorage将你的rating保存在一个UserDefaults属性列表文件中。一个属性列表文件是一种XML文件格式,用于存储结构化文本。所有的属性列表文件都包含一个DictionaryArray类型的Root,这个根包含一个带有数值的键的分层列表。

例如,你可以将HIITFit的练习分布在一个数组中,而不是将它们存储在一个属性列表文件中,并在应用程序开始时将它们读入一个数组:

Exercises in a property list file

这样做的好处是,在未来的版本中,你可以添加应用内练习的购买,并在属性列表文件中跟踪购买的练习。

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,其中burpeekey4value

你可以用多个值进行初始化并添加新的值:

var ratings = ["step-up": 2, "sun-salute": 3] 
ratings["squat"] = 5 // ratings now contains three items

Dictionary contents

最后一张图片来自Swift Playground,它向你展示了字典与数组不同,没有保证顺序。游乐场显示的顺序与创建的顺序是不同的。

如果你以前没有使用过Swift Playgrounds,它们对于测试代码片段是很有趣和有用的。你将在第24章"下载数据"中使用一个playground

你可以使用key来检索value

let rating = ratings["squat"]  // rating = 5

UserDefaults属性列表文件

➤ 在Finder中,打开你的应用程序的沙盒,找到Library/Preferences。在该目录中,打开com.raywenderlich.HIITFit.plist。这是UserDefaults文件,你的评级被储存在这里。你的应用程序在你第一次存储rating时自动创建了这个文件。

UserDefaults property list file

这个文件有一个条目:ratingNumber值为1,这是评级值,可以从15

属性列表文件可以包含:

  • Dictionary
  • Array
  • String
  • Number
  • Boolean
  • Data
  • Date

有了所有这些类型,你可以看到直接存储到属性列表文件比@AppStorage更灵活,它不支持字典或数组。你可以决定,为了存储你的评级数组,也许@AppStorage终究不是办法。但是等等--你所要做的就是进行一些数据处理。你可以将你的整数等级存储为一个字符数组,也称为"字符串"。

你最初会把评分存储为一个"0000"的字符串。当你需要,例如,第一个练习,你将读取字符串中的第一个字符。当你点击一个新的评级时,你将把新的评级存储回第一个字符。

这是可扩展的。如果你增加了更多的练习,你只需拥有一个更长的字符串。

Swift Dive: Strings

本节中你将学习的技能:Unicode; String索引; nil凝聚运算符; String字符替换

字符串并不像它们看起来那么简单。为了支持对表情符号不断增长的需求,一个字符串是由扩展的字素群组成的。这些是一连串的Unicode值,在平台支持的地方显示为单个字符。它们用于一些语言字符,也用于各种表情符号标签序列,例如肤色和旗帜。

Unicode tag sequences

威尔士旗使用七个标签序列来构建单字符🏴󠁧󠁢󠁷󠁬󠁳󠁿。在不支持标签序列的平台上,该旗帜将显示为黑色旗帜🏴。

一个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 LoafSwift String cheat sheet,网址是https://bit.ly/3aGRjWp

保存评级

现在你要存储ratingsRatingView是比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 ratingsnil值,确保你的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可以说是一种非常简洁的语言,在这段短短的代码中,有很多东西需要解读。

  1. 你的应用程序在每次视图最初出现时运行onAppear(perform:)
  2. ratings被标记为@AppStorage,所以它的值被存储在UserDefaults属性列表文件中。你创建了一个String.Index来使用exerciseIndex对字符串进行索引。
  3. 这里你使用String.Index从字符串中提取正确的字符。
  4. 将该字符转换为整数。如果该字符不是一个整数,wholeNumberValue的结果将是一个可选的值nil。这两个问号被称为nil凝聚运算符。如果wholeNumberValue的结果是nil,那么就使用问号后的值,在这种情况下,就是0。你将在下一章中学习更多关于选项的知识。

➤ 预览视图。你存储的评分目前是0000,你正在预览练习零。

Zero Rating

➤ 将@AppStorage("rating") private var ratings = "0000"改为:

@AppStorage("ratings") private var ratings = "4000"

Resume预览,练习零的评级变为四。

Rating of four

在一个字符串中存储评级

你现在正从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)

➤ 建立和运行并替换你所有练习的所有评级。现在每个练习都有其单独的评级。

Rating the exercise

➤ 在Finder中,检查应用程序库目录中的com.raywenderlich.HIITFit.plist

AppStorage

你可以从属性列表文件中删除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)
  }
}

翻阅代码:

  1. 如果你不自己定义init()Xcode会创建一个默认的初始化器来设置所有必要的属性。然而,如果你创建一个自定义的初始化器,你必须自己初始化它们。在这里,exerciseIndex是一个必要的属性,所以你必须把它作为一个参数接收,并存储到RatingView实例中。
  2. ratings必须有与你的练习一样多的字符。
  3. 如果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.plistratings将用零来填充。

Zero padding

多个场景

你将在本节中学习的技能:多个iPad窗口。

也许你的伙伴、狗或猫想在同一时间锻炼。或者你只是对HIITFit非常兴奋,你想在iPad上同时查看两个练习。在iPad分割视图中,你可以打开第二个窗口,这样你就可以比较你的深蹲和Burpee

首先确保你的应用程序支持多窗口。

➤ 在项目导航器中选择顶部的HIITFit组。

➤ 选择HIITFit目标,常规选项卡,在部署信息下找到支持多窗口。

➤ 确保该选项被选中。如果不勾选,就不能让自己的应用程序或自己的应用程序和另一个应用程序的两个窗口并排在一起。

Configure multiple windows

➤ 在你的iPad设备上或在iPad模拟器上建立和运行HIITFit

使用Command-Right Arrow.将模拟器转到横向。打开应用程序后,从底部边缘轻轻向上滑动以显示Dock。你会在Dock中看到HIITFit的图标。把它拖到屏幕的右侧或左侧边缘,然后放下。你可以通过拖动窗口之间的分隔线来调整窗口的大小。

Multiple iPad Windows

你现在有两个session开放。

做出评级反应

➤ 在每个窗口上,转到练习1蹲下并改变评级。你会注意到有一个问题,因为尽管评级是用AppStorage存储在UserDefaults中的,但这些窗口反映了两个不同的评级。当你在一个窗口中更新评级时,另一个窗口中的评级应该立即反应。

Non-reactive rating

➤ 打开RatingView.swift并查看代码。

通过AppStorage,你可以为每个应用程序保存一个rating值,无论有多少个窗口打开。你在onTapGesture(count:execute:)中改变ratings并更新rating。第二个窗口持有它自己的rating实例。当你改变一个窗口的评级时,第二个窗口应该对这一变化做出反应,并更新和重新绘制它的评级视图。

Outdated rating

如果你用来自AppStorageratings字符串,而不是提取的整数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,但它是一个占位符名称,你可以重命名它。

The App Hierarchy

WindowGroup的行为因平台不同而不同。在macOSiPadOS上,你可以打开一个以上的窗口或场景,但在iOStvOSwatchOS上,你只能有一个窗口。

SceneStorage恢复场景状态

目前,当你退出并重新启动你的应用程序时,你总是从欢迎屏幕开始。这可能是你喜欢的行为,但使用@SceneStorage,你可以持久化你的应用程序的每个场景的当前状态。

➤ 在iPad模拟器中建立并运行你的应用程序,在两个窗口中,并在第二个窗口中进入练习3。在模拟器中通过从底部向上滑动来退出应用程序。然后在Xcode中停止该应用。记住,你的应用程序可能不会保存数据,除非设备通知它状态已经改变。

➤ 重新运行该应用,由于该应用完全是用新的状态刷新的,因此该应用不记得你在其中一个窗口中做过练习3。

What exercise was I doing?

正如你可能猜到的,@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来存储轻量级的数据,并使用属性列表、JSONCore Data来存储随时间增长的主要应用程序数据。
  • 你的应用程序被沙盒化,所以其他应用程序不能访问其数据。你也不能从任何其他应用程序访问数据。你的应用程序的可执行文件被保存在只读的应用程序捆绑目录中,以及你的应用程序的所有资产。你可以使用FileManager访问你的应用程序的DocumentsLibrary目录。
  • 属性列表存储序列化的对象。如果你想在属性列表文件中存储自定义类型,你必须首先将它们转换为属性列表文件认可的数据类型,如StringBooleanData
  • 字符串操作可能相当复杂,但Swift提供了许多支持方法来提取字符串的一部分或在另一个字符串上附加一个字符串。
  • @SceneStorage管理场景。iPadmacOS可以有多个场景,但在iPhone上运行的应用程序只有一个。