Jaki jest najlepszy sposób komunikacji między kontrolerami widoku?

165

Będąc nowicjuszem w zakresie programowania Objective-C, Cocoa i iPhone'a w ogóle, pragnę jak najlepiej wykorzystać język i ramy.

Jednym z zasobów, z których korzystam, są notatki z klasy CS193P Stanforda, które pozostawili w Internecie. Zawiera notatki do wykładów, zadania i przykładowy kod, a ponieważ kurs został poprowadzony przez programistów Apple, zdecydowanie uważam, że jest on „z końskiej paszczy”.

Strona internetowa klasy:
http://www.stanford.edu/class/cs193p/cgi-bin/index.php

Wykład 08 jest powiązany z zadaniem zbudowania aplikacji opartej na UINavigationController, która ma wiele UIViewControllers wypychanych na stos UINavigationController. Tak działa UINavigationController. To logiczne. Jednak na slajdzie znajdują się poważne ostrzeżenia dotyczące komunikacji między kontrolerami UIViewControllers.

Cytuję z tego poważnego slajdu:
http://cs193p.stanford.edu/downloads/08-NavigationTabBarControllers.pdf

Strona 16/51:

Jak nie udostępniać danych

  • Zmienne globalne lub pojedyncze
    • Obejmuje to delegata aplikacji
  • Bezpośrednie zależności sprawiają, że kod jest trudniejszy do ponownego wykorzystania
    • I trudniejsze do debugowania i testowania

Dobrze. Nie mam tego. Nie wrzucaj na ślepo wszystkich metod, które będą używane do komunikacji między kontrolerem widoku do delegata aplikacji i odwołuj się do wystąpień viewcontroller w metodach delegata aplikacji. Fair 'nuff.

Nieco dalej otrzymujemy ten slajd, który mówi nam, co powinniśmy zrobić.

Strona 18/51:

Najlepsze praktyki dotyczące przepływu danych

  • Dowiedz się dokładnie, co należy przekazać
  • Zdefiniuj parametry wejściowe dla kontrolera widoku
  • Do komunikacji w górę hierarchii użyj luźnego połączenia
    • Zdefiniuj ogólny interfejs dla obserwatorów (np. Delegowanie)

Po tym slajdzie następuje slajd, który wydaje się być slajdem zastępczym, na którym wykładowca następnie najwyraźniej demonstruje najlepsze praktyki na przykładzie z UIImagePickerController. Chciałbym, żeby filmy były dostępne! :(

Ok, więc ... Obawiam się, że moje objc-fu nie jest tak mocne. Jestem też trochę zdezorientowany ostatnią linią powyższego cytatu. Sporo robiłem w Google na ten temat i znalazłem przyzwoity artykuł opisujący różne metody technik obserwacji / powiadamiania:
http://cocoawithlove.com/2008/06/five-approaches-to -listening-observing.html

Metoda nr 5 wskazuje nawet delegatów jako metodę! Z wyjątkiem obiektów .... można ustawić tylko jednego delegata naraz. Więc kiedy mam komunikację z wieloma kontrolerami widoku, co mam zrobić?

Ok, to gang z ustawieniami. Wiem, że mogę łatwo wykonywać moje metody komunikacji w delegacie aplikacji, korzystając z wielu wystąpień kontrolera widoku w moim appdelegate, ale chcę to zrobić we właściwy sposób.

Pomóż mi „postępować właściwie”, odpowiadając na następujące pytania:

  1. Kiedy próbuję wypchnąć nowy kontroler widoku na stosie UINavigationController, kto powinien wykonywać to wypychanie. Która klasa / plik w moim kodzie jest właściwym miejscem?
  2. Kiedy chcę wpłynąć na niektóre dane (wartość iVar) w jednym z moich kontrolerów UIViewControllers, gdy jestem w innym UIViewController, jaki jest „właściwy” sposób, aby to zrobić?
  3. Daj nam tylko jednego delegata ustawionego w danym momencie w obiekcie, jak wyglądałaby implementacja, gdy prowadzący powie: „Zdefiniuj ogólny interfejs dla obserwatorów (np. Delegacja)” . Przykład pseudokodu byłby tutaj bardzo pomocny, jeśli to możliwe.
Quinn Taylor
źródło
Niektóre z tych zagadnień są omówione w tym artykule firmy Apple - developer.apple.com/library/ios/#featuredarticles/…
James Moore
Krótka uwaga: filmy z klasy Stanford CS193P są teraz dostępne za pośrednictwem iTunes U. Najnowsze (2012-13) można zobaczyć na itunes.apple.com/us/course/coding-together-developing/ ... i spodziewam się że przyszłe filmy i slajdy zostaną ogłoszone na stronie cs193p.stanford.edu
Thomas Watson

Odpowiedzi:

224

To są dobre pytania i wspaniale jest widzieć, że robisz te badania i wydaje się, że zależy Ci na tym, aby nauczyć się „robić to dobrze”, zamiast po prostu je razem łamać.

Po pierwsze , zgadzam się z poprzednimi odpowiedziami, które koncentrują się na znaczeniu umieszczania danych w obiektach modelu, gdy jest to stosowne (zgodnie ze wzorcem projektowym MVC). Zwykle chcesz uniknąć umieszczania informacji o stanie wewnątrz kontrolera, chyba że są to dane ściśle „prezentacyjne”.

Po drugie , na stronie 10 prezentacji Stanforda znajduje się przykład programowego wciskania kontrolera na kontroler nawigacyjny. Aby zapoznać się z przykładem, jak to zrobić „wizualnie” przy użyciu programu Interface Builder, zapoznaj się z tym samouczkiem .

Po trzecie , i być może najważniejsze, zwróć uwagę, że „najlepsze praktyki” wspomniane w prezentacji na Uniwersytecie Stanforda są znacznie łatwiejsze do zrozumienia, jeśli myślisz o nich w kontekście wzorca projektowego „wstrzykiwania zależności”. W skrócie oznacza to, że kontroler nie powinien „wyszukiwać” obiektów, których potrzebuje do wykonania swojej pracy (np. Odwoływać się do zmiennej globalnej). Zamiast tego należy zawsze „wstrzykiwać” te zależności do kontrolera (tj. Przekazywać potrzebne obiekty za pomocą metod).

Jeśli zastosujesz się do wzorca iniekcji zależności, twój kontroler będzie modułowy i wielokrotnego użytku. A jeśli pomyślisz o tym, skąd pochodzą prezenterzy ze Stanford (np. Ich zadaniem jako pracowników Apple jest tworzenie klas, które można łatwo ponownie wykorzystać), ponowne wykorzystanie i modułowość są priorytetami. Wszystkie najlepsze praktyki, o których wspominają w zakresie udostępniania danych, są częścią wstrzykiwania zależności.

To istota mojej odpowiedzi. Poniżej zamieszczę przykład użycia wzorca iniekcji zależności z kontrolerem na wypadek, gdyby był pomocny.

Przykład użycia iniekcji zależności z kontrolerem widoku

Załóżmy, że tworzysz ekran, na którym znajduje się kilka książek. Użytkownik może wybrać książki, które chce kupić, a następnie dotknąć przycisku „Do kasy”, aby przejść do ekranu kasy.

Aby to zbudować, możesz utworzyć klasę BookPickerViewController, która kontroluje i wyświetla obiekty GUI / widoku. Skąd weźmie wszystkie dane książki? Powiedzmy, że zależy to od obiektu BookWarehouse. Więc teraz twój kontroler po prostu pośredniczy w przesyłaniu danych między obiektem modelu (BookWarehouse) a GUI / obiektami widoku. Innymi słowy, BookPickerViewController ZALEŻY od obiektu BookWarehouse.

Nie rób tego:

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

Zamiast tego zależności należy wstrzyknąć w następujący sposób:

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

Kiedy faceci z Apple mówią o wykorzystaniu wzorca delegowania do „komunikacji w górę hierarchii”, wciąż mówią o wstrzykiwaniu zależności. W tym przykładzie, co powinien zrobić BookPickerViewController, gdy użytkownik wybrał swoje książki i jest gotowy do pobrania? Cóż, to naprawdę nie jest jego praca. Powinien DELEGOWAĆ to działanie do innego obiektu, co oznacza, że ​​ZALEŻY to od innego obiektu. Więc możemy zmodyfikować naszą metodę inicjującą BookPickerViewController w następujący sposób:

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

Rezultatem tego wszystkiego jest to, że możesz dać mi swoją klasę BookPickerViewController (i powiązane obiekty GUI / widoku) i mogę z łatwością użyć jej w mojej własnej aplikacji, zakładając, że BookWarehouse i CheckoutController to ogólne interfejsy (tj. Protokoły), które mogę zaimplementować :

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

Wreszcie, Twój BookPickerController jest nie tylko wielokrotnego użytku, ale także łatwiejszy do przetestowania.

-(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
   ...
}
Clint Harris
źródło
19
Kiedy widzę takie pytania (i odpowiedzi), stworzone z taką starannością, nie mogę powstrzymać uśmiechu. Zasłużone uznanie dla naszego nieustraszonego pytającego i dla Ciebie !! W międzyczasie chciałem udostępnić zaktualizowany link do tego przydatnego linku do invasivecode.com, o którym wspomniałeś w drugim punkcie: invasivecode.com/2009/09/… - Jeszcze raz dziękuję za podzielenie się spostrzeżeniami i najlepszymi praktykami, a także za wsparcie ich przykładami!
Joe D'Andrea
Zgadzam się. Pytanie było dobrze sformułowane, a odpowiedź była po prostu fantastyczna. Zamiast tylko technicznej odpowiedzi, zawierała również psychologię dotyczącą tego, jak / dlaczego jest zaimplementowana przy użyciu DI. Dziękuję Ci! +1 w górę.
Kevin Elliott
Co zrobić, jeśli chcesz również użyć BookPickerController do wybierania książki na listę życzeń lub z jednego z kilku możliwych powodów wybierania książek. Czy nadal korzystasz z podejścia interfejsu CheckoutController (być może zmieniono nazwę na coś takiego jak BookSelectionController), czy może użyjesz NSNotificationCenter?
Les
Jest to nadal dość ściśle powiązane. Podnoszenie i konsumowanie wydarzeń ze scentralizowanego miejsca byłoby luźniejsze.
Neil McGuigan,
1
Wydaje się, że odnośnik, o którym mowa w punkcie 2, znowu się zmienił - oto działający link invasivecode.com/blog/archives/322
vikmalhotra
15

Takie rzeczy zawsze są kwestią gustu.

Powiedziawszy to, zawsze wolę koordynować (# 2) za pomocą obiektów modelu. Kontroler widoku najwyższego poziomu ładuje lub tworzy modele, których potrzebuje, a każdy kontroler widoku ustawia właściwości w swoich kontrolerach podrzędnych, aby powiedzieć im, z którymi obiektami modelu muszą pracować. Większość zmian jest przekazywana z powrotem w hierarchii za pomocą NSNotificationCenter; wypalanie powiadomień jest zwykle wbudowane w sam model.

Załóżmy na przykład, że mam aplikację z kontami i transakcjami. Mam również AccountListController, AccountController (który wyświetla podsumowanie konta z przyciskiem „pokaż wszystkie transakcje”), TransactionListController i TransactionController. AccountListController ładuje listę wszystkich kont i wyświetla je. Po dotknięciu elementu listy ustawia on właściwość .account jego AccountController i wypycha AccountController na stos. Po naciśnięciu przycisku „pokaż wszystkie transakcje”, AccountController ładuje listę transakcji, umieszcza ją we właściwości .transactionsListControllera TransactionList i umieszcza TransactionListController na stosie i tak dalej.

Jeśli, powiedzmy, TransactionController edytuje transakcję, dokonuje zmiany w swoim obiekcie transakcji, a następnie wywołuje swoją metodę „save”. „Zapisz” wysyła TransactionChangedNotification. Każdy inny kontroler, który musi się odświeżyć, gdy transakcja się zmienia, obserwowałby powiadomienie i aktualizował się. Prawdopodobnie TransactionListController; AccountController i AccountListController mogą, w zależności od tego, co próbowały zrobić.

W przypadku # 1 w moich wczesnych aplikacjach miałem pewien rodzaj displayModel: withNavigationController: metoda w kontrolerze podrzędnym, która ustawiałaby rzeczy i umieszczała kontroler na stosie. Ale kiedy poczułem się bardziej komfortowo z SDK, odpłynąłem od tego i teraz zazwyczaj rodzic popycha dziecko.

W przypadku # 3 rozważ ten przykład. Tutaj używamy dwóch kontrolerów, AmountEditor i TextEditor, aby edytować dwie właściwości transakcji. Redaktorzy nie powinni faktycznie zapisywać edytowanej transakcji, ponieważ użytkownik może zdecydować o rezygnacji z transakcji. Zamiast tego oboje przyjmują swój kontroler nadrzędny jako delegata i wywołują na nim metodę, mówiąc, czy cokolwiek zmienili.

@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

A teraz kilka metod z 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];
}

Należy zauważyć, że zdefiniowaliśmy ogólny protokół, którego redaktorzy mogą używać do komunikowania się ze swoim kontrolerem. W ten sposób możemy ponownie wykorzystać Edytorów w innej części aplikacji. (Być może konta też mogą zawierać notatki). Oczywiście protokół EditorDelegate może zawierać więcej niż jedną metodę; w tym przypadku jest to jedyne konieczne.

Brent Royal-Gordon
źródło
1
Czy to ma działać tak, jak jest? Mam problem z Editor.delegateczłonkiem. W mojej viewDidLoadmetodzie dostaję Property 'delegate' not found.... Po prostu nie jestem pewien, czy schrzaniłem coś innego. Lub jeśli jest to skrócone dla zwięzłości.
Jeff
To jest teraz dość stary kod, napisany w starszym stylu ze starszymi konwencjami. Nie skopiowałbym go i nie wklejał bezpośrednio do twojego projektu; Po prostu chciałbym się uczyć na wzorach.
Brent Royal-Gordon
Mam cię. Dokładnie to chciałem wiedzieć. Pracowałem z pewnymi modyfikacjami, ale byłem trochę zaniepokojony, że nie pasuje dosłownie.
Jeff,
0

Widzę twój problem ...

Stało się tak, że ktoś pomylił pojęcie architektury MVC.

MVC składa się z trzech części… modeli, widoków i kontrolerów… Podany problem wydaje się być połączeniem dwóch z nich bez powodu. widoki i kontrolery są oddzielnymi elementami logiki.

więc ... nie chcesz mieć wielu kontrolerów widoku ...

chcesz mieć wiele widoków i kontroler, który wybiera między nimi. (możesz również mieć wiele kontrolerów, jeśli masz wiele aplikacji)

poglądy NIE powinny wpływać na podejmowanie decyzji. Powinni to zrobić kontroler (y). Stąd rozdział zadań, logiki i sposobów ułatwienia sobie życia.

Więc ... upewnij się, że twój widok po prostu to robi, wyświetla ładny widok danych. pozwól administratorowi zdecydować, co zrobić z danymi iz jakiego widoku skorzystać.

(a kiedy mówimy o danych, mówimy o modelu ... fajny standardowy sposób przechowywania, uzyskiwania dostępu, modyfikowania ... kolejny oddzielny element logiki, który możemy spakować i zapomnieć)

Bingy
źródło
0

Załóżmy, że istnieją dwie klasy A i B.

wystąpienie klasy A to

A aInstance;

klasa A marki i egzemplarze klasy B, as

B bInstance;

Zgodnie z logiką klasy B, gdzieś jesteś zobowiązany do komunikowania się lub wyzwalania metody klasy A.

1) Zły sposób

Możesz przekazać aInstance do bInstance. teraz umieść wywołanie żądanej metody [aInstance nazwa metody] z żądanej lokalizacji w bInstance.

To by służyło twojemu celowi, ale podczas gdy uwolnienie doprowadziłoby do zablokowania pamięci, a nie uwolnienia.

W jaki sposób?

Kiedy przekazałeś aInstance do bInstance, zwiększyliśmy retaincount of aInstance o 1. Podczas zwalniania bInstance będziemy mieli zablokowaną pamięć, ponieważ aInstance nigdy nie może zostać sprowadzona do 0 retaincount przez bInstance, ponieważ bInstance jest obiektem aInstance.

Ponadto, z powodu zablokowania aInstance, pamięć bInstance również zostanie zablokowana (wyciekła). Więc nawet po cofnięciu alokacji samego aInstance, gdy nadejdzie później jego czas, jego pamięć również zostanie zablokowana, ponieważ bInstance nie może zostać zwolniony, a bInstance jest zmienną klasy aInstance.

2) Właściwy sposób

Definiując aInstance jako delegata bInstance, nie będzie żadnej zmiany retaincount ani splątania pamięci aInstance.

bInstance będzie mógł swobodnie wywoływać metody delegatów znajdujące się w aInstance. Po zwolnieniu alokacji bInstance wszystkie zmienne zostaną utworzone samodzielnie i zostaną zwolnione. W przypadku zwolnienia aInstance, ponieważ nie ma splątania aInstance w bInstance, zostaną one zwolnione w sposób czysty.

r & D_
źródło