Swiftで遊ぼう! on Hatena

あしたさぬきblogでやってた初心者オヤジのiOSプログラミング奮闘記がHatenaに来ました

Swiftで遊ぼう! - 582 - Core Dataチュートリアル:HitList

2017年3月25日:iOS 10でCoreDataがシンプルになり扱いやすくなったんで改訂*1

まえがき

iOS 10からCore Dataに変更が加わってかなり扱いやすくなったと思います。Xcodeでプロジェクトを立ち上げるとき「Use Core Data」にチェックマークを入れると、AppDelegate.swifにできるコードがかなりシンプルになっています。基本的に「NSPersistentContainer」クラス1つでCore Dataを一括して扱えるようになったんで、Core Dataを組み込んでいないプロジェクトに後から組み込みやすくなりました。私が楽に思える部分は、NSManagedObjectからサブクラスを生成するステップをとらなくてもエンティティを「クラス」として扱えるようになったところです。NSManagedObjectModelのエンティティをみれば、アトリビュート・インスペクタの「Codegen」という項目が増えています。デフォルトで「Class Definition」が選択されていて、これはエンティティを作成したら自動的に「クラス」扱いできるってことです。しかし、このままではクラスを拡張することができないんで、「Category/Extention」に変更するのがお勧めです。

f:id:yataiblue:20170325172204j:plain

ここで解説するCore Dataのチュートリアルチュートリアルで定評のあるRayさんのところの記事「Getting Started with Core Data Tutorial」を解説しています。本家の記事もSwift 3向けに改訂されているのですが内容が古いんです。こんなこともあるんですね。私が新しいスタイルに変更して解説しみます。iOS 10スタイルに変更するとかなりシンプルになるので、Rayさんのチュートリアルと比較しながら読んでみるといいでしょう*2

一般的なテーブルビュー・プロジェクト作成

まずBASIC 新規プロジェクト作成手順の手順に従って、新しいXcodeプロジェクトを作ります。Single View Applicationテンプレートを選んで、「Product Name:」を「HitList」にします。今回はiPhoneだけで動作させるので「Devices:」を「iPhone」にします。

その下にある3つのチェックボックス(Use Core Data、Include Unit Tests、Include UI Tests」全てにマークを入れます。こうするとCore Dataの機能が組み込まれます。

この「Use Core Data」のチェックマークを入れる*3ことで、Core Data Stackが初期化されます。この初期化ステップはAppDelegate.swiftファイルにCoreData関連のクラスの記述があります。iOS 10から、このスタックはNSPersistentContainerクラスにまとめられすっきりして利用しやすくなっています。

下記の内容は古い内容なので今後改訂していきます。

yataiblue.hatenablog.com

CoreDataを扱えるプロジェクトを作りました。

f:id:yataiblue:20151231144735j:plain BASIC Navigation Controllerを組み込む

このプロジェクトはSingle View Applicationなのでテーブルビューを組み込みます。テーブルビューを扱う場合、普通は項目のリスト表示と詳細ビューの切り替わりをさせるためにNavigation Controllerを使用します。Xcodeを使用すれば、Navigation Controllerの組み込みも超お手軽です。

View Controllerを選択した状態で、メニューの「Editor」>「Embed In」>「Navigation Controller」を選ぶだけでいいんです。

f:id:yataiblue:20160214203539j:plain

すると自動的にNavigation Controllerが組み込まれ、View Controllerの上部にNavigation Barのスペースができます。次にオブジェクト・ライブラリから「Table View」を下部の空いたスペースにドラッグ&ドロップします。

UIオブジェクト(ここではTable View)を既存のビューに設定するときに重要な作業は「コンストレイントの設定」です。このやり方も色々あります。コードで実装もできますがXcodeを使用するのが一般的です。私はオートレイアウトを利用するのに慣れて(BASIC オートレイアウトの調整)いるので「Pin ボタン」で設定しました。しかし、Rayさんのチュートリアルをみると、ドキュメント・アウトラインからTable Viewを選択して、親Viewに「Ctrl + ドラッグ」して選択していました。

f:id:yataiblue:20160214210416j:plain

  1. Leading Space to Container Margin
  2. Trailing Space to Container Margin
  3. Vertical Spacing to Top Layout Guide
  4. Vertical Spacing to Bottom Layout Guide

基本的に4つのルールを決めれば位置決めはできます。

テーブルビューのコンストレイントを設定したので、オブジェクト・ライブラリから「Bar Button Item」をドラッグしてNavigation Barの右隅に設置します。Itemという名前をダブルクリックして「+」に変更します*4

ここまでの作業をできていれば、Main.storyboardは次のようになっています。

f:id:yataiblue:20170325114938j:plain

次はTable Viewのデリゲーション設定です(BASIC テーブルビュー実装の基本)。Table ViewをView Controllerに「Ctrl + ドラッグ」して、DelegateとDataSourceのプロパティを持たせます*5

Table Viewのインスタンス化も必要なので、Table Viewを選択してViewControllerのコード内に「Ctrl + ドラッグ」して「@IBOutlet」プロパティを作ります。

@IBOutlet weak var tableView: UITableView!

次はBar Button Itemの「+」をViewControllerのコード内に「Ctrl + ドラッグ」して「@IBAction」メソッドを作ります。

@IBAction func addName(_ sender: UIBarButtonItem) {
 // まだ実装コードは無いです。
}

Table Viewで表示するデータを用意するのですがViewControllerにプロパティとして持たせます。

var names = [String]()

String型の空のアレー型の「names」を用意します。

テーブルビューの実装法は、「BASIC テーブルビュー実装の基本」を確認しながら進める必要があります。Rayさんのチュートリアルを見れば分かりますが、Table Viewのアトリビュート・インスペクタから設定できる「Prototype Cells」の設定は「0」です。ということはTable View Cellのインスタンス化をコードでする必要があります。TableViewクラスが持っている「register()」メソッドを使ってインスタンス化します。どこで? そうですアプリの立ち上がった時にCellのインスタンス化をさせるので、お約束のviewDidLoad()メソッド内にコードします。

override func viewDidLoad() {
    super.viewDidLoad()
    title = displayTitle
    tableView.register(UITableViewCell.self,
                           forCellReuseIdentifier: cellIdentifier)
}

少し説明が要ります。「title」はViewControllerが保持するString型の値です。「displayTitle」はハードコーディングを避けるための定数です。まだ宣言指定なので、次のメソッド内の定数「cellIdentifier」と一緒にViewControllerクラスコードの上部に次のコードを加えます。

let displayTitle = "\"The List\""
let cellIdentifier = "Cell"

Cellのインスタンス化ができたので、これをTable Viewで利用していきます。テーブルビューに具体的な値を持たせる必要がありデリゲーションを使います。TableViewクラスのプロトコールに準拠するためにViewControllerクラス宣言部分に次のコードを加えます*6

class ViewController: UIViewController, 
      UITableViewDataSource, UITableViewDelegate {

プロトコールを加えるとエラーが表示されます。デリゲーションメソッドを用意していないからです。TableViewプロトコールに準拠するために必要な必須メソッドは2つあるので次のコードを加えます*7

func tableView(_ tableView: UITableView,
               numberOfRowsInSection section: Int) -> Int {
    return names.count
}
    
func tableView(_ tableView: UITableView,
               cellForRowAt indexPath: IndexPath)
                   -> UITableViewCell {
 let cell =
     tableView.dequeueReusableCell(withIdentifier: cellIdentifier,
                                              for: indexPath)
 cell.textLabel!.text = names[indexPath.row]
 return cell
}

これでテーブルビューの実装ができました。

新しい項目を加えるためにバーアイテムのボタン「+」を押すとAlertControllerでAlertビューを出現させて新しい項目を追加させるステップを組み込みます。「addName()」メソッドに実装させます。

これをお手本に「+」ボタンのメソッドを加えます。

@IBAction func addName(_ sender: UIBarButtonItem) {
    let alert = UIAlertController(title: "New Name", 
                                message: "Add a new name", 
                         preferredStyle: .alert)

    let saveAction = UIAlertAction(title: "Save", 
                                   style: .default, 
                                 handler: {[unowned self] action in
        guard let textField = alert.textFields?.first, 
                      let nameToSave = textField.text else {
            return
        }
            
        self.names.append((textField?.text)!)
        self.tableView.reloadData()    
    })
        
    let cancelAction = UIAlertAction(title: "Cancel",
                                         style: .default)
        
    alert.addTextField()
        
    alert.addAction(saveAction)
    alert.addAction(cancelAction)
        
    present(alert, animated: true)
}

まだまだ分かっていないところがあります。テキストフィールドを設置するのに空のクロージャーを用意しなければならない? まだまだ勉強が足りません(^^;)

f:id:yataiblue:20170325121319j:plain

テーブルビューの実装は終わりました。「+」ボタンを押せばデータ入力ができます。「Save」ボタンを押して入力データをテーブル表示することができます。

f:id:yataiblue:20170325121430j:plain

ここまでの流れはテーブル実装の復習です。これからCore Dataの実装に入ります。入力されたデータは今のとこメモリーに保持されているだけなので、アプリを終了して再び立ち上げるとテーブルは真っ白になります(T_T)

f:id:yataiblue:20170325121526j:plain

これからデータに永続性を持たせるステップに入ります。

さっそく取り組もう!

Managed Object Model

保存すべきデータをエンティティとして扱います。クラスと同様と考えればいいでしょう*8。そしてエンティティはアトリビュートとリレーションシップを持っています。複数のエンティティの関連性をシェーマと呼び、これが「managed data model」です。プロジェクトを作成した時に「Use Core Data」を選択していれば自動的に作成されます。プロジェクト・ナビゲータをみれば「HitList.xcdatamodeld」があります。これがmanaged object modelです。

f:id:yataiblue:20160222174607j:plain

あくまでもmanaged object modelはエンティティの関係性を保持するクラスなので、実際のデータを保持するためにエンティティを作らないといけません。それが左下部にある「Add Entity」ボタンです。新しい「Entity」が作られるので、ダブルクリックして「Person」に変更します。

f:id:yataiblue:20160222180130j:plain

エンティティはデータを保持するCore DataのNSManagedObjectクラスになります。「エンティティ」と呼ぶのは、データベース用語になるようです。データベースはテーブル(table)形式に相当すると考えるといいでしょう。iOS 10になりこの状態で自動的に「クラス」になっています。冒頭で説明したように、アトリビュート・インスペクタにある「Codegene」の設定がデフォルトで「Class definition」になっているからです。初心者はこのままでもかまわないでしょう。このチュートリアルも変更する必要はありません。しかし、クラスをコードで拡張することができないんで、普通は「Category/Extention」に変更が勧められています。このチュートリアルでも変更します。

もう一つ重傷な設定があります。Category/Extentionに変更した場合、その上にある「Module」を「Current Project Module」にする必要があります。これは、このエンティティのスコープの範囲のことで、未設定ならグローバルになりプロジェクトがグローバルな場合思わぬエラーに遭遇してしまうようです。

f:id:yataiblue:20170325175458j:plain

このテーブルで保持する1列の項目をフィールドと呼び、これが「アトリビュート」です。

複数のエンティティの関係性は「リレーションシップ」として定義します。

これらの言葉はすべてデータベース用語です。データーベースを設計するために体系だった知識が必要になります。「正規化」なんて言葉もやっと理解できたところです(^_^;)

それではアトリビュートを1つ作ります。右下にある「Add Atribute」ボタンを押して、名前を「name」に変更して、タイプを「String」に変更します。

f:id:yataiblue:20160222181812j:plain

このチュートリアルで扱うデータはこれだけです。複雑な関連性を表現するチュートリアルがあればいいのですが、これは自分で考えて取り組まないといけませんね。

これだけで「Person」というクラスが利用できるのですが、拡張する方法を説明します。メニューから「File」->「New」->「File...」を選択して、「Cocoa Touch Class」を選択してから、「NSManagedObject」を選んで、nameはエンティティと同じ名前をつけます。このチュートリアルでは「Person」です。

import UIKit

class Person: NSManagedObject {

}

クラスが生成された状態ではエラーが出ています。どういうわけか「import CoreData」が一緒に作られないからです。まだまだ修正の余地がありますね。CoreDataをインポートします。

import UIKit
import CoreData

class Person: NSManagedObject {

}

これでmangaged data modelの準備はできました。このチュートリアルでPersonを拡張しないので、ファイルを作った意味はありませんが、やり方は知っておく必要があるでしょう。

データをセーブする

managed object modelの準備ができたので、実際にCore Dataを使って、データをセーブする機能を実装していきます。

まずCore DataのAPIを使えるようにしなければなりません。Core Dataをインポートします。

import UIKit
import CoreData

「import UIKit」の下に「import CoreData」を加えてAPIを利用できるようにします。

HitListプロジェクトのデータはViewControllerクラスの次の1行のArray型で管理していたので、NSManagedObjectクラスを継承したに書き換えます。

// 変更前
var names = [String]()
// 
// CoreDataを組み込んだので以下に変更
var people = [Person]()

これでArray型として「Person型」のデータを保持することができます。この変更に伴って、UITableViewプロトコールを準拠させるための必須メソッドにエラーが出現するでしょう。

// 変更前
func tableView(_ tableView: UITableView,
               numberOfRowsInSection section: Int) -> Int {
    return nemes.count
}

// 「names.count」は存在しないので次に変更
func tableView(_ tableView: UITableView,
               numberOfRowsInSection section: Int) -> Int {
    return peoole.count
}

もう一つCellのインスタンス化メソッドも変更します。

// 変更前
func tableView(_ tableView: UITableView,
               cellForRowAt indexPath: IndexPath)
    -> UITableViewCell {

    let cell = 
        tableView.dequeueReusableCellWithIdentifier(cellIdentifier, 
                         forIndexPath: indexPath) as UITableViewCell
    cell.textLabel!.text = names[indexPath.row]
    return cell
}

// 以前のバージョンならNSManagedObjectのデータはKVCを使ってデータ抽出
// しなければならなかったんですが、iOS 10からクラス扱いできるので
// キャスト無しでプロパティとしてアトリビュートにアクセスできます。
// 超簡単になりました!
func tableView(_ tableView: UITableView,
               cellForRowAt indexPath: IndexPath)
    -> UITableViewCell {
            
    let cell =
        tableView.dequeueReusableCell(withIdentifier: cellIdentifier,
                                          for: indexPath)
    cell.textLabel?.text = people[indexPath.row].name
    return cell
}

これで必須メソッドの変更はできました。

CoreDataを使ってデータを保存するステップを変更していきます。まず、「+」ボタンに割り付けられているメソッドをみます。

@IBAction func addName(_ sender: UIBarButtonItem) {
        
    let alert = UIAlertController(title: "New Name", 
                                message: "Add a new name", 
                         preferredStyle: .alert)

   let saveAction = UIAlertAction(title: "Save", 
                                   style: .default, 
                                 handler: {[unowned self] action in
        guard let textField = alert.textFields?.first, 
                      let nameToSave = textField.text else {
            return
        }
            
        self.names.append((textField?.text)!)
// Array型に要素を加えるメソッドを使って新しい項目を付け加えていました。
// このメソッドをそのまま使って「String型」を「Person型」に
// 変換して登録することはできません!
        self.tableView.reloadData()    
    })
        
    let cancelAction = UIAlertAction(title: "Cancel",
        style: .Default) { action in
    }
        
    alertController.addTextFieldWithConfigurationHandler {
        action in
    }
        
        
    alertController.addAction(cancelAction)
    alertController.addAction(saveAction)
        
    presentViewController(alertController, 
                  animated: true, completion: nil)
}

問題になる標準の「append」メソッドは使えない部分は、独自メソッドを用意します。

// 変更前
self.names.append((textField?.text)!)

// これを次のように変更
self.save(name: nameToSave)
// このsaveメソッドはカスタムメソッドです。

そしてカスタムメソッド「save()」を用意します。

func save(name: String) {
// 1
    guard let context = 
        (UIApplication.shared.delegate as? 
            AppDelegate)?.persistentContainer.viewContext else {
            return
        }
        
// 2
    let savePerson = Person(context: context)
        
// 3
    savePerson.name = name
        
// 4
    do {
        try context.save()
        people.append(savePerson)
    } catch let error as NSError 
        print("Could not save. \(error), \(error.userInfo)")
    }
}

さあ、今日のメイントピックとなる「String型の値を取りこんで、Person型に変換して永続データとして保存、表示させる」メソッドを説明します。

  1. データのセーブとロードは「NSManagedObjectContext」を使います。これをスケッチバッドと表現しています。全然理解できていない人は「Swiftで遊ぼう! - 530 - Core Data - Swiftで遊ぼう! on Hatena」を先に読んだ方がいいですね。NSManagedObjectのセーブや抽出作業をする場です。データの変化を記録してredoやundoもできます、SQLiteデータベースへの永続的保存や読み込みもできる優れものです。そして、一番重要なことは、自分でコーディングしなくていいんです! プロジェクト作成時に「Use Core Data」にチェックマークを入れていると、自動的にAppDelegate.swiftに組み込まれます。ということで利用だけろ考えればいいんです。当然、外部データベースへの書き込みや読み込みを担当するので重複性を避けるためシングルトンパターンでの利用になります
  2. 次はPersonクラスのインスタンスを作る必要があります。新しくインスタンスを作るステップは当然、イニシャライザーです。「init(context:NSManagedObjectContext)」を使用します。このイニシャライザーに必要な引数はCoreDataの中枢になるcontextです。これでデーターベースにスペースを確保したような状態です。
  3. Personクラスのインスタンスが生成されているので、情報を入力するステップです。
  4. そして私語に重要の重要なステップがこれです。NSMangaedObjectContextの持っているメソッド「save()」を使ってSQLiteデータベースへ永続的なデータ保存が生じます。しかし、このメソッド、「throw」が組み込まれているので、必ず「try」で投げる必要があります。「try」を付けるということは、エラーが発生する可能性があるので、「do-catch」を使用しなければならいないんです。そして、「people.append(saveOerson)」はテーブルにデータを表示させるために必要です。

CoreDataも利用することだけを考えればそれほど難しくはないですね。

Core Dataを使ったSQLiteデータベースへの保存はできるようになりました。

プロジェクトをランして、「+」ボタンを押して新規に名前を入力して「Save」ボタンを押せば、新しく入力した名前がテーブルに表示されます。この挙動は以前と同じです。

f:id:yataiblue:20170325121430j:plain

次にアプリを終了させます。再びアプリを立ち上げみると...

f:id:yataiblue:20170325121526j:plain

あれ? 永続的にデータは保持されているはずですが、テーブルに表示されません。

データを抽出

実はSQLiteデータベースに名前のデータは残っているはずなのですが、アプリを終了して立ち上げた時に読み込み機能を組み込んでいいないので表示されないんです。データベースからデータを読み込むことを「フェッチ(Fetch)」といいます。

どこでフェッチするのか?

私はviewDidLoad()でもいいんじゃないかと思いましたが、ViewControllerが複数ある場合は、RayさんのチュートリアルのようにviewWillAppear()にコードする方が望ましいと思います

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
        
    guard let context = 
        (UIApplication.shared.delegate as? 
            AppDelegate)?.persistentContainer.viewContext else {
        return
    }
        
    let request: NSFetchRequest<Person> = Person.fetchRequest()
        
    do {
        people = try context.fetch(request)
    }  catch let error as NSError {
        print("Could not fetch. \(error), \(error.userInfo)")
    }
        
}    

これでCore Dataの初級チュートリアルは終了です。

プロジェクトをランしてデータを入力します。続けてもう一つ入力します。テーブルに2つのデータが表示されている状態でアプリを終了させます。もう一度アプリを立ち上げてみます。

f:id:yataiblue:20170325121430j:plain

ほら! データは復帰しています!

下記チュートリアルiOS 10スタイルに変更して日本語解説にしました。

www.raywenderlich.com


し・か・し、Core Dataの本質にも触れたわけじゃありません。CoreDataの初歩だけです。ここからCore Dataの深みに入っていきましょう。

今日はここまで。

*1:2016年2月:Xcodeはバージョン7.2.1を使用

*2:Rayさんのところもコメントで指摘を受けているんで近々改訂されるでしょうね。

*3:このオプションは、Single View ApplicationとMaster_Detail Applicationの2つのテンプレートにしかありません。

*4:ダブルクリックして変更よりも、ボタンが選択された状態でアトリビュート・インスペクタのSystem Itemから「Add」に変更する方が一般的でしょう。f:id:yataiblue:20170325114741j:plain

*5:6ステップ実装法を理解してください。

*6:6ステップ プロトコール・デリゲーション実装法ステップ4です。

*7:6ステップ プロトコール・デリゲーション実装法ステップ6です。

*8:実はiOS 10からエンティティはデフォルトの状態で、Class Definitionになっています。