Каков наилучший способ связи между контроллерами представления?


165

Будучи новичком в target-c, какао и iPhone-разработчике в целом, я очень хочу получить максимальную пользу от языка и фреймворков.

Одним из ресурсов, которые я использую, являются заметки Стэнфорда CS193P, которые они оставили в Интернете. Он включает в себя примечания к лекциям, задания и пример кода, и, поскольку курс читался разработчиками Apple, я определенно считаю его «изо рта лошади».

Веб-сайт класса:
http://www.stanford.edu/class/cs193p/cgi-bin/index.php

Лекция 08 посвящена назначению для создания приложения на основе UINavigationController, в котором несколько стилей UIViewController помещены в стек UINavigationController. Вот как работает UINavigationController. Это логично. Однако на слайде есть несколько строгих предупреждений о связи между вашими UIViewControllers.

Я собираюсь процитировать этот серьезный слайд:
http://cs193p.stanford.edu/downloads/08-NavigationTabBarControllers.pdf

Страница 16/51:

Как не делиться данными

  • Глобальные переменные или синглтоны
    • Это включает в себя вашу заявку делегата
  • Прямые зависимости делают ваш код менее пригодным для повторного использования
    • И более сложный для отладки и тестирования

Хорошо. Я покончил с этим. Не бросайте вслепую все ваши методы, которые будут использоваться для связи между viewcontroller, в ваш делегат приложения и ссылайтесь на экземпляры viewcontroller в методах делегата приложения. Ярмарка

Чуть дальше, мы получаем этот слайд, рассказывающий нам, что мы должны делать.

Страница 18/51:

Лучшие практики для потока данных

  • Выясните, что именно нужно сообщить
  • Определите входные параметры для вашего контроллера представления
  • Для связи по иерархии используйте слабую связь
    • Определить общий интерфейс для наблюдателей (например, делегирование)

Затем за этим слайдом следует то, что выглядит как слайд заполнителя, на котором лектор затем демонстрирует лучшие практики, используя пример с UIImagePickerController. Я хотел бы, чтобы видео были доступны! :(

Хорошо, так что ... я боюсь, что мой объект не так силен. Я также немного смущен последней строкой в ​​приведенной выше цитате. Я делал свою честную акцию по поиску в Google и нашел то, что кажется приличной статьей, рассказывающей о различных методах наблюдения / уведомления:
http://cocoawithlove.com/2008/06/five-approaches-to -Прослушивание-observing.html

Метод № 5 даже указывает делегатов как метод! За исключением .... объекты могут устанавливать только один делегат за раз. Так что, когда у меня есть несколько представлений viewcontroller, что мне делать?

Хорошо, это настроенная банда. Я знаю, что могу легко использовать свои методы связи в делегате приложения, ссылаясь на множественные экземпляры viewcontroller в моем appdelegate, но я хочу сделать это правильно .

Пожалуйста, помогите мне «поступить правильно», ответив на следующие вопросы:

  1. Когда я пытаюсь вставить новый viewcontroller в стек UINavigationController, кто должен делать это push. Какой класс / файл в моем коде является правильным местом?
  2. Когда я хочу повлиять на какой-то фрагмент данных (значение iVar) в одном из моих UIViewController, когда я нахожусь в другом UIViewController, каков «правильный» способ сделать это?
  3. Предположим, что за один раз в объекте может быть установлен только один делегат, как будет выглядеть реализация, когда лектор говорит: «Определить общий интерфейс для наблюдателей (например, делегирование)» . Пример с псевдокодом будет очень полезен здесь, если это возможно.

Некоторые из них рассматриваются в этой статье от Apple - developer.apple.com/library/ios/#featuredarticles/…
Джеймс Мур,

Просто быстрое замечание: видео для класса Stanford CS193P теперь доступно через iTunes U. Последние (2012-13) можно увидеть на itunes.apple.com/us/course/coding-together-developing/… и я ожидаю что будущие видео и слайды будут объявлены на cs193p.stanford.edu
Томас Уотсон,

Ответы:


224

Это хорошие вопросы, и приятно видеть, что вы проводите это исследование и, похоже, заинтересованы в том, чтобы научиться «делать все правильно», а не просто взламывать его вместе.

Во-первых , я согласен с предыдущими ответами, которые фокусируются на важности размещения данных в объектах модели, когда это уместно (согласно шаблону проектирования MVC). Обычно вы хотите избежать размещения информации о состоянии внутри контроллера, если это не строго данные представления.

Во-вторых , см. Стр. 10 презентации Stanford для примера того, как программно вставить контроллер в контроллер навигации. Для примера того, как сделать это «визуально» с помощью Interface Builder, взгляните на этот учебник .

Третий , и, возможно, самое главное, обратите внимание, что «лучшие практики», упомянутые в презентации Стэнфорда, гораздо легче понять, если вы подумаете о них в контексте шаблона проектирования «внедрения зависимости». Короче говоря, это означает, что ваш контроллер не должен «искать» объекты, необходимые для его работы (например, ссылаться на глобальную переменную). Вместо этого вы всегда должны «вводить» эти зависимости в контроллер (т. Е. Передавать нужные ему объекты с помощью методов).

Если вы будете следовать шаблону внедрения зависимостей, ваш контроллер будет модульным и может использоваться повторно. И если вы подумаете о том, откуда берутся докладчики из Стэнфорда (то есть, будучи сотрудниками Apple, их задача - создавать классы, которые можно легко использовать повторно), многократное использование и модульность являются первоочередными задачами. Все лучшие практики, которые они упоминают для обмена данными, являются частью внедрения зависимости.

Это суть моего ответа. Ниже приведен пример использования шаблона внедрения зависимостей с контроллером на случай, если это будет полезно.

Пример использования внедрения зависимости с контроллером представления

Допустим, вы создаете экран, на котором перечислены несколько книг. Пользователь может выбрать книги, которые он / она хочет купить, а затем нажать кнопку «Оформить заказ», чтобы перейти на экран оформления заказа.

Чтобы построить это, вы можете создать класс BookPickerViewController, который контролирует и отображает объекты GUI / представления. Где он получит все данные книги? Допустим, это зависит от объекта BookWarehouse. Итак, теперь ваш контроллер в основном выполняет посреднические функции между объектом модели (BookWarehouse) и объектами GUI / представления. Другими словами, BookPickerViewController ЗАВИСИТ от объекта BookWarehouse.

Не делай этого:

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

Вместо этого зависимости должны быть введены следующим образом:

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

Когда парни из Apple говорят об использовании шаблона делегирования для «обратной связи по иерархии», они все еще говорят о внедрении зависимости. В этом примере, что должен делать BookPickerViewController, когда пользователь выбрал свои книги и готов их проверить? Ну, это не совсем его работа. Он должен ДЕЛЕГАТИРОВАТЬ эту работу с другим объектом, что означает, что он ЗАВИСИТ от другого объекта. Таким образом, мы можем изменить наш метод инициализации BookPickerViewController следующим образом:

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

Конечным результатом всего этого является то, что вы можете дать мне свой класс BookPickerViewController (и связанные объекты GUI / представления), и я могу легко использовать его в своем собственном приложении, предполагая, что BookWarehouse и CheckoutController являются общими интерфейсами (то есть протоколами), которые я могу реализовать :

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

Наконец, ваш BookPickerController не только можно использовать повторно, но и легче тестировать.

-(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
Когда я вижу подобные вопросы (и ответы), созданные с такой тщательностью, я не могу удержаться от улыбки. Заслуженная благодарность нашему бесстрашному спрашивающему и вам !! Тем временем я хотел бы поделиться обновленной ссылкой на эту удобную ссылку на invasivecode.com, на которую вы ссылались во втором пункте: invasivecode.com/2009/09/… - Еще раз спасибо за то, что вы поделились своими знаниями и лучшими практиками, а также подкрепили их примерами!
Джо д'Андреа

Я согласен. Вопрос был правильно сформулирован, а ответ был просто фантастическим. Вместо того, чтобы просто иметь технический ответ, он также включал некоторую психологию того, как / почему это реализовано с использованием DI. Спасибо! +1 вверх.
Кевин Эллиотт

Что делать, если вы также хотите использовать BookPickerController для выбора книги по списку пожеланий или по одной из нескольких возможных причин выбора книги. Вы все еще используете подход интерфейса CheckoutController (возможно, переименованный во что-то вроде BookSelectionController) или, возможно, используете NSNotificationCenter?
Les

Это все еще довольно тесно связано. Повышение и потребление событий из централизованного места было бы более свободным.
Нил Макгиган

1
Ссылка, указанная в пункте 2, похоже, снова изменилась - вот рабочая ссылка invasivecode.com/blog/archives/322
vikmalhotra

15

Подобные вещи - дело вкуса.

Сказав это, я всегда предпочитаю выполнять координацию (# 2) с помощью модельных объектов. Контроллер представления верхнего уровня загружает или создает необходимые модели, и каждый контроллер представления устанавливает свойства в своих дочерних контроллерах, чтобы сообщить им, с какими объектами модели они должны работать. Большинство изменений передаются обратно в иерархию с помощью NSNotificationCenter; запуск уведомлений обычно встроен в саму модель.

Например, предположим, у меня есть приложение со счетами и транзакциями. У меня также есть AccountListController, AccountController (который отображает сводку учетной записи с помощью кнопки «показать все транзакции»), TransactionListController и TransactionController. AccountListController загружает список всех учетных записей и отображает их. Когда вы нажимаете на элемент списка, он устанавливает свойство .account своего AccountController и помещает AccountController в стек. Когда вы нажимаете кнопку «показать все транзакции», AccountController загружает список транзакций, помещает его в свойство .transactions своего TransactionListController и помещает TransactionListController в стек и так далее.

Если, например, TransactionController редактирует транзакцию, он вносит изменения в свой объект транзакции, а затем вызывает его метод «save». «save» отправляет TransactionChangedNotification. Любой другой контроллер, который должен обновить себя при изменении транзакции, будет наблюдать уведомление и обновлять себя. TransactionListController предположительно будет; AccountController и AccountListController могут, в зависимости от того, что они пытались сделать.

Для # 1, в моих ранних приложениях у меня был своего рода displayModel: withNavigationController: метод в дочернем контроллере, который настраивал бы вещи и помещал контроллер в стек. Но так как мне стало удобнее с SDK, я отошел от этого, и теперь я обычно заставляю родителей подталкивать ребенка.

Для # 3 рассмотрим этот пример. Здесь мы используем два контроллера, AmountEditor и TextEditor, чтобы редактировать два свойства транзакции. Редакторы не должны на самом деле сохранять редактируемую транзакцию, так как пользователь может решить отказаться от транзакции. Таким образом, вместо этого они оба принимают свой родительский контроллер в качестве делегата и вызывают для него метод, сообщающий, изменили ли они что-нибудь.

@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

А теперь несколько методов из 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];
}

Следует отметить, что мы определили общий протокол, который редакторы могут использовать для связи со своим контроллером-владельцем. Таким образом, мы можем повторно использовать редакторы в другой части приложения. (Возможно, учетные записи также могут иметь заметки.) Конечно, протокол EditorDelegate может содержать более одного метода; в этом случае это единственное, что необходимо.


1
Это должно работать как есть? У меня проблемы с Editor.delegateучастником. По моему viewDidLoadметоду я получаю Property 'delegate' not found.... Я просто не уверен, что напортачил. Или если это сокращено для краткости.
Джефф

Теперь это довольно старый код, написанный в старом стиле со старыми соглашениями. Я бы не стал копировать и вставлять его прямо в ваш проект; Я бы просто попытался извлечь уроки из шаблонов.
Брент Роял-Гордон

Попался. Это именно то, что я хотел знать. Я работал с некоторыми модификациями, но меня немного беспокоило, что это не совпадает дословно.
Джефф

0

Я вижу твою проблему ..

Случилось так, что кто-то запутал идею архитектуры MVC.

MVC состоит из трех частей: моделей, представлений и контроллеров. Кажется, заявленная проблема объединила две из них без веской причины. представления и контроллеры - это отдельные кусочки логики.

так что ... вы не хотите иметь несколько контроллеров представления ..

Вы хотите иметь несколько представлений и контроллер, который выбирает между ними. (у вас также может быть несколько контроллеров, если у вас несколько приложений)

мнения не должны принимать решения. Контроллер (ы) должны сделать это. Отсюда разделение задач, логика и способы облегчения вашей жизни.

Итак ... убедитесь, что ваше мнение просто делает это, выдает хороший вид данных. пусть ваш контроллер решит, что делать с данными и какой вид использовать.

(а когда мы говорим о данных, мы говорим о модели ... хороший стандартный способ хранения, доступа, изменения ... еще один отдельный элемент логики, который мы можем отделить и забыть)


0

Предположим, есть два класса A и B.

Экземпляр класса А является

Экземпляр;

класс A делает и экземпляр класса B, как

B bInstance;

И в вашей логике класса B, где-то вы обязаны общаться или вызывать метод класса A.

1) Неправильный путь

Вы можете передать экземпляр в экземпляр. Теперь поместите вызов нужного метода [имя метода aInstance] из нужного места в bInstance.

Это послужило бы вашей цели, но в то время как освобождение привело бы к блокировке памяти, а не освобождению.

Как?

Когда вы передали aInstance в bInstance, мы увеличили резервный счет aInstance на 1. При освобождении bInstance у нас будет заблокирована память, потому что aInstance никогда не может быть доведен до 0 retaincount по причине bInstance, так как bInstance сам по себе является объектом aInstance.

Кроме того, из-за зависания экземпляра память bInstance также будет зависать (вытекать). Таким образом, даже после освобождения самого aInstance, когда наступит его время, его память также будет заблокирована, поскольку bInstance не может быть освобожден, а bInstance является переменной класса aInstance.

2) Правильный путь

Определяя aInstance как делегат bInstance, не будет никакого изменения сохраняемого счета или запутывания памяти aInstance.

bInstance сможет свободно вызывать методы делегата, лежащие в aInstance. При освобождении bInstance все переменные будут созданы самостоятельно и будут освобождены. При освобождении aInstance, поскольку в bInstance нет запутывания aInstance, он будет освобожден чисто.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.