[iOS]StoryboardでTODOリストアプリを作ろう(3/3) 仕上げ編 Delegateパターンについて

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Storyboardで始めるiPhoneアプリ開発シリーズ」の第五回目になります。
前回の「[iOS]StoryboardでTODOリストアプリを作ろう(2/3) TODO項目の入力画面とカスタムクラスの作成編」では、
TODO項目を入力する画面と画面用のカスタムクラスを作りました。
今回は入力したTODO項目をメイン画面に渡す方法を解説し、その方法を実装してTODOアプリを完成させます。

Delegateパターン

入力画面で入力されたTODO項目をメイン画面へ伝えるための仕組みを作っていきます。
この伝達にはdelegate(デリゲート)パターンを使ってみたいと思います。

delegateとは?

delegateについて、Appleの公式ドキュメントには以下のように書いてあります。

デリゲート(delegate, 委譲)とは、あるオブジェクトがプログラム中でイベントに遭遇したとき、それに代わって、または連携して処理するオブジェクトのことです。 Objective-Cプログラミングの概念

今回のTODOリストアプリでは

  • saveボタンを押したタイミングでメイン画面へ入力内容を伝える。入力画面を閉じてもらうことをメイン画面に依頼する。
  • cancelボタンを押したタイミングで、入力画面を閉じてもらうことをメイン画面に依頼する。

という風に入力画面からメイン画面へ情報を通知する必要がありますがdelegateを使うことで実現することができます。

UIAlertViewDelegate

delegateはもともと用意されているクラスにも使われているので参考にしてみます。
例えば、UIAlertViewはユーザーにお知らせを表示したり、確認をとったりする時に使います。UIAlertViewにはdelegateが備えられていて、イベント発生の通知はdelegateを使って処理することができます。

下の図は、メイン画面(ViewController)からAlertViewを表示してユーザーに確認をとり、「はい」か「いいえ」のどちらが押されたかによって処理を分けるアプリになります。
このアプリにおいて、メイン画面はどちらのボタンが押されたのかを知りたいわけですが、UIAlertViewにはもともとdelegateが用意されているので、
delegateで宣言されたメソッドをメイン画面のViewControllerに実装するだけで、どちらのボタンが押されたかを知ることができます。
intro-storybord3-65

このように、通知を受ける側に予め定められているメソッドを実装するだけで通知を受けることができます。汎用性があります。
このdelegateパターンを使って、TODOリストアプリの入力画面からメイン画面への通知を実現していきます。

AddItemViewControllerを編集する

まずは通知する側(今回は入力画面)のクラスにdelegateのプロトコルを定義します。
AddItemViewController .hファイルを開きます。
intro-storybord3-52

AddItemViewController.hファイルの下部の@endの下に
下記の4行を追加してください。ここはdelegateプロトコルを定義している部分で、delegateメソッドを宣言しています。

@protocol AddItemViewControllerDelegate <NSObject>

- (void)addItemViewControllerDidCancel:(AddItemViewController *)controller;

- (void)addItemViewControllerDidFinish:(AddItemViewController *)controller item:(NSString *)item;

@end

@interface AddItemViewController : UITableViewControllerに以下のコードを追加します。

@protocol AddItemViewControllerDelegate; 

また@interface AddItemViewController : UITableViewControllerに以下のコードを追加します。
delegateには、後でメイン画面(MasterViewController)への参照をセットします。

@property (weak, nonatomic) id<AddItemViewControllerDelegate> delegate; 

修正後のAddItemViewController.hは以下のようになります。

#import <UIKit/UIKit.h>

@protocol AddItemViewControllerDelegate;

@interface AddItemViewController : UITableViewController

@property (weak, nonatomic) id<AddItemViewControllerDelegate> delegate;

@end

@protocol AddItemViewControllerDelegate <NSObject>

- (void)addItemViewControllerDidCancel:(AddItemViewController *)controller;

- (void)addItemViewControllerDidFinish:(AddItemViewController *)controller item:(NSString *)item;

@end

これでdelegateの宣言とプロパティの準備ができました。

MasterViewControllerを編集する

次に通知される側のクラスを編集していきます。
MasterViewController.mファイルを開きます。
intro-storybord3-53

さきほどAddItemViewController.hでAddItemViewControllerDelegateプロトコルの宣言を書きましたが、
AddItemViewControllerDelegateプロトコルを実装することで通知を受けれるようになります。
MasterViewController.mファイルにプロトコルを実装していきます。

ファイルの先頭は以下のようになっていると思います。

#import "MasterViewController.h"

#import "DetailViewController.h"

@interface MasterViewController () {
    NSMutableArray *_objects;
}
@end

AddItemViewController.hをインポートする文を追加し、デリゲートプロトコルを準拠している旨を書き加えます。
書き換え後のMasterViewController.mファイルの冒頭部分は以下のようになります。

#import "MasterViewController.h"
#import "DetailViewController.h"

// 追加
#import "AddItemViewController.h"

@interface MasterViewController () <AddItemViewControllerDelegate> // "<"から">"までは、AddItemViewControllerDelegateプロトコルを準拠しているという宣言
{
    NSMutableArray *_objects;
}
@end

ここでwarning(黄色い三角形のアイコン)が出るようになったかと思います。
これは、AddItemViewControllerDelegateプロトコルを準拠しているけど、AddItemViewController.hファイルで宣言したデリゲートのメソッドの実装がまだできていないので出ています。
intro-storybord3-54

次はデリゲートメソッドの実装をやっていきます。
- (void)didReceiveMemoryWarningメソッドの下の- (void)insertNewObject:(id)senderメソッドは不要なので削除します。

/* 削除する
- (void)insertNewObject:(id)sender
{
    if (!_objects) {
        _objects = [[NSMutableArray alloc] init];
    }
    [_objects insertObject:[NSDate date] atIndex:0];
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
    [self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
*/

delegateメソッドを実装する

- (void)didReceiveMemoryWarningメソッドの下に AddItemViewControllerクラスで宣言した- (void)addItemViewControllerDidCancel:(AddItemViewController *)controllerメソッドを追加します。 このメソッドは入力画面のcancelボタンが押された時にAddItemViewControllerクラス側から呼びます。入力画面を閉じる処理を担当します。

// 追加する
- (void)addItemViewControllerDidCancel:(AddItemViewController *)controller
{
    NSLog(@"addItemViewControllerDidCancel");
   
    // 画面を閉じるメソッドを呼ぶ
    [self dismissViewControllerAnimated:YES completion:NULL];
}

また、今追加したメソッドの下に- (void)addItemViewControllerDidFinish: item:メソッドを追加します。このメソッドもAddItemViewControllerクラスで宣言したものです。
このメソッドは入力画面のsaveボタンが押された時にAddItemViewControllerクラス側から呼びます。入力画面の入力値の保存と、入力画面を閉じる処理を担当します。

// 追加する
- (void)addItemViewControllerDidFinish:(AddItemViewController *)controller item:(NSString *)item
{
    NSLog(@"addItemViewControllerDidFinish item:%@",item);
   
    // 保存するための配列の準備ができていない場合は、配列を生成し、初期化する
    if (!_objects) {
        _objects = [[NSMutableArray alloc] init];
    }
   
    // 受け取ったitemを配列に格納する
    [_objects insertObject:item atIndex:0];
   
    // TableViewに行を挿入する
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
    [self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
   
    // 画面を閉じるメソッドを呼ぶ
    [self dismissViewControllerAnimated:YES completion:NULL];
}

デリゲートメソッドの実装はこれで完了です。

あとは入力画面への遷移する時に呼ばれるメソッドを少し修正します。
MasterViewController.mファイルの一番下に- (void)prepareForSegue: sender:メソッドがあります。

このメソッドはセグエで遷移する前に呼ばれます。
セグエのidentifierが"showDetail"の場合if文の{ }内を実行するようになっていますが、
メイン画面から入力画面の画面遷移用のセグエにはidentifierをまだ設定していませんでしたので設定します。

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([[segue identifier] isEqualToString:@"showDetail"]) {
        NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
        NSDate *object = _objects[indexPath.row];
        [[segue destinationViewController] setDetailItem:object];
    }
}

MainStoryboard.storyboardを選択し、standerdEditorユーティリティエリアを表示させます。
メイン画面から入力画面へのセグエを選択し、identifier項目ShowAddItemViewと入力します。
intro-storybord3-55

identifierの設定ができました。

再びMasterViewController.mファイルを表示させ、MasterViewController.mファイルの編集に戻ります。
- (void)prepareForSegue: sender:メソッドを以下のように修正します。
if文の条件式内の"showDetail""ShowAddItemView"に変更し、AddItemViewControllerインスタンスのdelegateプロパティに自分自身をセットしています。

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([[segue identifier] isEqualToString:@"ShowAddItemView"]) {
       
        // 遷移先のAddItemViewControllerのインスタンスを取得
        AddItemViewController *addItemViewController = (AddItemViewController *)[[[segue destinationViewController]viewControllers]objectAtIndex:0];
       
        // delegateプロパティに self(MasterViewController自身)をセット
        addItemViewController.delegate = self;
    }
}

これでMasterViewController.mの修正はおしまいです。

AddItemViewControllerを編集する(再)

以前作った- (IBAction)clickedSaveButton:メソッドと- (IBAction)clickedCancelButtonメソッドにAddItemViewControllerDelegateのメソッドを呼び出すコードを追加します。
- (IBAction)clickedSaveButton:メソッドではaddItemViewControllerDidFinish: item:メソッドを呼び出すコードを追加します。2つめの引数にはTextLabelに入力された文字列を渡します。
また、- (IBAction)clickedCancelButtonメソッドではaddItemViewControllerDidCancel:メソッドを呼び出します。
修正後の両メソッドは以下のようになります。

- (IBAction)clickedSaveButton:(id)sender
{
    NSLog(@"clickedSaveButton");
    [self.delegate addItemViewControllerDidFinish:self item:self.textLabel.text];
}

- (IBAction)clickedCancelButton:(id)sender
{
    NSLog(@"clickedCancelButton");
    [self.delegate addItemViewControllerDidCancel:self];
}

これでコードの修正はおしまいです。

実行する

ついに簡易版TODOリストアプリ完成しましたので、
Runボタンをクリックして実行してみましょう。
intro-storybord3-56

ここでiPhoneシミュレーターの言語設定が英語になっている場合は、日本語に変更します。
シミュレーターのSettingsアプリを開き「General」→「International」→「Language」と辿り、日本語を選択します。
intro-storybord3-57

Runボタンをクリックして実行してみましょう。
(言語設定を日本語に変更した場合は日本語に変わっています。)
追加ボタンを押して入力画面を開きます。
intro-storybord3-58

TODOリストに追加したい項目を入力し、saveボタンを押して保存します。
intro-storybord3-59

メイン画面遷移します。追加されています。
intro-storybord3-60

他にも3つ追加してみました。次はEditボタンを押してTableViewを編集モードにしてみましょう。
intro-storybord3-61

左端の赤いアイコンをクリックして、
Deleteボタンを押すと項目を削除出来ます。
intro-storybord3-62

削除できました。Doneボタンを押すと通常モードに戻ります。
intro-storybord3-63

まとめ

TODOリストアプリが完成

今回は簡易的なTODOリストアプリを作ってみました。
無事に動かすことができましたでしょうか。

画面や画面間の遷移をStorybordで作り、データの受け渡しや保存はコードを書いて実現しました。
今回作ったTODOリストアプリは項目の追加と削除しか出来ません。
また、入力したデータをファイルに保存する処理(データの永続化処理)を実装していないので、アプリを終了すると入力データは消えてしまいます。

さらに改良していくとしたら何を追加しますか。
「期限や優先度の項目」や「項目をグループに分ける機能」でしょうか。
または、「項目を並べ替える機能」や「期限前に通知で教えてくれる機能」などでしょうか。

付け加えたい機能を実現するための技術を調べたり、
追加した機能がうまく動くように調整したりしながらアプリ開発を進めていきます。

Storyboardの使いどころ

さて、第一回目から第五回目まで続いた「Storyboardで始めるiPhoneアプリ開発」シリーズですが、
第一回目と第二回目で基本的な画面遷移を、第三回目から五回目までを使って簡易的なTODOリストアプリを作りました。

アプリを開発していく上で、Storyboardはどう活用していくのが良いのでしょうか。
「ストーリーボード」を辞書で調べると、以下の様な説明が書かれています。

絵コンテ。テレビ広告表現のストーリーと場面を示す草稿。場面の絵とせりふがコマ割りで描かれる。 ストーリーボード【storyboard】- goo辞書

「画面内のレイアウト」や「画面間の遷移」などの視覚的要素はStoryboardで作成し、それ以外のデータの操作等はコードを書いて実現するのが基本になると思います。
また、Storyboardはプロトタイプの作成にも向いていて、画面のレイアウトと簡単な動きを見せるだけであればほとんどコードを書かずに作れてしまうので、開発者でなくても作成できると思います。

先日のWWDCのキーノートでiOS7が発表され、当日のうちに開発者向けβ版などが提供開始されました。
iOS7の登場によって変わっていく部分が多いと思うので、これからも勉強し続けていきたいと思います。

参考ページ

Objective-Cプログラミングの概念
画面間でのデータの受け渡しに付いて
Static Cells で楽々UITableViewプログラミング
Storyboardでテーブルビューのセルをカスタマイズする