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文件,展示效果如下:
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:
在此我们已经有了数据模型,有了数据模型的子类,有了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的合成。
希望对有需求的同学有所帮助,谢谢阅读。