Core Data 初识Demo

1、前言

最近打算将以往不太深入研究的技术研究研究,其中之一就是Core Data。购买了一本objec.io | ObjC 中国出版的Core Data来一步一步实现其demo并理解Core Data的奥义。
Core Data是Apple针对数据管理和数据库相关方面给出的解决方案。它并不是单纯的数据库,而是一套对象图管理系统。它默认使用SQLite作为底层存储,通过由低向高地将相关的管理组建构造为一个栈,来提供缓存和对象管理机制。
本文章将比较详细的描述如何使用Core Data来实现一个学校师生管理APP。
初步需求:管理学生列表。
学生属性:id, name, gender, classNum
实现结果:通过TableView来展示学生列表

Github demo在这里

2、新建项目和配置CoreData


一、配置Core Data文件
1、新建一个Xcode项目,右键文件->NewFile->Core Data Model
2、打开新建的.xcdatamodeld文件,展示效果如下:

Student.xcdatamodeld

3、点击Add Entity,添加实体Student,这里ENTITIES处多了一个Student
4、点击Student,在Attributes中添加属性,属性选择类型,右边的配置栏中有可选和Index选项。


二、配置好.xcdatamodeld之后,我们需要新建一个对应的子类来使用CoreData
新建Swift文件,来实现对应的CoreData子类:

import UIKit
import CoreData

public final class Student: NSManagedObject {
    @NSManaged public private(set) var student_id: Int16
    @NSManaged public private(set) var name: String
    @NSManaged public private(set) var gender: Bool
    @NSManaged public private(set) var class_num: Int16
}

其中NSManagedObject是一个空的类,表明类由Core Data管理。
@NSManaged关键字表明属性由Core Data管理。
在此,我们已经新建了CoreData图管理文件,和对应的一个Student子类,我们需要将两者建立关系。
如下图,打开.xcdatamodeld,右侧配置栏中,选择Class 和Module为正确的内容:

为数据模型配置子类

三、设置Core Data栈
我们已经有数据模型和其子类了,接下来需要设置一个Core Data 栈来使用它们。
我们会在整个APP都使用下面这一个上下文:为了不出现混乱,建议一个APP使用一个Core Data上下文。所以以下的代码我们写在AppDelegate.swift中。

1、确定存储地址

private let StoreURL = URL.init(string: NSHomeDirectory() + "/Documents")?.appendingPathExtension("Student.student")

2、创建APP使用的Core Data 上下文

public func createStudentMainContext() -> NSManagedObjectContext {
        let bundles = [Bundle(for: Student.self)] // 获取数据对象模型所在的Bundle,这样就算代码在其他Bundle中,也可以工作
        // 搜索所有的Bundle,并将其合并成一个托管对象
        guard let model = NSManagedObjectModel.mergedModel(from: bundles) else {
            fatalError("model not found")
        }
        // 创建持久化协调器,用Model初始化,用SQLite存储,存储路径是之前确定的Document路径
        let psc = NSPersistentStoreCoordinator(managedObjectModel: model)
        try! psc.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: StoreURL, options: nil)
        
        // 新建上下文,使用mainQueue来配置,表示在UI线程中我们可以安全的访问它
        let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
        context.persistentStoreCoordinator = psc // 将持久化协调器配置为之前创建的协调器psc
        return context
    }

3、在AppDelegate中创建一个Context:

创建Context

在此我们已经有了数据模型,有了数据模型的子类,有了Core Data上下文,可以开始使用这个上下文做一些相关的数据操作了,此处书中是按上述方法创建的Context,但是传递过程有一些繁琐,我将其修改为了单例模式,在单例中获取APP的Core Data 上下文:详情见下列代码

import UIKit
import CoreData

class CoreDataManager: NSObject {
    static let manager = CoreDataManager()
    private let StoreURL = URL.init(string: NSHomeDirectory() + "/Documents")?.appendingPathExtension("Student.student")
    var managedObjectContext: NSManagedObjectContext?
    
    override init() {
        super.init()
        managedObjectContext = createStudentMainContext()
    }
    
    public func createStudentMainContext() -> NSManagedObjectContext {
        let bundles = [Bundle(for: Student.self)] // 获取数据对象模型所在的Bundle,这样就算代码在其他Bundle中,也可以工作
        // 搜索所有的Bundle,并将其合并成一个托管对象
        guard let model = NSManagedObjectModel.mergedModel(from: bundles) else {
            fatalError("model not found")
        }
        // 创建持久化协调器,用Model初始化,用SQLite存储,存储路径是之前确定的Document路径
        let psc = NSPersistentStoreCoordinator(managedObjectModel: model)
        try! psc.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: StoreURL, options: nil)
        
        // 新建上下文,使用mainQueue来配置,表示在UI线程中我们可以安全的访问它
        let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
        context.persistentStoreCoordinator = psc // 将持久化协调器配置为之前创建的协调器psc
        return context
    }
}

四、获取请求 Fetch
一般的,我们使用下列代码来实现Fetch:

        let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Student")
        let sortDescriptor = NSSortDescriptor(key: "student_id", ascending: false)
        request.sortDescriptors = [sortDescriptor]
        request.fetchBatchSize = 20

        let result = try! CoreDataManager.manager.managedObjectContext?.execute(request)
        // 这里的CoreDataManager是上文中的单例

但是在APP越来越庞大之后,我们可以做一些优化,让代码更简洁更易懂:
优化过程:
1)编写一个协议,让协议作为中间者,使得代码解耦:

/// Core Data Entity 需要遵循的协议,面向协议编程
public protocol ManagedObjectType {
    static var entityName: String { get } // 返回Entity的名字
    static var defaultSortDescriptors: [NSSortDescriptor] { get } // 返回排序的要求
}

extension ManagedObjectType {
    public static var defaultSortDescriptors: [NSSortDescriptor] {
        return []
    }
    
    public static var sortedFetchRequest: NSFetchRequest<NSFetchRequestResult> {
        let request = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
        request.sortDescriptors = defaultSortDescriptors
        return request
    } // 返回配置好的Request
}

2)然后使得数据模型对应的类Student遵循这个协议:

extension Student: ManagedObjectType {
    public static var entityName: String {
        return "Student"
    }
    
    public static var defaultSortDescriptors: [NSSortDescriptor] {
        return [NSSortDescriptor.init(key: "student_id", ascending: false)]
    }
}

3)通过以上的优化,之前的Fetch代码就变成了以下形式:

let request = Student.sortedFetchRequest
request.fetchBatchSize = 20

4)然后使用上下文来执行这个Request就行:

let result = try! CoreDataManager.manager.managedObjectContext?.execute(request)
// 这里的CoreDataManager是上文中的单例

知道如何查询之后,我们再给Core Data增加一些实际数据,这样来完成实际的功能。


五、操作数据
1、增加数据
普通的增加数据代码如下:

        guard let student = NSEntityDescription.insertNewObject(forEntityName: "Student", into: CoreDataManager.manager.managedObjectContext!) as? Student else {
            fatalError("student not found")
        }
        student.student_id = 12
        student.name = "bbh"
        student.gender = true
        student.class_num = 1
        try! CoreDataManager.manager.managedObjectContext?.save()

这样的操作是可行的,但是在这个项目中是不能够编译的,因为我们将student的属性设置成了只读,所以如果没有在Student类中插入,那么久无法实现set属性。而且插入的地方有很多,如果每个插入都写这么多代码,会很繁琐,接下来我们来优化一下其中的代码:
1)首先扩展一下NSManagedObjectContext

// 这里使用了Swift 4的新特性,可以用 & 符号连接类和协议
extension NSManagedObjectContext {
    public func insertObject<A: NSManagedObject & ManagedObjectType>() -> A {
        guard let obj = NSEntityDescription.insertNewObject(forEntityName: A.entityName, into: self) as? A else {
            fatalError("Wrong object type")
        }
        return obj
    }

    public func saveOrRollBack() -> Bool {
        do {
            try save()
            return true
        } catch {
            rollback()
            return false
        }
    }
    
    public func performChanges(block: @escaping ()->()) {
        perform {
            block()
            if self.saveOrRollBack() {
                print("保存成功")
            } else {
                print("保存失败")
            }
        }
    }
}

2)在给Student类扩展,让其可以一个方法就添加数据

extension Student {
    public static func insertIntoContext(moc: NSManagedObjectContext, contentDic: [String:Any]) -> Student {
        let student: Student = CoreDataManager.manager.managedObjectContext!.insertObject()
        student.student_id = contentDic["student_id"] as? Int16 ?? 0
        student.name = contentDic["name"] as? String ?? "没有数据"
        student.gender = contentDic["gender"] as? Bool ?? false
        student.class_num = contentDic["class_num"] as? Int16 ?? 0
        return student
    }
}

3)然后再使用时候就是这个样子的了:

CoreDataManager.manager.managedObjectContext?.performChanges {
            Student.insertIntoContext(moc: CoreDataManager.manager.managedObjectContext!, contentDic: ["student_id":1, "name":"bbh", "gender":true, "class_num":7])
        }

个人觉得这里需要一定时间和经验来理解,我的Github demo在这里 ,大家可以去看看高亮的代码,下载下来跑跑。

2、删除数据
删除数据本身来说非常简单:

// 这里使用了之前的单例来获取ManagedObjectContext
var s = Student()
CoreDataManager.manager.managedObjectContext?.performChanges {
            s = Student.insertIntoContext(moc: CoreDataManager.manager.managedObjectContext!, contentDic: ["student_id":1, "name":"bbh", "gender":true, "class_num":7])
        } // 这里是添加了一个数据
CoreDataManager.manager.managedObjectContext?.performChanges {
            CoreDataManager.manager.managedObjectContext?.delete(s)
        }// 这里删除了相应的数据

在实际使用过程中,建议大家使用监听数据的形式,不然则需要手动管理数据与UI之间的关系。
从这里开始,我们基本完成了CoreData的初始化,配置相应的类,知道了如何增删改查,下面我们将Demo完善就好


六、APP主要代码
这里是搭建了一个tableView来展示CoreData中保存的数据,查看Demo 的全部代码请点击这里
ViewController代码:

class ViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var name: UITextField!
    @IBOutlet weak var gender: UISegmentedControl!
    @IBOutlet weak var student_id: UITextField!
    @IBOutlet weak var class_num: UITextField!
    var fetchedResultsController: NSFetchedResultsController<NSFetchRequestResult>?
    @IBAction func addInfo(_ sender: Any) {
        // 添加同学信息
        CoreDataManager.manager.managedObjectContext?.performChanges {
            let student_id: String = self.student_id.text!
            let name: String = self.name.text!
            let gender: Bool = self.gender.selectedSegmentIndex == 0 ? true : false
            let class_num: String = self.class_num.text!
            Student.insertIntoContext(moc: CoreDataManager.manager.managedObjectContext!, contentDic: ["student_id":Int16(student_id) ?? -1, "name":name, "gender":gender, "class_num":Int16(class_num) ?? -1 as Int16])
        }
    }
    @IBAction func tapBackView(_ sender: Any) {
        self.view.endEditing(true)
    }
}

ViewController扩展:

// MARK: - Life Circle
extension ViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        initFetchedResultsController()
    }
}


// MARK: - Actions
extension ViewController {
    func initFetchedResultsController() {
        let request = Student.sortedFetchRequest
        request.fetchBatchSize = 20
        fetchedResultsController = NSFetchedResultsController<NSFetchRequestResult>(fetchRequest: request, managedObjectContext: CoreDataManager.manager.managedObjectContext!, sectionNameKeyPath: nil, cacheName: "cacheName")
        fetchedResultsController?.delegate = self
        try! fetchedResultsController?.performFetch()
    }
}

ViewController tableView协议:

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if fetchedResultsController == nil {
            return 0
        }
        return (fetchedResultsController?.sections![section].numberOfObjects)!
    }
    func numberOfSections(in tableView: UITableView) -> Int {
        if fetchedResultsController == nil {
            return 0
        }
        return (fetchedResultsController?.sections?.count)!
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! StudentTableViewCell
        guard let s = fetchedResultsController?.sections?[indexPath.section].objects?[indexPath.row] as? Student else { return cell }
        cell.name.text = s.name
        cell.gender.text = s.gender ? "男" : "女"
        cell.class_num.text = "\(s.class_num)"
        cell.student_id.text = "\(s.student_id)"
        return cell
    }
    
}

下面是NSFetchedResultsControllerDelegate:

extension ViewController: NSFetchedResultsControllerDelegate {
    func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.beginUpdates()
    }
    
    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
        switch type {
        case .insert:
            guard let indexPath = newIndexPath else { fatalError("Index path should be not nil") }
            tableView.insertRows(at: [indexPath], with: .fade)
        case .update:
            break
            /*
            guard let indexPath = indexPath else { fatalError("Index path should be not nil") }
            let object = objectAtIndexPath(indexPath)
            guard let cell = tableView.cellForRow(at: indexPath) as? Cell else { break }
            delegate.configure(cell, for: object)
 */
        case .move:
            guard let indexPath = indexPath else { fatalError("Index path should be not nil") }
            guard let newIndexPath = newIndexPath else { fatalError("New index path should be not nil") }
            tableView.deleteRows(at: [indexPath], with: .fade)
            tableView.insertRows(at: [newIndexPath], with: .fade)
        case .delete:
            guard let indexPath = indexPath else { fatalError("Index path should be not nil") }
            tableView.deleteRows(at: [indexPath], with: .fade)
        }
    }
    
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.endUpdates()
    }
}

3、尾巴

在本文中主要涵盖的要点有:
CoreData模型的建立,CoreData新建模型对应子类,CoreData的操作(使用上下文封装来实现增删改查),以及使用NSFetchedResultsController来实现数据和UI的合成。
希望对有需求的同学有所帮助,谢谢阅读。

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

推荐阅读更多精彩内容