Qual è il modo migliore per comunicare tra i controller di visualizzazione?

Essendo nuovo agli obiettivi ogg-c, cocoa e iPhone in generale, ho un forte desiderio di ottenere il massimo dal linguaggio e dalle strutture.

Una delle risorse che sto usando sono le note di class CS193P di Stanford che hanno lasciato sul web. Include note di lezione, compiti e codice di esempio, e poiché il corso è stato dato da Apple Dev’s, lo considero sicuramente “dalla bocca del cavallo”.

Sito web della class:
http://www.stanford.edu/class/cs193p/cgi-bin/index.php

La lezione 08 è correlata a un compito per creare un’app basata su UINavigationController che ha più UIViewControllers inseriti nello stack UINavigationController. È così che funziona UINavigationController. È logico. Tuttavia, ci sono alcuni avvisi severi nella diapositiva sulla comunicazione tra i tuoi UIViewControllers.

Ho intenzione di citare da questo serio di diapositive:
http://cs193p.stanford.edu/downloads/08-NavigationTabBarControllers.pdf

Pagina 16/51:

Come non condividere i dati

  • Variabili globali o singleton
    • Ciò include il delegato dell’applicazione
  • Le dipendenze dirette rendono il tuo codice meno riutilizzabile
    • E più difficile da eseguire il debug e test

Ok. Sono giù con quello. Non gettare alla cieca tutti i metodi che verranno utilizzati per comunicare tra il viewcontroller nel delegato dell’app e fare riferimento alle istanze di viewcontroller nei metodi di delega dell’app. Fair ’nuff.

Un po ‘più avanti, otteniamo questa slide dicendoci cosa dovremmo fare.

Pagina 18/51:

Best practice per il stream di dati

  • Capire esattamente ciò che deve essere comunicato
  • Definisci i parametri di input per il tuo controller di visualizzazione
  • Per comunicare il backup della gerarchia, utilizzare l’accoppiamento libero
    • Definire un’interfaccia generica per gli osservatori (come la delega)

Questa diapositiva viene quindi seguita da quella che sembra essere una diapositiva del posto dove il docente dimostra quindi le migliori pratiche utilizzando un esempio con UIImagePickerController. Vorrei che i video fossero disponibili! 🙁

Ok, quindi … temo che il mio objc-fu non sia così forte. Sono anche un po ‘confuso dalla riga finale nella citazione di cui sopra. Ho fatto la mia giusta dose di googling su questo e ho trovato quello che sembra essere un articolo decente che parla dei vari metodi delle tecniche di osservazione / notifica:
http://cocoawithlove.com/2008/06/five-approaches-to-listening-observing.html

Il metodo # 5 indica anche i delegati come metodo! Tranne …. gli oggetti possono impostare solo un delegato alla volta. Quindi, quando ho più comunicazioni con il viewcontroller, cosa devo fare?

Ok, questa è la gang di installazione. So che posso facilmente eseguire i miei metodi di comunicazione nell’app delegato per riferimento alle istanze multiple di viewcontroller nel mio appdelegate ma voglio fare questo genere di cose nel modo giusto .

Per favore aiutami a “fare la cosa giusta” rispondendo alle seguenti domande:

  1. Quando sto provando a spingere un nuovo viewcontroller sullo stack UINavigationController, chi dovrebbe fare questa spinta. Quale class / file nel mio codice è il posto giusto?
  2. Quando voglio influenzare alcuni dati (valore di un iVar) in uno dei miei UIViewControllers quando sono in un altro UIViewController, qual è il modo “giusto” per farlo?
  3. Diamo che possiamo avere un solo delegato alla volta in un object, come sarebbe l’implementazione quando il docente dice “Definisci un’interfaccia generica per gli osservatori (come la delega)” . Un esempio di pseudocodice sarebbe terribilmente utile qui, se ansible.

Queste sono buone domande, ed è bello vedere che stai facendo questa ricerca e sembra preoccuparti di imparare a “fare bene” invece di hackerarlo insieme.

Innanzitutto , sono d’accordo con le risposte precedenti che si concentrano sull’importanza di mettere i dati negli oggetti del modello, quando appropriato (secondo lo schema di progettazione MVC). Di solito si vuole evitare di inserire informazioni di stato all’interno di un controller, a meno che non si tratti di dati strettamente “di presentazione”.

In secondo luogo , vedere la pagina 10 della presentazione di Stanford per un esempio di come spingere a livello di programmazione un controller sul controller di navigazione. Per un esempio di come “visivamente” utilizzare Interface Builder, dai un’occhiata a questo tutorial .

Terzo , e forse più importante, notare che le “migliori pratiche” menzionate nella presentazione di Stanford sono molto più facili da capire se ci si pensa nel contesto del modello di progettazione della “dipendenza da iniezione”. In poche parole, ciò significa che il controller non deve “cercare” gli oggetti di cui ha bisogno per svolgere il proprio lavoro (ad esempio, fare riferimento a una variabile globale). Invece, dovresti sempre “iniettare” queste dipendenze nel controllore (cioè passare gli oggetti di cui ha bisogno tramite i metodi).

Se segui il pattern di iniezione delle dipendenze, il controller sarà modulare e riutilizzabile. E se si pensa a dove vengono i presentatori di Stanford (vale a dire, come dipendenti Apple il loro compito è quello di creare classi che possano essere facilmente riutilizzate), la riusabilità e la modularità sono priorità elevate. Tutte le migliori pratiche citate per la condivisione dei dati fanno parte dell’iniezione delle dipendenze.

Questo è l’essenza della mia risposta. Includerò un esempio di utilizzo del modello di iniezione delle dipendenze con un controller di seguito nel caso sia utile.

Esempio di utilizzo dell’iniezione delle dipendenze con un controller di visualizzazione

Diciamo che stai costruendo uno schermo in cui sono elencati diversi libri. L’utente può scegliere i libri che desidera acquistare, quindi toccare un pulsante “checkout” per andare alla schermata di checkout.

Per creare questo, è ansible creare una class BookPickerViewController che controlli e visualizzi gli oggetti della vista / GUI. Dove prenderà tutti i dati del libro? Diciamo che dipende da un object BookWarehouse per quello. Quindi ora il controller è fondamentalmente un intermediario tra un object modello (BookWarehouse) e la GUI / vista oggetti. In altre parole, BookPickerViewController DEPENDS sull’object BookWarehouse.

Non farlo:

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

Invece, le dipendenze dovrebbero essere iniettate in questo modo:

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

Quando i ragazzi della Apple stanno parlando dell’utilizzo del modello di delega per “comunicare il backup della gerarchia”, stanno ancora parlando dell’iniezione di dipendenza. In questo esempio, cosa dovrebbe fare BookPickerViewController una volta che l’utente ha selezionato i suoi libri ed è pronto per il check-out? Bene, non è proprio il suo lavoro. DELEGARE che funziona con qualche altro object, il che significa che DEPENDE su un altro object. Quindi potremmo modificare il nostro metodo init di BookPickerViewController come segue:

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

Il risultato di tutto questo è che puoi darmi la tua class BookPickerViewController (e relativi oggetti GUI / vista) e posso facilmente usarla nella mia applicazione, supponendo che BookWarehouse e CheckoutController siano interfacce generiche (cioè protocolli) che posso implementare :

 @interface MyBookWarehouse : NSObject  { ... } @end @implementation MyBookWarehouse { ... } @end @interface MyCheckoutController : NSObject  { ... } @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]; } 

Infine, non solo il tuo BookPickerController è riutilizzabile ma anche più facile da testare.

 -(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 ... } 

Questo genere di cose è sempre una questione di gusti.

Detto questo, preferisco sempre fare il mio coordinamento (n. 2) tramite gli oggetti del modello. Il controller di visualizzazione di livello superiore carica o crea i modelli necessari e ciascun controller di visualizzazione imposta le proprietà nei relativi controller figlio per comunicare loro quali oggetti modello devono essere utilizzati. La maggior parte delle modifiche viene comunicata al backup della gerarchia utilizzando NSNotificationCenter; l’triggerszione delle notifiche è solitamente integrata nel modello stesso.

Ad esempio, supponiamo di avere un’app con Account e transazioni. Ho anche un AccountListController, un AccountController (che visualizza un riepilogo dell’account con un pulsante “mostra tutte le transazioni”), un TransactionListController e un TransactionController. AccountListController carica un elenco di tutti gli account e li visualizza. Quando tocchi una voce di elenco, imposta la proprietà .account del suo AccountController e spinge l’AccountController nello stack. Quando si tocca il pulsante “mostra tutte le transazioni”, AccountController carica l’elenco delle transazioni, lo inserisce nella proprietà .transactions di TransactionListController e inserisce TransactionListController nello stack e così via.

Se, ad esempio, TransactionController modifica la transazione, apporta la modifica nel suo object di transazione e quindi chiama il metodo “salva”. ‘save’ invia una TransactionChangedNotification. Qualsiasi altro controller che deve aggiornare se stesso quando la transazione cambia osservare la notifica e aggiornarsi. TransactionListController sarebbe presumibilmente; AccountController e AccountListController potrebbero, in base a ciò che stavano cercando di fare.

Per il n. 1, nelle mie prime app avevo una sorta di displayModel: withNavigationController: metodo nel controller figlio che avrebbe impostato le cose e avrebbe spinto il controller in pila. Ma siccome sono diventato più a mio agio con l’SDK, mi sono allontanato da questo, e ora di solito il genitore spinge il bambino.

Per il n. 3, considera questo esempio. Qui stiamo usando due controller, AmountEditor e TextEditor, per modificare due proprietà di una Transazione. Gli editor non dovrebbero effettivamente salvare la transazione in fase di modifica, poiché l’utente potrebbe decidere di abbandonare la transazione. Quindi, invece, entrambi prendono il loro controllore genitore come delegato e chiamano un metodo su di esso dicendo se hanno cambiato qualcosa.

 @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  delegate; } @property (retain) Model * model; @property (assign) id  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  { AmountEditor * amountEditor; TextEditor * textEditor; Transaction * transaction; } ...properties and methods here... @end 

E ora alcuni metodi da 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 cosa da notare è che abbiamo definito un protocollo generico che gli editori possono utilizzare per comunicare con il proprio controllore proprietario. In tal modo, possiamo riutilizzare gli editor in un’altra parte dell’applicazione. (Forse anche gli account possono avere delle note.) Naturalmente, il protocollo EditorDelegate potrebbe contenere più di un metodo; in questo caso è l’unico necessario.

Vedo il tuo problema

Quello che è successo è che qualcuno ha confuso l’idea dell’architettura MVC.

MVC ha tre parti … modelli, viste e controller. Il problema dichiarato sembra averne combinati due senza una buona ragione. viste e controller sono pezzi di logica separati.

quindi … non vuoi avere più controller di visualizzazione …

vuoi avere più viste e un controller che sceglie tra loro. (potresti anche avere più controller, se hai più applicazioni)

le opinioni NON dovrebbero prendere decisioni. Il controller (i) dovrebbe farlo. Da qui la separazione dei compiti, della logica e dei modi per semplificarti la vita.

Quindi … assicurati che il tuo punto di vista lo faccia, e metta in bella evidenza i dati. lascia che il tuo controller decida cosa fare con i dati e quale vista usare.

(e quando parliamo di dati, stiamo parlando del modello … un bel modo standard di essere corretti, accessibili, modificati .. un’altra logica separata che possiamo parcellizzare e dimenticare)

Supponiamo che ci siano due classi A e B.

istanza di class A è

A aInstance;

class A fa e istanza di class B, come

B bInstance;

E nella tua logica della class B, da qualche parte ti viene richiesto di comunicare o triggersre un metodo di class A.

1) Modo sbagliato

È ansible passare l’aInstance a bInstance. ora effettua la chiamata del metodo desiderato [a method method] dalla posizione desiderata in bInstance.

Questo sarebbe servito al tuo scopo, ma mentre il rilascio avrebbe portato a una memoria bloccata e non liberata.

Come?

Quando hai passato l’aInstance a bInstance, abbiamo aumentato il retaincount di aInstance di 1. Quando deallociamo bInstance, avremo la memoria bloccata perché aInstance non può mai essere portato a 0 retaincount da bInstance perché la ragione stessa è un object di un’Instance.

Inoltre, a causa di una sostanza bloccata, anche la memoria di bInstance sarà bloccata (trapelata). Quindi, anche dopo aver disassegnato una stessa Instance quando arriva il momento successivo, anche la sua memoria sarà bloccata perché bInstance non può essere liberato e bInstance è una variabile di class di aInstance.

2) Giusto modo

Definendo aInstance come delegato di bInstance, non ci sarà alcuna modifica del retaincount o entanglement della memoria di aInstance.

bInstance sarà in grado di invocare liberamente i metodi delegati che si trovano nell’Instance. Sulla deallocazione di bInstance, tutte le variabili saranno create e saranno rilasciate sulla deallocazione di On astance, poiché non vi è alcun entanglement di aInstance in bInstance, sarà rilasciato in modo pulito.