【Swift】Core Dataの使い方。Relationshipを使った具体的な実装方法(Swift 2.1、XCode 7.2)
Relationshipの説明の続き
前回記事ではRelationship(以下、リレーションシップ)の基本的な考え方について説明した。⇒「前回記事」
本記事はその続きで、リレーションシップの具体的な実装方法を説明する。
前回記事で用いた生徒一覧から部活一覧を参照して部員一覧を表示するものを実装する。また、生徒を削除すると部員一覧からも消えるようにする。
以降の手順を行う前のXcodeプロジェクトをGitHubに置いたので、試してみる人はご利用されたし。
⇒「テスト用プロジェクト」
事前準備では、生徒一覧をテーブルビューに表示し、左スワイプから削除できるようにしておいた。生徒エンティティと部活エンティティのモデルは定義済みである。
以下はStudentTableViewController.swiftの変更前のソースコード。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
// // StudentTableViewController.swift // import UIKit import CoreData class StudentTableViewController: UITableViewController { //管理オブジェクトコンテキスト var managedContext:NSManagedObjectContext! //検証用データ(生徒一覧) let studentList = [[1,"佐藤", 29, "野球部"], [2,"田中", 20, "サッカー部"], [3,"中西", 37, "野球部"], [4,"鈴木", 24, "野球部"], [5,"高橋", 21, "サッカー部"], [6,"伊藤", 30, "テニス部"]] //検証用データ(部一覧) let clubList = [["野球部", 32000, "花田球場", "火水"], ["サッカー部", 25000, "日野グラウンド", "月木金"], ["テニス部", 18000, "千代田コート", "水"]] //検索結果配列 var searchResult = [Student]() //最初からあるメソッド override func viewDidLoad() { super.viewDidLoad() //管理オブジェクトコンテキストを取得する。 let applicationDelegate = UIApplication.sharedApplication().delegate as! AppDelegate managedContext = applicationDelegate.managedObjectContext //検証用データを保存する。 insertData() //検索結果配列にデータを格納する。 makeSearchResult() //テーブルビューを再読み込みする。 self.tableView.reloadData() } //検証用データを保存するメソッド func insertData(){ do { //管理オブジェクトコンテキストの中のオブジェクトの件数を取得する。。 let fetchRequest = NSFetchRequest(entityName: "Student") fetchRequest.resultType = .CountResultType let result = try managedContext.executeFetchRequest(fetchRequest) as! [Int] if(result[0] == 0){ //何も保存されていない場合は検証用データを保存する。 for data in studentList { let student = NSEntityDescription.insertNewObjectForEntityForName("Student", inManagedObjectContext: managedContext) as! Student student.number = data[0] as? Int //番号 student.name = data[1] as? String //名前 student.age = data[2] as? Int //年齢 } //管理オブジェクトコンテキストの中身を保存する。 try managedContext.save() } } catch { print(error) } } //検索結果配列作成メソッド func makeSearchResult() { //検索結果配列を空にする。 searchResult.removeAll() //フェッチリクエストのインスタンスを生成する。 let fetchRequest = NSFetchRequest(entityName: "Student") do { //フェッチリクエストを実行する。 searchResult = try managedContext.executeFetchRequest(fetchRequest) as! [Student] } catch { print(error) } } //データの個数を返すメソッド override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return searchResult.count } //データを返すメソッド override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { //セルを取得する。 let cell = tableView.dequeueReusableCellWithIdentifier("TestCell", forIndexPath:indexPath) as UITableViewCell //セルのラベルに本のタイトルを設定する。 let student = searchResult[indexPath.row] cell.textLabel?.text = "No.\(student.number!) \(student.name!) \(student.age!)" return cell } //編集可否を答えるメソッド override func tableView(tableView: UITableView,canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { //すべての行を編集可能にする。 return true } //テーブルビュー編集時の呼び出しメソッド override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { if editingStyle == UITableViewCellEditingStyle.Delete { do { //選択行のオブジェクトを管理オブジェクトコンテキストから取得する。 let fetchRequest = NSFetchRequest(entityName: "Student") fetchRequest.predicate = NSPredicate(format:"name = %@", searchResult[indexPath.row].name!) if let result = try managedContext.executeFetchRequest(fetchRequest) as? [Student] { //検索結果配列から選択行のオブジェクトを削除する。 searchResult.removeAtIndex(indexPath.row) //テーブルビューから選択行を削除する。 tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Fade) //管理オブジェクトコンテキストから選択行のオブジェクトを削除する。 managedContext.deleteObject(result[0]) //管理オブジェクトコンテキストの中身を保存する。 try managedContext.save() } } catch { print(error) } } } } |
リレーションシップを追加する
まずは、生徒エンティティと部活エンティティにリレーションシップを追加する。
下図赤枠の「プロジェクト名.xcdatamodeld」を選択、黄緑枠のStudentエンティティを選択、紫枠の「+」ボタンを押してRelationshipの行を追加する。
入力項目のRelationshipに「club」、Destinationに「Club」を入力する。これで、生徒エンティティから部活エンティティへのリレーションシップが追加された。
続いて、下図水色枠のClubエンティティを選択、紫枠の「+」ボタンを押してRelationshipの行を追加する。
入力項目のRelationshipに「student」、Destinationに「Student」、Inverseに「club」を入力する。これで、部活エンティティから生徒エンティティへのリレーションシップが追加された。
黄緑枠のデータモデルインスペクタボタンを押して設定画面を表示し、Typeに「To Many」を設定する。これは、前回記事で説明した「1対多」の設定である。
赤枠のStudentを再度選択して先ほど追加したリレーションシップを見ると、Inverseに「student」が自動で設定されているはずである。Inverseは逆関係を設定する項目である。つまり、clubとstudentの双方向のリレーションシップが確立されたということだ。
エンティティのサブクラスを作る
下図赤枠の「プロジェクト名.xcdatamodeld」を選択したあと、メニュー⇒「Editor」⇒「Create NSManagedObject Subclass」とたどる。
データモデルの一覧が表示されるので、「プロジェクト名」にチェックが入っていることを確認して「Next」ボタンを押す。
データモデルに定義されているEntityの一覧が表示されるので、StudentとClubの両方にチェックを入れて「Next」ボタンを押す。
保存先を指定する画面が表示されるので、プロジェクトのフォルダが指定されていることを確認して「Create」ボタンを押す。
上記の結果、「Student+CoreDataProperties.swift」と「Club+CoreDataProperties.swift」の2つのファイルが上書きされる。
「Student+CoreDataProperties.swift」は以下のコードになる。部活オブジェクトを設定するためのclubプロパティが追加されている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// // Student+CoreDataProperties.swift // import Foundation import CoreData extension Student { @NSManaged var number: NSNumber? @NSManaged var name: String? @NSManaged var age: NSNumber? @NSManaged var club: Club? } |
「Club+CoreDataProperties.swift」は以下のコードになる。複数の生徒オブジジェクトを設定するためのNSSet型のstudentプロパティが追加されている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// // Club+CoreDataProperties.swift // import Foundation import CoreData extension Club { @NSManaged var clubName: String? @NSManaged var money: NSNumber? @NSManaged var place: String? @NSManaged var schedule: String? @NSManaged var student: NSSet? } |
リレーションのプロパティを利用して実装する
StudentTableViewController.swiftの「検証用データを保存するメソッド」を以下のコードに変更する。
生徒の検証用データを保存するときに、部活のオブジェクトを取得してclubプロパティに設定するようにした。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
//検証用データを保存するメソッド func insertData(){ do { //管理オブジェクトコンテキストの中のオブジェクトの件数を取得する。。 let fetchRequest = NSFetchRequest(entityName: "Student") fetchRequest.resultType = .CountResultType let result = try managedContext.executeFetchRequest(fetchRequest) as! [Int] //何も保存されていない場合に検証用データを保存する。 if(result[0] == 0){ //部活の検証用データを保存する。 for data in clubList { let club = NSEntityDescription.insertNewObjectForEntityForName("Club", inManagedObjectContext: managedContext) as! Club club.clubName = data[0] as? String //部名 club.money = data[1] as? Int //部費 club.place = data[2] as? String //活動場所 club.schedule = data[3] as? String //活動日 } //生徒の検証用データを保存する。 for data in studentList { let student = NSEntityDescription.insertNewObjectForEntityForName("Student", inManagedObjectContext: managedContext) as! Student student.number = data[0] as? Int //番号 student.name = data[1] as? String //名前 student.age = data[2] as? Int //年齢 //所属部のオブジェクトを検索し、clubプロパティに設定する。 let fetchRequest = NSFetchRequest(entityName: "Club") fetchRequest.predicate = NSPredicate(format:"clubName = %@", data[3] as! String) let result = try managedContext.executeFetchRequest(fetchRequest) as? [Club] if(result!.count > 0) { student.club = result![0] } } //管理オブジェクトコンテキストの中身を保存する。 try managedContext.save() } } catch { print(error) } } |
StudentTableViewController.swiftの「データを返すメソッド」を以下のコードに変更する。
セルのラベルに所属部の名前を表示するようにした。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//データを返すメソッド override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { //セルを取得する。 let cell = tableView.dequeueReusableCellWithIdentifier("TestCell", forIndexPath:indexPath) as UITableViewCell //セルのラベルに本のタイトルを設定する。 let student = searchResult[indexPath.row] cell.textLabel?.text = "No.\(student.number!) \(student.name!) \(student.age!) \(student.club!.clubName!)" return cell } |
ここまでで実行すると、以下の動画のようになる。エラーが出た人は次の記事を確認されたし。⇒「データ不整合によるエラー」
逆関係を利用して部員一覧を表示する
次に、ナビゲーションバーにある「部活一覧へ」ボタンを押すと、部活ごとの部員一覧が表示されるようにする。
ClubTableViewController.swfitを以下のコードに変更する。
「データを返すメソッド」で、逆関係のstudentプロパティから所属部員のオブジェクトを取り出して、ラベルに設定している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
// // ClubTableViewController.swift // import UIKit import CoreData class ClubTableViewController: UITableViewController { //管理オブジェクトコンテキスト var managedContext:NSManagedObjectContext! //検索結果配列 var searchResult = [Club]() override func viewDidLoad() { super.viewDidLoad() //管理オブジェクトコンテキストを取得する。 let applicationDelegate = UIApplication.sharedApplication().delegate as! AppDelegate managedContext = applicationDelegate.managedObjectContext //Clubエンティティのフェッチリクエストを生成する。 let fetchRequest = NSFetchRequest(entityName: "Club") do { //フェッチリクエストを実行し、検索結果配列に格納する。 searchResult = try managedContext.executeFetchRequest(fetchRequest) as! [Club] } catch { print(error) } } //セクション名を返すメソッド override func tableView(tableView:UITableView, titleForHeaderInSection section:Int) -> String?{ return searchResult[section].clubName } //セクション数を返すメソッド override func numberOfSectionsInTableView(tableView: UITableView) -> Int { return searchResult.count } //データ数を返すメソッド override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return 1 } //データを返すメソッド override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { //セルを取得する。 let cell = tableView.dequeueReusableCellWithIdentifier("TestCell2", forIndexPath:indexPath) as UITableViewCell //セルのラベルに部員の名前を列挙する。 var str = "" for student in searchResult[indexPath.section].student! { str += " " + student.name } cell.textLabel?.text = str return cell } } |
気がついたかも知れないが、部活エンティティのstudentプロパティ(NSSet型)に値を設定するコードはどこにもないにも関わらず、「データを返すメソッド」でstudentプロパティから生徒のオブジェクトを取り出して使っている。
これは、最初の画面で生徒オブジェクトのclubプロパティに部活オブジェクトを設定するときに、逆関係のstudentプロパティに自動で生徒オブジェクトが追加されるためである。
しかも、生徒オブジェクトを削除すると、studentプロパティの参照も自動で消してくれるのでclubとstudentの整合性を気にする必要がない(設定によっては自動で消さないようにもできる)。
逆関係の定義は必須ではないが、あると役に立つことが多いので作っておきたい存在だ。逆関係便利なり。
以下は実際のプレイ動画