关于数据存储相关的。是一个图书app。

关于 @Banding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import SwiftUI

struct TestButton: View {
let ---
title: String
var colorOn = [Color.white, Color.orange]
var colorOff = [Color.gray, Color.black]
@Binding var toState: Bool

var body: some View {
Button(action: {
self.toState.toggle()
}) {
Text(title)
}
.padding()
.background(LinearGradient(gradient: Gradient(colors: toState ? colorOn : colorOff), startPoint: .top, endPoint: .bottom))
.foregroundColor(toState ? .black : .white)
.clipShape(Capsule())
.shadow(radius: toState ? 0 : 3)

}
}

struct Test: View {
@State private var toState = false
var body: some View {
VStack {
TestButton(---
title: "hello", toState: $toState)
Text(toState ? "on" : "off")
}
}
}

struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}

1792

结合使用大小类和AnyView类型擦除

SwiftUI使我们每个视图都可以访问称为环境的共享信息池,并且在关闭工作表时已经使用了它。如果你还记得的话,这意味着要创建一个如下所示的属性:

1
@Environment(\.presentationMode) var presentationMode

然后,当我们准备好了时,可以像这样解散工作表:

1
2
3
4
Text("Hello World")
.onTapGesture {
self.presentationMode.wrappedValue.dismiss()
}

这种方法使SwiftUI可以确保在关闭视图时更新正确的状态- @State例如,如果我们附加了一个属性来显示工作表,则在关闭工作表时会将其设置回false。

这个环境实际上充满了很多有趣的东西,我们可以阅读这些东西来帮助我们的应用更好地工作。在这个项目中,我们将使用该环境来处理Core Data,但是在这里,我将向你展示它的另一个重要用途:size类。尺寸等级是苹果公司完全模糊的方式,可以告诉我们我们的视图有多少空间。

当我说“彻底模糊”时,我的意思是:我们在水平和垂直方向上只有两个尺寸级别,分别称为“紧凑型”和“常规型”。就是这样–涵盖了所有屏幕尺寸,从横向的最大iPad Pro到纵向的最小iPhone。这并不意味着它没有用-远非如此!–仅仅是让我们从最广泛的角度来考虑我们的用户界面。

为了演示实际的尺寸类别,我们可以创建一个视图,该视图具有一个属性以跟踪当前的尺寸类别并将其显示在文本视图中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct ContentView: View {
@Environment(\.horizontalSizeClass) var sizeClass

var body: some View {
if sizeClass == .compact {
return HStack {
Text("Active size class:")
Text("COMPACT")
}
.font(.largeTitle)
} else {
return HStack {
Text("Active size class:")
Text("REGULAR")
}
.font(.largeTitle)
}
}
}

请尝试在12.9英寸横向iPad Pro模拟器中运行该模拟器,以便获得完整的效果。首先,你应该看到显示“ REGULAR”,因为我们的应用将全屏显示。但是,如果你从模拟器屏幕的底部轻轻向上滑动,则会出现扩展坞,你可以将Safari之类的东西拖到iPad的右侧,以进入多任务处理模式。

即使我们的应用程序只有一半的屏幕,你仍然会看到我们的“常规”标签出现。但是,如果你将拆分器向左拖动(即,仅给我们的应用程序提供了可用空间的四分之一左右),现在它将变为“ COMPACT”。

因此,在全屏宽度下,我们处于常规尺寸级别,而在半屏宽度下,我们仍处于常规尺寸级别,但是当我们变小时,最终我们变得紧凑。就像我说的:这是广义的术语。

如果我们想根据环境更改布局,事情就会变得更加有趣。在这种情况下,使用VStack小号而不是HStack小号是更有意义的,但是这比你想象的要棘手。

首先,更改代码,以便我们返回a VStack或a HStack:

1
2
3
4
5
6
7
8
9
10
11
12
13
if sizeClass == .compact {
return VStack {
Text("Active size class:")
Text("COMPACT")
}
.font(.largeTitle)
} else {
return HStack {
Text("Active size class:")
Text("REGULAR")
}
.font(.largeTitle)
}

在构建代码时,你会看到一个不祥的错误:“函数声明了不透明的返回类型,但是其主体中的return语句没有匹配的基础类型。” 也就是说,some Viewreturn类型的body要求从代码的所有路径中返回一个单一类型–我们有时无法返回一个视图,而有时则无法返回其他视图。

你可能会认为自己会很聪明,然后将整个条件包装在另一个视图中,例如VStack,但这也不起作用。取而代之的是,我们需要一个更高级的解决方案,称为类型擦除。我之所以说“高级”,是因为从概念上讲它非常聪明,而且实现起来并不容易,但是从我们的角度(即实际使用它)来看,类型擦除非常简单。

首先,让我们看一下代码–用以下代码替换当前body代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if sizeClass == .compact {
return AnyView(VStack {
Text("Active size class:")
Text("COMPACT")
}
.font(.largeTitle))
} else {
return AnyView(HStack {
Text("Active size class:")
Text("REGULAR")
}
.font(.largeTitle))
}
我知道阅读起来很繁琐,所以让我简化一下更改:

return AnyView(HStack {
// ...
}
.font(.largeTitle))

如果再次构建代码,你会看到它可以干净地编译,甚至可以更好地在运行时看起来很好–该应用程序现在HStack可以VStack根据大小级别在a 和a 之间平滑切换。

所更改的是,我们将两个堆栈都包装在称为的新视图类型中AnyView,该类型称为类型擦除包装器。

AnyView符合相同的View协议为Text,Color,VStack等等,以及它也内包含的特定类型的视图。但是,从外部AnyView不公开其包含的内容– Swift将我们的条件视为返回an AnyView或an AnyView,因此将其视为相同的类型。这就是“类型擦除”名称的来源:AnyView有效地隐藏(或擦除)其包含的视图的类型。

现在,这里的合乎逻辑的结论是要问,为什么我们不使用AnyView 所有的时间,如果它可以让我们避免的限制some View。答案很简单:性能。当SwiftUI确切知道我们的视图层次结构中的内容时,它可以根据需要AnyView琐碎地添加和删除一小部分,但是当我们使用它时,我们会积极拒绝SwiftUI该信息。结果,当定期更改发生时,可能需要做更多的工作才能使我们的用户界面保持更新,因此通常最好避免使用,AnyView除非你特别需要它。

CoreData

我已经写好了一个CoreData的文章,关于这块的内容。

SwiftUI和Core Data相差将近十年,分别是iOS 13的SwiftUI和iPhoneOS 3的CoreData。很久以前,它还没有被称为iOS,因为iPad尚未发布。尽管时间相距遥远,Apple还是投入了大量工作以确保这两种强大的技术能够完美地相互配合使用,这意味着Core Data就像始终以这种方式设计一样,已集成到SwiftUI中。

首先,基础知识:核心数据是一个对象图和持久性框架,这是一种奇特的说法,它可以让我们定义对象和那些对象的属性,然后让我们从永久性存储中读取和写入它们。从表面上看,这听起来像是使用Codable和UserDefaults,但是它比这要先进得多:核心数据能够对我们的数据进行排序和过滤,并且可以处理更大的数据-实际上可以存储多少数据没有限制。更好的是,Core Data在你确实需要依靠它时实现了各种更高级的功能:数据验证,数据的延迟加载,撤消和重做等等。

在此项目中,我们将仅使用少量Core Data的功能,但这将很快扩展-我只想首先了解它。当你创建Xcode项目时,我要求你选中Use Core Data框,它应该导致对项目的更改:

现在,你有了一个名为Bookworm.xcdatamodeld的文件。这描述了你的数据模型,该数据模型实际上是类及其属性的列表。
AppDelegate.swift和SceneDelegate.swift中现在有用于设置核心数据的额外代码。
设置核心数据需要两个步骤:创建所谓的持久性容器(从容器存储中加载并保存实际数据),然后将其注入SwiftUI环境中,以便我们所有的视图都可以访问它。

Xcode模板已经为我们完成了这两个步骤。

因此,剩下的就是我们要决定要在Core Data中存储哪些数据,以及如何读出这些数据。首先,我们需要打开Bookworm.xcdatamodeld并开始使用Xcode的模型编辑器描述我们的数据。

之前我们描述过这样的数据:

1
2
3
4
struct Student {
var id: UUID
var name: String
}

但是,Core Data不能那样工作。你会看到,Core Data需要提前知道我们所有数据类型的样子,包含的内容以及它们之间的关系。这就是“ xcdatamodeld”文件的来源:我们将类型定义为“实体”,然后在其中创建属性作为“属性”,Core Data负责将其转换为可以在运行时使用的实际数据库布局。

为了进行试用,请按“添加实体”按钮创建一个新实体,然后双击其名称将其重命名为“学生”。接下来,单击“属性”表正下方的+按钮以添加两个属性:“ id”作为UUID和“ name”作为字符串。这将告诉Core Data创建学生并保存他们所需的一切,因此请回到ContentView.swift,以便我们编写一些代码。

使用获取请求从Core Data中检索信息-我们描述了我们想要的内容,应如何对其进行排序以及是否应使用任何过滤器,然后Core Data会发回所有匹配的数据。我们需要确保该获取请求随着时间的推移保持最新,以便在创建或删除学生时,我们的UI保持同步。

SwiftUI有一个解决方案,而且-你猜到了-这是另一个属性包装器。这次调用了@FetchRequest它,它带有两个参数:我们要查询的实体,以及我们希望结果如何排序。它具有非常特定的格式,因此让我们从为学生添加获取请求开始–请ContentView现在添加此属性:

@FetchRequest(entity: Student.entity(), sortDescriptors: []) var students: FetchedResults
分解后,会为我们的“学生”实体创建提取请求,不进行任何排序,而是将其放入名为studentstype 的属性中FetchedResults

从这里开始,我们可以students像常规的Swift数组一样开始使用,但是你会发现有一个问题。首先,一些将数组放入的代码List:

1
2
3
4
5
6
7
8
9
10
    var body: some View {
VStack {
List {
ForEach(students, id: \.id) { student in
Text(student.name ?? "Unknown")
}
}
}
}
}

你发现渔获了吗?是的,student.name是可选的–它可能有值,也可能没有。这是Core Data的一个领域,将极大地困扰你:它具有可选数据的概念,但与Swift的可选数据完全不同。如果我们对Core Data说“这不是必须的”(你可以在模型编辑器中完成),它仍然会生成可选的Swift属性,因为所有Core Data关心的是属性在保存时具有值–在其他时间它们可能为零。

你可以根据需要运行代码,但实际上并没有太多意义-该列表将为空,因为我们尚未添加任何数据,因此我们的数据库为空。为了解决这个问题,我们将在列表下方创建一个按钮,该按钮在每次点击时都会添加一个新的随机学生,但是首先我们需要一个新属性来存储托管对象上下文。

让我备份一下,因为这很重要。当我们定义“学生”实体时,实际上发生的是Core Data为我们创建了一个类,该类继承自其自己的一个类:NSManagedObject。我们无法在代码中看到此类,因为它是在构建项目时自动生成的,就像Core ML的模型一样。这些对象之所以称为托管对象,是因为Core Data会照料它们:它从持久性容器中加载它们并将它们的更改也写回。

我们所有的托管对象都生活在托管对象上下文中,该上下文负责实际获取托管对象以及保存更改等内容。如果需要,你可以有许多托管对象上下文,但是现在已经有一段路要走了-实际上,你可以长期使用它。

我们不需要创建此托管对象上下文,因为Xcode已经为我们创建了一个。更好的是,它已经将其添加到SwiftUI环境中,这正是使@FetchRequest属性包装器起作用的原因–它使用了环境中可用的任何托管对象上下文。

无论如何,在添加和保存对象时,我们需要访问SwiftUI环境中的托管对象上下文。这是@Environment属性包装的另一种用法–我们可以要求它提供当前的托管对象上下文,并将其分配给属性以供我们使用。

因此,ContentView现在添加此属性:

1
@Environment(\.managedObjectContext) var moc

设置好之后,下一步是添加一个按钮,该按钮生成随机的学生并将其保存在托管对象上下文中。为了帮助学生脱颖而出,我们将通过创建firstNames和lastNames数组来分配随机名称,然后使用randomElement()来选择每个名称。

首先在以下位置添加此按钮List:

1
2
3
4
5
6
7
8
9
Button("Add") {
let firstNames = ["Ginny", "Harry", "Hermione", "Luna", "Ron"]
let lastNames = ["Granger", "Lovegood", "Potter", "Weasley"]

let chosenFirstName = firstNames.randomElement()!
let chosenLastName = lastNames.randomElement()!

// more code to come
}

注意:不可避免地有人会抱怨我强行取消对的调用randomElement(),但实际上我们只是手工创建了具有值的数组-它将始终成功。如果你非常讨厌强制拆包,则可以将其替换为零合并和默认值。

现在,对于有趣的部分:我们将Student使用为我们生成的类Core Data 创建一个对象。这需要附加到托管对象上下文中,以便对象知道应将其存储在何处。然后,我们可以像通常为结构那样分配值。

因此,现在将这三行添加到按钮的操作关闭中:

1
2
3
let student = Student(context: self.moc)
student.id = UUID()
student.name = "\(chosenFirstName) \(chosenLastName)"

最后,我们需要询问托管对象上下文以保存自身。这是一个引发函数的调用,因为理论上它可能会失败。在实践中,所做的任何事情都没有失败的机会,因此我们可以使用try?– 称其为“错误”。

因此,将最后一行添加到按钮的操作中:

1
try? self.moc.save()

最后,你现在应该可以运行该应用程序并对其进行尝试-单击几次“添加”按钮以生成一些随机的学生,你应该看到他们滑入我们列表的某个位置。更好的是,如果你重新启动该应用程序,你会发现学生还在,因为Core Data保存了他们。