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

Будучи новым для objective-c, cocoa и iPhone dev в целом, у меня есть сильное желание максимально использовать язык и рамки.

Один из ресурсов, которые я использую, - это заметки класса Stanford CS193P, которые они оставили в Интернете. Он включает в себя лекционные заметки, задания и образец кода, а так как курс был дан разработчиками Apple, я определенно считаю, что это "из уст лошади".

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

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

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

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

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

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

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

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

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

Рекомендации по потоку данных

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

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

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

Метод # 5 даже указывает делегатов как метод! Кроме того, объекты могут устанавливать только один делегат за раз. Итак, когда у меня есть несколько соединений с viewcontroller, что мне делать?

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

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

  • Когда я пытаюсь нажать новый диспетчер представлений в стеке UINavigationController, кто должен делать это нажатие. Какой класс/файл в моем коде является правильным местом?
  • Когда я хочу повлиять на часть данных (значение iVar) в одном из моих UIViewControllers, когда я нахожусь в другом UIViewController, что такое "правильный" способ сделать это?
  • Дайте нам возможность иметь только один делегат за один раз в объекте, как будет выглядеть реализация, когда лектор говорит "Определить общий интерфейс для наблюдателей (например, делегирование)". Например, пример псевдокода был бы здесь очень полезен.

Ответ 1

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

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

Второй, см. стр. 10 презентации Стэнфорда, на примере того, как программно нажать контроллер на контроллер навигации. Пример того, как это сделать "визуально" с помощью Interface Builder, взгляните на этот учебник.

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

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

Это суть моего ответа. Я приведу пример использования шаблона инъекции зависимостей с помощью контроллера ниже, если он будет полезен.

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

Скажем, вы создаете экран, в котором перечислены несколько книг. Пользователь может выбрать книги, которые он хочет купить, а затем нажать кнопку "checkout", чтобы перейти на экран проверки.

Чтобы создать это, вы можете создать класс BookPickerViewController, который контролирует и отображает объекты GUI/view. Где он получит все данные книги? Скажем, это зависит от объекта BookWarehouse. Итак, теперь ваш контроллер в основном осуществляет обмен данными между объектами модели (BookWarehouse) и объектами графического интерфейса/представления. Другими словами, BookPickerViewController DEPENDS на объект 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 после того, как пользователь выбрал свои книги и готов проверить? Ну, это не совсем его работа. Он должен DELEGATE, чтобы работать с каким-то другим объектом, а это означает, что он ДЕПРЕССИРУЕТСЯ на другом объекте. Поэтому мы можем изменить наш метод инициализации BookPickerViewController следующим образом:

@implementation BookPickerViewController

-(void) initWithWarehouse:    (BookWarehouse*)warehouse 
        andCheckoutController:(CheckoutController*)checkoutController 
{
   myBookWarehouse = warehouse;
   myCheckoutController = checkoutController;
}

-(void) handleCheckout {
   // We've collected the user book picks in a "bookPicks" variable
   [myCheckoutController handleCheckout: bookPicks];
   ...
}

Конечным результатом всего этого является то, что вы можете дать мне свой класс BookPickerViewController (и связанные с ним объекты графического интерфейса/представления), и я могу легко использовать его в своем собственном приложении, предполагая, что 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 handleCheckout: method and passed it a valid
   // list of books
   ...
}

Ответ 2

Такая вещь всегда зависит от вкуса.

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

Например, предположим, что у меня есть приложение со счетами и транзакциями. У меня также есть AccountListController, AccountController (который отображает сводку учетной записи с кнопкой "показать все транзакции" ), TransactionListController и TransactionController. AccountListController загружает список всех учетных записей и отображает их. Когда вы нажимаете на элемент списка, он устанавливает свойство .account своего AccountController и толкает AccountController в стек. Когда вы нажимаете кнопку "показать все транзакции", AccountController загружает список транзакций, помещает его в свой Свойство TransactionListController.transactions и толкает 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 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 может содержать более одного метода; в этом случае требуется только один.

Ответ 3

Я вижу вашу проблему.

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

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

поэтому... вы не хотите иметь несколько контроллеров просмотра.

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

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

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

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

Ответ 4

Предположим, что существуют два класса A и B.

экземпляр класса A

A aInstance;

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

B bInstance;

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

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

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

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

Как?

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

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

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

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

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