Was ist der beste Weg, um zwischen View Controllern zu kommunizieren?


165

Als Neuling bei Objective-C, Kakao und iPhone-Entwicklern im Allgemeinen habe ich den starken Wunsch, das Beste aus der Sprache und den Frameworks herauszuholen.

Eine der Ressourcen, die ich verwende, sind die CS193P-Klassennotizen von Stanford, die sie im Web hinterlassen haben. Es enthält Vorlesungsunterlagen, Aufgaben und Beispielcode, und da der Kurs von Apple-Entwicklern gehalten wurde, halte ich ihn definitiv für "aus dem Maul des Pferdes".

Klassen-Website:
http://www.stanford.edu/class/cs193p/cgi-bin/index.php

Vorlesung 08 bezieht sich auf eine Aufgabe zum Erstellen einer UINavigationController-basierten App, bei der mehrere UIViewController auf den UINavigationController-Stapel übertragen werden. So funktioniert der UINavigationController. Das ist logisch. Die Folie enthält jedoch einige strenge Warnungen bezüglich der Kommunikation zwischen Ihren UIViewControllern.

Ich werde aus dieser ernsthaften Folie zitieren:
http://cs193p.stanford.edu/downloads/08-NavigationTabBarControllers.pdf

Seite 16/51:

So teilen Sie keine Daten

  • Globale Variablen oder Singletons
    • Dies schließt Ihren Anwendungsdelegierten ein
  • Direkte Abhängigkeiten machen Ihren Code weniger wiederverwendbar
    • Und schwieriger zu debuggen und zu testen

OK. Ich bin damit fertig. Werfen Sie nicht blind alle Ihre Methoden, die für die Kommunikation zwischen dem Viewcontroller verwendet werden, in Ihren App-Delegaten und verweisen Sie auf die Viewcontroller-Instanzen in den App-Delegat-Methoden. Fair 'nuff.

Ein Stück weiter, bekommen wir diese Folie uns sagen , was wir sollten tun.

Seite 18/51:

Best Practices für den Datenfluss

  • Finden Sie genau heraus , was kommuniziert werden muss
  • Definieren Sie Eingabeparameter für Ihren View Controller
  • Verwenden Sie für die Kommunikation zur Sicherung der Hierarchie eine lose Kopplung
    • Definieren Sie eine generische Schnittstelle für Beobachter (wie Delegierung).

Auf diese Folie folgt dann eine scheinbar Platzhalterfolie, auf der der Dozent anscheinend anhand eines Beispiels mit dem UIImagePickerController die Best Practices demonstriert. Ich wünschte, die Videos wären verfügbar! :((

Ok, also ... ich fürchte, mein Objekt ist nicht so stark. Ich bin auch ein bisschen verwirrt von der letzten Zeile im obigen Zitat. Ich habe ziemlich viel darüber gegoogelt und einen anständigen Artikel gefunden, der über die verschiedenen Methoden der Beobachtungs- / Benachrichtigungstechniken spricht:
http://cocoawithlove.com/2008/06/five-approaches-to -listening-Observing.html

Methode 5 zeigt sogar Delegierte als Methode an! Außer .... Objekte können jeweils nur einen Delegaten festlegen. Was soll ich also tun, wenn ich mehrere Viewcontroller-Kommunikation habe?

Ok, das ist die aufgebaute Bande. Ich weiß, dass ich meine Kommunikationsmethoden im App-Delegaten leicht ausführen kann, indem ich auf die mehreren Viewcontroller-Instanzen in meinem Appdelegate verweise, aber ich möchte so etwas richtig machen.

Bitte helfen Sie mir, "das Richtige zu tun", indem Sie die folgenden Fragen beantworten:

  1. Wenn ich versuche, einen neuen Viewcontroller auf dem UINavigationController-Stack zu pushen, wer sollte diesen Push ausführen? Welche Klasse / Datei in meinem Code ist der richtige Ort?
  2. Was ist der "richtige" Weg, wenn ich Daten (Wert eines iVar) in einem meiner UIViewController beeinflussen möchte, wenn ich mich in einem anderen UIViewController befinde?
  3. Geben Sie an, dass in einem Objekt jeweils nur ein Delegat festgelegt werden kann. Wie würde die Implementierung aussehen, wenn der Dozent sagt: "Definieren Sie eine generische Schnittstelle für Beobachter (wie Delegation)" . Ein Pseudocode-Beispiel wäre hier nach Möglichkeit sehr hilfreich.

Einige davon werden in diesem Artikel von Apple behandelt - developer.apple.com/library/ios/#featuredarticles/…
James Moore

Nur eine kurze Bemerkung: Die Videos für die Stanford CS193P-Klasse sind jetzt über iTunes U verfügbar. Die neueste Version (2012-13) ist unter itunes.apple.com/us/course/coding-together-developing/… zu sehen, und ich gehe davon aus dass zukünftige Videos und Folien auf cs193p.stanford.edu
Thomas Watson

Antworten:


224

Dies sind gute Fragen, und es ist großartig zu sehen, dass Sie diese Forschung betreiben und sich anscheinend darum kümmern, zu lernen, wie man "es richtig macht", anstatt es nur zusammen zu hacken.

Erstens stimme ich den vorherigen Antworten zu, die sich darauf konzentrieren, wie wichtig es ist, Daten gegebenenfalls in Modellobjekte einzufügen (gemäß dem MVC-Entwurfsmuster). Normalerweise möchten Sie vermeiden, Statusinformationen in einen Controller einzufügen, es sei denn, es handelt sich ausschließlich um "Präsentations" -Daten.

Zweitens finden Sie auf Seite 10 der Stanford-Präsentation ein Beispiel für das programmgesteuerte Verschieben eines Controllers auf den Navigationscontroller. Ein Beispiel dafür, wie dies mit Interface Builder "visuell" ausgeführt wird, finden Sie in diesem Lernprogramm .

Drittens und vielleicht am wichtigsten ist zu beachten, dass die in der Stanford-Präsentation erwähnten "Best Practices" viel einfacher zu verstehen sind, wenn Sie sie im Kontext des Entwurfsmusters "Abhängigkeitsinjektion" betrachten. Kurz gesagt bedeutet dies, dass Ihr Controller die Objekte, die er für seine Arbeit benötigt, nicht "nachschlagen" sollte (z. B. auf eine globale Variable verweisen). Stattdessen sollten Sie diese Abhängigkeiten immer in den Controller "einfügen" (dh die benötigten Objekte über Methoden übergeben).

Wenn Sie dem Abhängigkeitsinjektionsmuster folgen, ist Ihr Controller modular und wiederverwendbar. Und wenn Sie darüber nachdenken, woher die Stanford-Moderatoren kommen (dh als Apple-Mitarbeiter müssen sie Klassen erstellen, die leicht wiederverwendet werden können), haben Wiederverwendbarkeit und Modularität hohe Priorität. Alle Best Practices, die sie für den Datenaustausch erwähnen, sind Teil der Abhängigkeitsinjektion.

Das ist der Kern meiner Antwort. Im Folgenden wird ein Beispiel für die Verwendung des Abhängigkeitsinjektionsmusters mit einem Controller aufgeführt, falls dies hilfreich ist.

Beispiel für die Verwendung der Abhängigkeitsinjektion mit einem View Controller

Angenommen, Sie erstellen einen Bildschirm, in dem mehrere Bücher aufgelistet sind. Der Benutzer kann Bücher auswählen, die er kaufen möchte, und dann auf die Schaltfläche "Kasse" tippen, um zum Kassenbildschirm zu gelangen.

Um dies zu erstellen, können Sie eine BookPickerViewController-Klasse erstellen, die die GUI- / Ansichtsobjekte steuert und anzeigt. Woher bekommt es alle Buchdaten? Angenommen, dies hängt von einem BookWarehouse-Objekt ab. Jetzt vermittelt Ihr Controller im Grunde genommen Daten zwischen einem Modellobjekt (BookWarehouse) und den GUI- / Ansichtsobjekten. Mit anderen Worten, BookPickerViewController hängt vom BookWarehouse-Objekt ab.

Tu das nicht:

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

Stattdessen sollten die Abhängigkeiten wie folgt eingefügt werden:

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

Wenn die Apple-Leute über die Verwendung des Delegierungsmusters sprechen, um "die Hierarchie zu sichern", sprechen sie immer noch über die Abhängigkeitsinjektion. Was sollte der BookPickerViewController in diesem Beispiel tun, wenn der Benutzer seine Bücher ausgewählt hat und zum Auschecken bereit ist? Nun, das ist nicht wirklich seine Aufgabe. Es sollte diese Arbeit auf ein anderes Objekt DELEGIEREN, was bedeutet, dass es von einem anderen Objekt abhängt. Daher können wir unsere BookPickerViewController-Init-Methode wie folgt ändern:

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

Das Nettoergebnis all dessen ist, dass Sie mir Ihre BookPickerViewController-Klasse (und verwandte GUI- / Ansichtsobjekte) geben können und ich sie problemlos in meiner eigenen Anwendung verwenden kann, vorausgesetzt, BookWarehouse und CheckoutController sind generische Schnittstellen (dh Protokolle), die ich implementieren kann ::

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

Schließlich ist Ihr BookPickerController nicht nur wiederverwendbar, sondern auch einfacher zu testen.

-(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
Wenn ich solche Fragen (und Antworten) sehe, die mit solcher Sorgfalt hergestellt wurden, kann ich nicht anders als zu lächeln. Ein wohlverdientes Lob an unseren unerschrockenen Fragesteller und an Sie !! In der Zwischenzeit wollte ich einen aktualisierten Link für diesen praktischen Link zu invasivcode.com freigeben, auf den Sie in Ihrem zweiten Punkt verwiesen haben: invasivcode.com/2009/09/… - Nochmals vielen Dank, dass Sie Ihre Erkenntnisse und Best Practices geteilt und mit Beispielen gesichert haben!
Joe D'Andrea

Genau. Die Frage war gut formuliert und die Antwort war einfach fantastisch. Anstatt nur eine technische Antwort zu haben, enthielt es auch einige Psychologie dahinter, wie / warum es mit DI implementiert wird. Danke dir! +1 auf.
Kevin Elliott

Was ist, wenn Sie BookPickerController auch zum Auswählen eines Buches für eine Wunschliste oder aus einem von mehreren möglichen Gründen für das Auswählen von Büchern verwenden möchten? Würden Sie weiterhin den CheckoutController-Schnittstellenansatz verwenden (möglicherweise in BookSelectionController umbenannt) oder NSNotificationCenter verwenden?
Les

Dies ist immer noch ziemlich eng gekoppelt. Das Auslösen und Konsumieren von Ereignissen von einem zentralen Ort aus wäre lockerer.
Neil McGuigan

1
Die Verbindung gemäß Punkt 2 scheint wieder geändert zu haben - hier ist die Arbeits Link invasivecode.com/blog/archives/322
vikmalhotra

15

So etwas ist immer Geschmackssache.

Trotzdem ziehe ich es immer vor, meine Koordination (# 2) über Modellobjekte durchzuführen. Der View Controller der obersten Ebene lädt oder erstellt die benötigten Modelle, und jeder View Controller legt Eigenschaften in seinen untergeordneten Controllern fest, um ihnen mitzuteilen, mit welchen Modellobjekten sie arbeiten müssen. Die meisten Änderungen werden mithilfe von NSNotificationCenter in der Hierarchie gesichert. Das Auslösen der Benachrichtigungen ist normalerweise in das Modell selbst integriert.

Angenommen, ich habe eine App mit Konten und Transaktionen. Ich habe auch einen AccountListController, einen AccountController (der eine Kontoübersicht mit der Schaltfläche "Alle Transaktionen anzeigen" anzeigt), einen TransactionListController und einen TransactionController. AccountListController lädt eine Liste aller Konten und zeigt sie an. Wenn Sie auf ein Listenelement tippen, wird die Eigenschaft .account des AccountControllers festgelegt und der AccountController auf den Stapel verschoben. Wenn Sie auf die Schaltfläche "Alle Transaktionen anzeigen" tippen, lädt AccountController die Transaktionsliste, fügt sie in die .transactions-Eigenschaft von TransactionListController ein und verschiebt den TransactionListController auf den Stapel usw.

Wenn beispielsweise TransactionController die Transaktion bearbeitet, nimmt er die Änderung in seinem Transaktionsobjekt vor und ruft dann seine 'save'-Methode auf. 'save' sendet eine TransactionChangedNotification. Jeder andere Controller, der sich selbst aktualisieren muss, wenn sich die Transaktion ändert, würde die Benachrichtigung beobachten und sich selbst aktualisieren. TransactionListController würde vermutlich; AccountController und AccountListController können je nachdem, was sie versucht haben.

Für # 1 hatte ich in meinen frühen Apps eine Art displayModel: withNavigationController: -Methode im untergeordneten Controller, die Dinge einrichtete und den Controller auf den Stapel schob. Aber als ich mich mit dem SDK wohler gefühlt habe, bin ich davon abgewichen, und jetzt lassen mich normalerweise die Eltern das Kind schieben.

Betrachten Sie für # 3 dieses Beispiel. Hier verwenden wir zwei Controller, AmountEditor und TextEditor, um zwei Eigenschaften einer Transaktion zu bearbeiten. Die Editoren sollten die zu bearbeitende Transaktion nicht speichern, da der Benutzer entscheiden könnte, die Transaktion abzubrechen. Stattdessen nehmen beide ihren übergeordneten Controller als Delegaten und rufen eine Methode auf, die angibt, ob sie etwas geändert haben.

@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

Und jetzt ein paar Methoden von 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];
}

Zu beachten ist, dass wir ein generisches Protokoll definiert haben, mit dem Redakteure mit ihrem eigenen Controller kommunizieren können. Auf diese Weise können wir die Editoren in einem anderen Teil der Anwendung wiederverwenden. (Möglicherweise können Konten auch Notizen enthalten.) Natürlich kann das EditorDelegate-Protokoll mehr als eine Methode enthalten. In diesem Fall ist dies der einzige notwendige Fall.


1
Soll das so funktionieren wie es ist? Ich habe Probleme mit dem Editor.delegateMitglied. In meiner viewDidLoadMethode bekomme ich Property 'delegate' not found.... Ich bin mir nur nicht sicher, ob ich etwas anderes vermasselt habe. Oder wenn dies der Kürze halber gekürzt ist.
Jeff

Dies ist jetzt ziemlich alter Code, der in einem älteren Stil mit älteren Konventionen geschrieben wurde. Ich würde es nicht kopieren und direkt in Ihr Projekt einfügen. Ich würde nur versuchen, aus den Mustern zu lernen.
Brent Royal-Gordon

Erwischt. Genau das wollte ich wissen. Ich habe es mit einigen Modifikationen zum Laufen gebracht, aber ich war ein wenig besorgt, dass es nicht wörtlich übereinstimmt.
Jeff

0

Ich sehe dein Problem ..

Was passiert ist, ist, dass jemand die Idee der MVC-Architektur verwirrt hat.

MVC besteht aus drei Teilen. Modelle, Ansichten und Controller. Das angegebene Problem scheint zwei davon ohne guten Grund kombiniert zu haben. Ansichten und Controller sind separate Logikelemente.

Also ... Sie möchten nicht mehrere View-Controller haben.

Sie möchten mehrere Ansichten und einen Controller haben, der zwischen ihnen wählt. (Sie könnten auch mehrere Controller haben, wenn Sie mehrere Anwendungen haben)

Ansichten sollten KEINE Entscheidungen treffen. Die Controller sollten dies tun. Daher die Trennung von Aufgaben, Logik und Möglichkeiten, Ihnen das Leben zu erleichtern.

Also ... stellen Sie sicher, dass Ihre Ansicht genau das tut und eine schöne Ansicht der Daten liefert. Lassen Sie Ihren Controller entscheiden, was mit den Daten geschehen soll und welche Ansicht verwendet werden soll.

(und wenn wir über Daten sprechen, sprechen wir über das Modell ... eine nette Standardmethode zum Storring, Zugriff, Modifizieren ... eine weitere separate Logik, die wir wegpacken und vergessen können)


0

Angenommen, es gibt zwei Klassen A und B.

Instanz der Klasse A ist

Eine Instanz;

Klasse A macht und Instanz von Klasse B, als

B Instanz;

Und in Ihrer Logik der Klasse B müssen Sie irgendwo eine Methode der Klasse A kommunizieren oder auslösen.

1) Falscher Weg

Sie können die Instanz an die Instanz übergeben. Platzieren Sie nun den Aufruf der gewünschten Methode [aInstance methodname] von der gewünschten Stelle in bInstance.

Dies hätte Ihren Zweck erfüllt, aber während der Veröffentlichung hätte ein Speicher gesperrt und nicht freigegeben.

Wie?

Wenn Sie die aInstance an bInstance übergeben haben, haben wir den Retaincount von aInstance um 1 erhöht. Bei der Freigabe von bInstance wird der Speicher blockiert, da aInstance niemals durch bInstance auf 0 Retaincount gebracht werden kann, da bInstance selbst ein Objekt von aInstance ist.

Da aInstance stecken bleibt, bleibt auch der Speicher von bInstance hängen (durchgesickert). Selbst wenn aInstance zu einem späteren Zeitpunkt freigegeben wird, wird auch sein Speicher blockiert, da bInstance nicht freigegeben werden kann und bInstance eine Klassenvariable von aInstance ist.

2) Richtiger Weg

Durch die Definition von aInstance als Delegat von bInstance kommt es nicht zu einer Änderung des Retaincount oder einer Speicherverschränkung von aInstance.

bInstance kann die in aInstance liegenden Delegatenmethoden frei aufrufen. Bei der Freigabe von bInstance werden alle Variablen selbst erstellt und freigegeben. Bei der Freigabe von aInstance werden sie sauber freigegeben, da aInstance nicht in bInstance verwickelt ist.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.