Core DataのクラスNSFetchedResultsControllerを使用すれば、テーブルビューで効率良くデータを表示することができます。

Core Dataのデータが変更されない場合は前回説明した方法で対応できますが、データが追加、更新、削除される場合は、NSFetchedResultsControllerDelegateのメソッドを使って、データの変更をテーブルビューに反映する必要がでてきます。

今回はNSFetchedResultsControllerDelegateを実装し、Core Dataに保存されたデータが変更された場合、テーブルビューの情報を正しく変更する方法を説明します。

サンプルプログラムの作成

.xcdatamodeldの作成

Xcodeを立ち上げ新規プロジェクトで「Single View Application」を選択します。プロジェクトの設定で「Use Core Data」のチェックを入れてプロジェクトを作成します。

Entity

.xcdatamodeldを選択し、新しくBookエンティティを追加します。publisher、titleという名前の属性を追加し両方の方をStringに変更、Optionalのチェックを外しておきます。

NSManagedObjectのサブクラスの作成

「File > New > File」を選択し「Core Data」から「NSManagedObject subclass」を選択します。先ほど作成した「Book」エンティティを選択すると以下の2つのファイルが作成されます。

ViewControllerの処理の作成

Main.storyboardにTable Viewをドロップして設置し、ViewController.swiftにControlドラッグしtableViewという名前でOutletを作成しします。またTable ViewのデータソースとしてViewControllerを指定しておきます。

まずソースコード全体を掲載します。

class ViewController: UIViewController, UITableViewDelegate, NSFetchedResultsControllerDelegate {

    @IBOutlet weak var tableView: UITableView!

    var managedObjectContext: NSManagedObjectContext! = nil
    var fetchedResultsController: NSFetchedResultsController! = nil
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
        managedObjectContext = appDelegate.managedObjectContext
        fetchedResultsController = createFetchedResultsController()
        if fetchedResultsController.fetchedObjects?.count == 0 {
            createDemoData()
        }
        dumpDemoData()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
    func createFetchedResultsController() -> NSFetchedResultsController {
        /////NSFetchedResultsControllerの生成
        let fetchRequest = NSFetchRequest()
        //エンティティの名前を指定
        let entity = NSEntityDescription.entityForName("Book", inManagedObjectContext: managedObjectContext)
        fetchRequest.entity = entity
        //ソートキーの指定。セクションが存在する場合セクションに対応した属性を必ず最初に指定する
        let publisherSortDescriptor = NSSortDescriptor(key: "publisher", ascending: true)
//        let titleSortDescriptor = NSSortDescriptor(key: "title", ascending: true)
        let priceSortDescriptor = NSSortDescriptor(key: "price", ascending: true)
        fetchRequest.sortDescriptors = [publisherSortDescriptor, priceSortDescriptor]
        
        //セクションの名前として"publisher"を指定
        let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, sectionNameKeyPath: "publisher", cacheName: nil)
        fetchedResultsController.delegate = self
        do {
            try fetchedResultsController.performFetch()
        } catch {
            abort()
        }
        return fetchedResultsController
    }
    
    func deleteDemoData() {
        let fetchRequest = NSFetchRequest(entityName: "Book")
        let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
        do {
            try managedObjectContext.executeRequest(deleteRequest)
            try managedObjectContext.save()
        }  catch let error as NSError {
            fatalError("Failed to delete books: \(error)")
        }
        print("削除成功")
    }
    
    func createDemoData() {
        /////ダミーデータの生成
        let bookDataArray = [
            ["早川書房", "エターナル・フレイム", 2138]
            ,["東京創元社", "帰還兵の戦場1", 792]
            ,["東京創元社", "叛逆航路", 1404]
            ,["東京創元社", "ラドチ戦史", 1080]
            ,["岩波書店", "影との戦い", 777]
            ,["岩波書店", "こわれた腕環", 734]
            ,["岩波書店", "さいはての島へ", 820]
            ,["岩波書店", "帰還", 820]
            ,["岩波書店", "ホビットの冒険 上", 777]
            ,["文藝春秋", "ミスター・メルセデス 上", 1899]
            ,["早川書房", "クロックワーク・ロケット", 2041]
            ,["早川書房", "神の水", 1944]
            ,["早川書房", "ロックイン-統合捜査-", 1555]
            ,["文藝春秋", "11/22/63(上)", 2199]
        ]
        for bookData in bookDataArray {
            let book = NSEntityDescription.insertNewObjectForEntityForName("Book", inManagedObjectContext: fetchedResultsController.managedObjectContext) as! Book
            book.publisher = bookData[0] as! String
            book.title = bookData[1] as! String
            book.price = bookData[2] as! NSNumber
        }
        do {
            try fetchedResultsController.managedObjectContext.save()
        } catch {
            print(error)
            abort()
        }
    }
    
    func dumpDemoData() {
        ///// 取得済みデータの一覧表示
        let fetchedObjects: [AnyObject]? = fetchedResultsController.fetchedObjects
        if let fetchedObjects = fetchedObjects {
            for fetchedObject in fetchedObjects {
                let book = fetchedObject as! Book
                print("publisher=\(book.publisher) title=\(book.title) price=\(book.price)")
            }
        }
    }
    
    // MARK: - Table View
    func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        //セクションタイトルを返す
        return fetchedResultsController.sections?.count ?? 0
    }
    
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        //セクション内のオブジェクトの数を返す
        let sectionInfo = fetchedResultsController.sections![section]
        return sectionInfo.numberOfObjects
    }
    
    func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        //セクション名を返す
        let sectionInfo = fetchedResultsController.sections![section]
        return sectionInfo.name
    }
    
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        //セルに値の設定
        let cell = tableView.dequeueReusableCellWithIdentifier("myCell", forIndexPath: indexPath)
        configureCell(cell, object: fetchedResultsController.objectAtIndexPath(indexPath))
        return cell
    }
    
    func configureCell(cell: UITableViewCell, object: AnyObject) {
        let book = object as! Book
        cell.textLabel?.text = book.title
        cell.detailTextLabel?.text = String(book.price)
    }

    // MARK: -NSFetchedResultsControllerDelegate
    func controllerWillChangeContent(controller: NSFetchedResultsController) {
        self.tableView.beginUpdates()
    }
    
    func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
        //CoreData側が変更された時、その変更をテーブルビューに反映するための処理
        switch type {
        case .Insert:
            if let newIndexPath = newIndexPath {
                tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation:UITableViewRowAnimation.Fade)
            }
        case .Delete:
            if let indexPath = indexPath {
                tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Fade)
            }
aliases: [/coredata-nsfetchedresultscontrollerdelegate/]
        case .Update:
            if let indexPath = indexPath {
                let cell = tableView.cellForRowAtIndexPath(indexPath)
                if let cell = cell {
                    configureCell(cell, object: anObject)
                }
            }
        case .Move:
            if let indexPath = indexPath  {
                if let newIndexPath = newIndexPath {
                    tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Fade)
                    tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
                }
            }
        }
    }
    
    func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
        //CoreData側が変更されセクションが追加削除された場合、その変更をテーブル変更するための処理
        switch(type) {
        case .Insert:
            tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Fade)
        case .Delete:
            tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Fade)
            
        default:
            break
        }
    }
    
    func controllerDidChangeContent(controller: NSFetchedResultsController) {
        tableView.endUpdates()
    }
    
    @IBAction func addTapped(sender: AnyObject) {
        do {
            let book = NSEntityDescription.insertNewObjectForEntityForName("Book", inManagedObjectContext: fetchedResultsController.managedObjectContext) as! Book
            book.publisher = "早川書房"
            book.title = "虐殺器官"
            book.price = 583
            try fetchedResultsController.managedObjectContext.save()
        } catch {
            print(error)
            abort()
        }
    }
    
    @IBAction func updateTapped(sender: AnyObject) {
        let fetchRequest = NSFetchRequest(entityName: "Book")
        let predicate = NSPredicate(format: "publisher=%@", "早川書房") //削除するオブジェクトの検索条件
        fetchRequest.predicate = predicate
        do {
            let books = try managedObjectContext.executeFetchRequest(fetchRequest) as! [Book]
            for book in books {
                book.title = "★" + book.title
            }
            try fetchedResultsController.managedObjectContext.save()
        } catch {
            print(error)
        }
    }
    
    @IBAction func deleteTapped(sender: AnyObject) {
        let fetchRequest = NSFetchRequest(entityName: "Book")
        let predicate = NSPredicate(format: "publisher=%@", "早川書房") //削除するオブジェクトの検索条件
        fetchRequest.predicate = predicate
        do {
            let books = try managedObjectContext.executeFetchRequest(fetchRequest) as! [Book]
            for book in books {
                fetchedResultsController.managedObjectContext.deleteObject(book)
            }
            try fetchedResultsController.managedObjectContext.save()
        } catch {
            print(error)
        }
    }

    @IBAction func resetTapped(sender: AnyObject) {
        deleteDemoData()
        //バッチ削除した場合fetchedResultsControllerを取得しなおす必要あり。
        do {
            try fetchedResultsController.performFetch()
        } catch {
            abort()
        }
        tableView.reloadData()
        createDemoData()
    }
}

以下ポイントを説明します。

最初にdelegateの設定を行いNSFetchedResultsControllerDelegateの各メソッドが呼ばれるようにします。

        let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, sectionNameKeyPath: "publisher", cacheName: nil)
        fetchedResultsController.delegate = self

NSFetchedResultsControllerDelegateの実装は以下の通りです。

    func controllerWillChangeContent(controller: NSFetchedResultsController) {
        self.tableView.beginUpdates()
    }
    
    func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
        //CoreData側が変更された時、その変更をテーブルビューに反映するための処理
        switch type {
        case .Insert:
            if let newIndexPath = newIndexPath {
                tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation:UITableViewRowAnimation.Fade)
            }
        case .Delete:
            if let indexPath = indexPath {
                tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Fade)
            }
aliases: [/coredata-nsfetchedresultscontrollerdelegate/]
        case .Update:
            if let indexPath = indexPath {
                let cell = tableView.cellForRowAtIndexPath(indexPath)
                if let cell = cell {
                    configureCell(cell, object: anObject)
                }
            }
        case .Move:
            if let indexPath = indexPath  {
                if let newIndexPath = newIndexPath {
                    tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Fade)
                    tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
                }
            }
        }
    }
    
    func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
        //CoreData側が変更されセクションが追加削除された場合、その変更をテーブル変更するための処理
        switch(type) {
        case .Insert:
            tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Fade)
        case .Delete:
            tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: UITableViewRowAnimation.Fade)
            
        default:
            break
        }
    }
    
    func controllerDidChangeContent(controller: NSFetchedResultsController) {
        tableView.endUpdates()
    }

全体の処理がtableView.beginUpdates/endUpdatesで囲まれているのはデータと画面表示の整合性がとれなくなるのを防ぐためです。

didChangeObjectがCore Dataのデータが変更されたときに呼ばれるメソッドで、変更の種類(Insert、Delete、Update、Move)に応じてテーブルの情報を変更しています。

セクションが存在する場合、セクション情報を追加、削除する処理も必要となります。

データの追加や更新は本来別の画面を作成しその画面とデータをやりとりして行うのが普通ですが、今回は説明のため画面下のツールバーボタンをタップした際、固定のデータを追加、更新、削除する処理としました。

    @IBAction func addTapped(sender: AnyObject) {
        do {
            let book = NSEntityDescription.insertNewObjectForEntityForName("Book", inManagedObjectContext: fetchedResultsController.managedObjectContext) as! Book
            book.publisher = "早川書房"
            book.title = "虐殺器官"
            book.price = 583
            try fetchedResultsController.managedObjectContext.save()
        } catch {
            print(error)
            abort()
        }
    }
    
    @IBAction func updateTapped(sender: AnyObject) {
        let fetchRequest = NSFetchRequest(entityName: "Book")
        let predicate = NSPredicate(format: "publisher=%@", "早川書房") //削除するオブジェクトの検索条件
        fetchRequest.predicate = predicate
        do {
            let books = try managedObjectContext.executeFetchRequest(fetchRequest) as! [Book]
            for book in books {
                book.title = "★" + book.title
            }
            try fetchedResultsController.managedObjectContext.save()
        } catch {
            print(error)
        }
    }
    
    @IBAction func deleteTapped(sender: AnyObject) {
        let fetchRequest = NSFetchRequest(entityName: "Book")
        let predicate = NSPredicate(format: "publisher=%@", "早川書房") //削除するオブジェクトの検索条件
        fetchRequest.predicate = predicate
        do {
            let books = try managedObjectContext.executeFetchRequest(fetchRequest) as! [Book]
            for book in books {
                fetchedResultsController.managedObjectContext.deleteObject(book)
            }
            try fetchedResultsController.managedObjectContext.save()
        } catch {
            print(error)
        }
    }

    @IBAction func resetTapped(sender: AnyObject) {
        deleteDemoData()
        //バッチ削除した場合fetchedResultsControllerを取得しなおす必要あり。
        do {
            try fetchedResultsController.performFetch()
        } catch {
            abort()
        }
        tableView.reloadData()
        createDemoData()
    }

実行結果

Add

▲「追加」ボタンをタップすると本のデータが1つ追加されます。

Update

▲「更新」ボタンをタップすると「早川書房」の書籍のタイトルに「★」が追加されました。

Delete

▲「削除」ボタンをタップすると「早川書房」の本が全て削除されました。

まとめ

NSFetchedResultsControllerDelegateに対応する方法を説明しました。

実コードの場合、例えば編集処理は、タップされた行のNSManagedObjectを編集画面に引き渡し、「完了」ボタンが押されたタイミングで一覧画面側の情報が書き換わることになります。NSManagedObjectContextが同じならばNSFetchedResultsControllerDelegateの処理を変更する必要は特にありません。

また削除処理ではテーブルビューのcommitEditingStyleでNSManagedObjectを削除するような処理になります。この場合もNSFetchedResultsControllerDelegate側の処理はこのまま利用することができます。