Core Data 入门1:创建CoreData、添加并显示数据
本文部分内容转载/翻译自Hacking With Swift,包括两篇教程:Creating books with Core Data 和 How to combine Core Data and SwiftUI。这网站上的教程真滴好。
Core Data is an object graph and persistence framework, which is a fancy way of saying it lets us define objects and properties of those objects, then lets us read and write them from permanent storage. On the surface, this sounds like using Codable and UserDefaults, but it’s much more advanced than that: Core Data is capable of sorting and filtering of our data, and can work with much larger data – there’s effectively no limit to how much data it can store. Even better, Core Data implements all sorts of more advanced functionality for when you really need to lean on it: data validation, lazy loading of data, undo and redo, and much more.
Core Data概述
from Apple's Developer Document
总览
使用Core Data来保存应用程序的永久数据,以供脱机使用,并支持在应用程序中添加撤消功能。
通过
Core Data Data Model Editor 数据类型编辑器
,您可以定义数据的类型和关系,并生成相应的class
定义。然后Core Data可以在运行时管理对象实例,以提供以下特性:
1.永久储存(Persistence)
Core Data抽象化了将对象映射到存储区的详细信息,这使得从Swift和Objective-C中保存数据变得很容易,而无需直接管理数据库。
2. 撤销和重做,单数据修改和批量修改(Undo and Redo of Individual or Batched Changes)
Core Data的撤消管理器可以跟踪更改,并可以单独、分组或一次全部回滚这些更改,从而可以轻松地向应用程序添加撤消和重做支持。
3. 后台数据任务(Background Data Tasks)
在后台执行潜在的UI-Blocking Data Tasks,比如将json解析为对象。然后可以缓存或存储结果,以减少服务器往返。
4. 视图同步(View Synchronization)
5. 版本更新和迁移(Versioning and Migration)
创建Core Data
-
在创建新工程的时候勾选“Use Core Data”
-
Project中的
.xcdatamodeld
即为Core Data文件
构造Core Data
构造Core Data的过程非常简单,且无需代码。
- 在xcdatamodeld文件中点击
Add Entity
添加新实体 - 双击实体可更改实体名称。
- 在右侧的属性
Attributes
中添加实体的变量,包括变量名和变量类型。
例如,我们想创建一个“书籍”的类型,包括书名、作者、类型、评分,以及我自己的评论。
- id, UUID – a guaranteed unique identifier we can use to distinguish between books
- title, String – the title of the book
- author, String – the name of whoever wrote the book
- genre, String – one of several strings from the genres in our app
- review, String – a brief overview of what the user thought of the book
- rating, Integer 16 – the user’s rating for this book
在使用传统的struct时,我们会这样申明:
struct Book{
var name: String
var author: String
var genre: String
var rating: Int
var comment: String
var id: UUID
}
对应的,我们只需在Core Data中创建以下Attributes即可:
Integer 16, Integer 32, Integer 64
Int16,Int32,Int64的区别在于可储存的数据大小(2^16 ,2^32 ,2^64)。
需要注意的是,一般不可以在这几种类型之间互相交换数据(interchangeable)。
至此,对Core Data的构造就完成了。
往Core Data中添加数据并显示数据
从一个简单的小程序开始
直接上手一个“book”的工程可能比较复杂,毕竟里面的变量太多了。我们先尝试一个简单的小程序:一个“学生花名册”,点击Add
按钮可以往其中加入新的学生,然后用一个List显示所有学生的名字。
1. 构造Core Data
添加名为“Student”的Entity,其中包括“name: String”和“id: UUID”
2. 使用Fetch Request
从Core Data中获取数据
Swift提供了一个property wrapper:@FetchRequest
来为我们实现这个功能。 Fetch Request需要两个参数:1. 你想获得哪个entity中的数据, 2. 你想要数据怎么排序。
我们在ContentView中加入以下申明,从名叫Student的entity中获取无排序的数据,以构造一个students的property。它的类型是FetchedResults<Student>
。
@FetchRequest(entity: Student.entity(), sortDescriptors: []) var students: FetchedResults<Student>
至此,“获取数据”的任务就完成了。我们在View中用VStack包裹一个List来显示所有学生的名字:
var body: some View {
VStack {
List {
ForEach(students, id: \.id) { student in
Text(student.name ?? "Unknown")
}
}
}
}
需要注意的是,student.name是个Optional的变量——可以有值,也可以是nil—— Core Data中的所有数据都是Optional的变量,而且这种Optional跟Swift中的Optional概念完全不同。因为Core Data关心的只是property在保存时有值,其他时候都可以为nil。
3. 向Core Data中添加数据
当我们定义“Student”实体时,核心数据为我们创建了一个继承(inherit)自它自己的class的class:NSManagedObject。我们在代码中看不到这个class,因为它是在我们构建项目时自动生成的,就像Core ML的模型一样。这些对象被称为managed,因为核心数据在照顾它们:它从永久的容器加载它们,并将它们的更改写回容器中。
我们所有的managed objects都位于managed object context中,它负责实际获取managed objects,以及保存更改。如果需要,我们可以创造很多的managed object context。
我们并不需要手动创造managed object context,因为XCode已经帮我们写好,甚至加入了SwiftUI中。
因此,我们可以使用@Environment
的property wrapper来要求managed object context,并把它分配给我们ContentView中的property。
@Environment(\.managedObjectContext) var moc
接下来,我们设计一个按钮来随机生成名字,并将生成的名字添加至Core Data中
生成随机的名字
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
}
创建一个新的student
let student = Student(context: self.moc)
student.id = UUID()
student.name = "\(chosenFirstName) \(chosenLastName)"
// 将这些代码替换到上面的“More Code to Come”中
要求Core Data储存这些数据
需要注意的是,“储存”的过程可能会失败,因此我们加入try?
来规避错误。
try? self.moc.save()
全部代码
Import SwiftUI
Import CoreData
struct ContentView: View {
@Environment(\.managedObjectContext) var moc
@FetchRequest(entity: Student.entity(), sortDescriptors: []) var students: FetchedResults<Student>
var body: some View {
VStack {
List {
ForEach(students, id: \.id) { student in
Text(student.name ?? "Unknown")
}
}
Button("Add") {
let firstNames = ["Ginny", "Harry", "Hermione", "Luna", "Ron"]
let lastNames = ["Granger", "Lovegood", "Potter", "Weasley"]
let chosenFirstName = firstNames.randomElement()!
let chosenLastName = lastNames.randomElement()!
let student = Student(context: self.moc)
student.id = UUID()
student.name = "\(chosenFirstName) \(chosenLastName)"
try? self.moc.save()
}
}
}
}
其实,我们可以在save外面加一句话检测内容是否改变,因而避免无意义的保存。
if self.moc.hasChanges {
try? self.moc.save()
}
中场休息!送猫一只!
结束完了这个小工程后,我们回到我们的"书"程序中
创建一个“添加书本”的视图
类似的,我们需要向Core Data中写入数据,所以申明一个moc
@Environment(\.managedObjectContext) var moc
然后使用@State
申明并跟踪书籍的信息,并添加书籍的预设种类。
@State private var title = ""
@State private var author = ""
@State private var rating = 3
@State private var genre = ""
@State private var review = ""
let genres = ["Fantasy", "Horror", "Kids", "Mystery", "Poetry", "Romance", "Thriller"]
再然后,在body中写入“新建书籍”的视图
NavigationView {
Form {
Section {
TextField("Name of book", text: $title)
TextField("Author's name", text: $author)
Picker("Genre", selection: $genre) {
ForEach(genres, id: \.self) {
Text($0)
}
}
}
Section {
Picker("Rating", selection: $rating) {
ForEach(0..<6) {
Text("\($0)")
}
}
TextField("Write a review", text: $review)
}
Section {
Button("Save") {
// add the book
}
}
}
.navigationBarTitle("Add Book")
}
会得到以下场景:
并在Button中添加以下代码以实现功能:
let newBook = Book(context: self.moc)
newBook.title = self.title
newBook.author = self.author
newBook.rating = Int16(self.rating)
newBook.genre = self.genre
newBook.review = self.review
newBook.id = UUID()
try? self.moc.save()
“Add Book”按钮还要实现一个功能:点击时能自动返回主视图
为了做到这点,我们在主视图中加入以下功能:
- 使用一个@State的变量
showingAddScreen
来追踪“窗体是否活跃”。- 在主视图点击“新建书”按钮时,showingAddScreen变量会toggle。
- 使用一个Sheet()的modifier。当showingAddScreen为真时,Sheet会显示“添加书籍”的视图。
原文里还有一些关于NavigationView理论知识的讨论,放在这:
However, this time there’s a small piece of bonus work and it stems from the way SwiftUI’s environment works. You see, when we place an object into the environment for a view, it becomes accessible to that view and any views that can call it an ancestor. So, if we have View A that contains inside it View B, anything in the environment for View A will also be in the environment for View B. Taking this a step further, if View A happens to be a NavigationView, any views that are pushed onto the navigation stack have that NavigationView as their ancestor so they share the same environment.
Now think about sheets – those are full-screen pop-up windows on iOS. Yes, one screen might have caused them to appear, but does that mean the presented view can call the original its ancestor? SwiftUI has an answer, and it’s “no”, which means that when we present a new view as a sheet we need to explicitly pass in a managed object context for it to use. As the new AddBookView will be shown as a sheet from ContentView, we need to add a managed object context property to ContentView so it can be passed in.
因此,我们在主程序中加入以下代码:
@Environment(\.managedObjectContext) var moc
@FetchRequest(entity: Book.entity(), sortDescriptors: []) var books: FetchedResults<Book>
@State private var showingAddScreen = false
这为我们提供了一个可以传递到AddBookView的managed object context,一个读取我们拥有的所有书籍的fetch请求(这样我们就可以测试所有的工作了),以及一个跟踪"AddScreen是否显示"的布尔变量。
在主程序的body中,我们使用NavigationView,因此右上角可以显示一个“+”号来添加书籍。
总之,最后程序就是这样:
import SwiftUI
struct AddBookView: View{
@Environment(\.presentationMode) var presentationMode
@Environment(\.managedObjectContext) var moc
@State private var title = ""
@State private var author = ""
@State private var rating = 3
@State private var genre = ""
@State private var review = ""
let genres = ["Fantasy", "Horror", "Kids", "Mystery", "Poetry", "Romance", "Thriller"]
var body: some View {
NavigationView {
Form {
Section {
TextField("Name of book", text: $title)
TextField("Author's name", text: $author)
Picker("Genre", selection: $genre) {
ForEach(genres, id: \.self) {
Text($0)
}
}
}
Section {
Picker("Rating", selection: $rating) {
ForEach(0..<6) {
Text("\($0)")
}
}
TextField("Write a review", text: $review)
}
Section {
Button("Save") {
let newBook = Book(context: self.moc)
newBook.title = self.title
newBook.author = self.author
newBook.rating = Int16(self.rating)
newBook.genre = self.genre
newBook.review = self.review
newBook.id = UUID()
try? self.moc.save()
self.presentationMode.wrappedValue.dismiss()
}
}
}
.navigationBarTitle("Add Book")
}
}
}
struct ContentView: View {
@Environment(\.managedObjectContext) var moc
@FetchRequest(entity: Book.entity(), sortDescriptors: []) var books: FetchedResults<Book>
@State private var showingAddScreen = false
var body: some View {
VStack {
NavigationView {
Form{
Text("Count: \(books.count)")
.navigationBarTitle("Bookworm")
.navigationBarItems(trailing: Button(action: {
self.showingAddScreen.toggle()
}) {
Image(systemName: "plus")
})
.sheet(isPresented: $showingAddScreen) {
AddBookView().environment(\.managedObjectContext, self.moc)
}
ForEach (books, id: \.id) { book in
VStack{
HStack{
Text(book.title ?? "Unknown").font(.title)
Spacer()
ForEach (0..<Int(book.rating)) {_ in
Image(systemName: "star.fill").foregroundColor(.yellow)
}
}
HStack{
Text("Genre")
Text(book.genre ?? "Unknown").font(.callout)
Spacer()
Text("Author")
Text(book.author ?? "Unknown")
Spacer()
Text("Review")
Text(book.review ?? "Unknown")
Spacer()
}.font(.caption)
}
}
}
}
}
}
}