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


165

Essendo nuovo per gli sviluppatori di obiettivi-c, cacao e iPhone in generale, ho un forte desiderio di ottenere il massimo dalla lingua e dai framework.

Una delle risorse che sto usando sono le note sulla classe CS193P di Stanford che hanno lasciato sul web. Include appunti di lezione, compiti e codice di esempio, e poiché il corso è stato impartito dagli sviluppatori di Apple, lo considero sicuramente "dalla bocca del cavallo".

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

La lezione 08 è correlata a un'assegnazione per la creazione di un'app basata su UINavigationController con più UIViewController inseriti nello stack UINavigationController. Ecco come funziona UINavigationController. Questo è logico. Tuttavia, ci sono alcuni avvertimenti severi nella diapositiva sulla comunicazione tra i tuoi UIViewController.

Citerò questo serio di diapositive:
http://cs193p.stanford.edu/downloads/08-NavigationTabBarControllers.pdf

Pagina 16/51:

Come non condividere i dati

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

Ok. Sono giù con quello. Non lanciare alla cieca tutti i metodi che verranno utilizzati per comunicare tra il viewcontroller nel delegato dell'app e fare riferimento alle istanze del viewcontroller nei metodi delegato dell'app. Discreto.

Un po 'più avanti, abbiamo questa diapositiva che ci dice cosa dovremmo fare.

Pagina 18/51:

Best practice per il flusso di dati

  • Scopri esattamente cosa deve essere comunicato
  • Definire i parametri di input per il controller della vista
  • Per comunicare il backup della gerarchia, utilizzare l'accoppiamento libero
    • Definire un'interfaccia generica per gli osservatori (come la delega)

Questa diapositiva è quindi seguita da quella che sembra essere una diapositiva di segnaposto in cui il docente dimostra apparentemente le migliori pratiche usando 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 dall'ultima riga della citazione 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 di osservazione / tecniche di notifica:
http://cocoawithlove.com/2008/06/five-approaches-to -listening-observing.html

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

Ok, questa è la banda organizzata. So di poter facilmente eseguire i miei metodi di comunicazione nel delegato dell'app facendo riferimento alle istanze multiple del viewcontroller nel mio delegato dell'app, ma voglio fare questo tipo di cose nel modo giusto .

Ti prego, 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 questo push. Quale classe / file nel mio codice è il posto giusto?
  2. Quando voglio influenzare alcuni dati (valore di un iVar) in uno dei miei UIViewController quando mi trovo in un UIViewController diverso , qual è il modo "giusto" per farlo?
  3. Supponiamo che in un oggetto sia possibile impostare un solo delegato alla volta, come sarebbe l'implementazione quando il docente dice "Definire un'interfaccia generica per gli osservatori (come la delega)" . Un esempio di pseudocodice sarebbe estremamente utile qui, se possibile.

Parte di questo è trattata in questo articolo da Apple - developer.apple.com/library/ios/#featuredarticles/…
James Moore,

Solo una breve osservazione: i video per la classe Stanford CS193P sono ora disponibili tramite iTunes U. L'ultimo (2012-13) può essere visto su itunes.apple.com/us/course/coding-together-developing/… e mi aspetto che i video e le diapositive future saranno annunciati su cs193p.stanford.edu
Thomas Watson,

Risposte:


224

Queste sono buone domande, ed è bello vedere che stai facendo questa ricerca e sembra preoccupato di imparare a "farlo bene" invece di hackerarlo insieme.

In primo luogo , sono d'accordo con le risposte precedenti che si concentrano sull'importanza di inserire i dati negli oggetti modello quando appropriato (secondo il modello di progettazione MVC). Di solito si desidera evitare di inserire informazioni sullo stato all'interno di un controller, a meno che non siano strettamente dati di "presentazione".

In secondo luogo , vedere la pagina 10 della presentazione di Stanford per un esempio di come spingere programmaticamente un controller sul controller di navigazione. Per un esempio di come eseguire questa operazione "visivamente" utilizzando Interface Builder, dai un'occhiata a questo tutorial .

In terzo luogo , e forse soprattutto, si noti 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 "iniezione di dipendenza". In breve, ciò significa che il tuo controller non dovrebbe "cercare" gli oggetti di cui ha bisogno per fare il suo lavoro (ad esempio, fare riferimento a una variabile globale). Invece, dovresti sempre "iniettare" quelle dipendenze nel controller (cioè passare gli oggetti di cui ha bisogno tramite i metodi).

Se segui il modello di iniezione delle dipendenze, il tuo controller sarà modulare e riutilizzabile. E se pensi a dove provengono i presentatori di Stanford (ovvero, come dipendenti Apple, il loro compito è quello di costruire classi che possono 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 di dipendenza.

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

Esempio di utilizzo dell'iniezione di dipendenza con un controller di visualizzazione

Supponiamo che tu stia creando una schermata in cui sono elencati diversi libri. L'utente può scegliere i libri che desidera acquistare, quindi toccare un pulsante "Verifica" per accedere alla schermata di verifica.

Per creare questo, è possibile creare una classe BookPickerViewController che controlla e visualizza gli oggetti GUI / view. Dove troveranno tutti i dati del libro? Diciamo che dipende da un oggetto BookWarehouse per quello. Quindi ora il tuo controller sostanzialmente sta mediando i dati tra un oggetto modello (BookWarehouse) e gli oggetti GUI / view. In altre parole, BookPickerViewController DIPENDE dall'oggetto 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 di Apple stanno parlando dell'uso 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 scelto i suoi libri ed è pronto per il checkout? Bene, non è proprio il suo lavoro. Dovrebbe DELEGARE quel lavoro su qualche altro oggetto, il che significa che DIPENDE da un altro oggetto. Quindi potremmo modificare il nostro metodo init 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 netto di tutto ciò è che puoi darmi la tua classe BookPickerViewController (e i relativi oggetti GUI / view) e posso facilmente usarla nella mia stessa applicazione, supponendo che BookWarehouse e CheckoutController siano interfacce generiche (cioè protocolli) che posso implementare :

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

Infine, il tuo BookPickerController non è solo 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
   ...
}

19
Quando vedo domande (e risposte) come questa, realizzate con tanta cura, non posso fare a meno di sorridere. Complimenti meritati al nostro intrepido interrogatore ea te !! Nel frattempo, volevo condividere un link aggiornato per quel pratico link invasivecode.com a cui hai fatto riferimento nel tuo secondo punto: invasivecode.com/2009/09/… - Grazie ancora per aver condiviso le tue conoscenze e le migliori pratiche, oltre a supportarlo con esempi!
Joe D'Andrea,

Sono d'accordo. La domanda era ben formata e la risposta era semplicemente fantastica. Piuttosto che avere solo una risposta tecnica, includeva anche un po 'di psicologia dietro come / perché è implementato usando DI. Grazie! +1 in su.
Kevin Elliott,

Che cosa succede se si desidera utilizzare anche BookPickerController per scegliere un libro per una lista dei desideri o uno dei diversi motivi di scelta dei libri. Utilizzeresti ancora l'approccio dell'interfaccia di CheckoutController (forse rinominato in qualcosa come BookSelectionController) o potresti usare NSNotificationCenter?
Les

Questo è ancora abbastanza strettamente accoppiato. Raccogliere e consumare eventi da un luogo centralizzato sarebbe più libero.
Neil McGuigan,

1
Il link di cui al punto 2 sembra essere cambiato di nuovo - ecco il link di lavoro invasivecode.com/blog/archives/322
vikmalhotra,

15

Questo genere di cose è sempre una questione di gusti.

Detto questo, preferisco sempre fare il mio coordinamento (n. 2) tramite oggetti modello. Il controller di vista di livello superiore carica o crea i modelli di cui ha bisogno e ogni controller di vista imposta le proprietà nei propri controller figlio per comunicare con quali oggetti modello devono lavorare. La maggior parte delle modifiche viene comunicata come backup della gerarchia utilizzando NSNotificationCenter; l'attivazione delle notifiche di solito è integrata nel modello stesso.

Ad esempio, supponiamo che io abbia 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 si tocca un elemento dell'elenco, imposta la proprietà .account del suo AccountController e inserisce 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, per esempio, TransactionController modifica la transazione, apporta la modifica all'oggetto transazione e quindi chiama il suo metodo "salva". 'save' invia una TransactionChangedNotification. Qualsiasi altro controller che deve aggiornarsi quando le modifiche alla transazione osservano la notifica e si aggiornano. TransactionListController presumibilmente sarebbe; AccountController e AccountListController potrebbero, a seconda di 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 impostava le cose e spingeva il controller nello stack. Ma quando mi sono sentito più a mio agio con l'SDK, mi sono allontanato da quello, e ora di solito il genitore spinge il bambino.

Per # 3, considera questo esempio. Qui stiamo usando due controller, AmountEditor e TextEditor, per modificare due proprietà di una Transazione. I redattori non dovrebbero effettivamente salvare la transazione in corso di modifica, poiché l'utente potrebbe decidere di abbandonare la transazione. Quindi, invece, prendono entrambi il controllore principale come delegato e chiamano un metodo dicendo che 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 <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

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 controller. In questo 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.


1
Questo dovrebbe funzionare così com'è? Ho dei problemi con il Editor.delegatemembro. Nel mio viewDidLoadmetodo, sto ottenendo Property 'delegate' not found.... Non sono sicuro di aver rovinato qualcos'altro. O se questo è ridotto per brevità.
Jeff,

Questo è ora un codice piuttosto vecchio, scritto in uno stile più vecchio con convenzioni più vecchie. Non lo copiare e incollare direttamente nel tuo progetto; Proverei solo ad imparare dagli schemi.
Brent Royal-Gordon,

Gotcha. Questo è esattamente quello che volevo sapere. Ho funzionato con alcune modifiche, ma ero un po 'preoccupato che non corrispondesse alla lettera.
Jeff,

0

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 combinate due senza motivo. le viste e i controller sono elementi logici separati.

quindi ... non vuoi avere più controller di visualizzazione ..

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

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

Quindi ... assicurati che la tua vista lo faccia e metta in risalto 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 stordito, accessibile, modificato .. un altro pezzo di logica separata che possiamo separare e di cui dimenticare)


0

Supponiamo che ci siano due classi A e B.

istanza di classe A è

Un'istanza;

classe A e istanza di classe B, come

Bistanza;

E nella tua logica di classe B, da qualche parte ti viene richiesto di comunicare o attivare un metodo di classe A.

1) Modo sbagliato

È possibile passare aInstance a bInstance. ora colloca la chiamata del metodo desiderato [aInstance methodname] dalla posizione desiderata in bInstance.

Questo sarebbe servito al tuo scopo, ma mentre il rilascio avrebbe comportato il blocco e la liberazione di una memoria.

Come?

Quando hai passato aInstance a bInstance, abbiamo aumentato il conteggio di aInstance di 1. Quando deallocazione di bInstance, avremo la memoria bloccata perché aInstance non può mai essere portato a 0 conteggio in base al fatto che bInstance stessa è un oggetto di aInstance.

Inoltre, a causa di un'istanza bloccata, anche la memoria di bInstance verrà bloccata (trapelata). Quindi, anche dopo aver deallocato una stessa Istanza quando verrà il suo momento successivo, anche la sua memoria verrà bloccata perché bInstance non può essere liberata e bInstance è una variabile di classe di aInstance.

2) Modo giusto

Definendo aInstance come delegato di bInstance, non si verificherà alcuna modifica del conteggio dei conti o entanglement di memoria di aInstance.

bInstance sarà in grado di invocare liberamente i metodi delegati che si trovano in aInstance. Sulla deallocazione di bInstance, tutte le variabili saranno create da loro e saranno rilasciate Sulla deallocazione di aInstance, poiché non vi è alcun intreccio di aInstance in bInstance, verrà rilasciato in modo pulito.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.