QSD的Swift妙妙屋4:Core Data入门1

Core Data 入门1:创建CoreData、添加并显示数据

本文部分内容转载/翻译自Hacking With Swift,包括两篇教程:Creating books with Core DataHow 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

  1. 在创建新工程的时候勾选“Use Core Data”


    creating a new project
  2. Project中的.xcdatamodeld即为Core Data文件

    xcdatamodeld文件

构造Core Data

构造Core Data的过程非常简单,且无需代码。


  1. 在xcdatamodeld文件中点击Add Entity添加新实体
  2. 双击实体可更改实体名称。
  3. 在右侧的属性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”按钮还要实现一个功能:点击时能自动返回主视图
为了做到这点,我们在主视图中加入以下功能:

  1. 使用一个@State的变量showingAddScreen来追踪“窗体是否活跃”。
  2. 在主视图点击“新建书”按钮时,showingAddScreen变量会toggle。
  3. 使用一个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)
                        }
                    }
                }
            }
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353