CoreData 学习笔记五-CoreData+TableView使用

这篇笔记主要是介绍 CoreData 和 TableView 一起使用时候的方法。

使用 CoreData 应用中有很多列表展示的场景底层数据需要读取本地数据,我以前的做法并没有遵循官方文档的做法使用 NSFetchedResultsController (opens new window) 这个类去进行数据管理,而是我自己通过 CoreData 的 API 去读写数据,自己管理 UITableView 刷新的,其实是维护了两份数据源,一份内存中的数据,一份本地的数据,当用户插入数据的时候先通过 CoreData 将数据插入本地,然后再插入内存。这样管理非常费劲,而且在后面加入 iCloud 同步的时候会面临什么时候将本地数据加载入内存中这样的问题。看了官方文档之后才知道有更好的做法,于是变更了自己使用 CoreData+UITableview 的方式,使用官方推荐的 API NSFetchedResultsController 来做列表数据管理。

NSFetchedResultsController: A controller that you use to manage the results of a Core Data fetch request and to display data to the user. 即管理 CoreData 获取结果,并将结果展示给用户的过程。

使用一个简单的例子来快速熟悉 NSFetchedResultsController 的使用,假设我们有 Book 类型

extension Book {
    @nonobjc public class func fetchRequest() -> NSFetchRequest<Book> {
        return NSFetchRequest<Book>(entityName: "Book")
    }
    @NSManaged public var name: String?
    @NSManaged public var id: String?
}

# NSFetchedResultsController 使用

  1. viewDidLoad 中初始化 NSFetchedResultsController

    override func viewDidLoad() {
    	//1. 初始化 NSFetchRequest
    	//2. 设置获取数据源时候的排序规则 NSSortDescriptor
    	//3. 使用 NSFetchRequest+CoreDataContext 来初始化 NSFetchedResultsController
    	//4. 设置 NSFetchedResultsController 代理为当前视图控制器
    	let request = NSFetchRequest<Book>.init(entityName: "Book")
    	let nameSort = NSSortDescriptor(key: "name", ascending: true)
    	request.sortDescriptors = [nameSort]
    	self.fetchedResultsController = NSFetchedResultsController<Book>.init(fetchRequest: request, managedObjectContext: coreDataContext, sectionNameKeyPath: nil, cacheName: nil)
    	self.fetchedResultsController.delegate = self
    	do {
    	    try self.fetchedResultsController.performFetch()
    	} catch {
    	    NSLog("fetch error \(error)")
    	}
    }
    

    NSFetchedResultsController 设置代理后,这个控制器会注册接收对应 ManagedObjectContext 改变的通知。当 ManagedObjectContext 发生影响改变结果集(FetchedResults)的时候, NSFetchedResultsController 会通知代理结果的变化,进而更新视图。后面步骤会涉及到这部分。

  2. ViewController 需要正常实现 TableView 的代理方法和数据源方法。这里主要用到 self.fetchedResultsController 的 sections 属性。

    func numberOfSections(in tableView: UITableView) -> Int {
        return self.fetchedResultsController.sections?.count ?? 0
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let sectionInfo:NSFetchedResultsSectionInfo? = self.fetchedResultsController.sections?[section]
        return sectionInfo?.numberOfObjects ?? 0
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        var cell = tableView.dequeueReusableCell(withIdentifier: "cell")
        if cell == nil {
            cell = UITableViewCell(style: .subtitle, reuseIdentifier: "cell")
        }
        let book = self.fetchedResultsController.object(at: indexPath)
        var content = cell!.defaultContentConfiguration()
        content.text = book.name
        cell!.contentConfiguration = content
        return cell!
    }
    

    sections 属性类型是实现了 [NSFetchedResultsSectionInfo](https://developer.apple.com/documentation/coredata/nsfetchedresultssectioninfo) 协议的实例数组类型。

  3. 视图控制器需要实现 [NSFetchedResultsControllerDelegate](https://developer.apple.com/documentation/coredata/nsfetchedresultscontrollerdelegate) 协议中的方法。这些代理方法的目的就是数据变动的时候通知界面,让开发者有机会对数据变化做出响应。

    //Notifies the delegate that section and object changes are about to be processed and notifications will be sent.
    func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.beginUpdates()
    }
    //Notifies the receiver of the addition or removal of a section.
    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
        switch type {
        case .insert:
            let indexSet = IndexSet.init(integer: sectionIndex)
            self.tableView.insertSections(indexSet, with: .fade)
            break
        case .delete:
            let indexSet = IndexSet.init(integer: sectionIndex)
            self.tableView.deleteSections(indexSet, with: .fade)
            break
        case .move:
            fallthrough
        case .update:
            break
        @unknown default:
            break
        }
    }
    //Notifies the receiver that a fetched object has been changed due to an add, remove, move, or update.
    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
        switch type {
        case .insert:
            self.tableView.insertRows(at: [newIndexPath!], with: .fade)
        case .delete:
            self.tableView.deleteRows(at: Array([indexPath!]), with: .fade)
        case .update:
            self.tableView.reloadRows(at: Array([indexPath!]), with: .fade)
        case .move:
            self.tableView.reloadData()
        @unknown default: break
            //
        }
    }
    //Notifies the delegate that all section and object changes have been sent.
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.endUpdates()
    }
    
  4. 当要增加条目的时候只需要向 CoreData 中插入数据,视图就会自动更新了。删除数据同理

    func insertBook(with id:String,name:String) {
        let book = Book.init(context: self.coreDataContext)
        book.id = id
        book.name = name
        do {
            try coreDataContext.save()
        } catch {
            NSLog("core data insert error: \(error)")
        }
    }
    

这样我们就不用再维护内存中的数据了,底层数据变动系统都帮我们做好了。同理 UICollectionView 也可以使用类似的方法来进行数据管理。更详细的说明官方文档和用到的这些类的 API 介绍的都比较清楚,这里不再赘述了。

参考地址:

  1. Core Data Programming Guide-Connecting the Model to Views (opens new window)