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文件,展示效果如下:

Core Data 初识Demo_第1张图片
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 初识Demo_第2张图片
为数据模型配置子类

三、设置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:

Core Data 初识Demo_第3张图片
创建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(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 {
        let request = NSFetchRequest(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 {
        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?
    @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(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) {
        tableView.beginUpdates()
    }
    
    func controller(_ controller: NSFetchedResultsController, 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) {
        tableView.endUpdates()
    }
}

3、尾巴

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

你可能感兴趣的:(Core Data 初识Demo)