View Controller間で通信するための最良の方法は何ですか?


165

一般的に、objective-c、cocoa、iPhone devの初心者なので、言語とフレームワークを最大限に活用したいという強い思いがあります。

私が使用しているリソースの1つは、スタンフォード大学のCS193Pクラスのメモで、Web上に残したものです。講義ノート、課題、サンプルコードが含まれており、コースはApple開発者によって提供されたので、間違いなく「馬の口から」であると考えています。

クラスのウェブサイト:http :
//www.stanford.edu/class/cs193p/cgi-bin/index.php

レクチャー08は、UINavigationControllerスタックにプッシュされた複数のUIViewControllersを持つUINavigationControllerベースのアプリを構築する割り当てに関連しています。これがUINavigationControllerの仕組みです。それは論理的です。ただし、スライドには、UIViewControllers間の通信に関する厳しい警告がいくつかあります。

この深刻なスライドから引用します。http
//cs193p.stanford.edu/downloads/08-NavigationTabBarControllers.pdf

ページ16/51:

データを共有しない方法

  • グローバル変数またはシングルトン
    • これにはアプリケーションデリゲートが含まれます
  • 直接的な依存関係により、コードの再利用性が低下します
    • そして、デバッグとテストがより難しい

OK。私はそれに落ち込んでいます。ビューコントローラーとアプリデリゲートの間の通信に使用されるすべてのメソッドを盲目的に放り投げたり、アプリデリゲートメソッドでビューコントローラーインスタンスを参照したりしないでください。フェアナフ。

少し先に、このスライドで何をすべきかを説明します。

ページ18/51:

データフローのベストプラクティス

  • 把握を正確に伝達する必要があるもの
  • ビューコントローラーの入力パラメーター定義する
  • 階層を遡って通信するには、疎結合を使用します
    • オブザーバー(委任など)の汎用インターフェースを定義する

次に、このスライドの後にプレースホルダースライドのように見えるスライドが続きます。ここで、講師はUIImagePickerControllerの例を使用してベストプラクティスを明らかに示します。ビデオが利用可能になればいいのに!:(

はい、そうです...私のobjc-fuはそれほど強くありません。上記の引用の最後の行にも少し混乱しています。私はこれについてグーグルの私の公平な分け前をしていて、観察/通知技術のさまざまな方法について話しているまともな記事であると思われるものが見つかりました:http :
//cocoawithlove.com/2008/06/five-approaches-to -listening-observing.html

メソッド#5はデリゲートをメソッドとしても示します!例外...オブジェクトは一度に1つのデリゲートしか設定できません。それで、複数のビューコントローラー通信がある場合、どうすればよいですか?

OK、それはセットアップのギャングです。アプリデリゲート内の複数のビューコントローラーインスタンスを参照することで、アプリデリゲートで通信メソッドを簡単に実行できることはわかっていますが、このようなことを正しい方法で実行したいと考えています。

次の質問に答えて、「正しいこと」をしてください。

  1. UINavigationControllerスタックに新しいビューコントローラーをプッシュしようとすると、このプッシュを実行する必要があります。 私のコードのどのクラス/ファイルが正しい場所ですか?
  2. 別の UIViewControllerにいるときに、UIViewControllersの1つにあるデータ(iVarの値)に影響を与えたい場合、これを行う「正しい」方法は何ですか?
  3. オブジェクトに一度に設定できるデリゲートは1つだけで、講師が「オブザーバー(デリゲートのような)の汎用インターフェイスを定義する」と言ったときの実装はどのようになると想定してください。可能であれば、疑似コードの例が非常に役立ちます。

このうちのいくつかは、アップルから、この記事で取り上げている- developer.apple.com/library/ios/#featuredarticles/...
ジェームズ・ムーア

だけで簡単に発言:スタンフォードCS193Pクラスのビデオは、最新の(2012-13)はで見ることができるのiTunes Uを経由して利用できるようになりましたitunes.apple.com/us/course/coding-together-developing/...と私は期待今後のビデオとスライドはcs193p.stanford.edu
トーマスワトソン

回答:


224

これらは良い質問です。あなたがこの研究を行っており、一緒にハッキングするのではなく、「正しく実行する」方法を学ぶことに関心があるように見えるのは素晴らしいことです。

最初に、MVCデザインパターンに従って、適切な場合にモデルオブジェクトにデータを配置することの重要性に焦点を当てた以前の回答に同意します。厳密に「プレゼンテーション」データでない限り、通常、コントローラ内に状態情報を配置しないようにします。

次に、プログラムでコントローラーをナビゲーションコントローラーにプッシュする方法の例については、スタンフォード大学のプレゼンテーションの10ページを参照してください。Interface Builderを使用してこれを「視覚的に」行う方法の例については、このチュートリアルをご覧ください

3番目、そしておそらく最も重要なこととして、スタンフォード大学のプレゼンテーションで言及されている「ベストプラクティス」は、「依存性注入」設計パターンのコンテキストで考えると、はるかに理解しやすいことに注意してください。簡単に言うと、これは、コントローラーが、ジョブを実行するために必要なオブジェクトを「ルックアップ」してはならないことを意味します(たとえば、グローバル変数を参照する)。代わりに、それらの依存関係を常にコントローラーに「注入」する必要があります(つまり、メソッドを介して必要なオブジェクトを渡します)。

依存関係注入パターンに従うと、コントローラーはモジュール式になり、再利用可能になります。そして、スタンフォードのプレゼンターがどこから来ているのか(つまり、Appleの従業員としての仕事は、簡単に再利用できるクラスを構築すること)を考える場合、再利用性とモジュール性は高い優先順位です。彼らがデータを共有するために言及するすべてのベストプラクティスは、依存関係注入の一部です。

それが私の返事の要点です。役立つ場合に備えて、以下のコントローラーで依存性注入パターンを使用する例を示します。

ビューコントローラーでの依存性注入の使用例

複数の本が一覧表示されている画面を作成しているとします。ユーザーは購入したい本を選び、「チェックアウト」ボタンをタップしてチェックアウト画面に移動できます。

これを構築するには、GUI /ビューオブジェクトを制御および表示するBookPickerViewControllerクラスを作成します。すべての書籍データはどこで取得されますか?それはそのためのBookWarehouseオブジェクトに依存しているとしましょう。したがって、コントローラーは基本的にモデルオブジェクト(BookWarehouse)とGUI /ビューオブジェクトの間でデータを仲介しています。言い換えると、BookPickerViewControllerはBookWarehouseオブジェクトに依存します。

これを行わないでください:

@implementation BookPickerViewController

-(void) doSomething {
   // I need to do something with the BookWarehouse so I'm going to look it up
   // using the BookWarehouse class method (comparable to a global variable)
   BookWarehouse *warehouse = [BookWarehouse getSingleton];
   ...
}

代わりに、依存関係は次のように注入する必要があります。

@implementation BookPickerViewController

-(void) initWithWarehouse: (BookWarehouse*)warehouse {
   // myBookWarehouse is an instance variable
   myBookWarehouse = warehouse;
   [myBookWarehouse retain];
}

-(void) doSomething {
   // I need to do something with the BookWarehouse object which was 
   // injected for me
   [myBookWarehouse listBooks];
   ...
}

Apple関係者が委任パターンを使用して「階層を遡って通信する」ことについて話しているとき、彼らは依然依存関係注入について話している。この例では、ユーザーが自分の本を選んでチェックアウトする準備ができたら、BookPickerViewControllerは何をすべきですか?まあ、それは本当にその仕事ではありません。他のオブジェクトに機能を委譲する必要があります。つまり、別のオブジェクトに依存します。したがって、BookPickerViewController initメソッドを次のように変更します。

@implementation BookPickerViewController

-(void) initWithWarehouse:    (BookWarehouse*)warehouse 
        andCheckoutController:(CheckoutController*)checkoutController 
{
   myBookWarehouse = warehouse;
   myCheckoutController = checkoutController;
}

-(void) handleCheckout {
   // We've collected the user's book picks in a "bookPicks" variable
   [myCheckoutController handleCheckout: bookPicks];
   ...
}

これらすべての最終結果は、BookPickerViewControllerクラス(および関連するGUI /ビューオブジェクト)を提供して、BookWarehouseとCheckoutControllerが実装可能な汎用インターフェイス(つまりプロトコル)であると想定して、自分のアプリケーションで簡単に使用できることです:

@interface MyBookWarehouse : NSObject <BookWarehouse> { ... } @end
@implementation MyBookWarehouse { ... } @end

@interface MyCheckoutController : NSObject <CheckoutController> { ... } @end
@implementation MyCheckoutController { ... } @end

...

-(void) applicationDidFinishLoading {
   MyBookWarehouse *myWarehouse = [[MyBookWarehouse alloc]init];
   MyCheckoutController *myCheckout = [[MyCheckoutController alloc]init];

   BookPickerViewController *bookPicker = [[BookPickerViewController alloc] 
                                         initWithWarehouse:myWarehouse 
                                         andCheckoutController:myCheckout];
   ...
   [window addSubview:[bookPicker view]];
   [window makeKeyAndVisible];
}

最後に、BookPickerControllerは再利用可能であるだけでなく、テストも簡単です。

-(void) testBookPickerController {
   MockBookWarehouse *myWarehouse = [[MockBookWarehouse alloc]init];
   MockCheckoutController *myCheckout = [[MockCheckoutController alloc]init];

   BookPickerViewController *bookPicker = [[BookPickerViewController alloc] initWithWarehouse:myWarehouse andCheckoutController:myCheckout];
   ...
   [bookPicker handleCheckout];

   // Do stuff to verify that BookPickerViewController correctly called
   // MockCheckoutController's handleCheckout: method and passed it a valid
   // list of books
   ...
}

19
このような細心の注意を払って作成されたこのような質問(および回答)を見ると、私は微笑まざるを得ません。私たちの勇敢な質問者とあなたに値する称賛!:一方で、私はあなたの第二の点で参照している便利なinvasivecode.comリンク用の更新されたリンク共有したいと思ったinvasivecode.com/2009/09/... -あなたの洞察力とベストプラクティスを共有し、プラスの例とそれをバックアップするために再び感謝を!
Joe D'Andrea、

同意する。質問の形式はよく、答えは素晴らしかった。技術的な答えだけではなく、DIを使用して実装する方法/理由の背後にある心理学も含まれていました。ありがとうございました!+1アップ。
ケビンエリオット2010年

また、BookPickerControllerを使用してウィッシュリストから本を選ぶ場合、またはいくつかの考えられる本のピック理由の1つを使用する場合はどうでしょうか。それでもCheckoutControllerインターフェースアプローチを使用しますか(おそらくBookSelectionControllerのような名前に変更されます)、またはNSNotificationCenterを使用しますか?
Les

これはまだかなり密結合です。一元化された場所からのイベントの発生と消費は緩くなります。
Neil McGuigan

1
ポイント2で参照されているリンクが再び変更されたようです-作業リンクはこちらですinvasivecode.com/blog/archives/322
vikmalhotra

15

この種のものは常に好みの問題です。

そうは言っても、私は常にモデルオブジェクトを介して調整(#2)を行うことを好みます。トップレベルのビューコントローラーは必要なモデルを読み込みまたは作成し、各ビューコントローラーは子コントローラーにプロパティを設定して、操作する必要のあるモデルオブジェクトを通知します。ほとんどの変更は、NSNotificationCenterを使用して階層を遡って伝達されます。通知の起動は通常、モデル自体に組み込まれています。

たとえば、アカウントとトランザクションを備えたアプリがあるとします。また、AccountListController、AccountController(「すべてのトランザクションを表示」ボタンでアカウントの概要を表示)、TransactionListController、およびTransactionControllerもあります。AccountListControllerは、すべてのアカウントのリストをロードして表示します。リストアイテムをタップすると、そのAccountControllerの.accountプロパティが設定され、AccountControllerがスタックにプッシュされます。「すべてのトランザクションを表示」ボタンをタップすると、AccountControllerはトランザクションリストをロードし、それをTransactionListControllerの.transactionsプロパティに配置して、TransactionListControllerをスタックにプッシュします。

たとえば、TransactionControllerがトランザクションを編集する場合、トランザクションオブジェクトに変更を加えてから、 'save'メソッドを呼び出します。「保存」はTransactionChangedNotificationを送信します。トランザクションが変更されたときに自分自身を更新する必要がある他のコントローラーは、通知を監視して自分自身を更新します。TransactionListControllerはおそらくそうでしょう。AccountControllerとAccountListControllerは、何をしようとしていたかによって異なります。

#1の場合、私の初期のアプリでは、子コントローラーにある種のdisplayModel:withNavigationController:メソッドがあり、それを設定してコントローラーをスタックにプッシュしていました。しかし、SDKに慣れてきて、それから離れ、今では通常、親に子をプッシュさせています。

#3については、この例を検討してください。ここでは、2つのコントローラー、AmountEditorとTextEditorを使用して、トランザクションの2つのプロパティを編集しています。ユーザーはトランザクションを破棄することを決定できるため、編集者は編集中のトランザクションを実際に保存しないでください。したがって、代わりに、親コントローラーをデリゲートとして受け取り、何か変更したかどうかを伝えるメソッドを呼び出します。

@class Editor;
@protocol EditorDelegate
// called when you're finished.  updated = YES for 'save' button, NO for 'cancel'
- (void)editor:(Editor*)editor finishedEditingModel:(id)model updated:(BOOL)updated;  
@end

// this is an abstract class
@interface Editor : UIViewController {
    id model;
    id <EditorDelegate> delegate;
}
@property (retain) Model * model;
@property (assign) id <EditorDelegate> delegate;

...define methods here...
@end

@interface AmountEditor : Editor
...define interface here...
@end

@interface TextEditor : Editor
...define interface here...
@end

// TransactionController shows the transaction's details in a table view
@interface TransactionController : UITableViewController <EditorDelegate> {
    AmountEditor * amountEditor;
    TextEditor * textEditor;
    Transaction * transaction;
}
...properties and methods here...
@end

そして、TransactionControllerのいくつかのメソッド:

- (void)viewDidLoad {
    amountEditor.delegate = self;
    textEditor.delegate = self;
}

- (void)editAmount {
    amountEditor.model = self.transaction;
    [self.navigationController pushViewController:amountEditor animated:YES];
}

- (void)editNote {
    textEditor.model = self.transaction;
    [self.navigationController pushViewController:textEditor animated:YES];
}

- (void)editor:(Editor*)editor finishedEditingModel:(id)model updated:(BOOL)updated {
    if(updated) {
        [self.tableView reloadData];
    }

    [self.navigationController popViewControllerAnimated:YES];
}

注目すべきことは、エディターが所有するコントローラーと通信するために使用できる汎用プロトコルを定義したことです。そうすることで、アプリケーションの別の部分でエディターを再利用できます。(おそらくアカウントもメモを持つことができます。)もちろん、EditorDelegateプロトコルには複数のメソッドを含めることができます。この場合、それだけが必要です。


1
これはそのまま動作するはずですか?Editor.delegateメンバーとのトラブルです。私のviewDidLoad方法では、を取得していProperty 'delegate' not found...ます。何か他のものを台無しにしてしまったのかどうかわからない。または、簡潔にするためにこれを省略した場合。
Jeff

これはかなり古いコードで、古い規則で古いスタイルで書かれています。直接コピーしてプロジェクトに貼り付けることはしません。私はパターンから学ぼうと思います。
ブレントRoyal-Gordon

ゴッチャ。それがまさに私が知りたかったことです。いくつかの変更を加えて動作させましたが、完全に一致しないのではないかと少し心配しました。
Jeff

0

あなたの問題がわかります。

何が起こったのかというと、誰かがMVCアーキテクチャの考えを混乱させているということです。

MVCには、モデル、ビュー、コントローラーの3つの部分があります。前述の問題は、正当な理由もなく、2つを組み合わせたようです。ビューとコントローラーは別々のロジックです。

だから...あなたは複数のビューコントローラを持ちたくありません。

複数のビューと、それらの間で選択するコントローラーが必要です。(複数のアプリケーションがある場合は、複数のコントローラーを持つこともできます)

ビューは決定を下すべきではありません。コントローラはそれを行うべきです。したがって、タスク、ロジック、およびあなたの人生を容易にする方法の分離。

したがって、ビューがそれを実行することを確認し、データの見栄えを良くします。コントローラーにデータの処理方法と使用するビューを決定させます。

(そして、私たちがデータについて話すときは、モデルについて話しています...格納、アクセス、変更を行うための素晴らしい標準的な方法です。別のロジックを分割して忘れることができます)


0

2つのクラスAとBがあるとします。

クラスAのインスタンスは

aInstance;

クラスAは、クラスBのインスタンスと、

B bInstance;

また、クラスBのロジックでは、クラスAのメソッドを通信またはトリガーする必要がある場所があります。

1)間違った方法

aInstanceをbInstanceに渡すことができます。ここで、bInstanceの目的の場所から目的のメソッド[aInstance methodname]を呼び出します。

これはあなたの目的を果たしましたが、リリースはメモリがロックされ、解放されないことにつながります。

どうやって?

aInstanceをbInstanceに渡すと、aInstanceの保持カウントが1増えました。bInstanceの割り当てを解除すると、bInstance自体がaInstanceのオブジェクトであるため、bInstanceの理由でaInstanceを0保持カウントにできないため、メモリがブロックされます。

さらに、aInstanceがスタックしているため、bInstanceのメモリもスタック(リーク)します。そのため、後でインスタンスが解放された後でも、bInstanceは解放できず、bInstanceはaInstanceのクラス変数であるため、メモリもブロックされます。

2)正しい方法

aInstanceをbInstanceのデリゲートとして定義することにより、aInstanceの保持カウントの変更やメモリの絡み合いがなくなります。

bInstanceは、aInstanceにあるデリゲートメソッドを自由に呼び出すことができます。bInstanceの割り当て解除では、すべての変数が独自に作成されて解放されます。aInstanceの割り当て解除では、bInstanceにaInstanceのもつれがないため、きれいに解放されます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.