A Day In The Life

とあるプログラマの備忘録

サルでもわかる Core Data 入門【実装編】

サルでもわかる Core Data 入門【概念編】の続きです。今回は実際に Core Data を使ったプログラムを作成します。

サンプルアプリの概要

アドレス帳アプリを作りながら Core Data の使い方を説明していきます。
以下はサンプルアプリの画面構成です。
画面遷移図
連絡先一覧画面と連絡先詳細画面の2画面構成になっています。
サンプルアプリのソースコードはこちらで公開しています。

開発の流れ

サンプルアプリの開発の流れは以下のようになります。

  1. プロジェクトの作成
  2. モデルクラスの作成
  3. エンティティの定義とエンティティとモデルクラスの関連付け
  4. ストーリーボード(Storyboard)を使ってビューコントローラの遷移と画面デザインを作成する
  5. 連絡先詳細画面の開発
  6. 連絡先一覧画面の開発

プロジェクトの作成

それでは初めにプロジェクトを作成しましょう。プロジェクトの作成手順は以下のようになります。

  1. Xcode の File メニューから New > New Project... を選択してください。
  2. Master-Detail Application を選択して Next ボタンを押してください。
    Master-Detail Application を選択
  3. Use Storyboard と Use Core Data、Use Automatic Reference Counting にチェックが入っていることを確認して Product Name と Company Identifer を入力後 Next ボタンを押してください。例ではそれぞれ AddressBook、sample と入力しています。
    プロジェクト名を決める
  4. 最後にプロジェクトの保存場所を指定して Create ボタンを押してください。
    プロジェクトを作成

プロジェクトの構成

作成されたプロジェクトの構成は以下のようになります。
プロジェクト構成
それでは実際のプログラムを見ていきましょう。

AppDelegate クラスの確認

AppDelegate クラスの中を見ていきましょう。AppDelegate.h を開いてください。Core Data を使わないプロジェクトと比べると以下のプロパティとメソッドが追加されているのが確認できます。

@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
@property (readonly, strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;
- (void)saveContext;
- (NSURL *)applicationDocumentsDirectory;

処理も色々と追加されていますが AppDelegate クラスを修正する必要はありません。このクラスでデータの保存場所の設定やエンティティとモデルクラスのマッピングを管理する AddressBook.xcdatamodeld ファイルの設定を行っています。

モデルクラスの作成

モデルクラスを作成していきましょう。プロジェクトに Person クラスと Address クラスを追加します。プロジェクトの AddressBook グループを選択してコンテクストメニューの New File... で新しいクラスを追加します。
まずはじめに Person クラスを作成します。親クラスには NSManagedObject クラスを指定してください。

#import <CoreData/CoreData.h>
#import "Address.h"

@interface Person : NSManagedObject

@property (strong, nonatomic) NSString *name;
@property (strong, nonatomic) Address *address;

@end

NSManagedObject のサブクラスを作る時は @synthesize ではなく @dynamic を使います。

#import "Person.h"

@implementation Person

@dynamic name, address;

@end

次に Address クラスを作成します。

#import <CoreData/CoreData.h>

@interface Address : NSManagedObject

@property (strong, nonatomic) NSString *zipCode;
@property (strong, nonatomic) NSString *state;
@property (strong, nonatomic) NSString *city;
@property (strong, nonatomic) NSString *other;

@end
#import "Address.h"

@implementation Address

@dynamic zipCode, state, city, other;

@end

エンティティの作成

AddressBook.xcdatamodeld を開いてエンティティの定義をします。まずはファイルに元からある Event エンティティを削除してください。その後、左下にある Add Entity ボタンを押して Person エンティティと Address を追加します。
xcdatamodeldの編集

属性(Attribute)の定義

Person エンティティに name という名前の String 型の属性を追加します。
同じように Address エンティティにそれぞれ zipCode, state, city, other という名前で String 型の属性を追加します。

関連(Relationship)の定義

Person エンティティに address という名前で関連を追加します。Destination には Address エンティティを指定します。
同じように Address エンティティにも person という名前で関連を追加します。Destination には Person エンティティを指定します。

エンティティとクラスの関連付けを定義

CONFIGURATIONS の下にある Default を選択してください。Person エンティティの Class に Person クラス、Address エンティティの Class に Address クラスを入力します。

ビューコントローラの遷移と画面デザインの作成

ストーリーボードを使ってビューコントローラの遷移と画面デザインを作成していきます。ビューコントローラの遷移と画面デザインは MainStoryboard_iPhone.storyboard ファイルに定義されていますので、このファイルを開いて修正していきます。初めに Detail View Controller に UI 部品を配置していきます。"Detail view content goes here" と表示されている UILabel を削除後、以下のように UI 部品を配置してください。
IBでUI部品の配置
次に連絡先一覧画面の「+」ボタンが押された時のビューコントローラの遷移の設定をします。Master View Controller の下に表示されている2つのアイコンのうち右側のアイコンを選択してください。アイコンを選択したらキーボードの control キーを押しながらマウスで Detail View Controller までドラッグします。
Segueの追加
マウスのボタンをはなすと Storyboard Segues という小さなポップアップが表示されます。この中の push という項目を選択してください。
Storyboard Segues ポップアップ
Master View Controller と Detail View Controller の間の矢印が一本増えたのが確認できます。2本の矢印のうち下の矢印を選択してください。選択すると右の詳細設定画面に Storyboard Segue という項目が表示されます。この中に Identifier という入力欄がありますのでそこに createDetail と入力してください。
Segueの設定
※segue(セグウェイ)という聞き慣れない単語が出てきますがこれは「ある状態から次の状態へ滑らかに移行する」という意味の動詞です。推測ですが、少し前に話題になった乗り物Segway は segue が語源だと思います。

DetailViewController クラスの修正

DetailViewController クラスを修正していきます。このクラスは連絡先詳細画面のためのコントローラクラスです。
この画面で連絡先の新規作成と既存連絡先の編集を行います。

プロパティの修正と追加

DetailViewController.h を開いてプロパティの修正と追加を行います。まず detailItem プロパティの型を id から Person に変更します。次に、以下のようにプロパティを追加します。

@interface DetailViewController : UIViewController <UISplitViewControllerDelegate>

@property (strong, nonatomic) Person *detailItem;
@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (strong, nonatomic) IBOutlet UIScrollView *scrollView;
@property (strong, nonatomic) IBOutlet UITextField *nameField;
@property (strong, nonatomic) IBOutlet UITextField *zipCodeField;
@property (strong, nonatomic) IBOutlet UITextField *stateField;
@property (strong, nonatomic) IBOutlet UITextField *cityField;
@property (strong, nonatomic) IBOutlet UITextField *otherField;

@end

scrollView, nameField, zipCodeField, stateField, cityField, otherField プロパティは MainStoryboard_iPhone.storyboard のUI 部品と IBOutlet 接続してください。

Done ボタンの追加とスクロールビューの調整

ここからは DetailViewController.m を修正していきます。まずはじめに Done ボタンの追加とスクロールビューの表示領域の設定を行います。Done ボタンは編集終了時に押すボタンです。64行目付近の viewDidLoad メソッドを以下のように修正してください。

- (void)viewDidLoad
{
  [super viewDidLoad];
  self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone
                                                        target:self 
                                                        action:@selector(done)] autorelease];
  self.scrollView.contentSize = CGSizeMake(320, 800);
  [self configureView];
}
データの表示

データの表示を行う処理を追加します。テキストフィールドに連絡先一覧画面で選択した連絡先のデータをセットしていきます。DetailViewController.m の47行目付近の configureView メソッドを修正してください。

- (void)configureView
{
  if (self.detailItem) {
    self.nameField.text = self.detailItem.name;
    self.zipCodeField.text = self.detailItem.address.zipCode;
    self.stateField.text = self.detailItem.address.state;
    self.cityField.text = self.detailItem.address.city;
    self.otherField.text = self.detailItem.address.other;
  }
}
表示データの保存

Done ボタンが押された時の動きを実装します。まずはじめにテキストフィールドの内容を detailItem オブジェクトの属性にセットします。セットが終わったら managedObjectContext オブジェクトの save メソッドを呼んでデータを保存します。DetailViewController.m にdone メソッドを作成して以下の処理を追加してください。

- (void)done
{
  self.detailItem.name = nameField.text;
  self.detailItem.address.zipCode = zipCodeField.text;
  self.detailItem.address.state = stateField.text;
  self.detailItem.address.city = cityField.text;
  self.detailItem.address.other = otherField.text;
    
  NSError *error = nil;
  if (![self.managedObjectContext save:&error]) {
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();
  }
  if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
    [self.navigationController popViewControllerAnimated:YES];
  }
}

MasterViewController クラスの修正

MasterViewController クラスを修正していきます。このクラスは連絡先一覧画面のためのコントローラクラスです。
連絡先一覧画面の機能は以下のようになっています。

  1. 連絡先の一覧表示
  2. 「+」ボタン押下で連絡先詳細画面に遷移
  3. 連絡先を選択したら連絡先詳細画面に遷移
  4. 「Edit」ボタン押下で連絡先を削除する機能

この機能のうち1と2に関してはプログラムを修正する必要があります。3と4は自動生成されたコードをそのまま使うことができます。またこのクラスはプロパティや公開メソッドの追加は行いませんので MasterViewController.h ファイルの修正は必要ありません。以降の修正は MasterViewController.m ファイルで行います。

連絡先の一覧表示

174行目付近の fetchedResultsController メソッドを修正していきます。自動生成されたコードでは各行に時間を表示するようになっているので、そこを時間ではなく名前を表示するように修正します。fetchedResultsController メソッドの以下の行を修正してください。
修正前

NSEntityDescription *entity = [NSEntityDescription entityForName:@"Event" inManagedObjectContext:self.managedObjectContext];

修正後

NSEntityDescription *entity = [NSEntityDescription entityForName:@"Person" inManagedObjectContext:self.managedObjectContext];

Event を Person に変更するだけです。NSEntityDescription クラスは AddressBook.xcdatamodeld ファイルで設定したエンティティの情報を管理するクラスです。entityForName: inManagedObjectContext: メソッドでエンティティの名前を指定してエンティティオブジェクトを生成します。
次に連絡先の表示順の設定をします。fetchedResultsController メソッドの以下の行を修正してください。
修正前

NSSortDescriptor *sortDescriptor = [[[NSSortDescriptor alloc] initWithKey:@"timeStamp" ascending:NO] autorelease];

修正後

NSSortDescriptor *sortDescriptor = [[[NSSortDescriptor alloc] initWithKey:@"name" ascending:NO] autorelease];

NSSortDescriptor クラスはデータの並び順を管理するクラスです。連絡先のデータを name の降順に並べます。
NSFetchRequest クラスや NSEntityDescription クラスについての説明はサルでもわかる Core Data 入門【概念編】を参照してください。

Person オブジェクトを生成する

Person オブジェクトを生成するための createNewPerson メソッドを作成します。初めに createNewPerson メソッドの定義を追加します。MasterViewController.m の14行目付近のクラス定義領域を以下のように修正します。

@interface MasterViewController ()
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath;
- (Person *)createNewPerson;
@end

次に createNewPerson メソッドの処理を追加します。MasterViewController.m の最終行にある @end の1行前付近に createNewPerson メソッドを作成して以下の処理を追加してください。

- (Person *)createNewPerson
{
  NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
  NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];
  Person *newPerson = [NSEntityDescription insertNewObjectForEntityForName:[entity name] 
                                                  inManagedObjectContext:context];
  Address *address = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([Address class]) 
                                                 inManagedObjectContext:context];
  newPerson.address = address;
  return newPerson;
}

NSManagedObject クラスのサブクラスは NSEntityDescription クラスのメソッドを使ってオブジェクトを生成します。alloc と init と使ってオブジェクトを生成するとエラーになるので注意してください。詳細はサルでもわかる Core Data 入門【概念編】を参照してください。

「+」ボタン押下で連絡先詳細画面に遷移

次に「+」ボタンを押すと連絡先詳細画面に遷移するように修正します。insertNewObject メソッドは「+」ボタンが押された時に呼ばれるメソッドです。
performSegueWithIdentifier: sender: メソッドを呼ぶと第1引数に指定された文字列を使ってストーリーボードのセグエを検索します。セグエがあればそこに定義されているビューコントローラに遷移します。
それでは MasterViewController.m の283行目付近にある insertNewObject メソッドを以下のように修正してください。

- (void)insertNewObject
{
  if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
    self.detailViewController.detailItem = [self createNewPerson];
    self.detailViewController.managedObjectContext = self.fetchedResultsController.managedObjectContext;
  } else {
    [self performSegueWithIdentifier:@"createDetail" sender:self];
  }
}

ビューコントローラ遷移時の処理を修正します。MasterViewController.m の164行目付近にある prepareForSegue: sender: メソッドを以下のように修正します。

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
  if ([[segue identifier] isEqualToString:@"showDetail"]) {
      NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
      Person *selectedObject = [[self fetchedResultsController] objectAtIndexPath:indexPath];
      [[segue destinationViewController] setDetailItem:selectedObject];
  } else {
      [[segue destinationViewController] setDetailItem:[self createNewPerson]];
  }
  [[segue destinationViewController] setManagedObjectContext:self.fetchedResultsController.managedObjectContext];
}

UIStoryboardSegue クラス型 segue オブジェクトの identifier プロパティには MainStoryboard_iPhone.storyboard ファイルの Storyboard Segue の Identifier で定義した文字列がわたってきます。連絡先の編集時には "showDetail"、連絡先の新規作成時には "createDetail" という文字列がわたります。
storyboardの画像
ビューコントローラ遷移時の詳しい動きを知りたい方は以下の記事を参考にしてください。

動作確認

以上でサンプルアプリの開発は終わりです。動作確認をしてアプリの動きをチェックしてください。

ソースコード

この記事で紹介したソースコードはこちらで公開しています。

ブログの記事が本になりました!

A Day In The Life の iOS に関する記事が書籍になりました。この記事の内容は書籍で読むことができます(2015年1月17日にSwiftに対応した改訂版がでました)。

本の内容に関する詳しい記事はこちらです。

参考

Storyboard についてはこちらの記事がとても参考になります。