2012年04月01日

画面間でのデータの受け渡しに付いて

インターネットの掲示板で初心者からの質問として、二つの画面の間でデータの受け渡しをやりたいがその方法がわからないというのをよく見かける。あ、もちろんiOS SDKプログラミングの話ね。

別にアップルがそのためになにか特別なAPIを用意してくれているわけではないので普通にC言語のブログラミングでやっているようにプログラムを作ればいいだけの話。でもiOSプログラミングでは画面の表示のためにはこのコントローラを使いなさいとか、初期化時にはこのメソッドをオーバーライドしなさいとか、色々決め毎があるのできっとなにか決まりがあるんだろうと思ってしまい見つけられずに質問をするのだろう。

とは言え、こう書いた方がiOS的には判りやすいとかデバッグがしやすいとか色々あるわけで、今回はそれをまとめてみた。

画面間での受け渡しとして
・親子関係のある場合
・親子関係がない場合
の二つに大別できる。ようはオブジェクトの間に直接参照があるかどうかの違いなので親子に限らないのだけど、判りやすくまとめると上記でいいだろう。

(1)親子関係がない場合
(1-a)グローバル変数
(1-b)シングルトンオブジェクト
(1-c)アプリケーションデリゲート
(1-d)外部ファイル
(1-e)ユーザーデフォルト
(2)親子関係がある場合
(2-a)画面を開く時(親から子)
(2-b)画面を閉じる時(子から親)
(2-c)画面を開く時(親から子)Segue
(2-d)画面を閉じる時(子から親)Segue

実際のサンプルコードを作ったので参考にしてほしい。

(1)親子関係がない場合



画面を切り替えてもオブジェクトが存在しているときに他方が存在しているかどうかもわからないわけで、この場合には誰かにデータの仲立ちをしてもらうしかない。

サンプルの動作は次の通り。二つのタブがありタブは同じUI。青のA画面(FirstViewController)と赤のB画面(SecondViewController)があり、画面間で値の交換をするので値は一致している。

shot1.png

shot2.png

値をセットするタイミングは値がUIから変更された時でもいいし、画面を閉じようとする時でもいい。

サンプルでは値が更新された時に値を書き込み、画面を表示するときに保存されたデータを読み出している。

(1-a)グローバル変数


やりかたがよく判らなければグローバル変数を使えばいい。グローバル変数を使うとデバッグで困るよと言われたかもしれないが、ここで迷っている人はまだ最初のサンプルプログラムを書いている段階だろうから、気にすることはない。リリースするアプリを作る頃には他の方法が使えるようになっているはず。こんなところで迷っているのは時間の無駄。さくっとグローバル変数を使って値を受け渡しもっと他の音楽の鳴らし方とかカメラの使い方とか面白いことを試してみよう。

サンプルプログラムでは以下だ。スイッチの値をA、B間でやり取りする。

まずグローバル変数の定義が必要。A側:
BOOL firstValue = NO;


B側では参照の宣言のみを行う。
extern BOOL firstValue;


これ以後は、ABでコードの内容はまったく同じ。値を読み出してセットする。
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
globalView.on = firstValue;
}


UIで変更した値をセットする。
-(void)switchDidChanged:(id)sender
{
firstValue = globalView.on;
}


ただし、Objective-Cクラスのオブジェクトを使う場合にはretainする責任が曖昧になるので注意が必要だ。

(1-b)シングルトンオブジェクト


オブジェクト指向でよく使うデータの保持役にシングルトンオブジェクトがある。アプリケーション全体からアクセス可能でプログラム実行中にひとつしかないオブジェクトだ。グローバル変数のオブジェクト版と思えばいい。単にデータの受け渡しに使うというのはあまりよい使い方とは言えないがグローバル変数よりかはいい。

サンプルプログラムは省略する。シングルトン+Objective-Cでググってくれ。

(1-c)アプリケーションデリゲート


シングルトンのクラスを定義するのが大変だって?実は予め容易されたシングルトンオブジェクトが一つ作成済みだ。それがアプリケーションデリゲート。ある意味全ての親だとも言える。だから全ての画面コントローラがこのアプリケーションデリゲートへの参照を最初から持っていて、データはこの人に預かっていてもらうのがいい。自分でオブジェクトを生成したのだったら生成時に参照を作り、xibで作ったのならxibでアウトレットを接続しておく。

そのやり方がわからない?大丈夫、アプリケーションオブジェクトはグローバル変数でアクセス可能なんだ。

サンプルプログラムでは以下だ。テキストフィールドに入力した文字列をA、B間でやり取りする。プログラムの内容はA、Bまったく同じ。
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
AppDelegate *appDelegate;
appDelegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
singleView.text = appDelegate.singleString;
}


UIでテキストが変更された時:
-(void)textFieldDidEndEditing:(UITextField *)textField
{
AppDelegate *appDelegate;
appDelegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
appDelegate.singleString = singleView.text;
}


(1-d)外部ファイル


画面Aでファイルに書き出して画面Bで読み出せばデータの受け渡しは可能だ。アプリケーションが終了してもデータが残っているので次回また使えるという副作用付き。まあでも結構大変だから今はまだそんなことしたくないよね。私も説明が面倒だ。だから説明は省略する。でも強力だからアプリケーション外のどこかに書き込むという手法もあることを覚えておいてくれ。iCloudっていう超強力な手段もあるがそれはまた別の話。

(1-e)ユーザーデフォルト


その外部ファイルに書き出しておくのを簡単に使えるのがこのユーザーデフォルトだ。初期設定などを書き込む場所だが、ちょっとだけなら画面間のデータの受け渡しに使っても怒られないだろう。もちろんファイルに保存されiTunesと同期したときにはバックアップされるから、1年後に再起動してもデータが取り出せる。

サンプルプログラムでは以下だ。セグメントの値をA、B間でやり取りする。プログラムの内容はA、Bまったく同じ。
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
if ( [[userDefaults valueForKey:@"selectedSegmentIndex"] intValue] == 0 ) {
defaultsView.selectedSegmentIndex = 0;
} else {
defaultsView.selectedSegmentIndex = 1;
}
}


UIで値が変更された時:
-(void)segmenetDidChanged:(id)sender
{
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setValue:[NSNumber numberWithInt:defaultsView.selectedSegmentIndex]
forKey:@"selectedSegmentIndex"];
[userDefaults synchronize];
}


(2)親子関係がある場合


親子関係があっても上記の手法を使っていいが親から子、子から親であれば直接そのオブジェクトを知っているのだから、オブジェクトにメッセージを送ってデータを押し付けてしまえばいいのだ。取り出す場合にくれという要求メソッドを呼び出してもいい。

直接データを渡す(もらう)メソッドを書く訳だからわざわざ説明することもないよね。が、いつ呼び出せばいいかわからない場合のために、例を作ってみた。

shot3.png

(2-a)画面を開く時(親から子)


親画面から子画面に画面が遷移する時、子画面のコントローラを親画面は持っているはずだ。だからそこで親から子画面コントローラにデータを渡すメソッドを読み出すんだ。

サンプルプログラムでは以下だ。親の入力データを子供にテキストとして渡している。
- (IBAction)showChildAction:(id)sender {
ChildViewController *aViewController = [[ChildViewController alloc]
initWithNibName:@"ChildViewController" bundle:nil];
NSString *aString = ...;
// 子へ値をセットする。
aViewController.text = aString;
aViewController.delegate = (id<ChildViewDelegate>)self;
// 子から受け取るための準備
[self presentModalViewController:aViewController
animated:YES];
}


(2-b)画面を閉じる時(子から親)


子画面でデータを変更して親に値を渡すには閉じるときがいい。この時、子画面コントローラが親への参照をインスタンス変数として持っていて親のメソッドを直接呼び出すようにするのがよい。でも、親が子供のことを知っていていいけど、子供はあまり親のことを知っているのは良くないこととされている。だって誰かに養子に出されたときに実の親はああだったこうだったと言うと育ての親は悲しむだろ?まあ例えが悪かったが、親のことは知らない方がいい。じゃあどうやって親のメソッドを呼び出すの?それがプロトコルとデリゲートだ。

子がデータを変更したことを知らせるメソッドを子の方で宣言しておく。親はそのメソッドを実装していることだけを保証すればいい。それがプロトコル。そして子供には親が誰だか特定しないでセットしておく。それがデリゲート(まあ名前は何でもいいし、ちょっと意味が違う気もするがデリゲートってしている例が多いからそれでいいだろう。ターゲットのほうが適当なのかもしれない)。

サンプルプログラムでは以下だ。子画面で入力したテキストを親画面のテキストビューにセットする。

まずChildViewController.h。
@class ChildViewController;

@protocol ChildViewDelegate %lt;NSObject>

- (void)childViewDidChanged:(ChildViewController*)viewController;

@end

@interface ChildViewController : UIViewController

@property(strong, nonatomic) NSString *text;
@property(weak, nonatomic) id<ChildViewDelegate> delegate;

@end


子画面を閉じるときに親(=デリゲート)を呼び出す。
- (IBAction)okAction:(id)sender {
[self.textView resignFirstResponder];
self.text = self.textView.text;
if ( [self.delegate respondsToSelector:@selector(childViewDidChanged:)] ) {
[self.delegate childViewDidChanged:self];
}
[self dismissModalViewControllerAnimated:YES];
}


親画面ではそれを受け取る。
- (void)childViewDidChanged:(ChildViewController*)viewController
{
resultsView.text = viewController.text;
}


(2-c)画面を開く時(親から子)Segue



Storyboardを使っている場合には書く場所がちょっと違う。子画面を作ったり開いたりするのはフレームワークが面倒を見てくれていて自分では書かないから。

サンプルプロジェクトは変わってSegueSampleのほう。ViewControllerではスライダーで値を変更して、NextViewControllerの側ではステッパーで値を変更する。同じ値を受け渡すので常に両方の画面の値は同じになる。

shotS1.png

shotS2.png

サンプルプログラムでは以下だ。
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ( [[segue identifier] isEqualToString:@"Push"] ) {
NextViewController *nextViewController = [segue destinationViewController];
nextViewController.value = self.value;
nextViewController.delegate = (id<NextViewDelegate>)self;
}
}


(2-d)画面を閉じる時(子から親)Segue



サンプルプログラムでは以下だ。

子画面を閉じるときに親に値を渡す。
-(void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
if ( [self.delegate respondsToSelector:@selector(nextViewValueDidChanged:)] ) {
[self.delegate nextViewValueDidChanged:self.value];
}
}


それを受ける親のメソッド。
-(void)nextViewValueDidChanged:(int32_t)value
{
self.sliderView.value = value;
[self sliderValueDidChnaged:self.sliderView];
}



--
もちろん他にも実現方法はいくらでもあるだろう。が初心者が普通に作るならこの位の中から使うのがいいだろう。

サンプルはVersion 4.3.1 (4E1019)でARC入りで作成。
ValueSample.zip

Storyboardを使った場合。
SegueSample.zip
posted by 永遠製作所 at 23:25| 東京 ☀| Comment(3) | TrackBack(0) | iPhone/iPod touch | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
最近、iPhoneアプリの開発を始めました。

目から鱗です!本当にありがとうございます。

一つ質問させて頂きたいのですが、こちらの記事のおかげで、親から子への値の引き渡しの方法は分かったのですが、親から孫・孫から親への値のやり取りはどのようにすればいいのでしょうか。

よろしくお願いします。
Posted by ゆう at 2012年07月02日 21:37
親と孫の間のデータのやりとりは、
a)二つの画面間に親子関係がない場合のやりかたを使う。
b)親から子、子から孫とリレー形式でデータを受け渡す。
c)子から親へのポインタをつくり、子が孫へ親へのポインタを渡し孫から親へ尋ねる。
のいずれかだと思います。

子ではそのデータをまったく使わないのであればaがいいですが、画面に親子孫の関係があるのならデータにもなんらかの関係がありそうなので、子を経由させるbがいいと思います。

cは面倒なので今回は説明を省略します。
Posted by 永遠製作所 at 2012年07月02日 22:19
お返事ありがとうございます。

早速やってみます!
Posted by ゆう at 2012年07月04日 15:35
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント: [必須入力]

※ブログオーナーが承認したコメントのみ表示されます。

この記事へのトラックバック