iOS. Приемы программирования Нахавандипур Вандад
NSString *const kHConstraint = @"H:|-[_bottomGrayView]-|";
NSString *const kVConstraint =
@"V:|-[_topGrayView]-[_bottomGrayView(==100)]";
/* Горизонтальные ограничения */
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat: kHConstraint
options:0
metrics: nil
views: views]
];
/* Вертикальные ограничения */
[constraints addObjectsFromArray:
[NSLayoutConstraint constraintsWithVisualFormat: kVConstraint
options:0
metrics: nil
views: views]
];
[self.bottomGrayView.superview addConstraints: constraints];
}
Вертикальные ограничения для нижнего серого вида, выраженные на языке визуального форматирования, выглядят длинновато, но, в сущности, они тривиальны. Приглядевшись к ним повнимательнее, вы заметите, что эти ограничения просто выравнивают верхний и нижний серые виды по сторонам их общего вышестоящего вида с контроллером. При этом используются указатели стандартного расстояния и постоянная высота, равная 100 точкам.
Следующий и, пожалуй, последний компонент пользовательского интерфейса, для которого мы собираемся написать ограничения, — это кнопка, расположенная в нижнем сером виде. Метод, который будет заниматься ее ограничениями, называется applyConstraintsToButtonOnBottomGrayView
. Перед тем как его реализовать, обсудим требования, которым должны соответствовать ограничения для нижней кнопки:
• кнопка должна быть вертикально центрирована в нижнем сером виде;
• ее левый край должен быть выровнен по правому краю кнопки, находящейся в верхнем сером виде;
• с ней не должны применяться строго определенные значения высоты и ширины; ее высота и ширина должны зависеть от содержимого — в данном случае от текста Button, который мы на ней записываем.
— (void) applyConstraintsToButtonOnBottomGrayView{
NSDictionary *views = NSDictionaryOfVariableBindings(_topButton,
_bottomButton);
NSString *const kHConstraint = @"H:[_topButton][_bottomButton]";
/* Горизонтальные ограничения */
[self.bottomGrayView.superview addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat: kHConstraint
options:0
metrics: nil
views: views]
];
/* Вертикальные ограничения */
[self.bottomButton.superview addConstraint:
[NSLayoutConstraint constraintWithItem: self.bottomButton
attribute: NSLayoutAttributeCenterY
relatedBy: NSLayoutRelationEqual
toItem: self.bottomGrayView
attribute: NSLayoutAttributeCenterY
multiplier:1.0f
constant:0.0f]
];
}
Наконец, мы должны удостовериться в том, что контроллер вида сообщает среде времени исполнения, что он может обрабатывать любые варианты ориентации. Ведь именно этот аспект наиболее сильно интересовал нас в данном разделе. Поэтому мы переопределим метод supportedInterfaceOrientations
в виде UIViewController
:
— (NSUInteger) supportedInterfaceOrientations{
return UIInterfaceOrientationMaskAll;
}
Итак, работа с этим контроллером вида завершена. Запустим приложение и посмотрим, как оно работает при книжной ориентации (рис. 3.7).
Рис. 3.7. Приложение отображает компоненты пользовательского интерфейса в книжной ориентации согласно требованиям, которые мы предъявили
А теперь момент истины! Будет ли оно работать в альбомном режиме? Попробуем (рис. 3.8).
Рис. 3.8. Как и ожидалось, тот же самый код отлично работает и при альбомной ориентации экрана
Отлично! Все получилось.
См. также
Раздел 3.0.
3.4. Конфигурирование ограничений автоматической компоновки в конструкторе интерфейсов
Постановка задачи
Требуется задействовать весь потенциал конструктора интерфейсов для создания ограничений при работе с пользовательским интерфейсом.
Решение
Выполните следующие шаги.
1. Откройте в конструкторе интерфейсов файл XIB или файл раскадровки, который вы собираетесь редактировать.
2. Убедитесь, что в конструкторе интерфейсов вы выбрали объект вида, в котором собираетесь активизировать автоматическую компоновку. Просто щелкните на этом объекте.
3. Щелкните на элементе меню View — Utilities — Show File Inspector (Вид — Утилиты — Показать инспектор файлов).
4. Убедитесь, что в элементе File Inspector (Инспектор файлов) в разделе Interface Builder Document (Документ конструктора интерфейсов) установлен флажок Use Autolayout (Использовать автоматическую компоновку) (рис. 3.9).
Рис. 3.9. Активизируем автоматическую компоновку в конструкторе интерфейсов
Обсуждение
Конструктор интерфейсов значительно упрощает для программиста создание ограничений, причем наше участие в этом сводится к минимуму. До того как в iOS появилась возможность автоматической компоновки, приходилось, как правило, пользоваться специальными ориентировочными панелями (guideline bars). Эти панели появлялись на экране, пока вы перемещали компоненты пользовательского интерфейса. Ориентировочные панели были связаны с масками для автоматической подгонки размеров, которые вы могли создавать и в коде, точно так же, как ограничения. Но после того, как в конструкторе интерфейсов будет установлен флажок Use Autolayout (Использовать автоматическую компоновку), ориентировочные панели приобретут несколько иное значение. Теперь они сообщают о тех ограничениях, которые создает для вас в фоновом режиме сам конструктор интерфейсов.
Немного поэкспериментируем. Создадим в Xcode приложение с одним видом (Single View Application). Таким образом, будет создано приложение, содержащее всего один контроллер вида. Этот контроллер вида будет относиться к классу ViewController, а. xib-файл для него будет называться ViewController.xib. Просто щелкните на этом файле, чтобы конструктор интерфейсов открыл его. Убедитесь, что в инспекторе файлов установлен флажок Use Autolayout (Использовать автоматическую компоновку) так, как описано в подразделе «Решение» этого раздела.
Теперь просто найдите в библиотеке объектов кнопку (Button) и перетащите ее в центр экрана. Дождитесь, пока в конструкторе интерфейсов появятся ориентировочные панели, по которым будет понятно, что центр кнопки соответствует центру экрана. В меню Edit (Правка) установите флажок Show Document Outline (Показать структуру документа). Если у вас в конструкторе интерфейсов уже открыт раздел Document Outline (Структура документа), то вместо Show Document Outline (Показать структуру документа) на этом месте будет отображаться надпись Hide Document Outline (Скрыть структуру документа) — в таком случае ничего делать не надо. Теперь найдите в разделе Document Outline (Структура документа) новый подраздел, отмеченный голубым цветом. Он был создан специально для вас и называется Constraints (Ограничения). Раскройте ограничения, созданные конструктором интерфейсов для этой кнопки. То, что вы теперь увидите, должно напоминать рис. 3.10.
Рис. 3.10. Конструктор интерфейсов создал ограничения компоновки
С помощью конструктора интерфейсов вы можете создать довольно много ограничений, и для этого не потребуется писать ни единой строки кода. Но случается, что нужные вам ограничения настолько сложны, что их лучше запрограммировать в коде. Определившись с тем, как компоненты пользовательского интерфейса должны быть расположены на экране, вы поймете, как лучше поступить — сформулировать их в конструкторе интерфейса, выразить в коде или сделать и то и другое.
См. также
Раздел 3.0.
Глава 4. Создание и использование табличных видов
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 () <UITableViewDelegate>
@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 () <UITableViewDataSource>
@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];
}