4.0. Введение
Табличный вид — это обычный вид с прокручиваемым контентом, который разделен на секции. Каждая такая секция, в свою очередь, подразделяется на строки. Каждая строка (Row) является экземпляром класса UITableViewCell. Вы можете создавать собственные варианты строк в табличном виде, делая подклассы этого класса.
Табличный вид — это сущность, которая идеально подходит для представления пользователю списка элементов. В ячейки табличных видов можно встраивать изображения, текст и другие объекты. Можно самостоятельно настраивать высоту, контуры, группирование ячеек и многие другие параметры. Благодаря структурной простоте табличные виды отлично подходят для адаптации под конкретные задачи.
Табличный вид можно наполнить данными, используя источник данных табличного вида. Вы можете получать различные события и управлять оформлением табличных видов с помощью объекта-делегата табличного вида. Источник данных для табличного вида определяется в протоколе UITableViewDataSource, а делегат табличного вида — в протоколе UITableViewDelegate.
Хотя экземпляр UITableView является подклассом от UIScrollView, табличные виды можно прокручивать только по вертикали. Это скорее благо, чем ограничение. В данной главе мы обсудим различные способы создания табличных видов, их настройки и управления ими.
Табличные виды можно использовать двумя способами:
• с помощью класса UITableViewController. Этот класс напоминает UIViewController (см. раздел 1.9) в том, что фактически это контроллер вида, но в нем отображается не обычный вид, а таблица. Красота этого класса заключается в том, что каждый его экземпляр уже соответствует протоколам UITableViewDelegate и UITableViewDataSource. Итак, по умолчанию контроллер табличного вида становится источником данных и одновременно делегатом того табличного вида, которым он управляет. Таким образом, чтобы реализовать, например, источник данных для табличного вида, вам всего лишь потребуется реализовать контроллер для табличного вида, а не устанавливать вручную контроллер вида в качестве источника данных для табличного вида;
• вручную инстанцировав класс UITableView.
Оба этих метода вполне допустимы для создания табличных видов. Первый метод обычно используется для создания табличного вида, который целиком заполняет свой контейнер (либо окно/экран, если данный контроллер вида является корневым контроллером вида основного окна приложения). Второй метод более удобен в ситуациях, когда вы собираетесь отобразить табличный вид в качестве небольшого компонента пользовательского интерфейса, чтобы таблица, скажем, наполовину занимала экран по ширине и/или высоте. Но вы с тем же успехом можете использовать второй метод для установки высоты и ширины табличного вида в значения высоты и ширины его объемлющего окна так, чтобы табличный вид занимал целый экран. В этой главе мы исследуем оба описанных метода.
Рассмотрим пример создания табличного вида в приложении. Примеры контроллеров табличных видов будут подробно изучены в разделе 4.9, а пока мы просто займемся созданием таких видов в коде и будем добавлять их к имеющемуся контроллеру вида.
Класс UITableView инстанцируется с помощью метода initWithFrame: style:. Далее перечислены параметры, которые мы должны передать этому методу, а также значения этих параметров.
• initWithFrame — это параметр типа CGRect. Он указывает, как именно должен быть расположен табличный вид в вышестоящем виде. Если вы хотите, чтобы таблица просто полностью накрывала вышестоящий вид, передайте этому параметру значение свойства bounds вида с контроллером.
• style — это параметр типа UITableViewStyle, определяемый следующим образом:
typedef NS_ENUM(NSInteger, UITableViewStyle) {
UITableViewStylePlain,
UITableViewStyleGrouped
};
На рис. 4.1 показана разница между обычным и сгруппированным табличными видами.
Рис. 4.1. Табличные виды различных типов
Мы заполняем табличный вид информацией, используя его источник данных, как будет показано в разделе 4.1. Табличные виды также обладают делегатами. Делегаты получают различные события от табличного вида. Объекты делегатов должны соответствовать протоколу UITableViewDelegate. Далее перечислены отдельные методы этого протокола, которые необходимо знать.
• tableView: viewForHeaderInSection: — вызывается в делегате, когда табличному виду требуется отобразить заголовочный вид раздела. Каждый раздел табличного вида может содержать верхний колонтитул, некоторое количество ячеек и нижний колонтитул. В этой главе мы подробно обсудим все эти участки таблицы. Верхний и нижний колонтитул — это обычные экземпляры UIView. Данный метод является необязательным, но если вы хотите сконфигурировать заголовок для разделов вашего табличного вида, то пользуйтесь этим методом, чтобы создать экземпляр вида и передать его обратно в качестве возвращаемого значения. О верхних и нижних колонтитулах табличных видов подробнее рассказано в разделе 4.5.
• tableView: viewForFooterInSection: — делегатный метод, аналогичный tableView: viewForHeaderInSection:, но он возвращает вид с нижним колонтитулом таблицы. Как и заголовок, нижний колонтитул таблицы не является обязательным, но если он вам нужен, то его следует создавать здесь. Подробнее о верхних и нижних колонтитулах табличных видов рассказано в разделе 4.5.
• tableView: didEndDisplayingCell: forRowAtIndexPath: — вызывается в объекте-делегате, когда в ходе прокрутки таблицы ячейка уходит с экрана. Этот метод действительно очень удобен для вызова в делегате, так как вы можете удалять объекты и выбрасывать их из памяти, если эти объекты ассоциированы с ячейкой, которая ушла с экрана, а вы полагаете, что связанные с ней объекты вам больше не понадобятся.
• tableView: willDisplayCell: forRowAtIndexPath: — этот метод вызывается в делегате табличного вида всякий раз, когда ячейка вот-вот отобразится на экране.
Чтобы задать делегат для табличного вида, просто укажите в качестве значения свойства delegate экземпляра UITableView такой объект, который соответствует протоколу UITableViewDelegate. Если табличный вид является частью контроллера вида, то можно просто сделать этот контроллер делегатом вашего табличного вида, вот так:
#import «ViewController.h»
@interface ViewController ()
@property (nonatomic, strong) UITableView *myTableView;
@end
@implementation ViewController
— (void)viewDidLoad{
[super viewDidLoad];
self.myTableView = [[UITableView alloc]
initWithFrame: self.view.bounds
style: UITableViewStylePlain];
self.myTableView.delegate = self;
[self.view addSubview: self.myTableView];
}
@end
Можно считать делегат табличного вида объектом, который слушает различные события, отправляемые табличным видом. Например, такие события происходят, когда пользователь выделяет одну из ячеек в таблице либо табличному виду требуется узнать высоту всех входящих в него ячеек.
Объект-делегат обязан отвечать на сообщения, помеченные протоколом UITableViewDelegate как @required. Отвечать на другие сообщения не обязательно, но делегат должен отвечать на все сообщения, которые, по вашему замыслу, будут изменять табличный вид.
Сообщения, отправляемые объекту-делегату табличного вида, несут с собой параметр, который сообщает делегату, какой именно табличный вид инициировал данное событие в своем делегате. Это очень важно отметить, так как в определенных обстоятельствах вы можете разместить в одном объекте (как правило, в виде) более одной таблицы. Поэтому настоятельно рекомендую принимать соответствующие решения с учетом того, какой именно табличный вид послал конкретное сообщение объекту-делегату:
— (CGFloat) tableView:(UITableView *)tableView
heightForRowAtIndexPath:(NSIndexPath *)indexPath{
if ([tableView isEqual: self.myTableView]){
return 100.0f;
}
return 40.0f;
}
}
Расположение ячейки в табличном виде представляется индексным путем этой ячейки. Индексный путь — это комбинация данных о разделе и индекса строки. В данном случае индекс раздела имеет нулевую базу и указывает, к каким группе или разделу относится каждая ячейка. Индекс ячейки также имеет нулевую базу и означает положение данной ячейки в ее разделе.
4.1. Наполнение табличного вида данными
Постановка задачи
Требуется наполнить табличный вид данными.
Решение
Необходимо создать объект, соответствующий протоколу UITableViewDataSource, и присвоить этот объект экземпляру табличного вида. Затем, отвечая на сообщения источника данных, предоставьте информацию для вашего вида. Продолжим данный пример и объявим. h-файл контроллера нашего вида. Позже в коде для этого вида будет создана таблица:
#import «ViewController.h»
static NSString *TableViewCellIdentifier = @"MyCells";
@interface ViewController ()
@property (nonatomic, strong) UITableView *myTableView;
@end
Экземпляр TableViewCellIdentifier содержит идентификаторы ячеек в виде статической строковой переменной. Как вы вскоре узнаете, каждая ячейка может иметь идентификатор, и это очень помогает при повторном использовании ячеек. Пока считайте эту переменную просто уникальным идентификатором всех ячеек табличного вида — на данном этапе этого достаточно.
В методе viewDidLoad контроллера вида создадим табличный вид и присвоим ему контроллер вида в качестве источника данных:
— (void)viewDidLoad{
[super viewDidLoad];
self.myTableView =
[[UITableView alloc] initWithFrame: self.view.bounds
style: UITableViewStylePlain];
[self.myTableView registerClass: [UITableViewCell class]
forCellReuseIdentifier: TableViewCellIdentifier];
self.myTableView.dataSource = self;
/* Убеждаемся, что табличный вид правильно масштабируется. */
self.myTableView.autoresizingMask =
UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight;
[self.view addSubview: self.myTableView];
}
В этом фрагменте кода все элементарно, кроме метода registerClass: forCellReuseIdentifier:, который мы вызываем в экземпляре табличного вида. Что же делает этот метод? Параметр registerClass этого метода просто принимает имя класса, соответствующее типу объекта, который вы хотите загружать в табличном виде при отображении каждой ячейки. Все ячейки внутри табличного вида должны быть прямыми или непрямыми потомками класса UITableViewCell. Сам этот класс предоставляет программистам довольно широкий функционал. Но при желании этот класс можно и расширить — достаточно произвести от него подкласс, добавив к новому классу требуемый функционал. Итак, возвращаемся к параметру registerClass вышеупомянутого метода. Вам потребуется сообщить имя класса ячеек этому параметру, а потом передать идентификатор параметру forCellReuseIdentifier. Вот по какой причине мы ассоциируем классы табличного вида с идентификаторами: когда позже вы заполняете табличный вид данными, можете просто передать тот же самый идентификатор методу dequeueReusableCellWithIdentifier: forIndexPath: табличного вида, после чего приказать табличному виду инстанцировать ячейку таблицы, если в наличии нет ячеек, доступных для повторного использования. Все это просто отлично, так как в предыдущих версиях iOS SDK программистам приходилось инстанцировать эти ячейки самостоятельно, если из табличного вида не удавалось добыть уже готовый код, пригодный для повторного использования.
Теперь необходимо убедиться в том, что наш табличный вид реагирует на методы протокола UITableViewDataSource, помеченные как @required (обязательные). Нажмите на клавиатуре комбинацию клавиш Command+Shift+O, введите в диалоговое окно имя этого протокола, затем нажмите клавишу Enter. В результате вы увидите обязательные методы данного протокола.
Класс UITableView определяет свойство под названием dataSource. Это нетипизированный объект, который должен подчиняться протоколу UITableViewDataSource. Всякий раз, когда табличный вид обновляется и перезагружается с помощью метода reloadData, табличный вид будет вызывать в своем источнике данных различные методы, чтобы получить информацию о тех данных, которыми вы хотите заполнить таблицу. Источник данных табличного вида может реализовывать три важных метода, два из которых являются обязательными для любого источника данных:
• numberOfSectionsInTableView: — позволяет источнику данных информировать табличный вид о количестве разделов, которые должны быть загружены в таблицу;
• tableView: numberOfRowsInSection: — сообщает контроллеру вида, сколько ячеек или строк следует загрузить в каждый раздел. Номер раздела передается источнику данных в параметре numberOfRowsInSection. Реализация этого метода является обязательной для объекта источника данных;
• tableView: cellForRowAtIndexPath: — отвечает за возвращение экземпляров класса UITableViewCell как строк таблицы, которыми должен заполняться табличный вид. Реализация этого метода обязательна для объекта источника данных.
Итак, продолжим и реализуем эти методы в контроллере вида один за другим. Сначала сообщим табличному виду, что мы хотим отобразить три раздела:
— (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
if ([tableView isEqual: self.myTableView]){
return 3;
}
return 0;
}
Далее сообщим табличному виду, сколько строк хотим в нем отобразить для каждого раздела:
— (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section{
if ([tableView isEqual: self.myTableView]){
switch (section){
case 0:{
return 3;
break;
}
case 1:{
return 5;
break;
}
case 2:{
return 8;
break;
}
}
}
return 0;
}
Итак, на данный момент мы приказали табличному виду отобразить три раздела. В первом разделе три строки, во втором — пять, в третьем — восемь. Что дальше? Нужно вернуть табличному виду экземпляры UITableViewCell — тех ячеек, которые мы хотим отобразить в таблице:
— (UITableViewCell *) tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *result = nil;
if ([tableView isEqual: self.myTableView]){
cell = [tableView
dequeueReusableCellWithIdentifier: TableViewCellIdentifier
forIndexPath: indexPath];
cell.textLabel.text = [NSString stringWithFormat:
@"Section %ld, Cell %ld",
(long)indexPath.section,
(long)indexPath.row];
}
return cell;
}
Теперь, если запустить приложение в эмуляторе iPhone, мы увидим результат работы (рис. 4.2).
Рис. 4.2. Обычный табличный вид с тремя разделами
Когда табличный вид перезагружается или обновляется, он запрашивает источник данных через протокол UITableViewDataSource, требуя у источника данных различную информацию. В первую очередь он запросит количество разделов. Каждый раздел должен содержать строки или ячейки.
После того как источник данных укажет количество разделов, табличный вид запросит количество строк, которые должны быть загружены в каждый из разделов. Источник данных получает индекс с нулевой базой для каждого раздела и на базе этого индекса решает, сколько ячеек загрузить в каждый раздел.
Табличный вид, определив количество ячеек в разделах, продолжит запрашивать источник данных о видах — один такой вид соответствует каждой ячейке того или иного раздела. Вы можете выделять экземпляры класса UITableViewCell и возвращать их табличному виду. Разумеется, есть свойства, которые можно задать для каждой ячейки. Это, в частности, заголовок, подзаголовок и цвет ячейки.
4.2. Использование дополнительных элементов в ячейке табличного вида
Постановка задачи
Требуется привлечь внимание пользователя, отображая в таблице дополнительные элементы, и предложить альтернативные способы взаимодействия с каждой ячейкой в табличном виде.
Решение
Используйте свойство accessoryType класса UITableViewCell. Экземпляры этого класса вы предоставляете табличному виду в объекте его источника данных:
— (UITableViewCell *) tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell* result = nil;
if ([tableView isEqual: self.myTableView]){
result = [tableView
dequeueReusableCellWithIdentifier: MyCellIdentifier
forIndexPath: indexPath];
result.textLabel.text =
[NSString stringWithFormat:@"Section %ld, Cell %ld",
(long)indexPath.section,
(long)indexPath.row];
result.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;
}
return result;
}
— (NSInteger) tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section{
return 10;
}
— (void)viewDidLoad{
[super viewDidLoad];
self.myTableView = [[UITableView alloc]
initWithFrame: self.view.bounds
style: UITableViewStylePlain];
[self.myTableView registerClass: [UITableViewCell class]
forCellReuseIdentifier: MyCellIdentifier];
self.myTableView.dataSource = self;
self.myTableView.autoresizingMask =
UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight;
[self.view addSubview: self.myTableView];
}
Обсуждение
Можно присваивать любые значения, определенные в перечне UITableViewCellAccessoryType, свойству accessoryType экземпляра класса UITableViewCell. Среди полезных дополнительных элементов следует особо отметить индикатор подробного описания и кнопку детализации. Оба этих элемента содержат угловую скобку, подсказывающую пользователю, что если прикоснуться пальцем к соответствующей ячейке таблицы, то откроется новый вид или контроллер вида. Проще говоря, пользователь перейдет на новый экран с более подробной информацией об актуальном селекторе. Разница между двумя этими элементами заключается с том, что индикатор подробного описания не инициирует никакого события, а вот кнопка детализации при нажатии запускает событие, направляемое к делегату. Иными словами, эффект от нажатия кнопки не равен эффекту от нажатия самой ячейки. Следовательно, кнопка детализации позволяет пользователю осуществлять два разных, но связанных действия применительно к одной и той же строке.
На рис. 4.3 показаны два этих дополнительных элемента в табличном виде. В первой строке мы видим индикатор подробного описания, а во второй — кнопку детализации.
Рис. 4.3. Две ячейки табличного вида с различными дополнительными элементами
Если прикоснуться к любой кнопке детализации, присвоенной ячейке табличного вида, то сразу становится очевидно, что это, в сущности, самостоятельная кнопка. А теперь внимание — вопрос! Как табличный вид узнает, что пользователь нажал такую кнопку?
Как объяснялось ранее, табличный вид инициирует события, направляемые его объекту-делегату. Кнопка детализации из табличного вида также запускает событие, которое может быть принято объектом-делегатом табличного вида:
— (void) tableView:(UITableView *)tableView
accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath{
/* Делаем что-либо при нажатии дополнительной кнопки. */
NSLog(@"Accessory button is tapped for cell at index path = %@",
indexPath);
UITableViewCell *ownerCell = [tableView cellForRowAtIndexPath: indexPath];
NSLog(@"Cell Title = %@", ownerCell.textLabel.text);
}
Данный код ищет ячейку табличного вида, в которой была нажата кнопка детализации, и выводит в окне консоли содержимое текстовой метки данной ячейки. Напоминаю: чтобы отобразить окно консоли в Xcode, нужно выполнить команду Run\Console (Запуск\Консоль).
4.3. Создание специальных дополнительных элементов в ячейке табличного вида
Постановка задачи
Дополнительных элементов, предоставляемых в iOS, недостаточно для решения задачи, и вы хотели бы создать собственные дополнительные элементы.
Решение
Присвойте экземпляр класса UIView свойству accessoryView любого экземпляра класса UITableViewCell:
— (UITableViewCell *) tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell* cell = nil;
cell = [tableView dequeueReusableCellWithIdentifier: MyCellIdentifier
forIndexPath: indexPath];
cell.textLabel.text = [NSString stringWithFormat:@"Section %ld, Cell %ld",
(long)indexPath.section,
(long)indexPath.row];
UIButton *button = [UIButton buttonWithType: UIButtonTypeSystem];
button.frame = CGRectMake(0.0f, 0.0f, 150.0f, 25.0f);
[button setTitle:@"Expand"
forState: UIControlStateNormal];
[button addTarget: self
action:@selector(performExpand:)
forControlEvents: UIControlEventTouchUpInside];
cell.accessoryView = button;
return cell;
}
Как видите, в этом коде используется метод performExpand:. Он играет роль селектора для каждой кнопки. Вот определение данного метода:
— (void) performExpand:(id)paramSender{
/* Обрабатываем событие нажатия кнопки */
}
В данном примере кода специальная создаваемая нами кнопка присваивается дополнительному виду в каждой строке выбранной таблицы. Результат показан на рис. 4.4.
Рис. 4.4. Ячейки табличного вида со специальными дополнительными видами
Обсуждение
Объект типа UITableViewCell содержит свойство accessoryView. Это тот вид, которому вы можете присвоить значение, если вас не вполне устраивают встроенные в SDK iOS дополнительные виды для табличных ячеек. После того как задано это свойство, Cocoa Touch будет игнорировать значение свойства accessoryType и станет использовать вид, присвоенный свойству accessoryView, в качестве дополнительного элемента, который отображается в ячейке таблицы.
В коде, приведенном в подразделе «Решение» данного раздела, мы создаем кнопки для всех ячеек, находящихся в табличном виде. При нажатии кнопки в любой ячейке вызывается метод performExpand:. И если вы думаете примерно так же, как я, то вы уже стали задаваться вопросом: как же определить, к какой именно ячейке относится кнопка-отправитель? Итак, теперь нам нужно как-то связать кнопки с теми ячейками, к которым они относятся.
Один из способов разрешения этой ситуации связан с использованием свойства tag экземпляра кнопки. Это свойство-метка представляет собой обычное целое число, которое, как правило, используется для ассоциирования вида с другим объектом. Например, если вы хотите ассоциировать кнопку с третьей ячейкой в вашем табличном виде, то следует задать для свойства-метки этой кнопки значение 3. Но здесь возникает проблема: в табличных видах есть разделы, и каждый раздел может содержать n ячеек. Следовательно, нам требуется возможность определить и раздел таблицы, и ячейку, которая владеет нашей кнопкой. А поскольку значением свойства-метки может быть только одно целое число, эта задача существенно усложняется. Поэтому мы можем отказаться от метки и вместо работы с ней запрашивать вышестоящий вид о дополнительном виде, рекурсивно проходя вверх по цепочке видов, пока не найдем ячейку типа UITableViewCell, вот так:
— (UIView *) superviewOfType:(Class)paramSuperviewClass
forView:(UIView *)paramView{
if (paramView.superview!= nil){
if ([paramView.superview isKindOfClass: paramSuperviewClass]){
return paramView.superview;
} else {
return [self superviewOfType: paramSuperviewClass
forView: paramView.superview];
}
}
return nil;
}
— (void) performExpand:(UIButton *)paramSender{
/* Обрабатываем событие нажатия кнопки */
__unused UITableViewCell *parentCell =
(UITableViewCell *)[self superviewOfType: [UITableViewCell class]
forView: paramSender];
/* Теперь, если желаете, можете еще что-нибудь сделать с ячейкой */
}
Здесь мы используем простой рекурсивный метод, принимающий вид (в данном случае нашу кнопку) и имя класса (в данном случае UITableViewCell), а затем просматриваем иерархию вида, являющегося вышестоящим для данного, чтобы найти вышестоящий вид, относящийся к интересующему нас классу. Итак, он начинает работу с вида, являющегося вышестоящим для заданного, и если этот вышестоящий вид не относится к требуемому типу, то просматривает и его вышестоящий вид, и так до тех пор, пока не найдет один из вышестоящих видов, относящийся к требуемому классу. Как видите, в качестве первого параметра метода superviewOfType: forView: мы используем структуру Class. В этом типе данных может содержаться имя любого класса из языка Objective-C, и это весьма кстати, если вы ищете или запрашиваете у программиста конкретные имена классов.
4.4. Обеспечение удаления смахиванием в ячейках табличных видов
Постановка задачи
Необходимо предоставить пользователям приложения возможность без труда удалять строки из табличного вида.
Решение
Реализуйте в делегате табличного вида селектор tableView: editingStyleForRowAtIndexPath:, а в источнике данных табличного вида — селектор tableView: commitEditingStyle: forRowAtIndexPath::
— (UITableViewCellEditingStyle)tableView:(UITableView *)tableView
editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath{
return UITableViewCellEditingStyleDelete;
}
— (void) setEditing:(BOOL)editing
animated:(BOOL)animated{
[super setEditing: editing
animated: animated];
[self.myTableView setEditing: editing
animated: animated];
}
— (void) tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath{
if (editingStyle == UITableViewCellEditingStyleDelete){
/* Сначала удаляем этот объект из источника данных */
[self.allRows removeObjectAtIndex: indexPath.row];
/* Потом удаляем ассоциированную с ним ячейку из табличного вида */
[tableView deleteRowsAtIndexPaths:@[indexPath]
withRowAnimation: UITableViewRowAnimationLeft];
}
}
Метод tableView: editingStyleForRowAtIndexPath: позволяет выполнять операции удаления. Он вызывается табличным видом, а его возвращаемое значение определяет, какие операции пользователь может делать в табличном виде (вставлять информацию, удалять информацию и т. д.). Метод tableView: commitEditingStyle: forRowAtIndexPath: выполняет затребованную пользователем операцию удаления. Второй из указанных методов определяется в делегате, но его функционал несколько перегружен: этот метод применяется не только для удаления данных, но и для удаления строк из таблицы.
Обсуждение
Табличный вид реагирует на жест смахивания (Swipe), отображая кнопку в правой части затронутой строки (рис. 4.5). Как видите, табличный вид не находится в режиме редактирования, но эта кнопка позволяет пользователю удалить строку.
Рис. 4.5. Кнопка для удаления, появляющаяся в ячейке табличного вида
Такой режим активизируется путем реализации метода tableView: editingStyleForRowAtIndexPath: (определяемого в протоколе UITableViewDelegate), чье возвращаемое значение указывает, будут ли в таблице разрешаться операции вставки, или удаления, или обе эти операции, или ни одна из них. Реализуя метод tableView: commitEditingStyle: forRowAtIndexPath: в источнике данных табличного вида, можно также получать уведомление о том, какую операцию выполнил пользователь, вставку или удаление.
Второй параметр метода deleteRowsAtIndexPaths: withRowAnimation: позволяет указывать метод анимации, который будет выполняться при удалении строк из табличного вида. В примере мы задали, что удаляемые строки будут уходить с экрана в направлении справа налево.
4.5. Создание верхних и нижних колонтитулов в табличных видах
Постановка задачи
Необходимо создать в таблице верхний и/или нижний колонтитул.
Решение
Создайте вид (это может быть подпись, вид с изображением или любой другой класс, прямо или опосредованно производимый от UIView) и присвойте этот вид верхнему и/или нижнему колонтитулу табличного раздела. Кроме того, как вы вскоре увидите, для верхнего или нижнего колонтитулов можно выделять конкретное количество точек.
Обсуждение
Табличный вид может иметь несколько верхних и нижних колонтитулов. У каждого раздела табличного вида может быть свой верхний и нижний колонтитул, так что если у вас в табличном виде три раздела, то в нем может быть максимум три верхних и три нижних колонтитула. Вы не обязаны создавать верхние и нижние колонтитулы в каком-либо из разделов и сами решаете, сообщать или нет табличному виду, что в определенном его разделе будут верхний и нижний колонтитулы. Эти виды-колонтитулы передаются табличному виду через его делегат — если вы решите их сделать. Верхние и нижние колонтитулы становятся частью табличного вида. Это означает, что, когда содержимое таблицы прокручивается, одновременно с ним прокручиваются и колонтитулы табличных разделов. Рассмотрим примеры верхнего и нижнего колонтитулов в табличном виде (рис. 4.6).
Рис. 4.6. Нижний колонтитул в верхнем разделе и верхний колонтитул Shortcuts (Быстрый доступ) в последнем разделе табличного вида
Как видите, в верхнем разделе (там, где находятся элементы Check Spelling (Проверка правописания) и Enable Caps Lock (Зафиксировать верхний регистр)) в нижнем колонтитуле написано: Double tapping the space bar will insert a period followed by a space (Двойное нажатие клавиши пробела вставляет точку, за которой следует пробел). Это нижний колонтитул верхнего раздела рассматриваемого вида. Причина, по которой этот фрагмент находится именно в нижнем, а не в верхнем колонтитуле, в том, что он прикреплен к нижней, а не к верхней части раздела. В последнем разделе данной таблицы также есть верхний колонтитул, на котором написано Shortcuts (Быстрый доступ). Здесь, наоборот, колонтитул является верхним, а не нижним, так как он прикреплен к верхней части раздела.
Для указания высоты верхнего и нижнего колонтитулов в разделе табличного вида применяются методы, определяемые в протоколе UITableViewDataSource. Чтобы задать сам вид, который будет соответствовать верхнему/нижнему колонтитулу в разделе табличного вида, нужно использовать методы, определяемые в протоколе UITableViewDelegate.
Идем дальше. Создадим простое приложение, внутри которого будет табличный вид. Потом сделаем две метки типа UILabel, одна будет играть роль верхнего колонтитула, а другая — нижнего в единственном разделе нашего табличного вида. Этот раздел будет заполнен всего тремя ячейками. В верхнем колонтитуле мы напишем Section 1 Header (Верхний колонтитул раздела 1), а в нижнем — Section 1 Footer (Нижний колонтитул раздела 1). Начнем с файла реализации контроллера вида, где определим табличный вид:
#import «ViewController.h»
static NSString *CellIdentifier = @"CellIdentifier";
@interface ViewController ()
@property (nonatomic, strong) UITableView *myTableView;
@end
@implementation ViewController
После этого создадим сгруппированный табличный вид и загрузим в него три ячейки:
— (UITableViewCell *) tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = nil;
cell = [tableView dequeueReusableCellWithIdentifier: CellIdentifier
forIndexPath: indexPath];
cell.textLabel.text = [[NSString alloc] initWithFormat:@"Cell %ld",
(long)indexPath.row];
return cell;
}
— (NSInteger) tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section{
return 3;
}
— (void)viewDidLoad{
[super viewDidLoad];
self.myTableView =
[[UITableView alloc] initWithFrame: self.view.bounds
style: UITableViewStyleGrouped];
[self.myTableView registerClass: [UITableViewCell class]
forCellReuseIdentifier: CellIdentifier];
self.myTableView.dataSource = self;
self.myTableView.delegate = self;
self.myTableView.autoresizingMask = UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight;
[self.view addSubview: self.myTableView];
}
И тут начинается самое интересное. Мы можем воспользоваться двумя важными методами (определяемыми в протоколе UITableViewDelegate), чтобы сделать метку и для верхнего и для нижнего колонтитула того раздела, который мы загрузили в табличный вид. Вот эти методы:
• tableView: viewForHeaderInSection: — ожидает возвращаемого значения типа UIView. Вид, возвращаемый этим методом, отобразится как верхний колонтитул раздела и будет указан в параметре viewForHeaderInSection;
• tableView: viewForFooterInSection: — ожидает возвращаемого значения типа UIView. Вид, возвращаемый этим методом, отобразится как нижний колонтитул раздела и будет указан в параметре viewForFooterInSection.
Теперь наша задача заключается в том, чтобы реализовать эти методы и вернуть экземпляр UILabel. На метке верхнего колонтитула мы укажем текст Section 1 Header (Верхний колонтитул раздела 1), а на метке нижнего — Section 1 Footer (Нижний колонтитул раздела 1), как и планировали:
— (UILabel *) newLabelWithTitle:(NSString *)paramTitle{
UILabel *label = [[UILabel alloc] initWithFrame: CGRectZero];
label.text = paramTitle;
label.backgroundColor = [UIColor clearColor];
[label sizeToFit];
return label;
}
— (UIView *) tableView:(UITableView *)tableView
viewForHeaderInSection:(NSInteger)section{
if (section == 0){
return [self newLabelWithTitle:@"Section 1 Header"];
}
return nil;
}
— (UIView *) tableView:(UITableView *)tableView
viewForFooterInSection:(NSInteger)section{
if (section == 0){
return [self newLabelWithTitle:@"Section 1 Footer"];
}
return nil;
}
Если теперь запустить приложение в эмуляторе, получится такая картинка, как на рис. 4.7.
Рис. 4.7. Метки для верхнего и нижнего колонтитулов табличного вида, выровненные неправильно
Причина такого неправильного выравнивания в том, что родительский вид не знает высоты видов-меток. Для указания высоты видов верхнего и нижнего колонтитулов следует использовать два следующих метода, определяемых в протоколе UITableViewDelegate:
• tableView: heightForHeaderInSection: — возвращаемое значение данного метода относится к типу CGFloat. Оно указывает высоту верхнего колонтитула раздела табличного вида. Индекс раздела передается в параметре heightForHeaderInSection;
• tableView: heightForFooterInSection: — возвращаемое значение данного метода относится к типу CGFloat. Оно указывает высоту нижнего колонтитула раздела табличного вида. Индекс раздела передается в параметре heightForHeaderInSection.
— (CGFloat) tableView:(UITableView *)tableView
heightForHeaderInSection:(NSInteger)section{
if (section == 0){
return 30.0f;
}
return 0.0f;
}
— (CGFloat) tableView:(UITableView *)tableView
heightForFooterInSection:(NSInteger)section{
if (section == 0){
return 30.0f;
}
return 0.0f;
}
Запустив это приложение, вы увидите, что теперь метки верхнего и нижнего колонтитулов имеют фиксированную высоту. Но в написанном нами коде все еще остается какая-то ошибка — дело в левом поле меток верхнего и нижнего колонтитулов. В этом можно убедиться, взглянув на рис. 4.8.
Рис. 4.8. Левые поля меток в верхнем и нижнем колонтитулах — неправильные
Причина заключается в том, что по умолчанию табличный вид размещает верхний и нижний колонтитулы в точке с координатой 0.0f по оси Х. Можно подумать, что эта проблема решается изменением контуров меток верхнего и нижнего колонтитулов, но, к сожалению, это мнение ошибочно. Проблема решается созданием универсального вида UIView, где и размещаются метки для верхнего и нижнего колонтитулов. Возвратите в качестве верхнего/нижнего колонтитула такой универсальный вид, но измените положение меток по оси Х в этом виде.
Теперь изменим реализацию методов tableView: viewForHeaderInSection: и tableView: viewForFooterInSection::
— (UIView *) tableView:(UITableView *)tableView
viewForHeaderInSection:(NSInteger)section{
UIView *header = nil;
if (section == 0){
UILabel *label = [self newLabelWithTitle:@"Section 1 Header"];
/* Перемещаем метку на 10 точек вправо. */
label.frame = CGRectMake(label.frame.origin.x + 10.0f,
5.0f, /* Опускаемся на 5 точек вниз
по оси y. */
label.frame.size.width,
label.frame.size.height);
/* Делаем ширину содержащего вида на 10 точек больше,
чем ширина метки, так как для метки требуется
10 дополнительных точек ширины в левом поле. */
CGRect resultFrame = CGRectMake(0.0f,
0.0f,
label.frame.size.width + 10.0f,
label.frame.size.height);
header = [[UIView alloc] initWithFrame: resultFrame];
[header addSubview: label];
}
return header;
}
— (UIView *) tableView:(UITableView *)tableView
viewForFooterInSection:(NSInteger)section{
UIView *footer = nil;
if (section == 0){
UILabel *label = [[UILabel alloc] initWithFrame: CGRectZero];
/* Перемещаем метку на 10 точек вправо. */
label.frame = CGRectMake(label.frame.origin.x + 10.0f,
5.0f, /* Опускаемся на 5 точек вниз по оси y*/
label.frame.size.width,
label.frame.size.height);
/* Делаем ширину содержащего вида на 10 точек больше,
чем ширина метки, так как для метки требуется
10 дополнительных точек ширины в левом поле. */
CGRect resultFrame = CGRectMake(0.0f,
0.0f,
label.frame.size.width + 10.0f,
label.frame.size.height);
footer = [[UIView alloc] initWithFrame: resultFrame];
[footer addSubview: label];
}
return footer;
}
Теперь, запустив приложение, вы получите примерно такой результат, как на рис. 4.9.
Рис. 4.9. В табличном виде отображаются метки верхнего и нижнего колонтитулов
Пользуясь изученными здесь методами, вы также можете размещать изображения в верхнем и нижнем колонтитулах табличных видов. Экземпляры класса UIImageView являются производными от класса UIView, поэтому вы легко можете ставить картинки в виды для изображений и возвращать их как верхние/нижние колонтитулы табличного вида. Если вы не собираетесь помещать в верхних и нижних колонтитулах табличных видов ничего, кроме текста, то можете пользоваться двумя удобными методами, определяемыми в протоколе UITableViewDataSource. Эти методы избавят вас от массы проблем. Чтобы не создавать собственные метки и не возвращать их как верхние/нижние колонтитулы табличного вида, просто пользуйтесь следующими методами:
• tableView: titleForHeaderInSection: — возвращаемое значение этого метода относится к типу NSString. Табличный вид будет автоматически помещать в метке строку, которая будет отображаться как верхний колонтитул раздела, указываемый в параметре titleForHeaderInSection;
• tableView: titleForFooterInSection: — возвращаемое значение этого метода относится к типу NSString. Табличный вид будет автоматически помещать в метке строку, которая будет отображаться как нижний колонтитул раздела, указываемый в параметре titleForFooterInSection.
Итак, чтобы упростить код приложения, избавимся от реализаций методов tableView: viewForHeaderInSection: и tableView: viewForFooterInSection:, заменив их реализациями методов tableView: titleForHeaderInSection: и tableView: titleForFooterInSection::
— (NSString *) tableView:(UITableView *)tableView
titleForHeaderInSection:(NSInteger)section{
if (section == 0){
return @"Section 1 Header";
}
return nil;
}
— (NSString *) tableView:(UITableView *)tableView
titleForFooterInSection:(NSInteger)section{
if (section == 0){
return @"Section 1 Footer";
}
return nil;
}
Теперь запустите ваше приложение в эмуляторе iPhone. Вы увидите, что табличный вид автоматически создал для верхнего колонтитула метку, выровненную по левому краю, а для нижнего колонтитула — метку, выровненную по центру, и поместил их в единственном разделе табличного вида. В iOS 7 по умолчанию верхний и нижний колонтитулы выравниваются по левому краю. В более ранних версиях iOS верхний колонтитул выравнивался по левому краю, а нижний — по центру. В любой версии выравнивание этих меток может задаваться табличным видом (рис. 4.10).
Рис. 4.10. Табличный вид, в верхнем и нижнем колонтитулах которого отображается текст
4.6. Отображение контекстных меню в ячейках табличных видов
Постановка задачи
Необходимо дать пользователям возможность применять операции копирования и вставки. Предполагается, что при этом пользователь будет удерживать пальцем определенную ячейку таблицы на экране устройства, где отображается приложение.
Решение
Реализуйте следующие три метода протокола UITableViewDelegate в объекте-делегате вашего табличного вида.
• tableView: shouldShowMenuForRowAtIndexPath: — возвращаемое значение данного вида относится к типу BOOL. Если вернуть от этого метода значение YES, то система iOS отобразит для ячейки табличного вида контекстное меню. Индекс этой ячейки будет передан вам в параметре shouldShowMenuForRowAtIndexPath.
• tableView: canPerformAction: forRowAtIndexPath: withSender: — возвращаемое значение данного метода также относится к типу BOOL. Как только вы позволите iOS отображать контекстное меню для ячейки табличного вида, iOS вызовет этот метод несколько раз и сообщит вам селектор действия. После этого вы сможете решить, следует ли отображать это действие в командах контекстного меню. Итак, если iOS спрашивает вас, хотите ли вы отобразить для пользователя меню Copy (Копировать), то рассматриваемый метод будет вызван в объекте-делегате вашего табличного вида и параметр canPerformAction данного метода будет равен @selector(copy:). Подробнее этот вопрос рассматривается в подразделе «Обсуждение» данного раздела.
• tableView: performAction: forRowAtIndexPath: withSender: — как только вы разрешите отобразить определенное действие в списке вариантов контекстного меню ячейки табличного вида, возникает такая ситуация: когда пользователь выбирает это действие в меню, данный метод вызывается в объекте-делегате вашего табличного вида. Здесь нужно сделать все необходимое, чтобы удовлетворить пользовательский запрос. Например, если пользователь выбрал меню Copy (Копировать), то вы должны применить буфер обмена (Pasteboard), куда помещается содержимое из ячейки табличного вида.
Обсуждение
Табличный вид может дать системе iOS ответ «да» или «нет», позволив или не позволив отобразить доступные системные элементы меню для данной табличной ячейки. iOS пытается вывести контекстное меню для табличной ячейки, когда пользователь удерживает эту ячейку пальцем в течение определенного временного промежутка — обычно примерно 1 с. Затем iOS пытается узнать табличный вид, одна из ячеек которого инициировала появление контекстного меню на экране. Если табличный вид ответит, то iOS сообщит ему, какие команды можно отобразить в контекстном меню, а табличный вид сможет утвердительно или отрицательно отреагировать на каждый из этих вариантов. Например, если доступны пять вариантов (элементов) и табличный вид отвечает «да» на два из них, то будут отображены только два этих элемента.
После того как элементы меню будут показаны пользователю, последний может нажать либо на любой из этих элементов, либо на экран за пределами контекстного меню, чтобы убрать это меню. Как только пользователь прикоснется к одному из элементов меню, iOS пошлет табличному виду сообщение от делегата, информирующее табличный вид о том, какой именно элемент меню был выбран пользователем. В зависимости от полученной информации табличный вид может решить, что делать с выбранным действием.
Предлагаю сначала рассмотреть, какие действия доступны в контекстном меню ячейки табличного вида. Поэтому создадим табличный вид и отобразим в нем несколько ячеек:
— (NSInteger) tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section{
return 3;
}
— (UITableViewCell *) tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = nil;
cell = [tableView dequeueReusableCellWithIdentifier: CellIdentifier
forIndexPath: indexPath];
cell.textLabel.text = [[NSString alloc]
initWithFormat:@"Section %ld Cell %ld",
(long)indexPath.section,
(long)indexPath.row];
return cell;
}
— (void)viewDidLoad{
[super viewDidLoad];
self.myTableView = [[UITableView alloc]
initWithFrame: self.view.bounds
style: UITableViewStylePlain];
[self.myTableView registerClass: [UITableViewCell class]
forCellReuseIdentifier: CellIdentifier];
self.myTableView.autoresizingMask = UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight;
self.myTableView.dataSource = self;
self.myTableView.delegate = self;
[self.view addSubview: self.myTableView];
}
Теперь реализуем три упомянутых ранее метода, определенных в протоколе UITableViewDelegate, и просто преобразуем доступные действия (типа SEL) в строку, после чего выведем доступные результаты на консоль:
— (BOOL) tableView:(UITableView *)tableView
shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath{
/* Разрешаем отображение контекстного меню для каждой ячейки */
return YES;
}
— (BOOL) tableView:(UITableView *)tableView
canPerformAction:(SEL)action
forRowAtIndexPath:(NSIndexPath *)indexPath
withSender:(id)sender{
NSLog(@"%@", NSStringFromSelector(action));
/* Пока разрешим любые действия. */
return YES;
}
— (void) tableView:(UITableView *)tableView
performAction:(SEL)action
forRowAtIndexPath:(NSIndexPath *)indexPath
withSender:(id)sender{
/* Пока оставим пустым. */
}
А теперь запустим приложение в эмуляторе или на устройстве. После этого мы увидим, что в табличный вид загружены три ячейки. Удерживайте на ячейке палец (если работаете с устройством) или указатель мыши (если с эмулятором) и смотрите, какая информация появляется в окне консоли:
cut:
copy:
select:
selectAll:
paste:
delete:
_promptForReplace:
_showTextStyleOptions:
_define:
_addShortcut:
_accessibilitySpeak:
_accessibilitySpeakLanguageSelection:
_accessibilityPauseSpeaking:
makeTextWritingDirectionRightToLeft:
makeTextWritingDirectionLeftToRight:
Все это действия, которые система iOS позволяет вывести на экран для пользователя, если такие действия вам понадобятся. Допустим, вы хотите разрешить пользователям операцию копирования (Copy). Для этого перед отображением команды просто найдите в методе tableView: canPerformAction: forRowAtIndexPath: withSender:, на какое действие запрашивает у вас разрешение система iOS, а потом верните значение YES или NO:
— (BOOL) tableView:(UITableView *)tableView
canPerformAction:(SEL)action
forRowAtIndexPath:(NSIndexPath *)indexPath
withSender:(id)sender{
if (action == @selector(copy:)){
return YES;
}
return NO;
}
На следующем этапе перехватываем информацию о том, какой именно элемент был выбран пользователем в контекстном меню. В зависимости от того, что выяснится, мы можем совершить нужное действие. Например, если пользователь выберет в контекстном меню команду Copy (Копировать) (рис. 4.11), мы воспользуемся UIPasteBoard, чтобы скопировать эту ячейку в компоновочный буфер и иметь возможность применять ее позже:
— (void) tableView:(UITableView *)tableView
performAction:(SEL)action
forRowAtIndexPath:(NSIndexPath *)indexPath
withSender:(id)sender{
if (action == @selector(copy:)){
UITableViewCell *cell = [tableView cellForRowAtIndexPath: indexPath];
UIPasteboard *pasteBoard = [UIPasteboard generalPasteboard];
[pasteBoard setString: cell.textLabel.text];
}
}
Рис. 4.11. Команда Copy (Копировать), отображенная в контекстном меню ячейки табличного вида
4.7. Перемещение ячеек и разделов в табличных видах
Постановка задачи
Требуется перемещать и тасовать ячейки и разделы внутри табличного вида, сопровождая весь процесс плавной и интуитивно понятной анимацией.
Решение
Используйте метод moveSection: toSection: табличного вида, чтобы переместить раздел на новое место. Кроме того, можно применять метод moveRowAtIndexPath: toIndexPath:, чтобы перемещать ячейку табличного вида на новое место с того места, которое она сейчас занимает.
Обсуждение
Процесс перемещения разделов и ячеек таблицы отличается от их замены. Рассмотрим пример, помогающий лучше понять эту разницу. Допустим, у нас есть табличный вид с тремя разделами, A, B и C. Если передвинуть раздел A к разделу C, то табличный вид заметит это и переместит раздел B туда, где до этого находился раздел A. Но если раздел B будет перемещен на место раздела C, то табличному виду вообще не придется перемещать раздел A, так как он находится «выше» двух перемещаемых разделов и не участвует в передвижениях B и C. В данном случае раздел B попадет на место раздела C, а раздел C — на место раздела B. Такая же логика применяется в табличных видах при перемещении ячеек.
Для демонстрации таких взаимодействий создадим табличный вид и загрузим в него три раздела, в каждом из которых есть три собственные ячейки. Начнем с файла реализации контроллера вида:
#import «ViewController.h»
static NSString *CellIdentifier = @"CellIdentifier";
@interface ViewController ()
@property (nonatomic, strong) UITableView *myTableView;
@property (nonatomic, strong) NSMutableArray *arrayOfSections;
@end
Контроллер вида становится источником данных для табличного вида. В табличном виде есть разделы, а в каждом разделе — ячейки. Мы, в сущности, работаем с массивом массивов: массив первого порядка содержит разделы, а каждый раздел, в свою очередь, является массивом, содержащим ячейки. Отвечать за этот функционал будет элемент arrayOfSections, определяемый в заголовочном файле контроллера вида. Итак, заполним этот массив:
— (NSMutableArray *) newSectionWithIndex:(NSUInteger)paramIndex
withCellCount:(NSUInteger)paramCellCount{
NSMutableArray *result = [[NSMutableArray alloc] init];
NSUInteger counter = 0;
for (counter = 0;
counter < paramCellCount;
counter++){
[result addObject: [[NSString alloc] initWithFormat:@"Section %lu
Cell %lu",
(unsigned long)paramIndex,
(unsigned long)counter+1]];
}
return result;
}
— (NSMutableArray *) arrayOfSections{
if (_arrayOfSections == nil){
NSMutableArray *section1 = [self newSectionWithIndex:1
cellCount:3];
NSMutableArray *section2 = [self newSectionWithIndex:2
cellCount:3];
NSMutableArray *section3 = [self newSectionWithIndex:3
cellCount:3];
_arrayOfSections = [[NSMutableArray alloc] initWithArray:@[
section1,
section2,
section3
]
];
}
return _arrayOfSections;
}
Затем мы инстанцируем табличный вид и реализуем необходимые методы в протоколе UITableViewDataSource, чтобы заполнить табличный вид данными:
— (NSInteger) numberOfSectionsInTableView:(UITableView *)tableView{
return self.arrayOfSections.count;
}
— (NSInteger) tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section{
NSMutableArray *sectionArray = self.arrayOfSections[section];
return sectionArray.count;
}
— (UITableViewCell *) tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = nil;
cell = [tableView dequeueReusableCellWithIdentifier: CellIdentifier
forIndexPath: indexPath];
NSMutableArray *sectionArray = self.arrayOfSections[indexPath.section];
cell.textLabel.text = sectionArray[indexPath.row];
return cell;
}
— (void)viewDidLoad{
[super viewDidLoad];
self.myTableView =
[[UITableView alloc] initWithFrame: self.view.bounds
style: UITableViewStyleGrouped];
[self.myTableView registerClass: [UITableViewCell class]
forCellReuseIdentifier: CellIdentifier];
self.myTableView.autoresizingMask =
UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight;
self.myTableView.delegate = self;
self.myTableView.dataSource = self;
[self.view addSubview: self.myTableView];
}
Теперь посмотрим, что получается. Сначала проверим, как разделы перемещаются на новое место. Напишем метод, который будет перемещать раздел 1 на место раздела 3:
— (void) moveSection1ToSection3{
NSMutableArray *section1 = [self.arrayOfSections objectAtIndex:0];
[self.arrayOfSections removeObject: section1];
[self.arrayOfSections addObject: section1];
[self.myTableView moveSection:0
toSection:2];
}
Оставляю на ваш выбор окончательное решение о том, как инициировать этот метод, ведь на данный момент у нас в пользовательском интерфейсе нет специальной кнопки для этой цели. Можно просто создать навигационный контроллер и разместить на нем навигационную кнопку, которая и будет запускать данный метод.
Как только вы запустите приложение в обычном режиме, на экране появятся разделы с 1-го по 3-й (рис. 4.12).
Рис. 4.12. Табличный вид с тремя разделами, в каждом из которых находятся по три ячейки
После запуска метода moveSection1ToSection3 вы увидите, что раздел 1 переходит на место раздела 3, раздел 3 переходит на место, ранее занятое разделом 2, и, наконец, раздел 2 перемещается на то место, где раньше находился раздел 1 (рис. 4.13).
Рис. 4.13. Раздел 1 перешел на место раздела 3, после чего последовательно переместились и другие разделы
Перемещение ячеек очень напоминает перемещение разделов. Для этого нужно просто пользоваться методом moveRowAtIndexPath: toIndexPath:. Не забывайте, что ячейка может перемещаться либо в пределах одного раздела, либо из одного раздела в другой. Начнем с простого — переместим ячейку 1 из 1-го раздела на место ячейки 2 того же раздела и посмотрим, что получится:
— (void) moveCell1InSection1ToCell2InSection1{
NSMutableArray *section1 = [self.arrayOfSections objectAtIndex:0];
NSString *cell1InSection1 = [section1 objectAtIndex:0];
[section1 removeObject: cell1InSection1];
[section1 insertObject: cell1InSection1
atIndex:1];
NSIndexPath *sourceIndexPath = [NSIndexPath indexPathForRow:0
inSection:0];
NSIndexPath *destinationIndexPath = [NSIndexPath indexPathForRow:1
inSection:0];
[self.myTableView moveRowAtIndexPath: sourceIndexPath
toIndexPath: destinationIndexPath];
}
Что же происходит в этом коде? Нам нужно гарантировать, что в источнике данных содержится корректная информация, которая отобразится в табличном виде по окончании всех перестановок. Поэтому сначала убираем ячейку 1 в разделе 1. В результате ячейка 2 переходит на место, освобожденное ячейкой 1, а ячейка 3 — на место, ранее занятое ячейкой 2. В массиве остается всего 2 ячейки. Потом мы вставляем ячейку 1 в индекс 1 (второй объект) массива. Таким образом, в массиве будут содержаться ячейка 2, ячейка 1, а потом ячейка 3. И вот теперь мы на самом деле переместили ячейки в табличном виде.
Теперь немного усложним задачу. Попробуем переместить ячейку 2 из раздела 1 на место ячейки 1 из раздела 2:
— (void) moveCell2InSection1ToCell1InSection2{
NSMutableArray *section1 = [self.arrayOfSections objectAtIndex:0];
NSMutableArray *section2 = [self.arrayOfSections objectAtIndex:1];
NSString *cell2InSection1 = [section1 objectAtIndex:1];
[section1 removeObject: cell2InSection1];
[section2 insertObject: cell2InSection1
atIndex:0];
NSIndexPath *sourceIndexPath = [NSIndexPath indexPathForRow:1
inSection:0];
NSIndexPath *destinationIndexPath = [NSIndexPath indexPathForRow:0
inSection:1];
[self.myTableView moveRowAtIndexPath: sourceIndexPath
toIndexPath: destinationIndexPath];
}
Результаты перехода показаны на рис. 4.14.
Рис. 4.14. Ячейка 2 из раздела 1 перемещена на место ячейки 1 из раздела 2
4.8. Удаление ячеек и разделов в табличных видах
Постановка задачи
Требуется удалять из табличных видов разделы и/или ячейки, сопровождая этот процесс анимацией.
Решение
Для удаления разделов из табличного вида выполните следующие шаги.
1. Сначала удалите раздел (-ы) в источнике данных независимо от того, с какой именно моделью данных вы работаете — Core Data или словарь/массив.
2. Примените к табличному виду метод экземпляра deleteSections: withRowAnimation:, относящийся к UITableView. Первый параметр, который нужно передать данному методу, имеет тип NSIndexSet. Этот объект можно инстанцировать с помощью метода класса indexSetWithIndex:, относящегося к классу NSIndexSet, где указываемый индекс — это беззнаковое целое число. Применяя такой подход, вы можете удалять только один раздел за раз. Если вы собираетесь удалить за раз более одного раздела, пользуйтесь методом класса indexSetWithIndexesInRange:, также относящимся к классу NSIndexSet, чтобы создать индексное множество с указанием диапазона. Это индексное множество передается описанному ранее методу экземпляра, относящемуся к UITableView.
Если вы хотите удалить ячейки в табличном виде, выполните следующие шаги.
1. Сначала удалите ячейку (ячейки) из источника данных. Здесь также не имеет значения, работаете ли вы с Core Data, обычным словарем, массивом или чем-то еще. Самое важное в данном случае — удалить из источника данных те объекты, которые соответствуют ячейкам табличного вида.
2. Теперь для удаления самих ячеек, соответствующих объектам данных, примените метод экземпляра deleteRowsAtIndexPaths: withRowAnimation:, относящийся к табличному виду. Первый параметр, который необходимо передать данному методу, — это массив типа NSArray. Данный массив должен содержать объекты типа NSIndexPath, и каждый индексный путь представляет одну ячейку в табличном виде. В каждом индексном пути содержится указание на раздел и на строку табличного вида. Этот путь составляется с помощью метода класса indexPathForRow: inSection:, относящегося к классу NSIndexPath.
Обсуждение
В коде вашего пользовательского интерфейса вам может понадобиться удалять ячейки и/или разделы. Например, у вас может иметься переключатель (типа UISwitch, см. раздел 1.2). Когда пользователь нажимает переключатель, вам, возможно, требуется добавить в табличный вид несколько строк. После того как пользователь вернет переключатель в исходное положение, вам, вероятно, потребуется вновь убрать эти строки с экрана. Но такие операции удаления не всегда ограничиваются ячейками (строками) табличного вида. Иногда из табличного вида требуется одновременно удалить целый раздел (или несколько разделов). Ключевой аспект при удалении разделов и ячеек из табличных видов состоит в том, что сначала из источника данных удаляется информация, соответствующая этим элементам (ячейкам или разделам), а потом вызываются соответствующие методы удаления, применяемые к табличному виду. После того как метод удаления завершит работу, табличный вид снова будет ссылаться на свой объект из источника данных. Если же после операции удаления количество ячеек/разделов в источнике данных не совпадет с количеством ячеек/разделов в табличном виде, приложение аварийно завершится. Но не волнуйтесь — даже если вы и допустите такую ошибку, то отладочное сообщение, которое появится на консоли, будет достаточно подробным для того, чтобы вы могли подправить код.
Рассмотрим, как удалять разделы из табличного вида. В данном разделе мы отобразим табличный вид в контроллере вида, который, в свою очередь, будет находиться в навигационном контроллере. Внутри табличного вида будет два раздела: один для нечетных чисел, другой — для четных. В табличном виде в разделе с нечетными числами мы отобразим только 1, 3, 5 и 7, а в разделе с четными — 0, 2, 4 и 6. В первом упражнении мы собираемся создать на навигационной панели специальную кнопку, которая будет удалять раздел с нечетными числами. На рис. 4.15 показано, какой результат мы хотим получить.
Рис. 4.15. Пользовательский интерфейс для отображения двух разделов табличного вида; в интерфейсе есть кнопка, удаляющая раздел Odd Numbers (Нечетные числа)
Начнем с главного. Определим контроллер вида:
#import
static NSString *CellIdentifier = @"NumbersCellIdentifier";
@interface ViewController: UIViewController
UITableViewDataSource>
@property (nonatomic, strong) UITableView *tableViewNumbers;
@property (nonatomic, strong) NSMutableDictionary *dictionaryOfNumbers;
@property (nonatomic, strong) UIBarButtonItem *barButtonAction;
@end
Свойство tableViewNumbers соответствует нашему табличному виду. Свойство barButtonAction соответствует кнопке для удаления, которая будет отображаться на навигационной панели. И последнее, но немаловажное свойство dictionaryOfNumbers — это источник данных для табличного вида. В данном словаре мы поместим два значения типа NSMutableArray, которые будут содержать числа типа NSNumber. Это изменяемые массивы, позже в данной главе мы сможем удалять их отдельно от массивов, содержащихся в словаре. Ключи для этих массивов мы будем хранить как статические значения в файле реализации контроллера вида. По этой причине позже просто сможем извлечь массивы из словаря, пользуясь статическими ключами. (Если бы ключи не были статическими, то для нахождения массивов в словаре пришлось бы выполнять сравнение строк. А эта операция требует больше времени, чем обычное ассоциирование объекта со статическим ключом, не изменяющимся на протяжении всего существования контроллера вида.) Теперь синтезируем наши свойства и определим статические строковые ключи для массивов, находящихся в словаре источника данных:
static NSString *SectionOddNumbers = @"Odd Numbers";
static NSString *SectionEvenNumbers = @"Even Numbers";
@implementation ViewController
Теперь, перед тем как создать табличный вид, необходимо заполнить информацией словарь источника данных. Вот простой метод, который автоматически заполнит словарь:
— (NSMutableDictionary *) dictionaryOfNumbers{
if (_dictionaryOfNumbers == nil){
NSMutableArray *arrayOfEvenNumbers =
[[NSMutableArray alloc] initWithArray:@[
@0,
@2,
@4,
@6,
]];
NSMutableArray *arrayOfOddNumbers =
[[NSMutableArray alloc] initWithArray:@[
@1,
@3,
@5,
@7,
]];
_dictionaryOfNumbers =
[[NSMutableDictionary alloc]
initWithDictionary:@{
SectionEvenNumbers: arrayOfEvenNumbers,
SectionOddNumbers: arrayOfOddNumbers,
}];
}
return _dictionaryOfNumbers;
}
Пока все нормально? Как видите, у нас два массива, в каждом из которых содержатся некоторые числа (в одном нечетные, в другом — четные). Мы ассоциируем массивы с ключами SectionEvenNumbers и SectionOddNumbers, которые ранее определили в файле реализации контроллера вида. Теперь инстанцируем табличный вид:
— (void)viewDidLoad
{
[super viewDidLoad];
self.barButtonAction =
[[UIBarButtonItem alloc]
initWithTitle:@"Delete Odd Numbers"
style: UIBarButtonItemStylePlain
target: self
action:@selector(deleteOddNumbersSection:)];
[self.navigationItem setRightBarButtonItem: self.barButtonAction
animated: NO];
self.tableViewNumbers = [[UITableView alloc]
initWithFrame: self.view.frame
style: UITableViewStyleGrouped];
self.tableViewNumbers.autoresizingMask = UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight;
self.tableViewNumbers.delegate = self;
self.tableViewNumbers.dataSource = self;
[self.view addSubview: self.tableViewNumbers];
}
Далее нужно заполнить табличный вид информацией внутри словаря источника с данными:
— (NSInteger) numberOfSectionsInTableView:(UITableView *)tableView{
return self.dictionaryOfNumbers.allKeys.count;
}
— (NSInteger) tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section{
NSString *sectionNameInDictionary =
self.dictionaryOfNumbers.allKeys[section];
NSArray *sectionArray = self.dictionaryOfNumbers[sectionNameInDictionary];
return sectionArray.count;
}
— (UITableViewCell *) tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = nil;
cell = [tableView dequeueReusableCellWithIdentifier: CellIdentifier
forIndexPath: indexPath];
NSString *sectionNameInDictionary =
self.dictionaryOfNumbers.allKeys[indexPath.section];
NSArray *sectionArray = self.dictionaryOfNumbers[sectionNameInDictionary];
NSNumber *number = sectionArray[indexPath.row];
cell.textLabel.text = [NSString stringWithFormat:@"%lu",
(unsigned long)[number unsignedIntegerValue]];
return cell;
}
— (NSString *) tableView:(UITableView *)tableView
titleForHeaderInSection:(NSInteger)section{
return self.dictionaryOfNumbers.allKeys[section];
}
Навигационная кнопка связана с селектором deleteOddNumbersSection:. Этот метод нам сейчас предстоит запрограммировать. Цель метода, как видно из его названия, — найти раздел, соответствующий всем нечетным числам в источнике данных, найти табличный вид, а потом удалить искомый раздел и из таблицы, и из источника данных. Вот как это делается:
— (void) deleteOddNumbersSection:(id)paramSender{
/* Сначала удаляем раздел из источника данных. */
NSString *key = SectionOddNumbers;
NSInteger indexForKey = [[self.dictionaryOfNumbers allKeys]
indexOfObject: key];
if (indexForKey == NSNotFound){
NSLog(@"Could not find the section in the data source.");
return;
}
[self.dictionaryOfNumbers removeObjectForKey: key];
/* Затем удаляем раздел из табличного вида. */
NSIndexSet *sectionToDelete = [NSIndexSet indexSetWithIndex: indexForKey];
[self.tableViewNumbers deleteSections: sectionToDelete
withRowAnimation: UITableViewRowAnimationAutomatic];
/* Наконец, убираем с навигационной панели кнопку,
так как она нам больше не понадобится. */
[self.navigationItem setRightBarButtonItem: nil animated: YES];
}
Все довольно просто. Теперь, когда пользователь нажмет кнопку на навигационной панели, раздел Odd Numbers (Нечетные числа) исчезнет из табличного вида. Как видите, в процессе удаления раздела табличный вид анимируется. Это происходит потому, что мы передали анимационный тип UITableViewRowAnimationAutomatic параметру withRowAnimation: метода deleteSections: withRowAnimation: табличного вида. Теперь запустите приложение в эмуляторе iOS и выполните Debug — Toggle Slow Animations (Отладка — Включить медленную анимацию). Потом попробуйте нажать кнопку на навигационной панели и посмотрите, что происходит. Как видите, удаление сопровождается медленной анимацией (движением). Красиво, правда? Когда удаление завершится, приложение будет выглядеть, как на рис. 4.16.
Рис. 4.16. Раздел, содержащий нечетные числа, удален из табличного вида
Вы уже знаете, как удалять разделы из табличных видов. Перейдем к удалению ячеек. Мы собираемся изменить функциональность навигационной кнопки так, чтобы при ее нажатии во всех разделах табличного вида удалялись все ячейки, содержащие числовое значение больше 2. Таким образом, мы удалим все четные и нечетные числа больше 2. Итак, изменим навигационную кнопку в методе viewDidLoad контроллера вида:
— (void)viewDidLoad {
[super viewDidLoad];
self.barButtonAction =
[[UIBarButtonItem alloc]
initWithTitle:@"Delete Numbers > 2"
style: UIBarButtonItemStylePlain
target: self
action:@selector(deleteNumbersGreaterThan2:)];
[self.navigationItem setRightBarButtonItem: self.barButtonAction
animated: NO];
self.tableViewNumbers = [[UITableView alloc]
initWithFrame: self.view.frame
style: UITableViewStyleGrouped];
self.tableViewNumbers.autoresizingMask =
UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleHeight;
self.tableViewNumbers.delegate = self;
self.tableViewNumbers.dataSource = self;
[self.view addSubview: self.tableViewNumbers];
}
На рис. 4.17 показано, как выглядит приложение при запуске в эмуляторе iPhone.
Рис. 4.17. Кнопка, удаляющая все ячейки с числами больше 2
Теперь кнопка навигационной панели связана с селектором deleteNumbersGreaterThan2:. Селектор — это метод, реализованный в контроллере вида. Но прежде, чем перейти к его программированию, определим, что этот метод должен сделать.
1. Найти оба массива с нечетными и четными числами в источнике данных и собрать индексные пути (типа NSIndexPath) чисел больше 2. Позже мы будем пользоваться этими индексными путями для удаления соответствующих ячеек в табличном виде.
2. Удалить все числа больше 2 из источника данных — как из словаря для нечетных чисел, так и из словаря для четных.
3. Удалить из табличного вида соответствующие ячейки. Индексные пути к этим ячейкам мы собрали на первом этапе.
4. Удалить кнопку с навигационной панели. Эта кнопка больше не понадобится, ведь ячейки уже удалены и из источника данных, и из табличного вида. В качестве альтернативы при желании можете просто отключить эту кнопку. Но мне кажется, что для удобства пользователя кнопку лучше просто удалить, поскольку отключенная кнопка все равно будет ему совершенно бесполезна.
— (void) deleteNumbersGreaterThan2:(id)paramSender{
NSMutableArray *arrayOfIndexPathsToDelete =
[[NSMutableArray alloc] init];
NSMutableArray *arrayOfNumberObjectsToDelete =
[[NSMutableArray alloc] init];
/* Шаг 1: собираем объекты, которые мы хотим удалить из
источника данных, а также их индексные пути. */
__block NSUInteger keyIndex = 0;
[self.dictionaryOfNumbers enumerateKeysAndObjectsUsingBlock:
^(NSString *key, NSMutableArray *object, BOOL *stop) {
[object enumerateObjectsUsingBlock:
^(NSNumber *number, NSUInteger numberIndex, BOOL *stop) {
if ([number unsignedIntegerValue] > 2){
NSIndexPath *indexPath =
[NSIndexPath indexPathForRow: numberIndex
inSection: keyIndex];
[arrayOfIndexPathsToDelete addObject: indexPath];
[arrayOfNumberObjectsToDelete addObject: number];
}
}];
keyIndex++;
}];
/* Шаг 2: удаляем объекты из источника данных. */
if ([arrayOfNumberObjectsToDelete count] > 0){
NSMutableArray *arrayOfOddNumbers =
self.dictionaryOfNumbers[SectionOddNumbers];
NSMutableArray *arrayOfEvenNumbers =
self.dictionaryOfNumbers[SectionEvenNumbers];
[arrayOfNumberObjectsToDelete enumerateObjectsUsingBlock:
^(NSNumber *numberToDelete, NSUInteger idx, BOOL *stop) {
if ([arrayOfOddNumbers indexOfObject: numberToDelete]
!= NSNotFound){
[arrayOfOddNumbers removeObject: numberToDelete];
}
if ([arrayOfEvenNumbers indexOfObject: numberToDelete]
!= NSNotFound){
[arrayOfEvenNumbers removeObject: numberToDelete];
}
}];
}
/* Шаг 3: удаляем все ячейки, соответствующие объектам. */
[self.tableViewNumbers
deleteRowsAtIndexPaths: arrayOfIndexPathsToDelete
withRowAnimation: UITableViewRowAnimationAutomatic];
[self.navigationItem setRightBarButtonItem: nil animated: YES];
}
После того как пользователь нажмет кнопку на навигационной панели, все ячейки, в которых содержатся числа больше 2, будут удалены из источника данных. Табличный вид и все приложение станут выглядеть как на рис. 4.18.
Рис. 4.18. Мы удалили все ячейки, в которых содержались числа больше 2
См. также
Раздел 1.2.
4.9. Использование UITableViewController для удобства при создании табличных видов
Постановка задачи
Требуется возможность быстро создавать табличные виды.
Решение
Используйте контроллер вида UITableViewController, который по умолчанию предоставляется с табличным контроллером вида.
Обсуждение
В инструментарии iOS SDK есть очень удобный класс UITableViewController, который предоставляется с заранее заготовленным экземпляром табличного вида. Чтобы пользоваться всеми его преимуществами, всего лишь потребуется создать новый класс, наследующий от указанного. Здесь я подробно опишу все этапы создания нового проекта Xcode, использующего табличный контроллер вида.
1. На панели меню Xcode выберите File-New-Project (Файл-Новый-Проект).
2. Убедитесь, что в левой части экрана выбрана категория iOS. Затем перейдите в подкатегорию Application (Приложение). В правой части экрана выберите шаблон Empty Application (Пустое приложение), а потом нажмите кнопку Next (Далее) (рис. 4.19).
Рис. 4.19. Создание нового пустого приложения, в котором позже будет находиться табличный контроллер
3. На следующем экране просто выберите название для вашего проекта. Кроме того, убедитесь, что вся информация у вас на экране, кроме Organization Name (Название организации) и Company Identifier (Идентификатор компании), в точности соответствует той, что приведена на рис. 4.20. Как только все будет готово, нажмите кнопку Next (Далее).
Рис. 4.20. Конфигурирование нового пустого приложения в Xcode
4. На следующем экране вам будет предложено сохранить приложение на диске. Просто сохраните приложение в месте, которое кажется вам целесообразным, и нажмите кнопку Create (Создать).
5. В Xcode выберите меню File-New-File (Файл-Новый-Файл).
6. Убедитесь, что в левой части диалогового окна категория iOS выбрана в качестве основной и при этом также выбрана подкатегория Cocoa Touch. Далее в правой части диалогового окна выберите класс Objective-C (рис. 4.21).
Рис. 4.21. Создание нового класса для табличного вида с контроллером
7. На следующем экране вам будет предложено выбрать суперкласс для нового класса. Это очень важный этап. Убедитесь, что в качестве суперкласса задан UITableViewController. Удостоверьтесь, что все остальные настройки у вас точно такие же, как и у меня на рис. 4.22. Когда все будет готово, нажмите кнопку Next (Далее).
Рис. 4.22. Задаем суперкласс для нового объекта, который станет контроллером табличного вида
8. На следующем экране вы сможете сохранить табличный контроллер вида в проекте. Сохраните его как класс ViewController и нажмите кнопку Create (Создать).
9. В файле реализации делегата вашего приложения обязательно импортируйте заголовочный файл этого контроллера вида, а затем создайте экземпляр этого класса и установите его в качестве корневого контроллера вида приложения, как показано далее:
#import «AppDelegate.h»
#import «ViewController.h»
@implementation AppDelegate
— (BOOL) application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
ViewController *controller = [[ViewController alloc]
initWithStyle: UITableViewStylePlain];
self.window = [[UIWindow alloc]
initWithFrame: [[UIScreen mainScreen] bounds]];
self.window.rootViewController = controller;
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
Теперь, если вы попытаетесь скомпилировать проект, компилятор выдаст вам следующие предупреждения:
ViewController.m:47:2: Potentially incomplete method implementation.
ViewController.m:54:2: Incomplete method implementation.
Итак, необходимо иметь в виду, что компилятор выдает определенные предупреждения, об устранении которых придется позаботиться в файле реализации контроллера вида. Открыв этот файл, вы увидите, что Apple вставила в шаблон класса табличного контроллера вида макрокоманды #warning — инструкции для компилятора (именно они приводят к тому, что на экран выводятся показанные ранее предупреждения). Одно из предупреждений находится в методе numberOfSectionsInTableView:, другое — в методе tableView: numberOfRowsInSection:. Мы видим на экране предупреждения, потому что не запрограммировали логику для этих методов. Минимальная информация, необходимая табличному контроллеру вида, — это количество разделов для отображения, количество строк для отображения, а также объект ячейки, который должен отображаться в каждой из строк. Мы не видим никаких предупреждений, связанных с отсутствием реализации объекта ячейки, но только потому, что Apple по умолчанию предоставляет формальную реализацию этого метода, создающую за вас пустые ячейки.
По умолчанию контроллер табличного вида является источником данных и делегатом табличного вида. Вам не придется отдельно указывать источник данных и делегат для этого табличного вида.
Перейдем к файлу реализации табличного контроллера вида и убедимся, что у нас есть массив строк (просто для примера), которыми мы можем заполнить табличный вид:
#import «ViewController.h»
static NSString *CellIdentifier = @"Cell";
@interface ViewController ()
@property (nonatomic, strong) NSArray *allItems;
@end
@implementation ViewController
— (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle: style];
if (self) {
// Специальная инициализация
self.allItems = @[
@"Anthony Robbins",
@"Steven Paul Jobs",
@"Paul Gilbert",
@"Yngwie Malmsteen"
];
[self.tableView registerClass: [UITableViewCell class]
forCellReuseIdentifier: CellIdentifier];
}
return self;
}
— (void) viewDidLoad{
[super viewDidLoad];
}
— (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
return 1;
}
— (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section{
return self.allItems.count;
}
— (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier: CellIdentifier
forIndexPath: indexPath];
cell.textLabel.text = self.allItems[indexPath.row];
return cell;
}
@end
Теперь, запустив приложение, мы увидим результат, напоминающий рис. 4.23.
Рис. 4.23. Строки правильно отображаются в табличном виде
Вот практически и все, что следует знать о табличных контроллерах видов. Еще раз напомню, что табличный контроллер вида одновременно является и источником данных, и делегатом табличного вида. Итак, теперь вы можете реализовать методы протокола UITableViewDataSource, а также методы протокола UITableViewDelegate прямо в файле реализации табличного контроллера вида.
См. также
Раздел 4.1.
4.10. Отображение элемента управления, предназначенного для обновления информации в табличных видах
Постановка задачи
Требуется отображать в пользовательском интерфейсе красивый элемент управления для обновления информации. Этот элемент управления должен находиться над табличными видами и служить для пользователя интуитивно понятным инструментом: такой инструмент позволяет временно убрать таблицу с экрана, а потом вновь вывести ее, но уже с обновленной информацией. Пример показан на рис. 4.24.
Рис. 4.24. Элемент управления для обновления информации, расположенный над табличным видом
Решение
Создайте табличный контроллер вида (так, как описано в разделе 4.9) и задайте в качестве значения его свойства refreshControl новый экземпляр класса UIRefreshControl, как показано далее:
— (id)initWithStyle:(UITableViewStyle)style{
self = [super initWithStyle: style];
if (self) {
[self.tableView registerClass: [UITableViewCell class]
forCellReuseIdentifier: CellIdentifier];
self.allTimes = [NSMutableArray arrayWithObject: [NSDate date]];
/* Создаем элемент управления для обновления информации */
self.refreshControl = [[UIRefreshControl alloc] init];
self.refreshControl = self.refreshControl;
[self.refreshControl addTarget: self
action:@selector(handleRefresh:)
forControlEvents: UIControlEventValueChanged];
}
return self;
}
Обсуждение
Элементы управления для обновления информации — это простые визуальные индикаторы, располагающиеся над табличным видом и сообщающие пользователю, что какая-то информация в таблице сейчас обновится. Например, чтобы обновить содержимое почтового ящика в приложении Mail в версиях старше iOS 6, вам приходилось нажимать на специальную кнопку Refresh (Обновить). В новой iOS 7 вы можете просто потянуть список ваших писем вниз, как если бы хотели ознакомиться с какими-то письмами из верхней части списка, которые пока не успели прочитать. Как только iOS зафиксирует такой жест, система инициирует обновление. Круто, правда? Это нововведение впервые появилось в Twitter-клиенте для iPhone, большое спасибо за это его разработчикам. Apple по достоинству оценила всю элегантность и логичность этой возможности обновления видов, поэтому в SDK был добавлен специальный компонент для реализации такой функции. Класс, соответствующий этому компоненту, называется UIRefreshControl.
Чтобы создать новый экземпляр этого класса, достаточно просто вызвать его метод init. Сделав это, добавьте экземпляр к табличному контроллеру вида, как описано в подразделе «Решение» данного раздела.
Итак, вы хотите знать, когда пользователь инициирует обновление информации в табличном виде. Для этого просто вызовите метод экземпляра addTarget: action: forControlEvents: обновляющего элемента и передайте ему целевой объект вместе с селектором этого объекта — остальное система сделает за вас. Передайте событие UIControlEventValueChanged параметру forControlEvents этого метода.
Сейчас я это продемонстрирую. В данном примере имеем табличный контроллер вида, в котором отображаются дата и время в строковом формате. Как только пользователь обновит список, потянув его вниз, мы будем добавлять сюда новые актуальные значения даты и времени, изменяя таким образом таблицу. Итак, всякий раз, когда пользователь опускает таблицу, инициируется обновление, в ходе которого мы можем добавить в список актуальные значения даты и времени, обновив табличный вид. Итак, начнем с файла реализации контроллера вида. Определим элемент управления для обновления информации и источник данных:
#import «ViewController.h»
static NSString *CellIdentifier = @"Cell";
@interface ViewController ()
@property (nonatomic, strong) NSMutableArray *allTimes;
@property (nonatomic, strong) UIRefreshControl *refreshControl;
@end
@implementation ViewController
Свойство allTimes — это обычный изменяемый массив, который будет содержать все экземпляры NSDate в момент, когда завершится обновление таблицы. Мы уже рассмотрели инициализацию табличного контроллера вида в подразделе «Решение» данного раздела, поэтому я не буду вновь писать об этом. Но, как вы помните, мы прикрепили событие UIControlEventValueChanged обновляющего элемента управления к методу handleRefresh:. В этом методе мы всего лишь собираемся добавить к массиву дату и время, после чего обновить табличный вид:
— (void) handleRefresh:(id)paramSender{
/* Оставляем небольшую задержку между высвобождением обновляющего элемента
управления и самим моментом обновления. Так весь процесс выглядит
в интерфейсе более плавно, чем при использовании обычной анимации */
int64_t delayInSeconds = 1.0f;
dispatch_time_t popTime =
dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
/* Добавляем актуальную дату к имеющемуся списку дат;
Таким образом, при обновлении табличного вида новый информационный
элемент на экране будет находиться над старым и пользователь увидит
разницу во времени до и после обновления */
[self.allTimes addObject: [NSDate date]];
[self.refreshControl endRefreshing];
NSIndexPath *indexPathOfNewRow =
[NSIndexPath indexPathForRow: self.allTimes.count-1 inSection:0];
[self.tableView
insertRowsAtIndexPaths:@[indexPathOfNewRow]
withRowAnimation: UITableViewRowAnimationAutomatic];
});
}
Последний важный момент: мы записываем дату в табличный вид посредством методов делегата и источника данных табличного вида:
— (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
return 1;
}
— (NSInteger) tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section{
return self.allTimes.count;
}
— (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier: CellIdentifier
forIndexPath: indexPath];
cell.textLabel.text = [NSString stringWithFormat:@"%@",
self.allTimes[indexPath.row]];
return cell;
}
Опробуйте в эмуляторе или на устройстве то, что у нас получилось. Открыв приложение, вы сразу заметите только одно значение даты и времени в списке. Но если потянуть таблицу вниз, то постепенно перед вами будут открываться новые элементы (см. рис. 4.24).
См. также
Раздел 4.9.