14
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Core Dataで、各種Viewとデータバインディング

Last updated at Posted at 2019-10-06

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と違い、beginUpdatesendUpdatesが無く、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には、performBatchUpdatesbegin/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!

14
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?