Étant nouveau sur objectif-c, cacao et iPhone dev 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 classe CS193P de Stanford qu'ils ont laissées sur le Web. Il comprend des notes de cours, des devoirs et un exemple de code, et puisque le cours a été donné par Apple dev's, 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 leçon 08 est liée à une mission de création d'une application basée sur UINavigationController qui a plusieurs UIViewControllers insérés dans la pile UINavigationController. Voilà comment 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 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. Nuff juste.
Un peu plus loin, nous obtenons cette diapositive nous indiquant ce que nous devrions 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 la sauvegarde de la hiérarchie, tiliser 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 professeur démontre ensuite apparemment les meilleures pratiques en utilisant un exemple avec le UIImagePickerController. Je souhaite que les vidéos soient disponibles! :(
Ok, alors ... je crains 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 juste part de recherche sur 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-listening-observing.html
La méthode # 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 plusieurs communications avec le 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 par référence aux multiples instances de viewcontroller dans mon délégué d'application, mais je veux faire ce genre de chose de la manière à droite.
Aidez-moi à "faire la bonne chose" en répondant aux questions suivantes:
Ce sont de bonnes questions, et c'est formidable de voir que vous faites cette recherche et que vous semblez soucieux d'apprendre à "bien faire" au lieu de simplement le pirater ensemble.
d'abord, je suis d'accord avec les réponses précédentes qui mettent l'accent sur l'importance de mettre des données dans les objets du modèle le cas échéant (par 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".
Second, voir page 10 de la présentation de Stanford pour un exemple de la façon de pousser par programmation un contrôleur sur le contrôleur de navigation . Pour un exemple de la façon de faire "visuellement" à l'aide d'Interface Builder, jetez un œil à ce tutoriel .
Troisième, et peut-être plus important encore, notez que les "meilleures pratiques" mentionnées dans la présentation de Stanford sont beaucoup plus faciles à comprendre si vous y pensez dans le contexte du modèle de conception "injection de dépendance". 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 transmettre les objets dont il a besoin via des méthodes).
Si vous suivez le modèle d'injection de dépendance, votre contrôleur sera modulaire et réutilisable. Et si vous pensez d'où viennent les présentateurs de Stanford (c.-à-d., En tant qu'employés Apple employés, 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. les meilleures pratiques qu'ils mentionnent pour le partage de données font partie de l'injection de dépendance.
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
Supposons 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 accéder à l'écran de paiement.
Pour cela, vous pouvez créer une classe BookPickerViewController qui contrôle et affiche les objets GUI/view. 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 répartit essentiellement les 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 gars de Apple parlent d’utiliser le modèle de délégation pour "communiquer la hiérarchie", ils parlent toujours d’injection de dépendances. Dans cet exemple, que doit faire BookPickerViewController une fois que l’utilisateur a a choisi ses livres et est prêt à vérifier? 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/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 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
...
}
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 les propriétés de ses contrôleurs enfants pour leur indiquer les objets de modèle avec lesquels ils doivent travailler. La plupart des modifications sont communiquées pour sauvegarder la hiérarchie à l'aide de 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 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 sa propriété .transactions TransactionListController, et pousse TransactionListController sur la pile, etc.
Si, par exemple, TransactionController modifie la transaction, il modifie son objet de transaction, puis appelle sa méthode "save". 'save' envoie une TransactionChangedNotification. Tout autre contrôleur qui doit s'actualiser lorsque les modifications de transaction observent la notification et se mettent à jour. TransactionListController le ferait probablement; AccountController et AccountListController pourraient, selon ce qu'ils essayaient de faire.
Pour # 1, dans mes premières applications, j'avais une sorte de méthode displayModel: withNavigationController: dans le contrôleur enfant qui mettrait les choses en place et poussait le contrôleur sur la pile. Mais comme je suis devenu plus à l'aise avec le SDK, je me suis éloigné de cela, et maintenant j'ai généralement le parent Poussez l'enfant.
Pour # 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 peut 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 en disant 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 contrôleur propriétaire. Ce faisant, nous pouvons réutiliser les éditeurs dans une autre partie de l'application. (Peut-être que les comptes peuvent également contenir des notes.) Bien sûr, le protocole EditorDelegate peut contenir plusieurs méthodes; dans ce cas, c'est le seul nécessaire.
Supposons qu'il existe deux classes A et B.
instance de classe A est
Une aInstance;
marque de classe A 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) Mauvaise façon
Vous pouvez transmettre aInstance à bInstance. placez maintenant l'appel de la méthode souhaitée [nom de méthode aInstance] à partir de l'emplacement souhaité dans bInstance.
Cela aurait servi votre objectif, mais la libération aurait entraîné le verrouillage et non la libération d'une mémoire.
Comment?
Lorsque vous avez transmis aInstance à bInstance, nous avons augmenté le nombre de retenues de aInstance de 1. Lors de la désallocation de bInstance, nous verrons la mémoire bloquée car aInstance ne peut jamais être amenée à 0 retaincount par bInstance, car bInstance lui-même est un objet de aInstance.
De plus, en raison du blocage de aInstance, la mémoire de bInstance sera également bloquée (fuite). Ainsi, même après avoir désalloué une instance elle-même lorsque son heure viendra plus tard, sa mémoire sera également bloquée car bInstance ne peut pas être libéré 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 changement de compte à rebours ni d'intrication de mémoire dans 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 créées par elles-mêmes et seront libérées. Lors de la désallocation de aInstance, car il n'y a aucun enchevêtrement de aInstance dans bInstance, elle sera libérée proprement.
Je vois ton 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 déclaré semble avoir combiné deux d'entre eux sans raison valable. les vues et les contrôleurs sont des éléments logiques 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 doivent PAS prendre de décisions. Le ou les contrôleurs devraient 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 exactement cela, met une belle vue des données. laissez votre contrôleur 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 façon standard d'être stocké, accédé, modifié .. une autre logique distincte que nous pouvons regrouper et oublier)