Quelle est la meilleure façon de communiquer entre les contrôleurs de vue?

165

Étant nouveau dans le développement d'objectif-c, de cacao et d'iPhone en général, j'ai un fort désir de tirer le meilleur parti du langage et des frameworks.

L'une des ressources que j'utilise est les notes de cours CS193P de Stanford qu'ils ont laissées sur le Web. Il comprend des notes de cours, des devoirs et des exemples de code, et comme le cours a été donné par les développeurs d'Apple, je le considère définitivement comme "de la bouche du cheval".

Site Web de la classe:
http://www.stanford.edu/class/cs193p/cgi-bin/index.php

La conférence 08 est liée à une affectation pour créer une application basée sur UINavigationController qui a plusieurs UIViewControllers poussés sur la pile UINavigationController. C'est ainsi que fonctionne UINavigationController. C'est logique. Cependant, il y a quelques avertissements sévères dans la diapositive concernant la communication entre vos UIViewControllers.

Je vais citer ce sérieux de diapositives:
http://cs193p.stanford.edu/downloads/08-NavigationTabBarControllers.pdf

Page 16/51:

Comment ne pas partager de données

  • Variables globales ou singletons
    • Cela inclut votre délégué d'application
  • Les dépendances directes rendent votre code moins réutilisable
    • Et plus difficile à déboguer et tester

D'accord. Je suis d'accord avec ça. Ne jetez pas aveuglément toutes vos méthodes qui seront utilisées pour communiquer entre le viewcontroller dans votre délégué d'application et référencez les instances de viewcontroller dans les méthodes de délégué d'application. Fair 'nuff.

Un peu plus loin, nous voyons cette diapositive nous indiquant ce que nous devons faire.

Page 18/51:

Meilleures pratiques pour le flux de données

  • Déterminez exactement ce qui doit être communiqué
  • Définissez les paramètres d'entrée pour votre contrôleur de vue
  • Pour communiquer en arrière dans la hiérarchie, utilisez un couplage lâche
    • Définir une interface générique pour les observateurs (comme la délégation)

Cette diapositive est ensuite suivie de ce qui semble être une diapositive d'espace réservé où le conférencier démontre ensuite apparemment les meilleures pratiques à l'aide d'un exemple avec UIImagePickerController. J'aimerais que les vidéos soient disponibles! :(

Ok, alors ... j'ai bien peur que mon objc-fu ne soit pas si fort. Je suis également un peu confus par la dernière ligne de la citation ci-dessus. J'ai fait ma part de googler à ce sujet et j'ai trouvé ce qui semble être un article décent parlant des différentes méthodes d'observation / techniques de notification:
http://cocoawithlove.com/2008/06/five-approaches-to -écoute-observation.html

La méthode n ° 5 indique même les délégués comme méthode! Sauf que .... les objets ne peuvent définir qu'un seul délégué à la fois. Alors, quand j'ai une communication avec plusieurs viewcontroller, que dois-je faire?

Ok, c'est le gang mis en place. Je sais que je peux facilement faire mes méthodes de communication dans le délégué d'application en faisant référence aux multiples instances de viewcontroller dans mon appdelegate, mais je veux faire ce genre de chose de la bonne manière.

S'il vous plaît, aidez-moi à «faire ce qu'il faut» en répondant aux questions suivantes:

  1. Lorsque j'essaie de pousser un nouveau viewcontroller sur la pile UINavigationController, qui devrait faire cette poussée. Quelle classe / fichier dans mon code est le bon endroit?
  2. Lorsque je veux affecter un élément de données (valeur d'un iVar) dans l'un de mes UIViewControllers lorsque je suis dans un autre UIViewController, quelle est la «bonne» façon de le faire?
  3. Compte tenu du fait que nous ne pouvons avoir qu'un seul délégué à la fois dans un objet, à quoi ressemblerait l'implémentation lorsque le conférencier dit "Définir une interface générique pour les observateurs (comme la délégation)" . Un exemple de pseudocode serait extrêmement utile ici si possible.
Quinn Taylor
la source
Certains de ces problèmes sont abordés dans cet article d'Apple - developer.apple.com/library/ios/#featuredarticles/…
James Moore
Juste une remarque rapide: les vidéos de la classe Stanford CS193P sont maintenant disponibles via iTunes U. La dernière (2012-13) peut être vue sur itunes.apple.com/us/course/coding-together-developing/… et j'attends que les futures vidéos et diapositives seront annoncées à cs193p.stanford.edu
Thomas Watson

Réponses:

224

Ce sont de bonnes questions, et c'est formidable de voir que vous faites cette recherche et que vous semblez préoccupé d'apprendre à «bien faire les choses» au lieu de simplement les pirater ensemble.

Tout d'abord , je suis d'accord avec les réponses précédentes qui se concentrent sur l'importance de mettre des données dans des objets de modèle le cas échéant (selon le modèle de conception MVC). Habituellement, vous voulez éviter de mettre des informations d'état dans un contrôleur, sauf s'il s'agit strictement de données de "présentation".

Deuxièmement , voir la page 10 de la présentation de Stanford pour un exemple de la façon de pousser par programme un contrôleur sur le contrôleur de navigation. Pour un exemple de la façon de procéder «visuellement» à l'aide d'Interface Builder, jetez un œil à ce didacticiel .

Troisièmement , et peut-être le plus important, notez que les «meilleures pratiques» mentionnées dans la présentation de Stanford sont beaucoup plus faciles à comprendre si vous pensez à elles dans le contexte du modèle de conception «d'injection de dépendances». En un mot, cela signifie que votre contrôleur ne doit pas «rechercher» les objets dont il a besoin pour faire son travail (par exemple, référencer une variable globale). Au lieu de cela, vous devez toujours «injecter» ces dépendances dans le contrôleur (c'est-à-dire, passer les objets dont il a besoin via des méthodes).

Si vous suivez le modèle d'injection de dépendances, votre contrôleur sera modulaire et réutilisable. Et si vous pensez d'où viennent les présentateurs de Stanford (c'est-à-dire, en tant qu'employés d'Apple, leur travail consiste à créer des classes qui peuvent facilement être réutilisées), la réutilisabilité et la modularité sont des priorités élevées. Toutes les meilleures pratiques qu'ils mentionnent pour le partage de données font partie de l'injection de dépendances.

C'est l'essentiel de ma réponse. J'inclurai un exemple d'utilisation du modèle d'injection de dépendance avec un contrôleur ci-dessous au cas où cela serait utile.

Exemple d'utilisation de l'injection de dépendances avec un contrôleur de vue

Disons que vous créez un écran dans lequel plusieurs livres sont répertoriés. L'utilisateur peut choisir les livres qu'il souhaite acheter, puis appuyer sur un bouton «commander» pour accéder à l'écran de paiement.

Pour créer cela, vous pouvez créer une classe BookPickerViewController qui contrôle et affiche les objets GUI / vue. Où obtiendra-t-il toutes les données du livre? Disons que cela dépend d'un objet BookWarehouse pour cela. Alors maintenant, votre contrôleur court essentiellement des données entre un objet modèle (BookWarehouse) et les objets GUI / vue. En d'autres termes, BookPickerViewController DÉPEND de l'objet BookWarehouse.

Ne fais pas ça:

@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];
   ...
}

Au lieu de cela, les dépendances doivent être injectées comme ceci:

@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];
   ...
}

Quand les gars d'Apple parlent d'utiliser le modèle de délégation pour «communiquer en arrière dans la hiérarchie», ils parlent toujours d'injection de dépendances. Dans cet exemple, que doit faire le BookPickerViewController une fois que l'utilisateur a choisi ses livres et est prêt à sortir? Eh bien, ce n'est pas vraiment son travail. Il devrait DÉLÉGUER ce travail à un autre objet, ce qui signifie qu'il DÉPEND d'un autre objet. Nous pouvons donc modifier notre méthode d'initialisation BookPickerViewController comme suit:

@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];
   ...
}

Le résultat net de tout cela est que vous pouvez me donner votre classe BookPickerViewController (et les objets GUI / vue associés) et je peux facilement l'utiliser dans ma propre application, en supposant que BookWarehouse et CheckoutController sont des interfaces génériques (c'est-à-dire des protocoles) que je peux implémenter :

@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];
}

Enfin, non seulement votre BookPickerController est réutilisable mais aussi plus facile à tester.

-(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
   ...
}
Clint Harris
la source
19
Quand je vois des questions (et des réponses) comme celle-ci, élaborées avec un tel soin, je ne peux m'empêcher de sourire. Félicitations bien méritées à notre intrépide questionneur et à vous !! En attendant, je voulais partager un lien mis à jour pour ce lien invasivecode.com pratique que vous avez référencé dans votre deuxième point: invasivecode.com/2009/09/... - Merci encore d'avoir partagé votre perspicacité et vos meilleures pratiques, en plus de le sauvegarder avec des exemples!
Joe D'Andrea
Je suis d'accord. La question était bien formulée et la réponse était tout simplement fantastique. Plutôt que d'avoir simplement une réponse technique, il a également inclus une certaine psychologie derrière comment / pourquoi il est mis en œuvre à l'aide de DI. Je vous remercie! +1.
Kevin Elliott
Que faire si vous souhaitez également utiliser BookPickerController pour choisir un livre pour une liste de souhaits, ou l'une des nombreuses raisons possibles de bookpicking. Souhaitez-vous toujours utiliser l'approche d'interface CheckoutController (peut-être renommée en quelque chose comme BookSelectionController) ou peut-être utiliser NSNotificationCenter?
Les
Ceci est encore assez étroitement lié. Augmenter et consommer des événements à partir d'un endroit centralisé serait plus lâche.
Neil McGuigan
1
Le lien référencé au point 2 semble avoir changé à nouveau - voici le lien de travail invasivecode.com/blog/archives/322
vikmalhotra
15

Ce genre de chose est toujours une question de goût.

Cela dit, je préfère toujours faire ma coordination (# 2) via des objets modèles. Le contrôleur de vue de niveau supérieur charge ou crée les modèles dont il a besoin, et chaque contrôleur de vue définit des propriétés dans ses contrôleurs enfants pour leur indiquer les objets de modèle avec lesquels ils doivent travailler. La plupart des modifications sont communiquées à la hiérarchie en utilisant NSNotificationCenter; le déclenchement des notifications est généralement intégré au modèle lui-même.

Par exemple, supposons que j'ai une application avec des comptes et des transactions. J'ai également un AccountListController, un AccountController (qui affiche un récapitulatif de compte avec un bouton "Afficher toutes les transactions"), un TransactionListController et un TransactionController. AccountListController charge une liste de tous les comptes et les affiche. Lorsque vous appuyez sur un élément de liste, il définit la propriété .account de son AccountController et pousse le AccountController sur la pile. Lorsque vous appuyez sur le bouton «Afficher toutes les transactions», AccountController charge la liste des transactions, la place dans la propriété .transactions de son TransactionListController et pousse le TransactionListController sur la pile, et ainsi de suite.

Si, par exemple, TransactionController modifie la transaction, il effectue la modification dans son objet de transaction et appelle ensuite sa méthode «save». 'save' envoie une TransactionChangedNotification. Tout autre contrôleur qui a besoin de s'actualiser lorsque la transaction change observera la notification et se mettra à jour. TransactionListController le ferait probablement; AccountController et AccountListController pourraient, en fonction de ce qu'ils essayaient de faire.

Pour le n ° 1, dans mes premières applications, j'avais une sorte de displayModel: withNavigationController: méthode dans le contrôleur enfant qui mettrait les choses en place et pousserait le contrôleur sur la pile. Mais au fur et à mesure que je suis devenu plus à l'aise avec le SDK, je me suis éloigné de cela, et maintenant, le parent pousse généralement l'enfant.

Pour le n ° 3, considérez cet exemple. Ici, nous utilisons deux contrôleurs, AmountEditor et TextEditor, pour modifier deux propriétés d'une transaction. Les éditeurs ne doivent pas réellement enregistrer la transaction en cours de modification, car l'utilisateur pourrait décider d'abandonner la transaction. Au lieu de cela, ils prennent tous les deux leur contrôleur parent comme délégué et appellent une méthode dessus pour dire s'ils ont changé quelque chose.

@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

Et maintenant quelques méthodes de 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];
}

La chose à noter est que nous avons défini un protocole générique que les éditeurs peuvent utiliser pour communiquer avec leur propre contrôleur. Ce faisant, nous pouvons réutiliser les éditeurs dans une autre partie de l'application. (Peut-être que les comptes peuvent aussi avoir des notes.) Bien sûr, le protocole EditorDelegate peut contenir plus d'une méthode; dans ce cas, c'est le seul nécessaire.

Brent Royal-Gordon
la source
1
Est-ce censé fonctionner tel quel? J'ai des problèmes avec le Editor.delegatedéputé. Dans ma viewDidLoadméthode, je reçois Property 'delegate' not found.... Je ne suis pas sûr d'avoir foiré autre chose. Ou si cela est abrégé par souci de concision.
Jeff
C'est maintenant un code assez ancien, écrit dans un style plus ancien avec des conventions plus anciennes. Je ne le copierais pas et ne le collerais pas directement dans votre projet; J'essaierais juste d'apprendre des modèles.
Brent Royal-Gordon
Je t'ai eu. C'est exactement ce que je voulais savoir. Je l'ai fait fonctionner avec quelques modifications, mais j'étais un peu préoccupé par le fait qu'il ne correspondait pas textuellement.
Jeff
0

Je vois votre problème ...

Ce qui s'est passé, c'est que quelqu'un a une idée confuse de l'architecture MVC.

MVC a trois parties .. modèles, vues et contrôleurs. Le problème mentionné semble avoir combiné deux d'entre eux sans raison valable. les vues et les contrôleurs sont des éléments de logique distincts.

donc ... vous ne voulez pas avoir plusieurs contrôleurs de vue ..

vous voulez avoir plusieurs vues et un contrôleur qui choisit entre elles. (vous pouvez également avoir plusieurs contrôleurs, si vous avez plusieurs applications)

les opinions ne devraient PAS prendre de décisions. Le ou les contrôleurs doivent le faire. D'où la séparation des tâches, de la logique et des moyens de vous faciliter la vie.

Alors ... assurez-vous que votre vue fait juste cela, donne une belle vue des données. laissez votre responsable du traitement décider quoi faire des données et quelle vue utiliser.

(et quand nous parlons de données, nous parlons du modèle ... une belle manière standard d'être stocké, consulté, modifié ... un autre élément de logique séparé que nous pouvons regrouper et oublier)

Bingy
la source
0

Supposons qu'il existe deux classes A et B.

instance de la classe A est

Une aInstance;

la classe A fait et l'instance de la classe B, comme

B bInstance;

Et dans votre logique de classe B, quelque part vous êtes obligé de communiquer ou de déclencher une méthode de classe A.

1) Mauvaise manière

Vous pouvez passer le aInstance à bInstance. maintenant, placez l'appel de la méthode souhaitée [aInstance methodname] à partir de l'emplacement souhaité dans bInstance.

Cela aurait servi votre objectif, mais alors que la libération aurait conduit à une mémoire verrouillée et non libérée.

Comment?

Lorsque vous avez passé le aInstance à bInstance, nous avons augmenté le nombre de rétention de aInstance de 1. Lors de la désallocation de bInstance, nous aurons la mémoire bloquée car uneInstance ne peut jamais être amenée à 0 retaincount par bInstance, la raison étant que bInstance elle-même est un objet de aInstance.

De plus, en raison du blocage d'uneInstance, la mémoire de bInstance sera également bloquée (fuite). Ainsi, même après avoir désalloué aInstance elle-même lorsque son heure viendra plus tard, sa mémoire sera également bloquée car bInstance ne peut pas être libérée et bInstance est une variable de classe de aInstance.

2) Bonne façon

En définissant aInstance comme délégué de bInstance, il n'y aura pas de modification de retaincount ou d'enchevêtrement de mémoire de aInstance.

bInstance pourra invoquer librement les méthodes déléguées se trouvant dans aInstance. Lors de la désallocation de bInstance, toutes les variables seront ses propres créées et seront libérées. Sur la désallocation de aInstance, comme il n'y a pas d'intrication de aInstance dans bInstance, elle sera libérée proprement.

rd_
la source