Який найкращий спосіб спілкуватися між контролерами перегляду?


165

Будучи новим у розробці предметів Objective-c, какао та iPhone в цілому, у мене є сильне бажання максимально використати мову та рамки.

Один із ресурсів, якими я користуюся, - це примітки класу CS193P Стенфорда, які вони залишили в Інтернеті. Він включає в себе конспекти лекцій, завдання та зразок коду, а оскільки курс дав Apple Dev's, я, безумовно, вважаю, що це "з конячих вуст".

Веб-сайт класу:
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:

Як не ділитися даними

  • Глобальні змінні чи одиночні
    • Сюди входить делегат вашої програми
  • Прямі залежності роблять ваш код менш використаним
    • І складніше налагодження та тестування

Гаразд. Я з цим вниз. Не перекидайте сліпо всі ваші методи, які будуть використовуватися для спілкування між контролером перегляду, до делегата програми та посилайтеся на випадки перегляду контролерів у методах делегування програми. Справедливий нюх.

Трохи далі, ми отримаємо цей слайд говорить нам , що ми повинні робити.

Сторінка 18/51:

Кращі практики для потоку даних

  • З’ясуйте, що саме потрібно повідомляти
  • Визначте вхідні параметри контролера перегляду
  • Для передачі резервної копії ієрархії використовуйте нещільне з'єднання
    • Визначте загальний інтерфейс для спостерігачів (наприклад, делегування)

Після цього слайду слідує слайд власника місця, де лектор, очевидно, демонструє кращі практики, використовуючи приклад з UIImagePickerController. Я хочу, щоб відео були доступні! :(

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

Метод №5 навіть вказує делегатів як метод! За винятком .... об'єктів може встановлювати лише один делегат одночасно. Тож коли я маю декілька комунікаційних каналів перегляду, що мені робити?

Гаразд, це створена банда. Я знаю, що я можу легко робити свої способи спілкування в делеґаті додатка, використовуючи посилання на декілька екземплярів перегляду в моєму додатку, але я хочу зробити цю справу правильно .

Будь ласка, допоможіть мені "зробити все правильно", відповівши на наступні запитання:

  1. Коли я намагаюся натиснути новий контролер перегляду на стек UINavigationController, хто повинен робити цей натиск. Який клас / файл у моєму коді є правильним місцем?
  2. Коли я хочу вплинути на частину даних (значення iVar) в одному з моїх контролерів UIViewControllers, коли я перебуваю в іншому 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
Thomas Watson

Відповіді:


224

Це хороші запитання, і це чудово, коли ви робите це дослідження і, здається, переймаєтеся тим, як навчитися «робити це правильно», а не просто хакерствувати разом.

По-перше , я погоджуюся з попередніми відповідями, які зосереджуються на важливості розміщення даних у модельних об'єктах, коли це доречно (за схемою проекту 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 init наступним чином:

@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 (і пов'язані з ним графічні інтерфейси / переглядати об'єкти), і я можу легко використовувати його у власній програмі, припускаючи, що 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?
Лесь

Це все ще досить щільно поєднано. Підвищення та споживання подій із централізованого місця було б втратнішим.
Ніл МакГуйган

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

15

Така річ - це завжди справа смаку.

Сказавши це, я завжди вважаю за краще здійснювати свою координацію (№2) через об'єкти моделі. Контролер перегляду верхнього рівня завантажує або створює потрібні йому моделі, і кожен контролер перегляду встановлює властивості у своїх дочірніх контролерах, щоб повідомити їм, з якими об’єктами моделі потрібно працювати. Більшість змін повідомляється резервне копіювання ієрархії за допомогою NSNotificationCenter; запуск сповіщень зазвичай вбудований у саму модель.

Наприклад, припустимо, у мене є додаток із обліковими записами та транзакціями. У мене також є AccountListController, AccountController (який відображає підсумок облікового запису за допомогою кнопки "показати всі транзакції"), TransactionListController і TransactionController. AccountListController завантажує список усіх облікових записів і відображає їх. Коли ви натискаєте на елемент списку, він встановлює властивість .account свого AccountController і натискає AccountController на стек. Якщо натиснути кнопку "показати всі транзакції", AccountController завантажує список транзакцій, поміщає його у властивість .transaction TransactionListController і натискає TransactionListController на стек тощо.

Якщо, скажімо, TransactionController редагує транзакцію, вона вносить зміни в об'єкт транзакції, а потім викликає його метод "збереження". 'Зберегти' надсилає повідомлення TransactionChangedNotification. Будь-який інший контролер, який потребує оновлення, коли зміни транзакції спостерігатиме повідомлення та оновлення. TransactionListController імовірно буде; AccountController та AccountListController можуть залежно від того, що вони намагалися зробити.

Для №1 у моїх ранніх програмах у мене був якийсь режим дисплеяModel: 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.... Я просто не впевнений, чи накрутив щось інше. Або якщо це скорочено для стислості.
Джефф

Зараз це досить старий код, написаний у більш старому стилі зі старими умовами. Я б не копіював і не вставляв його безпосередньо у ваш проект; Я б просто спробував навчитися шаблонам.
Brent Royal-Gordon

Готча. Саме це я і хотів знати. Я отримав це, працюючи з деякими модифікаціями, але мене трохи непокоїло, що воно не відповідає дословно.
Джефф

0

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

Що сталося, це те, що хтось плутав уявлення про архітектуру MVC.

MVC складається з трьох частин .. моделей, поглядів та контролерів. Виявлена ​​проблема, здається, поєднала дві з них без поважних причин. погляди та контролери - це окремі фрагменти логіки.

так що ... ви не хочете мати кілька контролерів перегляду ..

ви хочете мати кілька переглядів і контролер, який вибирає між ними. (у вас також можуть бути кілька контролерів, якщо у вас є кілька додатків)

погляди НЕ повинні приймати рішення. Контролер (и) повинні це зробити. Звідси розділення завдань, логіки та способів полегшити своє життя.

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

(а коли ми говоримо про дані, ми говоримо про модель ... приємний стандартний спосіб зберігання, доступу, модифікації .. ще один окремий фрагмент логіки, про який ми можемо розібратись і забути)


0

Припустимо, є два класи A і B.

екземпляр класу A є

AInstance;

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

B bInstance;

І у вашій логіці класу B десь вам потрібно зв’язатись або запустити метод класу A.

1) Неправильний шлях

Ви можете передати Станцію до Близької. тепер розмістіть виклик потрібного методу [aInstance methodname] з потрібного місця в bInstance.

Це послужило б вашому призначенню, але поки випуск призвів до того, що пам'ять буде заблокована та не звільнена.

Як?

Коли ви передали aInstance до bInstance, ми збільшили повторний рахунок аInstance на 1. Під час розстановки bInstance у нас буде заблокована пам'ять, оскільки aInstance ніколи не може бути доведена до 0 перерахунку через bInstance, оскільки саме bInstance є об'єктом aInstance.

Крім того, через те, що 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.