Как избежать большого и неуклюжего UITableViewController на iOS?

36

У меня проблема при реализации MVC-паттерна на iOS. Я искал в Интернете, но, похоже, не нашел хорошего решения этой проблемы.

Многие UITableViewControllerреализации кажутся довольно большими. Большинство примеров, которые я видел, позволяют UITableViewControllerреализовать <UITableViewDelegate>и <UITableViewDataSource>. Эти реализации - большая причина, почему UITableViewControllerстановится большим. Одним из решений будет создание отдельных классов, которые реализуют <UITableViewDelegate>и <UITableViewDataSource>. Конечно, эти классы должны иметь ссылку на UITableViewController. Есть ли недостатки в использовании этого решения? В общем, я думаю, что вы должны делегировать функциональность другим классам "Помощник" или подобным, используя шаблон делегата. Есть ли какие-то устоявшиеся способы решения этой проблемы?

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

Как следует разделить контроллер MVC-реализации на более мелкие управляемые части? (Относится к MVC в iOS в этом случае)

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

Йохан Карлссон
источник
1
«Также аргумент, почему это решение является удивительным». :)
occulus
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. В этом случае вы можете думать о категориях как о реализации Decorator ; так 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, но им каким-то образом потребуется доступ к данным, для которых они делегируют.

Несколько идей по реорганизации для сокращения длины:

  • если вы создаете ячейки табличного представления в коде, рассмотрите возможность их загрузки из файла пера или из раскадровки. Раскадровки допускают ячейки прототипа и статической таблицы - посмотрите эти возможности, если вы не знакомы

  • если ваши методы делегата содержат много операторов if (или switch), это классический признак того, что вы можете провести рефакторинг

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

occulus
источник
Это хороший ответ. Однако в моей голове возникает пара вопросов. Почему вы находите много операторов if (или операторов switch) плохим дизайном? Вы действительно имеете в виду много вложенных операторов if и switch? Как вы рефакторинг, чтобы избежать if- или switch-операторов?
Йохан Карлссон
@JohanKarlsson один метод заключается в полиморфизме. Если вам нужно сделать одну вещь с одним типом объекта, а другую - с другим типом, сделайте эти объекты разными классами и позвольте им выбрать работу за вас.
@GrahamLee Да, я знаю полиморфизм ;-) Однако я не уверен, как применять его в этом контексте. Пожалуйста, уточните это.
Йохан Карлссон
@JohanKarlsson сделано;)
2

Вот примерно то, что я сейчас делаю, когда сталкиваюсь с подобной проблемой:

  • Переместите связанные с данными операции в класс XXXDataSource (который наследуется от BaseDataSource: NSObject). BaseDataSource предоставляет несколько удобных методов, таких как - (NSUInteger)rowsInSection:(NSUInteger)sectionNum;, подкласс переопределяет метод загрузки данных (поскольку приложения, как правило, имеют какой-то метод загрузки автономного кэша, выглядит - (void)loadDataWithUpdateBlock:(LoadProgressBlock)dataLoadBlock completion:(LoadCompletionBlock)completionBlock;так, что мы можем обновлять пользовательский интерфейс кэшированными данными, полученными в LoadProgressBlock, пока мы обновляем информацию из сети и в блоке завершения мы обновляем пользовательский интерфейс новыми данными и удаляем индикаторы прогресса, если таковые имеются). Эти классы НЕ соответствуют 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подклассов:

    • Переопределить configureCell (это обычно трансформируется в запрос dataSource для объекта для пути индекса и подачу его в configureWithXXX: метод ячейки или получение представления UITableViewCell объекта, как в ответе user4051)

    • Переопределить didSelectRowAtIndexPath: (очевидно)

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

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

Изменить: некоторые рекомендации можно найти по адресу http://www.objc.io/issue-1/lighter-view-controllers.html (который имеет ссылку на этот вопрос) и сопутствующей статье о tableviewcontrollers.

Тимур Кучкаров
источник
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. Диаграмма ниже объясняет все это:

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

Петр Вах
источник
Эта ссылка больше не работает.
Коен
1

Следование принципам SOLID решит любые подобные проблемы.

Если вы хотите , чтобы ваши классы , чтобы иметь только один ответственность, вы должны определить отдельные DataSourceи Delegateклассы и просто вводить их в tableViewвладелец (может быть UITableViewControllerили UIViewControllerили что - нибудь еще). Вот как вы преодолеваете разделение интересов .

Но если вы просто хотите иметь чистый и читаемый код и хотите избавиться от этого массивного файла viewController, и вы находитесь в Swif , вы можете использовать extensions для этого. Расширения одного класса могут быть записаны в разных файлах, и все они имеют доступ друг к другу. Но это не совсем решает проблему SoC, как я уже упоминал.

Мойтаба Хоссейни
источник