Core DataとNSFetchedResultsControllerを使えば、Core Dataに変更が入るたびにViewにも変更が自動反映されるようになって超便利なのですが、いっつも書き方を忘れるので、各種ビューでの実装方法を記述します。
ちなみに公式のドキュメントはこちら
以下は、Movie
という Core DataのEntityが存在すると仮定しての実装方法になります。
NSFetchedResultsControllerの実装方法例
fetchedResultsController.delegateに設定する NSFetchedResultsControllerDelegate
やソート条件は適宜環境に合わせて読み替えてください。
lazy var fetchedResultsController:NSFetchedResultsController<Movie> = {
let fetchRequest:NSFetchRequest<Movie> = Movie.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Movie.createAt, ascending: true)]
let fetchedResultsController = NSFetchedResultsController<Movie>(fetchRequest: fetchRequest, managedObjectContext:context, sectionNameKeyPath: nil, cacheName: nil)
fetchedResultsController.delegate = fetchedResultsControllerDelegate
try! fetchedResultsController.performFetch()
return fetchedResultsController
}()
各種ビューへの反映
UITableView
UITableViewDataSource
公式のドキュメントとほぼ同じのコードです。
func numberOfSections(in tableView: UITableView) -> Int {
if let count = fetchedResultsController.sections?.count {
return count
}
return 0
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let sections = fetchedResultsController.sections else {
fatalError("No sections in fetchedResultsController")
}
let sectionInfo = sections[section]
return sectionInfo.numberOfObjects
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let object = fetchedResultsController.object(at: indexPath)
let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
// 以下、cellにobjectのデータを反映させる処理
}
UITableView with NSFetchedResultsControllerDelegate
アニメーション部分 with
はお好みで。
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .delete:
tableView.deleteRows(at: [indexPath!], with: .automatic)
case .insert:
tableView.insertRows(at: [newIndexPath!], with: .automatic)
case .move:
tableView.moveRow(at: indexPath!, to: newIndexPath!)
case .update:
tableView.reloadRows(at: [indexPath!], with: .automatic)
@unknown default:
break
}
}
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
UICollectionView
UICollectionViewDataSource
func numberOfSections(in collectionView: UICollectionView) -> Int {
if let count = fetchedResultsController.sections?.count {
return count
}
return 0
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
guard let sections = fetchedResultsController.sections else {
fatalError("No sections in fetchedResultsController")
}
let sectionInfo = sections[section]
return sectionInfo.numberOfObjects
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath)
let object = fetchedResultsController.object(at: indexPath)
// 以下、cellにobjectのデータを反映させる処理
}
UICollectionView with NSFetchedResultsControllerDelegate
UICollectionViewには、UITableViewと違い、beginUpdates
やendUpdates
が無く、controller(_:didChange:at:for:newIndexPath:)
で変更点を蓄えておき、performBatchUpdates(_:completion:)
で反映を行います。
private var blocks:[() -> Void] = []
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .delete:
blocks.append { [weak self] in
self?.collectionView.deleteItems(at: [indexPath!])
}
case .insert:
blocks.append { [weak self] in
self?.collectionView.insertItems(at: [newIndexPath!])
}
case .move:
blocks.append { [weak self] in
self?.collectionView.moveItem(at: indexPath!, to: newIndexPath!)
}
case .update:
blocks.append { [weak self] in
self?.collectionView.reloadItems(at: [indexPath!])
}
@unknown default:
break
}
}
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
blocks.removeAll()
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
collectionView.performBatchUpdates({
self.blocks.forEach { (block) in
block()
}
}, completion: nil)
}
WKInterfaceTable for watchOS (おまけ)
WKInterfaceTableには、performBatchUpdates
やbegin/endUpdates
などの仕組みがないので、controllerDidChangeContent(_:)
でまとめて更新してしまうのが楽です。
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
}
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard let fetchedObjects = controller.fetchedObjects else { return }
table.setNumberOfRows(fetchedObjects.count, withRowType: identifier)
let rowCount = table.numberOfRows
for i in 0..<rowCount {
let object:Movie = fetchedObjects[i] as! Movie
let row = table.rowController(at: i)
// 以下、rowにobjectのデータを反映させる処理
}
}
つかおう! Core Data!