Як уникнути великих і незграбних UITableViewController на iOS?


36

У мене є проблема при впровадженні MVC-схеми на iOS. Я здійснив пошук в Інтернеті, але, здається, не знайшов жодного приємного рішення цієї проблеми.

Багато UITableViewControllerреалізацій здаються досить великими. Більшість прикладів, які я бачив, дозволяє UITableViewControllerреалізувати <UITableViewDelegate>та <UITableViewDataSource>. Ці реалізації є великою причиною того UITableViewController, що стає великим. Одним з рішень було б створення окремих класів, які реалізують <UITableViewDelegate>і <UITableViewDataSource>. Звичайно, ці класи повинні мати посилання на UITableViewController. Чи є недоліки використання цього рішення? Загалом, я думаю, ви повинні делегувати функціональність іншим класам "Helper" або подібним, використовуючи шаблон делегата. Чи існують чітко встановлені способи вирішення цієї проблеми?

Я не хочу, щоб модель містила занадто велику функціональність, а також вигляд. Я вважаю, що логіка дійсно повинна бути в класі контролера, оскільки це один з наріжних каменів MVC-схеми. Але велике питання:

Як слід розділити контролер реалізації MVC на менші керовані частини? (У цьому випадку стосується MVC в iOS)

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


1
"Також аргумент, чому це рішення є дивним." :)
окулус

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

Відповіді:


43

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

Я намагався розділити протоколи UITableViewDataSourceта UITableViewDelegateпротоколи на різні об'єкти, але це, як правило, виявляється помилковим розщепленням, оскільки майже кожен метод делегата потребує копання в джерело даних (наприклад, при виборі, делегат повинен знати, який об'єкт представлений вибраний рядок). Отже, я закінчую єдиним об'єктом, який є і джерелом даних, і делегатом. Цей об'єкт завжди пропонує метод, для -(id)tableView: (UITableView *)tableView representedObjectAtIndexPath: (NSIndexPath *)indexPathякого і джерело даних, і делегуючі аспекти повинні знати, над чим вони працюють.

Це мій проблемний "рівень 0". Рівень 1 займається, якщо мені доведеться представляти предмети різного типу в одному поданні таблиці. Наприклад, уявіть, що вам довелося написати додаток "Контакти" - для одного контакту можуть бути рядки, що представляють номери телефонів, інші рядки, що представляють адреси, інші, що представляють адреси електронної пошти тощо. Я хочу уникати такого підходу:

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
  id object = [self tableView: tableView representedObjectAtIndexPath: indexPath];
  if ([object isKindOfClass: [PhoneNumber class]]) {
    //configure phone number cell
  }
  else if …
}

Поки що два представлені рішення. Один полягає в динамічній побудові селектора:

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
  id object = [self tableView: tableView representedObjectAtIndexPath: indexPath];
  NSString *cellSelectorName = [NSString stringWithFormat: @"tableView:cellFor%@AtIndexPath:", [object class]];
  SEL cellSelector = NSSelectorFromString(cellSelectorName);
  return [self performSelector: cellSelector withObject: tableView withObject: object];
}

- (UITableViewCell *)tableView: (UITableView *)tableView cellForPhoneNumberAtIndexPath: (NSIndexPath *)indexPath {
  // configure phone number cell
}

У такому підході вам не потрібно редагувати епічне if()дерево для підтримки нового типу - просто додайте метод, який підтримує новий клас. Це чудовий підхід, якщо цей подання таблиці є єдиним, який повинен представляти ці об'єкти, або потрібно представити їх особливим чином. Якщо одні і ті ж об’єкти будуть представлені в різних таблицях з різними джерелами даних, цей підхід розпадається, оскільки методам створення комірок потрібен спільний доступ до джерел даних - ви можете визначити загальний надклас, який надає ці методи, або ви могли це зробити:

@interface PhoneNumber (TableViewRepresentation)

- (UITableViewCell *)tableView: (UITableView *)tableView representationAsCellForRowAtIndexPath: (NSIndexPath *)indexPath;

@end

@interface Address (TableViewRepresentation)

//more of the same…

@end

Потім у вашому класі джерела даних:

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
  id object = [self tableView: tableView representedObjectAtIndexPath: indexPath];
  return [object tableView: tableView representationAsCellForRowAtIndexPath: indexPath];
}

Це означає, що будь-яке джерело даних, яке повинно відображати телефонні номери, адреси тощо, може просто запитати будь-який об’єкт, представлений для комірки подання таблиці. Самому джерелу даних більше не потрібно нічого знати про об'єкт, що відображається.

"Але зачекайте, - чую гіпотетичний співрозмовник, - чи це не порушує MVC? Чи не вкладаєте ви деталі перегляду в модельний клас?"

Ні, це не порушує MVC. Ви можете вважати, що категорії в цьому випадку є реалізацією декоратора ; так PhoneNumberсамо є модельним класом, але PhoneNumber(TableViewRepresentation)є категорією перегляду. Джерело даних (об'єкт контролера) опосередковується між моделлю та поданням, тому архітектура MVC все ще зберігається.

Ви також можете бачити таке використання категорій як прикрасу в рамках Apple. NSAttributedString- клас моделей, який містить текст та атрибути. AppKit забезпечує, NSAttributedString(AppKitAdditions)а UIKit надає NSAttributedString(NSStringDrawing)категорії декораторів, які додають поведінку малювання до цих класів моделей.


Яке гарне ім’я для класу, який працює як джерело даних та делегат подання таблиці?
Йохан Карлссон

1
@JohanKarlsson Я часто просто називаю це джерелом даних. Можливо, це трохи неохайно, але я поєдную ці два досить часто, щоб знати, що моє "джерело даних" - це адаптація до більш обмеженого визначення Apple.

1
У цій статті: objc.io/issue-1/table-views.html пропонується спосіб обробки декількох типів комірок, за допомогою якого ви опрацьовуєте клас комірок у cellForPhotoAtIndexPathметоді джерела даних, а потім викликаєте відповідний заводський метод. Що, звичайно, можливо, лише якщо окремі класи передбачувано займають окремі рядки. Ваша система перегляду категорій на моделях набагато більш елегантна, я думаю, хоча це, можливо, неортодоксальний підхід до MVC! :)
Бенджі XVI

1
Я спробував продемонструвати цю модель на github.com/yonglam/TableViewPattern . Сподіваюся, комусь це корисно.
Андрій

1
Я проголосую остаточно " за" динамічний селекторний підхід. Це дуже небезпечно, оскільки проблеми проявляються лише під час виконання. Немає автоматизованого способу переконатись у тому, що даний селектор існує і що він правильно введений, і такий підхід врешті-решт розпадеться і є кошмаром для підтримки. Інший підхід, однак, дуже розумний.
mkko

3

Люди, як правило, багато пакують у UIViewController / UITableViewController.

Делегування в інший клас (не в контролер перегляду), як правило, працює добре. Делегатам не обов'язково потрібна посилання на контролер перегляду, оскільки всі методи делегування передаються посиланням на UITableView, але їм буде потрібен доступ якось до даних, для яких вони делегують.

Кілька ідей щодо реорганізації для зменшення тривалості:

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

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

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


Це приємна відповідь. Однак у мене в голові виникає пара питань. Чому ви вважаєте, що багато висловлювань if (або тверджень-переключачів) є поганим дизайном? Насправді ви маєте на увазі багато вкладених операторів if- і switch? Як ви можете переосмислити фактор, щоб уникнути операцій if- або switch?
Йохан Карлссон

@JohanKarlsson одна методика - через поліморфізм. Якщо вам потрібно зробити одну річ з одним типом об'єкта, а ще щось з іншим типом, зробіть ці об’єкти різними класами і дозвольте їм вибрати роботу для вас.

@GrahamLee Так, я знаю поліморфізм ;-) Однак я не впевнений, як застосувати його в цьому контексті. Будь ласка, докладно поясніть це.
Йохан Карлссон

@JohanKarlsson зроблено;)

2

Ось приблизно, що я зараз роблю, коли стикаюся з подібною проблемою:

  • Перемістіть операції, пов'язані з даними, до класу XXXDataSource (який успадковується від BaseDataSource: NSObject). BaseDataSource надає деякі зручні методи, такі як метод - (NSUInteger)rowsInSection:(NSUInteger)sectionNum;підкласу переосмислює метод завантаження даних (оскільки програми зазвичай мають якийсь метод завантаження кеша офлайну, - (void)loadDataWithUpdateBlock:(LoadProgressBlock)dataLoadBlock completion:(LoadCompletionBlock)completionBlock;так що ми можемо оновлювати інтерфейс користувача кешованими даними, отриманими в LoadProgressBlock, поки ми оновлюємо інформацію з мережі та в блоці завершення ми оновлюємо інтерфейс користувача новими даними та видаляємо індикатори progess, якщо такі є). Ці класи НЕ відповідають UITableViewDataSourceпротоколу.

  • У BaseTableViewController (що відповідає UITableViewDataSourceі UITableViewDelegateпротоколи) у мене є посилання на BaseDataSource, який я створюю під час ініціалізації контролера. У UITableViewDataSourceчастині контролера я просто повертаю значення з dataSource (як - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [self.tableViewDataSource sectionsCount]; }).

Ось мій CellForRow в базовому класі (не потрібно переопределяти підкласи):

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSString *cellIdentifier = [NSString stringWithFormat:@"%@%@", NSStringFromClass([self class]), @"TableViewCell"];
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if (!cell) {
        cell = [self createCellForIndexPath:indexPath withCellIdentifier:cellIdentifier];
    }
    [self configureCell:cell atIndexPath:indexPath];
    return cell;
}

configureCell повинен бути замінений підкласами і createCell повертає UITableViewCell, тому, якщо ви хочете користувацьку клітинку, її також замініть.

  • Після налаштування базових речей (власне, в першому проекті, який використовує таку схему, після чого цю частину можна повторно використовувати), що залишається для BaseTableViewControllerпідкласів:

    • Override configureCell (це зазвичай перетворюється на запит dataSource для об'єкта для шляху до індексу та подає його на configureWithXXX: метод або отримує представлення UITableViewCell об'єкта, як у відповіді user4051)

    • Перевизначення didSelectRowAtIndexPath: (очевидно)

    • Напишіть підклас BaseDataSource, який піклується про роботу з необхідною частиною Моделі (припустимо, є два класи Accountі Language, тому підкласи будуть AccountDataSource та LanguageDataSource).

І це все для частини подання таблиці. Я можу опублікувати якийсь код у GitHub, якщо потрібно.

Редагувати: деякі рекомендації можна знайти на веб- сайті http://www.objc.io/issue-1/lighter-view-controllers.html (який має посилання на це питання) та супутню статтю про контролери настільних оглядів.


2

Моя думка з цього приводу полягає в тому, що моделі потрібно надати масив об’єктів, який називається ViewModel або viewData, інкапсульований у cellConfigurator. CellConfigurator утримує CellInfo, необхідний для його видалення та налаштування. це дає клітині деякі дані, щоб клітина могла налаштувати своє самоврядування. це також працює з розділом, якщо ви додасте якийсь об'єкт SectionConfigurator, який містить у собі CellConfigurators. Я почав використовувати це деякий час назад, спочатку просто надаючи клітині viewData і мав ViewController займатися відключенням комірки. але я прочитав статтю, яка вказувала на це репоту gitHub.

https://github.com/fastred/ConfigurableTableViewController

це може змінити спосіб наближення до цього.


2

Нещодавно я написав статтю про те, як реалізувати делегатів та джерела даних для UITableView: http://gosuwachu.gitlab.io/2014/01/12/uitableview-controller/

Основна ідея полягає в розподілі обов'язків на окремі класи, наприклад, фабрика стільників, фабрика секцій та надання певного інтерфейсу для моделі, яку UITableView буде відображати. Діаграма нижче пояснює все це:

введіть тут опис зображення


Це посилання більше не працює.
koen

1

Дотримання принципів SOLID вирішить будь-які подібні проблеми.

Якщо ви хочете , щоб ваші класи , щоб мати тільки один відповідальність, ви повинні визначити окремі DataSourceі Delegateкласи і просто вводити їх в tableViewвласник (може бути UITableViewControllerабо UIViewControllerабо що - небудь ще). Ось як ви подолаєте розлуку турбот .

Але якщо ви просто хочете мати чистий і читабельний код і хочете позбутися цього масивного файла viewController, і ви перебуваєте в Swif , ви можете використовувати extensionдля цього s. Розширення одного класу можна записати в різні файли, і всі вони мають доступ один до одного. Але це ніт справді вирішує питання SoC, як я вже згадував.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.