128 votes

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

Étant donné que je suis novice en matière d'Objective-C, de Cocoa et de développement pour l'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 de Stanford CS193P qu'ils ont laissées sur le Web. Elles comprennent des notes de cours, des devoirs et des exemples de code, et comme le cours a été donné par des développeurs d'Apple, je considère qu'il vient de la "bouche du cheval".

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

Le cours 08 est lié à une mission consistant à construire une application basée sur UINavigationController qui comporte plusieurs UIViewControllers poussés sur la pile UINavigationController. C'est ainsi que fonctionne le UINavigationController. C'est logique. Cependant, la diapositive contient des avertissements sévères concernant la communication entre vos UIViewControllers.

Je vais citer un extrait de cette série de diapositives :
http://cs193p.stanford.edu/downloads/08-NavigationTabBarControllers.pdf

Page 16/51 :

Comment ne pas partager les données

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

Ok. 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 faites référence aux instances du viewcontroller dans les méthodes du délégué d'application. Fair 'nuff.

Un peu plus loin, nous avons cette diapositive nous disant ce que nous devrait faire.

Page 18/51 :

Meilleures pratiques pour le flux de données

  • Comprendre exactement ce qui doit être communiqué
  • Définir les paramètres d'entrée pour votre contrôleur de vue
  • Pour communiquer vers le haut de la hiérarchie, utiliser le 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 de remplacement où le conférencier démontre apparemment les meilleures pratiques en utilisant un exemple avec le UIImagePickerController. J'aimerais que les vidéos soient disponibles ! :(

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

La méthode n° 5 indique même les délégués comme une méthode ! Les objets Except.... ne peuvent définir qu'un seul délégué à la fois. Donc, lorsque j'ai plusieurs communications avec des contrôleurs de vues, que dois-je faire ?

Ok, c'est le gang de la mise en place. Je sais que je peux facilement faire mes méthodes de communication dans le délégué de l'application en référençant les multiples instances de viewcontroller dans mon délégué de l'application mais je veux faire ce genre de chose de la manière suivante droite manière.

Veuillez m'aider à "faire le bon choix" en répondant aux questions suivantes :

  1. Lorsque j'essaie de pousser un nouveau contrôleur de vue sur la pile UINavigationController, qui devrait faire cette poussée. Quel classe/fichier dans mon code est le bon endroit ?
  2. Lorsque je veux affecter un élément de données (la valeur d'une iVar) dans l'un de mes UIViewControllers alors que je suis dans un différents UIViewController, quelle est la "bonne" façon de procéder ?
  3. Étant donné que nous ne pouvons avoir qu'un seul délégué à la fois dans un objet, à quoi ressemblerait la mise en œuvre lorsque le professeur dit "Définir une interface générique pour les observateurs (comme la délégation)" . Un exemple en pseudocode serait très utile si possible.

186voto

Clint Harris Points 7163

Ce sont de bonnes questions, et c'est formidable de voir que vous faites ces recherches et que vous semblez vouloir apprendre à "bien faire les choses" au lieu de les bricoler.

Premier Je suis d'accord avec les réponses précédentes qui mettent l'accent sur l'importance de placer les données dans les objets du modèle lorsque cela est approprié (selon le modèle de conception MVC). En général, il faut éviter de placer des informations d'état dans un contrôleur, sauf s'il s'agit strictement de données de "présentation".

Deuxièmement Pour un exemple de la façon de procéder "visuellement" à l'aide d'Interface Builder, voir la page 10 de la présentation de Stanford pour un exemple de la façon de pousser programmatiquement un contrôleur sur le contrôleur de navigation. Pour un exemple de la manière de procéder "visuellement" à l'aide d'Interface Builder, consultez la page suivante ce tutoriel .

Troisièmement Enfin, et c'est 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 les envisagez dans le contexte du modèle de conception " injection de dépendances ". En un mot, cela signifie que votre contrôleur ne doit pas "chercher" 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 lui transmettre 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 à l'origine des présentateurs de Stanford (c'est-à-dire qu'en tant qu'employés d'Apple, leur travail consiste à créer des classes facilement réutilisables), la réutilisabilité et la modularité sont des priorités absolues. Toutes les meilleures pratiques qu'ils mentionnent pour le partage des données font partie de l'injection de dépendances.

C'est l'essentiel de ma réponse. Je vais inclure ci-dessous un exemple d'utilisation du modèle d'injection de dépendances avec un contrôleur, au cas où il serait utile.

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

Disons que vous construisez 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 passer à l'écran de commande.

Pour ce faire, vous pouvez créer une classe BookPickerViewController qui contrôle et affiche les objets de l'interface graphique/vue. Où obtiendra-t-il toutes les données sur les livres ? Disons qu'il dépend d'un objet BookWarehouse pour cela. À présent, votre contrôleur sert essentiellement de courtier en données entre un objet modèle (BookWarehouse) et les objets GUI/view. En d'autres termes, BookPickerViewController DÉPEND de l'objet BookWarehouse.

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

Lorsque les représentants d'Apple parlent d'utiliser le modèle de délégation pour "remonter 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 à passer à la caisse ? Eh bien, ce n'est pas vraiment son travail. Il devrait DÉLÉGUER cette tâche à un autre objet, ce qui signifie qu'il DÉPEND d'un autre objet. Nous pourrions donc modifier la méthode init de notre 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 ceci est que vous pouvez me donner votre classe BookPickerViewController (et les objets GUI/view 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 mettre en œuvre :

@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, votre BookPickerController est non seulement 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
   ...
}

13voto

Brent Royal-Gordon Points 8044

Ce genre de choses 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 avec quels objets de modèle ils doivent travailler. La plupart des changements sont communiqués en amont de 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ésumé du 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 la liste, il définit la propriété .account de son AccountController et place l'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, le TransactionController modifie la transaction, il apporte la modification dans son objet de transaction, puis appelle sa méthode "save". La méthode "save" envoie une notification TransactionChangedNotification. Tout autre contrôleur qui doit se rafraîchir lorsque la transaction est modifiée observera la notification et se mettra à jour. C'est probablement le cas de TransactionListController ; AccountController et AccountListController pourraient le faire, en fonction de ce qu'ils essaient de faire.

Pour le numéro 1, dans mes premières applications, j'avais une sorte de méthode displayModel:withNavigationController : dans le contrôleur enfant qui configurait les choses et poussait le contrôleur sur la pile. Mais au fur et à mesure que je me suis familiarisé avec le SDK, je me suis éloigné de cette méthode, et maintenant je demande généralement au parent de pousser l'enfant.

Pour le numéro 3, prenez 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 sauvegarder 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 sur celui-ci 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];
}

Ce qu'il faut remarquer, c'est que nous avons défini un protocole générique que les éditeurs peuvent utiliser pour communiquer avec leur contrôleur propriétaire. Ce faisant, nous pouvons réutiliser les éditeurs dans une autre partie de l'application. (Les comptes peuvent peut-être aussi avoir des notes.) Bien sûr, le protocole EditorDelegate pourrait contenir plus d'une méthode ; dans ce cas, c'est la seule nécessaire.

1voto

Bingy Points 547

Je vois votre problème

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

MVC a trois parties modèles, vues, et contrôleurs Le problème énoncé 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(s) contrôleur(s) doit(vent) le faire. D'où la séparation des tâches, de la logique et des moyens de vous faciliter la vie.

Donc assurez-vous que votre vue ne fait que ça, mettre une belle vue des données. laissez votre contrôleur décider de ce qu'il faut faire avec les données, et quelle vue utiliser.

(et quand nous parlons de données, nous parlons du modèle... une belle façon standard d'être stocké, accédé, modifié... un autre morceau séparé de logique que nous pouvons emballer et oublier).

0voto

rd_ Points 56

Supposons qu'il existe deux classes A et B.

L'instance de la classe A est

A aInstance ;

classe A fait et instance de classe B, comme

B bInstance ;

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

1) Mauvais chemin

Vous pourriez passer la aInstance à la bInstance. Placez maintenant l'appel de la méthode souhaitée [aInstance methodname] à partir de l'emplacement souhaité dans bInstance.

Cela aurait servi votre objectif, mais la libération aurait conduit à ce qu'une mémoire soit verrouillée et non libérée.

Comment ?

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

De plus, parce que aInstance est bloquée, la mémoire de bInstance sera également bloquée (leaked). Ainsi, même après avoir désalloué aInstance elle-même, lorsque son heure viendra, sa mémoire sera également bloquée car bInstance ne peut être libérée et bInstance est une variable de classe de aInstance.

2) La bonne voie

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

bInstance sera en mesure d'invoquer librement les méthodes déléguées se trouvant dans aInstance. Lors de la désallocation de la bInstance, toutes les variables seront créées par elle et seront libérées. Lors de la désallocation de l'aInstance, comme il n'y a pas d'enchevêtrement de l'aInstance dans la bInstance, elle sera libérée proprement.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X