iOS用の純正データベースフレームワーク「Core Data」にはテーブルビューと組み合わせて使用することができるNSFetchedResultsControllerというクラスが存在します。

NSFetchedResultsControllerではデータベースに格納されたデータをNSIndexPathを指定して取得することができるのでテーブルビューのセルに設定する情報を簡単に取得できます。

またNSFetchedResultsControllerDelegateを設定すればデータの変更通知を受け取り、きめ細かくテーブルビュー側に反映することも可能です。

今回はCore Dataに保存されたデータを、セクションごとに分類してテーブルビューに表示する方法を説明します。

目次

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

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

.xcdatamodeldの作成

Entity

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

NSManagedObjectのサブクラスの作成

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

Book.swift

class Book: NSManagedObject {

// Insert code here to add functionality to your managed object subclass

}

Book+CoreDataProperties.swift

extension Book {

    @NSManaged var title: String
    @NSManaged var publisher: String

}

「String?」となっていたら「String」に修正しておきます。

ViewControllerの処理の作成

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

「Use Core Data」にチェックを入れてプロジェクトを作成した場合、Core Dataを使用するためのお膳立てはAppDelegateで自動的に行われています。このためViewController内ではNSFetchedResultsControllerを生成するだけですみます。

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

class ViewController: UIViewController, UITableViewDataSource {
    var managedObjectContext: NSManagedObjectContext! = nil
    var fetchedResultsController: NSFetchedResultsController! = nil
    @IBOutlet weak var tableView: UITableView!
    
    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
        //一度に取得するサイズの指定
        fetchRequest.fetchBatchSize = 20
        //ソートキーの指定。セクションが存在する場合セクションに対応した属性を必ず最初に指定する
        let publisherSortDescriptor = NSSortDescriptor(key: "publisher", ascending: true)
        let titleSortDescriptor = NSSortDescriptor(key: "title", ascending: true)
        fetchRequest.sortDescriptors = [publisherSortDescriptor, titleSortDescriptor]
        
        //セクションの名前として"publisher"を指定
        let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest,
                                                                  managedObjectContext: managedObjectContext, sectionNameKeyPath: "publisher", cacheName: "Demo")
//        fetchedResultsController = self
        do {
            try fetchedResultsController.performFetch()
        } catch {
            abort()
        }
        return fetchedResultsController
    }

    func createDemoData() {
        /////ダミーデータの生成
        let bookDataArray = [
            ["早川書房", "エターナル・フレイム"]
            ,["東京創元社", "帰還兵の戦場1"]
            ,["東京創元社", "叛逆航路"]
            ,["東京創元社", "ラドチ戦史"]
            ,["岩波書店", "影との戦い"]
            ,["岩波書店", "こわれた腕環"]
            ,["岩波書店", "さいはての島へ"]
            ,["岩波書店", "帰還"]
            ,["岩波書店", "ホビットの冒険 上"]
            ,["文藝春秋", "ミスター・メルセデス 上"]
            ,["早川書房", "クロックワーク・ロケット"]
            ,["早川書房", "神の水"]
            ,["早川書房", "ロックイン-統合捜査-"]
            ,["文藝春秋", "11/22/63(上)"]
        ]
        for bookData in bookDataArray {
            let book = NSEntityDescription.insertNewObjectForEntityForName("Book", inManagedObjectContext: fetchedResultsController.managedObjectContext) as! Book
            book.publisher = bookData[0]
            book.title = bookData[1]
        }
        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)")
            }
        }
    }
    
    // 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)
        let book = fetchedResultsController.objectAtIndexPath(indexPath) as! Book
        cell.textLabel?.text = book.title
        return cell
    }
    
}

viewDidLoad()でNSFetchedResultsControllerを生成しています。データをセクション分けして表示する際ソートキーの指定が重要です。セクションを指定する場合そのセクションに対応した属性が第1ソートキーとなります。

//ソートキーの指定。セクションが存在する場合セクションに対応した属性を必ず最初に指定する
let publisherSortDescriptor = NSSortDescriptor(key: "publisher", ascending: true)
let titleSortDescriptor = NSSortDescriptor(key: "title", ascending: true)
fetchRequest.sortDescriptors = [publisherSortDescriptor, titleSortDescriptor]

sectionNameKeyPathでも”publisher”を指定していることに注目してください。

let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: managedObjectContext, sectionNameKeyPath: "publisher", cacheName: "Demo")

UITableViewDataSourceの実装ではfetchedResultsControllerから情報を取得します。Core DataへのアクセスをNSFetchedResultsControllerが仲介することでとてもシンプルなッコードになっていることがわかります。

// 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)
    let book = fetchedResultsController.objectAtIndexPath(indexPath) as! Book
    cell.textLabel?.text = book.title
    return cell
}

実行結果

Result

プログラムを実行すると上の画像のような結果が評価されます。出版社ごとに書籍がタイトル順で表示されました。

まとめ

NSFetchedResultsControllerを利用してUITableViewで効率良くCore Dataのデータを表示する方法を説明しました。

今回は説明していませんが、実際のアプリではデータが追加、更新、削除されるのが普通です。NSFetchedResultsControllerDelegateを利用してUITableViewの情報を更新する方法は次回説明します。