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

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

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

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

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

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

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

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

  • Global Variables or singletons
    • This includes your application delegate
  • Direct dependencies make your code less reusable
    • And more difficult to debug & test

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

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

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

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

  • Выясните точно, о чем нужно сообщить
  • Определите входные параметры для вашего контроллера представления.
  • For communicating back up the hierarchy, use loose coupling
    • Define a generic interface for observers (like delegation)

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

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

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

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

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

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

person Community    schedule 20.02.2009    source источник
comment
Некоторые из этих проблем рассматриваются в этой статье от Apple - nofollow. apple.com/library/ios/#featuredarticles/   -  person James Moore    schedule 31.10.2012
comment
Небольшое замечание: видеоролики для класса Stanford CS193P теперь доступны через iTunes U. Последнюю версию (2012–2013 гг.) Можно посмотреть по адресу itunes.apple.com/us/course/coding-together-developing/, и я ожидаю, что будущие видео и слайды будут анонсированы на cs193p.stanford.edu   -  person Thomas Watson    schedule 12.06.2013


Ответы (4)


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

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

Во-вторых, см. стр. 10 презентации Стэнфордского университета, где показан пример того, как программно установить контроллер на контроллер навигации. Пример того, как сделать это «визуально» с помощью Interface Builder, можно найти на странице это руководство.

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

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

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

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

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

Чтобы создать это, вы можете создать класс BookPickerViewController, который контролирует и отображает объекты графического интерфейса / представления. Где взять все данные о книгах? Допустим, это зависит от объекта BookWarehouse. Итак, теперь ваш контроллер в основном передает данные между объектом модели (BookWarehouse) и объектами графического интерфейса / представления. Другими словами, 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
   ...
}
person Clint Harris    schedule 22.02.2009
comment
Когда я вижу подобные вопросы (и ответы), созданные с такой заботой, я не могу не улыбнуться. Заслуженная благодарность нашему бесстрашному вопрошающему и вам !! Между тем, я хотел поделиться обновленной ссылкой на эту удобную ссылку invasivecode.com, на которую вы ссылались во втором пункте: invasivecode.com/2009/09/ - Еще раз спасибо за то, что поделились своим мнением и передовыми практиками, а также подкрепили их примерами! - person Joe D'Andrea; 29.10.2009
comment
Я согласен. Вопрос был хорошо сформулирован, и ответ был просто фантастическим. Вместо того, чтобы просто получить технический ответ, он также включал некоторую психологию, объясняющую, как / почему это реализовано с использованием DI. Спасибо! +1 вверх. - person Kevin Elliott; 10.01.2010
comment
Что делать, если вы также хотите использовать BookPickerController для выбора книги из списка желаний или по одной из нескольких возможных причин выбора книги. Будете ли вы по-прежнему использовать подход интерфейса CheckoutController (возможно, переименованный во что-то вроде BookSelectionController) или, возможно, использовать NSNotificationCenter? - person Les; 16.12.2010
comment
Это все еще довольно тесно связано. Поднимать и потреблять события из централизованного места было бы легче. - person Neil McGuigan; 05.06.2012
comment
Ссылка, указанная в пункте 2, похоже, снова изменилась - вот рабочая ссылка invasivecode.com/blog/ архивы / 322 - person vikmalhotra; 13.06.2012
comment
У этого шаблона была бы проблема в том, что CheckoutController теперь должен отвечать за выяснение того, как и когда он должен отображать свое представление в пользовательском интерфейсе, не так ли? Разве это не лучше подходит для UINavigationController? Если да, то нет ли более модульного способа передачи управления CheckoutController? (т.е. через родительский контроллер или событие или что-то еще). - person Jeff; 22.06.2013
comment
@ClintHarris вы реализуете свои контроллеры представления в UIAppDelegate, но что делать, если я использую раскадровку и хочу иметь аналогичную гибкость? Спасибо. - person Dima Deplov; 13.02.2014
comment
Я не понимаю Singleton vs. instance. Вы вводите глобальную переменную так же, как вводите переменную экземпляра? - person Honey; 19.12.2016

Такие вещи всегда дело вкуса.

Сказав это, я всегда предпочитаю осуществлять координацию (№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'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 может содержать более одного метода; в данном случае это единственное, что необходимо.

person Becca Royal-Gordon    schedule 22.02.2009
comment
Это должно работать как есть? У меня проблемы с участником Editor.delegate. В моем viewDidLoad методе я получаю Property 'delegate' not found.... Я просто не уверен, что я еще что-то облажался. Или, если это сокращено для краткости. - person Jeff; 22.06.2013
comment
Теперь это довольно старый код, написанный в более старом стиле со старыми соглашениями. Я бы не стал копировать и вставлять его прямо в ваш проект; Я просто попытаюсь учиться на шаблонах. - person Becca Royal-Gordon; 24.06.2013
comment
Попался. Это именно то, что я хотел знать. Я заставил его работать с некоторыми модификациями, но меня немного беспокоило, что это не соответствует дословно. - person Jeff; 25.06.2013

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

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

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

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

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

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

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

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

person Bingy    schedule 22.02.2009

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

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

A aInstance;

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

B bInstance;

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

1) Неправильно

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

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

Как?

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

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

2) Верно

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

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

person rd_    schedule 05.03.2012