iOS. Приемы программирования

Нахавандипур Вандад

Глава 5. Выстраивание сложных макетов с помощью сборных видов

 

 

5.0. Введение

Табличные виды очень хороши. Действительно. Тем не менее они отличаются удручающей негибкостью, так как их содержимое всегда ориентировано по вертикали. Это не настоящие таблицы-сетки, поэтому они и функционально не похожи на сетки. Однако программист может оказаться в ситуации, когда требуется отрисовать на экране таблицеподобный компонент со строками и столбцами, а затем поместить в каждую из ячеек различные элементы пользовательского интерфейса и каждый элемент сделать интерактивным. В табличном виде у вас фактически имеется всего один столбец с множеством строк. Если вы хотите создать иллюзию множества столбцов, то придется предоставить собственную специальную ячейку и оформить ее так, как будто она состоит из нескольких столбцов.

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

Именно поэтому в 6-й версии iOS компания Apple впервые внедрила сборные виды. Сборный вид можно сравнить с сильно усовершенствованным прокручиваемым видом. У него есть источник данных и делегат, как и у табличного вида. Но он обладает одним свойством, делающим его совершенно несхожим с табличным или прокручиваемым видами. Речь идет о макетном объекте.

В сущности, макетный объект вычисляет, где должен быть размещен каждый элемент, входящий в состав сборного вида. Однако Apple немного усложнила такую работу, внедрив конкретный класс для работы со сборными видами, причем этот класс нельзя инстанцировать напрямую. Вместо этого придется инстанцировать подкласс от этого класса, называемый UICollectionViewFlowLayout.

Этот подкласс обеспечивает последовательную компоновку, при которой ячейки из сборного вида распределяются на экране по секциям. Каждая секция — это группа ячеек сборного вида, так же как в табличном виде. Однако в сборном виде любая секция может компоноваться на экране разными способами, не обязательно вертикально. Например, у вас может быть три прямоугольника, в каждом из которых содержится своя маленькая таблица (рис. 5.1).

Рис. 5.1. Типичный макет с последовательной компоновкой в сборном виде

Как правило, секции располагаются на экране в виде таблиц, то есть образуя строки и столбцы. Именно эта задача решается с помощью класса последовательной компоновки. Если вы хотите добиться еще большей свободы действий при компоновке, то попробуйте изменить свойства класса последовательной компоновки. А если желаете сделать нечто, значительно отличающееся от стандартных возможностей последовательной компоновки, создайте для этого собственный класс. Например, такой специальный класс вам потребуется для создания сборного вида, который показан на рис. 5.2. Далее приведен специальный класс компоновки, располагающий соответствующие ячейки совсем не по табличному принципу.

Рис. 5.2. Специальный вариант компоновки для сборного вида

 

5.1. Создание сборных видов

 

Постановка задачи

Требуется отобразить на экране сборный вид.

 

Решение

Либо воспользуйтесь экземпляром UICollectionView, который в таком случае нужно сделать дочерним видом одного из видов вашего приложения (если хотите создать полноэкранный сборный вид), либо примените класс UICollectionViewController.

 

Обсуждение

Сборный вид, как и табличный, — это элемент, который может быть добавлен в качестве дочернего к другому виду. Итак, создавая приложение, определитесь с тем, должен ли сборный вид быть основным видом в контроллере или представлять собой небольшой фрагмент другого вида.

Сначала рассмотрим вариант с полноэкранным видом.

1. Откройте Xcode.

2. В меню File (Файл) выберите New (Новый), а затем Project (Проект).

3. Слева в качестве основной категории выберите iOS, а под ней — Application (Приложение). В правой части экрана выберите Empty Application (Пустое приложение), после чего нажмите кнопку Next (Далее).

4. На следующем экране введите информацию о вашем проекте и убедитесь, что установлен флажок Use Automatic Reference Counting (Использовать автоматический подсчет ссылок) (рис. 5.3). Как только введете все необходимые значения, нажмите кнопку Next (Далее).

Рис. 5.3. Создание нового проекта Пустое приложение (Empty Application) для сборного вида

5. После этого вам будет предложено сохранить проект на диске. Выберите подходящее для этого место и нажмите кнопку Create (Создать).

6. Теперь, когда проект подготовлен, создайте в нем новый класс и назовите его ViewController. Этот класс должен наследовать от UICollectionViewController. Вам не понадобится. xib-файл для этого контроллера вида, поэтому откажитесь от этой возможности (рис. 5.4).

Рис. 5.4. Добавляем в проект новый класс сборного вида

7. Найдите в проекте файл AppDelegate.m (это файл реализации делегата приложения) и откройте его, после чего создайте экземпляр сборного вида, а затем сделайте этот сборный вид корневым контроллером вида вашего приложения, как показано здесь:

— (BOOL) application:(UIApplication *)application

didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{

/* Инстанцируем контроллер сборного вида с нулевым макетным объектом.

Примечание: в результате программа выдаст исключение, но позже мы

изучим, как создавать макетные объекты и предоставлять их нашим сборным

видам */

ViewController *viewController = [[ViewController alloc]

initWithCollectionViewLayout: nil];

self.window = [[UIWindow alloc] initWithFrame:

[[UIScreen mainScreen] bounds]];

self.window.backgroundColor = [UIColor whiteColor];

/* Устанавливаем сборный вид в качестве корневого для нашего окна */

self.window.rootViewController = viewController;

[self.window makeKeyAndVisible];

return YES;

}

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

Итак, мы уже научились создавать контроллер сборного вида, и это хорошо, если вы хотите, чтобы такой вид при отображении занимал на устройстве весь экран. Однако если вы разрабатываете специальный компонент, входящий в состав другого, более крупного вида, то попробуйте просто инстанцировать объект типа UICollectionView, воспользовавшись его выделенным инициализатором — методом initWithFrame: collectionViewLayout:.

Чтобы это сделать, требуется просто инстанцировать сборный вид, воспользовавшись указанным инициализатором. После инициализации вы сможете добавить сборный вид в качестве дочернего к другому виду. Например, если вы хотите добавить его к вашему виду контроллера вида, просто вызовите метод addSubview: этого вида с контроллером и передайте экземпляр вашего сборного вида этому методу в качестве параметра. Кроме того, нужно убедиться, что в качестве значений свойств delegate и dataSource сборного вида заданы валидные объекты, соответствующие протоколам UICollectionViewDelegate и UICollectionViewDataSource. Выполнить остальные операции не составляет труда. Далее в этой главе описаны все приемы, используемые для наполнения сборного вида информацией из источника данных и реагирования на события с помощью объекта-делегата.

 

См. также

Раздел 5.0.

 

5.2. Присваивание источника данных сборному виду

 

Постановка задачи

Требуется предоставить для сборного вида данные, которые будут выводиться на экран.

 

Решение

Присвойте сборному виду источник данных, воспользовавшись свойством dataSource класса UICollectionView. Источник данных должен быть объектом, который соответствует протоколу UICollectionViewDataSource. Кроме того, само собой разумеется, что объект источника данных обязательно должен реализовывать методы и свойства этого протокола, относящиеся к категории required.

 

Обсуждение

Источник данных сборного вида, как и источник данных табличного вида, отвечает за предоставление сборному виду достаточного для отображения на экране количества информации. Способ отображения данных на экране не входит в компетенцию источника данных — это задача макета. Ячейки, отображаемые макетным объектом в сборном виде, в итоге будут предоставляться источником данных сборного вида.

Вот методы протокола UICollectionViewDataSource, которые обязательно требуется реализовать в вашем источнике данных.

• collectionView: numberOfItemsInSection: — этот метод возвращает объект NSInteger, сообщающий сборному виду количество элементов, которые должны быть отображены в заданной секции. Задаваемая секция сообщается данному методу как целое число, представляющее собой индекс с нулевым основанием для данной секции. Именно так происходит и при работе с табличными видами.

• collectionView: cellForItemAtIndexPath: — ваша реализация этого метода должна возвращать экземпляр UICollectionViewCell, соответствующий ячейке, к которой ведет указанный индексный путь. Класс UICollectionViewCell наследует от UICollectionReusableView. Фактически любая доступная для повторного использования ячейка, передаваемая сборному виду для отображения, должна прямо или косвенно наследовать от UICollectionReusableView, о чем мы подробно поговорим в этой главе. Индексный путь указывается в параметре cellForItemAtIndexPath этого метода. Вы можете запросить индексы section и row этого элемента из индексного пути.

Перейдем к файлу реализации контроллера сборного вида (ViewController.m). Этот контроллер мы создали в разделе 5.1. Реализуем в данном файле рассмотренные ранее методы источника данных сборного вида:

#import «ViewController.h»

@implementation ViewController

/* Пока мы не собираемся возвращать никаких секций */

— (NSInteger)collectionView:(UICollectionView *)collectionView

numberOfItemsInSection:(NSInteger)section{

return 0;

}

/* Мы пока не знаем, как возвращать секции в сборный вид, поэтому для начала возвратим здесь nil */

— (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView

cellForItemAtIndexPath:(NSIndexPath *)indexPath{

return nil;

}

@end

На данном этапе этот код можно считать полным. Но, как было указано в разделе 5.1, при попытке его запустить приложение аварийно завершится. Дело в том, что делегат приложения устанавливает макетный объект сборного вида в значение nil. Эта проблема никуда не исчезла, мы собираемся устранить ее в разделе 5.3.

 

См. также

Разделы 5.0 и 5.1.

 

5.3. Обеспечение последовательной компоновки в сборном виде

 

Постановка задачи

Требуется, чтобы ваш сборный вид был скомпонован как таблица (сетка), чтобы его содержимое отображалось примерно так же, как на рис. 5.1.

 

Решение

Создайте экземпляр класса UICollectionViewFlowLayout, инстанцируйте контроллер сборного вида с помощью выделенного метода-инициализатора initWithCollectionViewLayout: из класса UICollectionViewController, а затем передайте этому методу ваш макетный объект.

 

Обсуждение

Макет для последовательной компоновки очень легко инстанцировать. Но прежде, чем можно будет передать его сборному виду, он должен быть сконфигурирован. Здесь мы обсудим различные свойства экземпляра класса UICollectionViewFlowLayout и поговорим о том, как их можно корректировать. Кроме того, нас интересует, как эти свойства влияют на отображение ячеек сборного вида на экране.

• minimumLineSpacing — значение с плавающей точкой, сообщающее макету с последовательной компоновкой минимальное количество точек, которые необходимо зарезервировать между рядами. Макетный объект может выделить и больше пространства, чтобы компоновка выглядела красиво, но меньше выделить не может. Если ваш сборный вид слишком мал и в него не помещаются все элементы, они будут обрезаться, как и любые другие виды в iOS SDK.

• minimumInteritemSpacing — значение с плавающей точкой, сообщающее макету с последовательной компоновкой минимальное количество точек, которые необходимо зарезервировать между ячейками в одной строке. Опять же это минимальное количество точек, и макет может увеличить это количество в зависимости от размера сборного вида.

• itemSize — величина CGSize, соответствующая размеру каждой ячейки в сборном виде.

• scrollDirection — значение типа UICollectionViewScrollDirection, сообщающее макету с последовательной компоновкой, как должно прокручиваться содержимое сборного вида. Содержимое может прокручиваться либо по горизонтали, либо по вертикали, но не в обоих направлениях одновременно. По умолчанию это свойство имеет значение UICollectionViewScrollDirectionVertical, но вы можете изменить его на UICollectionViewScrollDirectionHorizontal.

• sectionInset — значение типа UIEdgeInsets, задающее размер полей вокруг каждой секции. В принципе, поля — это пространство, которое не относится ни к одной из ячеек. Для создания таких отступов можно воспользоваться функцией UIEdgeInsetsMake. У каждого поля есть верхний, нижний, правый и левый край, все они обозначаются числами с плавающей точкой. Не волнуйтесь, если это объяснение кажется путаным — вскоре все встанет на свои места.

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

#import «AppDelegate.h»

#import «ViewController.h»

@implementation AppDelegate

— (UICollectionViewFlowLayout *) flowLayout{

UICollectionViewFlowLayout *flowLayout =

[[UICollectionViewFlowLayout alloc] init];

flowLayout.minimumLineSpacing = 20.0f;

flowLayout.minimumInteritemSpacing = 10.0f;

flowLayout.itemSize = CGSizeMake(80.0f, 120.0f);

flowLayout.scrollDirection = UICollectionViewScrollDirectionVertical;

flowLayout.sectionInset = UIEdgeInsetsMake(10.0f, 20.0f, 10.0f, 20.0f);

return flowLayout;

}

— (BOOL) application:(UIApplication *)application

didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{

/* Инстанцируем контроллер сборного вида с валидным макетом

для последовательной компоновки */

ViewController *viewController =

[[ViewController alloc]

initWithCollectionViewLayout: [self flowLayout]];

self.window = [[UIWindow alloc] initWithFrame:

[[UIScreen mainScreen] bounds]];

self.window.backgroundColor = [UIColor whiteColor];

/* Задаем сборный вид в качестве корневого контроллера вида окна */

self.window.rootViewController = viewController;

[self.window makeKeyAndVisible];

return YES;

}

Реализация контроллера сборного вида остается такой же, как в разделе 5.2. Если сейчас запустить приложение, то вы увидите просто черный экран, так как в стандартной реализации контроллера сборного вида фон вида даже не заменяется на белый. Пока нас это устраивает. Как минимум приложение уже не завершается аварийно, поскольку у нас уже есть объекты макета.

 

См. также

Разделы 5.1 и 5.2.

 

5.4. Наполнение сборного вида простейшим содержимым

 

Постановка задачи

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

 

Решение

Для представления ваших ячеек либо напрямую воспользуйтесь классом UICollectionViewCell, либо произведите от него подкласс, на базе которого уже сможете написать собственную реализацию. Кроме того, как будет показано далее, у вас может быть файл. xib, ассоциированный с ячейкой.

 

Обсуждение

В данном разделе я исхожу из того, что вы уже проработали разделы 5.1–5.3 и выполнили базовую настройку проекта.

Будем работать по порядку. Начнем с самого простого и самого быстрого способа создания ячеек. Инстанцируем объекты типа UICollectionViewCell и занесем их в сборный вид в нашем источнике данных. У класса UICollectionViewCell есть свойство вида с содержимым, называемое contentView, куда вы можете добавлять для отображения собственные виды. Кроме того, можете задавать и многие другие свойства ячейки, например цвет фона. Именно цветом фона мы и займемся в этом примере. Но перед тем, как начать, опишем, чего мы собираемся добиться в данном разделе, подробно объясним стоящие перед нами требования.

Мы собираемся запрограммировать сборный вид с последовательной компоновкой, в котором будут отображаться три секции. В каждой из секций будет находиться от 20 до 40 ячеек, причем в первой секции все ячейки красные, во второй — зеленые, в третьей — синие (рис. 5.5).

Рис. 5.5. Простой сборный вид с последовательной компоновкой, в котором отображаются три секции с ячейками разных цветов

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

/* У нас будет три секции, и для каждой из них мы определим свой цвет ячеек. Для представления цвета используются самые обычные экземпляры UIColor, которые мы позже применим к каждой из ячеек в соответствующих секциях */

— (NSArray *) allSectionColors{

static NSArray *allSectionColors = nil;

if (allSectionColors == nil){

allSectionColors = @[

[UIColor redColor],

[UIColor greenColor],

[UIColor blueColor],

];

}

return allSectionColors;

}

После этого переопределите выделенный инициализатор initWithCollectionViewLayout: вашего контроллера сборного вида и зарегистрируйте UICollectionViewCell с конкретным идентификатором. Не волнуйтесь, если пока не совсем улавливаете ход мысли, взгляните на ситуацию таким образом: для каждой ячейки, которую должен отобразить ваш сборный вид, программа сначала просмотрит очередь ячеек, доступных для повторного использования, и определит, есть ли в этой очереди устраивающие ее ячейки. Если они найдутся, сборный вид извлечет такие ячейки из очереди, в противном случае он создаст новую ячейку и вернет ее вам для выполнения конфигурации.

В более ранних версиях iOS приходилось вручную создавать ячейки, если табличному виду не удавалось найти готовые ячейки для повторного использования (сборные виды в ранних версиях iOS отсутствовали). Однако с появлением новых API Apple выполнила очень интересную работу, связанную с многократно используемыми ячейками. Компания предоставила новые API как для табличных, так и для сборных видов. Поэтому вы можете зарегистрировать вызов и с табличным видом, и со сборным видом. Если же вам приходится конфигурировать новую ячейку, то вы просто требуете от табличного или сборного вида новую ячейку нужного рода. Если такая ячейка имеется в очереди многократного использования, то она (ячейка) будет вам предоставлена. Если нет — то табличный или сборный вид автоматически создаст такую ячейку. Этот механизм называется регистрацией многоразовой ячейки, он реализуется двумя способами:

• регистрацией ячейки с использованием имени класса;

• регистрацией ячейки с использованием. xib-файла.

Оба этих способа регистрации многоразовых ячеек вполне хороши и отлично работают со сборными видами. Чтобы зарегистрировать новую ячейку для сборного вида, воспользовавшись ее именем класса, применяется метод registerClass: forCellWithReuseIdentifier: класса UICollectionView, где идентификатор — обычная строка, которую вы сообщаете сборному виду. При попытке получить многоразовые ячейки вы запрашиваете у сборного вида ячейку с заданным идентификатором. Чтобы зарегистрировать со сборным видом. xib-файл, необходимо использовать метод экземпляра registerNib: forCellWithReuseIdentifier: сборного вида. Идентификатор этого метода также вполне функционален, об этом рассказано ранее в данном абзаце.

Nib-файл — это объект типа UINib, с ним мы подробнее познакомимся далее в этой главе.

— (instancetype) initWithCollectionViewLayout:(UICollectionViewLayout *)layout{

self = [super initWithCollectionViewLayout: layout];

if (self!= nil){

/* Регистрируем со сборным видом ячейку для ее удобного получения */

[self.collectionView registerClass: [UICollectionViewCell class]

forCellWithReuseIdentifier: kCollectionViewCellIdentifier];

}

return self;

}

Как видите, в качестве идентификатора для ячеек мы используем константное значение kCollectionViewCellIdentifier. Необходимо определить его в контроллере вида:

#import «ViewController.h»

static NSString *kCollectionViewCellIdentifier = @"Cells";

@implementation ViewController

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

— (NSInteger)numberOfSectionsInCollectionView

:(UICollectionView *)collectionView{

return [self allSectionColors].count;

}

Одно из требований, предъявляемых к нашему приложению, такое: каждая секция должна содержать не менее 20, но не более 40 ячеек. Эту задачу можно решить с помощью функции arc4random_uniform(x). Она возвращает положительные целые числа в диапазоне от 0 до x, где x — параметр, который вы сообщаете этой функции. Следовательно, если требуется сгенерировать число в диапазоне от 20 до 40, всего лишь нужно прибавить 20 к возвращаемому значению этой функции, а значение x также сделать равным 20. Зная это, реализуем метод collectionView: numberOfItemsInSection: из источника данных сборного вида:

— (NSInteger)collectionView:(UICollectionView *)collectionView

numberOfItemsInSection:(NSInteger)section{

/* Генерируем от 20 до 40 ячеек для заполнения каждой секции */

return 20 + arc4random_uniform(21);

}

Наконец, требуется предоставить ячейки для сборного вида. Для этого реализуем метод collectionView: cellForItemAtIndexPath:, относящийся к источнику данных сборного вида:

— (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView

cellForItemAtIndexPath:(NSIndexPath *)indexPath{

UICollectionViewCell *cell =

[collectionView

dequeueReusableCellWithReuseIdentifier: kCollectionViewCellIdentifier

forIndexPath: indexPath];

cell.backgroundColor = [self allSectionColors][indexPath.section];

return cell;

}

Индексные пути просто содержат номер секции и номер строки. Поэтому индексный путь 0, 1 указывает, что речь идет о второй строке первой секции, поскольку индексы имеют нулевое основание. Если же мы захотим указать пятую строку десятой секции, то обозначим индексный путь как 9, 4. Индексные пути очень широко используются при работе с табличными и сборными видами, так как органично подходят для описания секций, каждая из которых наполнена ячейками. Делегаты и источники данных для табличных и сборных видов при работе указывают целевую ячейку именно по ее индексному пути. Например, если пользователь нажмет ячейку в сборном виде, то вы получите его индексный путь. С помощью этого индексного пути вы также сможете просмотреть базовую структуру данных конкретной ячейки (речь идет о данных, которые использовались в вашем классе для создания этой ячейки).

Как видите, здесь используется метод экземпляра dequeueReusableCellWithReuseIdentifier: forIndexPath:, относящийся к сборному виду. Этот метод применяется для извлечения многоразовых ячеек из очереди. Этот метод ожидает получения двух параметров: идентификатора ячейки, которую вы ранее зарегистрировали с этим сборным видом, а также индексного пути, по которому должна быть отображена ячейка. Индексный путь вы получаете в том же самом методе collectionView: cellForItemAtIndexPath: в качестве параметра, поэтому остается всего лишь сообщить идентификатор ячейки.

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

Итак, остался последний шаг перед завершением данного примера. Сделаем фон сборного вида белым, чтобы он выглядел немного лучше, чем со стандартным черным фоном. Реализуйте метод viewDidLoad контроллера сборного вида и задайте фоновый цвет для данного вида прямо в этом методе:

— (void) viewDidLoad{

[super viewDidLoad];

self.collectionView.backgroundColor = [UIColor whiteColor];

}

Экземпляр UICollectionViewController имеет вид типа UIView, к которому можно получить доступ по его свойству view. Не путайте этот вид со свойством collectionView вашего контроллера, соответствующим той сущности, в которой располагается сам сборный вид.

Красота решения, предложенного в данном разделе, заключается в том, что оно отлично работает и на iPad, и на iPhone. На рис. 5.5 показано, как результат выглядит на iPad, на рис. 5.6 — как на iPhone.

Рис. 5.6. Простой сборный вид, изображенный в эмуляторе iPhone

 

См. также

Разделы 5.1–5.3.

 

5.5. Заполнение сборных видов специальными ячейками с помощью XIB-файлов

 

Постановка задачи

Требуется сконфигурировать ячейки сборного вида в конструкторе интерфейса и заполнить ими сборный вид для последующего отображения.

 

Решение

Выполните следующие шаги.

1. Создайте подкласс UICollectionViewCell и назовите его (в данном примере мы будем использовать имя CollectionViewCell).

2. Создайте пустой. xib-файл и назовите его MyCollectionViewCell.xib.

3. Поместите в конструктор интерфейса ячейку сборного вида (ее вы найдете в библиотеке объектов). Она должна оказаться в вашем пустом. xib-файле (рис. 5.7). В конструкторе интерфейсов измените имя объекта-ячейки на MyCollectionViewCell (рис. 5.8). Поскольку вы устанавливаете такую ассоциацию, когда загружаете. xib-файл программно, специальный класс MyCollectionViewCell будет автоматически попадать в память. Волшебство, да и только!

Рис. 5.7. Объект пользовательского интерфейса «ячейка сборного вида» в библиотеке объектов конструктора интерфейса

Рис. 5.8. Присваивание специального класса. xib-файлу ячейке специального сборного вида

4. Оформите ячейку в конструкторе интерфейса. Необходимо гарантировать, что для каждого компонента пользовательского интерфейса, который вы помещаете в ячейку, создается также ассоциированный с ней объект IBOutlet, расположенный либо в заголовочном файле, либо в файле реализации вашего класса (MyCollectionViewCell).

5. Зарегистрируйте с вашим сборным видом. nib-файл, воспользовавшись для этого методом экземпляра registerNib: forCellWithReuseIdentifier: сборного вида. Чтобы загрузить. nib-файл в память, используется метод класса nibWithNibName: bundle:, относящийся к классу UINib. Об этом методе мы вскоре поговорим.

 

Обсуждение

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

1. Откройте File — New — File (Файл — Новый — Файл).

2. В левой части экрана в категории iOS выберите запись User Interface (Пользовательский интерфейс), а в правой части — вариант Empty (Пустой).

3. Теперь система запросит у вас семейство устройств, к которому относится данный. xib-файл. Здесь просто выберите iPhone.

4. Далее вам будет предложено сохранить. xib-файл на диск. Сохраните его под именем MyCollectionViewCell.xib.

Кроме того, потребуется создать класс, который вы сможете связать с содержимым. xib-файла. Этот класс мы назовем MyCollectionViewCell, он будет наследовать от UICollectionViewCell. Чтобы его создать, выполните следующие шаги.

1. Откройте File-New-File (Файл-Новый-Файл).

2. В диалоговом окне для создания нового файла в категории iOS выберите вариант Cocoa Touch. В правой части экрана выберите Objective-C class (Класс Objective-C).

3. Назовите класс MyCollectionViewCell и выберите UICollectionViewCell в качестве его класса-предка.

4. Когда вам будет предложено сохранить файл на диске, так и сделайте.

Теперь нужно ассоциировать созданный класс с. xib-файлом. Для этого выполните следующие шаги.

1. Откройте файл MyCollectionViewCell.xib в конструкторе интерфейса. В библиотеке объектов просто найдите объект Collection View Cell (Ячейка сборного вида) и поместите ее в. xib-файл. По умолчанию эта ячейка будет очень маленькой (50 × 50 точек) и будет иметь черный фон.

2. Явно выберите эту ячейку в вашем. xib-файле, щелкнув на ней. Откройте инспектор идентичности (Identity Inspector) в конструкторе интерфейса и измените значение поля Class (Класс) на MyCollectionViewCell, как было показано на рис. 5.8.

Далее следует добавить в ячейку нужные компоненты пользовательского интерфейса. Позже при заполнении сборного вида информацией значения этих компонентов можно будет изменить. Для данного примера лучше всего подойдет вид с изображением. Поэтому, когда у вас в конструкторе интерфейса открыт файл MyCollectionViewCell.xib, поместите в него экземпляр UIImageView. Подключите этот вид с изображением к заголовочному файлу вашей ячейки (MyCollectionViewCell.h) и назовите его imageViewBackgroundImage, чтобы заголовочный файл ячейки выглядел примерно так:

#import 

@interface MyCollectionViewCell: UICollectionViewCell

@property (weak, nonatomic) IBOutlet UIImageView *imageViewBackgroundImage;

@end

Мы собираемся заполнить этот вид разными изображениями. В этом разделе я создал для работы три простых изображения, каждое размером 50 × 50 точек. Вы можете пользоваться любыми другими картинками на ваш выбор — просто поищите их в Интернете. Когда найдете понравившиеся вам картинки, добавьте их в свой проект. Убедитесь, что изображения называются 1.png, 2.png и 3.png и что их увеличенные вдвое аналоги для сетчатого дисплея называются , и .

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

Первое изменение, которое предстоит внести, заключается в написании метода, с помощью которого мы будем возвращать случайное изображение. Как было объяснено ранее, у нас есть массив изображений. После инстанцирования массива нам понадобится удобный небольшой метод, который будет выбирать из массива случайное изображение:

— (NSArray *) allImages{

static NSArray *AllSectionImages = nil;

if (AllSectionImages == nil){

AllSectionImages = @[

[UIImage imageNamed:@"1"],

[UIImage imageNamed:@"2"],

[UIImage imageNamed:@"3"]

];

}

return AllSectionImages;

}

— (UIImage *) randomImage{

return [self allImages][arc4random_uniform([self allImages].count)];

}

Далее потребуется переопределить выделенный метод-инициализатор контроллера сборного вида, чтобы зарегистрировать. nib-файл MyCollectionViewCell с этим сборным видом:

— (instancetype) initWithCollectionViewLayout:(UICollectionViewLayout *)layout{

self = [super initWithCollectionViewLayout: layout];

if (self!= nil){

/* Регистрируем nib-файл со сборным видом для удобства получения информации */

UINib *nib = [UINib nibWithNibName:

NSStringFromClass([MyCollectionViewCell class])

bundle: [NSBundle mainBundle]];

[self.collectionView registerNib: nib

forCellWithReuseIdentifier: kCollectionViewCellIdentifier];

}

return self;

}

В ответ на запрос о том, сколько у нас секций, также возвратим случайное число в диапазоне от 3 до 6. Это требование не является обязательным — мы вполне могли бы обойтись и одной секцией, но если их будет больше, это точно не помешает. Кроме того, в каждой секции должно быть от 10 до 15 ячеек:

— (NSInteger)numberOfSectionsInCollectionView

:(UICollectionView *)collectionView{

/* От 3 до 6 секций */

return 3 + arc4random_uniform(4);

}

— (NSInteger)collectionView:(UICollectionView *)collectionView

numberOfItemsInSection:(NSInteger)section{

/* В каждой секции — от 10 до 15 ячеек */

return 10 + arc4random_uniform(6);

}

Наконец, запросим у сборного вида ячейки, а затем сконфигурируем их со случайными фоновыми изображениями:

— (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView

cellForItemAtIndexPath:(NSIndexPath *)indexPath{

MyCollectionViewCell *cell =

[collectionView

dequeueReusableCellWithReuseIdentifier: kCollectionViewCellIdentifier

forIndexPath: indexPath];

cell.imageViewBackgroundImage.image = [self randomImage];

cell.imageViewBackgroundImage.contentMode = UIViewContentModeScaleAspectFit;

return cell;

}

Как видите, мы используем специальный класс MyCollectionViewCell в контроллере сборного вида. Чтобы программа успешно скомпилировалась, необходимо включить заголовочный файл ячейки в реализацию контроллера вида, вот так:

#import «ViewController.h»

#import «MyCollectionViewCell.h»

Запустив приложение, вы увидите примерно такую картинку, как на рис. 5.9. Разумеется, если вы используете в примере другие изображения, она будет другой, но у меня тут показаны нотки.

Рис. 5.9. Сборный вид со специальными ячейками, загруженными из. nib-файла

 

См. также

Раздел 5.4.

 

5.6. Обработка событий в сборных видах

 

Постановка задачи

Необходимо обрабатывать события, происходящие в сборных видах, например касания.

 

Решение

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

 

Обсуждение

У сборных видов есть свойства delegate, которые должны соответствовать протоколу UICollectionViewDelegate. Делегатный объект, создаваемый с их помощью, будет получать различные делегатные вызовы от сборного вида, сообщающего делегату о различных событиях, например о том, что элемент был подсвечен или выделен. Необходимо различать подсвеченное и выделенное состояние ячейки сборного вида. Когда пользователь нажимает пальцем на ячейку сборного вида, но не поднимает палец сразу после этого, ячейка под пальцем становится подсвеченной. Когда пользователь нажимает на ячейку, а затем сразу поднимает палец (это означает, что он хочет совершить с ячейкой какое-то действие), данная ячейка становится выделенной.

Ячейки сборных видов, относящиеся к типу UICollectionViewCell, имеют два очень полезных свойства — highlighted и selected.

Если вы хотите всего лишь изменить визуальное оформление вашей ячейки, когда она выделена, то задача тем более упрощается, поскольку ячейки типа UICollectionViewCell предоставляют свойство selectedBackgroundView типа UIView. В качестве значения этого свойства можно задать любой валидный вид. Затем этот вид отобразится на экране, как только ячейка будет выделена. Продемонстрируем эти возможности на основе кода, который мы написали в разделе 5.5. Как вы помните, там мы создали специальную ячейку, одно из свойств которой (imageViewBackgroundImage) снабжало ее фоновым изображением. Изображение заполняло весь фон ячейки. В этот вид с изображением мы загружали специально подобранные картинки. Теперь мы собираемся залить фон ячейки голубым цветом, как только она будет выделена. Поскольку вид с изображением находится поверх всех остальных компонентов сборного вида, перед заданием фонового цвета нам придется гарантировать, что этот вид с изображением будет прозрачным. Для этого оттенок фона вида с изображением нужно изменить на проницаемый. Дело в том, что по умолчанию фон у вида с изображением непрозрачный, поэтому если расположить такой вид поверх другого вида, имеющего фоновый цвет, то этот фоновый цвет, естественно, виден не будет. Соответственно, чтобы оставался виден фоновый цвет того вида, который является вышестоящим для вида с изображением, фон самого вида с изображением должен быть прозрачным. Итак, начнем:

#import «MyCollectionViewCell.h»

@implementation MyCollectionViewCell

— (void) awakeFromNib{

[super awakeFromNib];

self.imageViewBackgroundImage.backgroundColor = [UIColor clearColor];

self.selectedBackgroundView = [[UIView alloc] initWithFrame: self.bounds];

self.selectedBackgroundView.backgroundColor = [UIColor blueColor];

}

@end

Вот и все! Теперь если нажать любую ячейку в вашей программе, она сразу приобретет голубой цвет фона.

Конечно, есть и другие операции, для выполнения которых требуется слушать различные события, происходящие в сборном виде. Например, может понадобиться воспроизвести звук или анимацию, как только оказывается выделенной ячейка. Допустим, когда пользователь прикасается к ячейке на экране, мы хотим задействовать следующую анимацию: немедленно скрыть ячейку, а потом снова ее отобразить. Эта анимация повторяется с очень высокой частотой, в результате чего ячейка постепенно вырисовывается или постепенно исчезает из виду. Если мы хотим добиться именно такого эффекта, для начала зададим делегат для нашего сборного вида, так как в описанном сценарии мы действительно будем получать от вида множество событий. Как было указано ранее, ваш делегатный объект должен соответствовать протоколу UICollectionViewDelegate. В этом протоколе есть несколько полезных методов, которые мы можем реализовать. Далее перечислены некоторые важнейшие методы этого протокола.

Протокол UICollectionViewDelegateFlowLayout, как и рассмотренный нами в главе 4 протокол UITableViewDelegate, позволяет сообщать информацию о ваших элементах — например, значения их высоты и ширины, — а потом передавать эти значения макету с последовательной компоновкой. Можно сразу предоставить для всех элементов такого макета обобщенное значение размера, тогда они получатся одинаковыми. Другой вариант — реагировать на соответствующие сообщения, которые вы будете получать от протокола делегата макета с последовательной компоновкой. В этих сообщениях программа будет запрашивать у вас значения размеров для тех или иных ячеек в макете.

• collectionView: didHighlightItemAtIndexPath: — вызывается в делегате, когда ячейка подсвечивается.

• collectionView: didUnhighlightItemAtIndexPath: — вызывается в делегате, когда ячейка выходит из подсвеченного состояния. Этот метод срабатывает, когда пользователь успешно завершает событие касания (попадает пальцем по нужному элементу, а потом поднимает палец, совершая, таким образом, жест касания). В другом случае этот метод срабатывает, когда пользователь отменяет сделанное ранее выделение, выводя палец за пределы ячейки.

• collectionView: didSelectItemAtIndexPath: — этот метод вызывается в делегатном объекте, когда конкретная ячейка становится выделенной. Ячейка всегда является подсвеченной, перед тем как стать выделенной.

• collectionView: didDeselectItemAtIndexPath: — вызывается в делегате, когда ячейка выходит из выделенного состояния.

Итак, напишем приложение в соответствии с изложенными выше требованиями. Мы хотим, чтобы ячейка «развоплощалась», а потом вновь «вырисовывалась» на экране, когда ее выделяют. В экземпляре UICollectionViewController реализуем метод collectionView: didSelectItemAtIndexPath:, вот так:

#import «ViewController.h»

#import «MyCollectionViewCell.h»

static NSString *kCollectionViewCellIdentifier = @"Cells";

@implementation ViewController

— (void) collectionView:(UICollectionView *)collectionView

didSelectItemAtIndexPath:(NSIndexPath *)indexPath{

UICollectionViewCell *selectedCell =

[collectionView cellForItemAtIndexPath: indexPath];

const NSTimeInterval kAnimationDuration = 0.20;

[UIView animateWithDuration: kAnimationDuration animations: ^{

selectedCell.alpha = 0.0f;

} completion: ^(BOOL finished) {

[UIView animateWithDuration: kAnimationDuration animations: ^{

selectedCell.alpha = 1.0f;

}];

}];

}

Мы пишем этот код в контроллере сборного вида, который по умолчанию выбирается системой и в качестве источника данных, и в качестве делегата этого сборного вида. Он соответствует протоколам UICollectionViewDataSource и UICollectionViewDelegate. Следовательно, вы просто можете реализовать любой метод делегата или источника данных прямо в файле реализации вашего контроллера сборного вида.

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

Итак, тут все было просто. Рассмотрим другой пример. Допустим, когда ячейка подсвечивается, мы хотим сделать ее вдвое крупнее обычного ее размера, а при выходе этой ячейки из подсвеченного состояния вернуть ей исходный размер. Таким образом, когда пользователь прикасается пальцем к ячейке (но еще не поднимает палец), ячейка увеличивается вдвое, а когда пользователь убирает палец — вновь уменьшается, тоже вдвое. Для этого нам потребуется реализовать в контроллере сборного вида методы collectionView: didHighlightItemAtIndexPath: и collectionView: didUnhighlightItemAtIndexPath: из протокола UICollectionViewDelegate. Как вы помните, контроллеры сборных видов по умолчанию соответствуют протоколам UICollectionViewDelegate и UICollectionViewDataSource:

#import «ViewController.h»

#import «MyCollectionViewCell.h»

static NSString *kCollectionViewCellIdentifier = @"Cells";

const NSTimeInterval kAnimationDuration = 0.20;

@implementation ViewController

— (void) collectionView:(UICollectionView *)collectionView

didHighlightItemAtIndexPath:(NSIndexPath *)indexPath{

UICollectionViewCell *selectedCell =

[collectionView cellForItemAtIndexPath: indexPath];

[UIView animateWithDuration: kAnimationDuration animations: ^{

selectedCell.transform = CGAffineTransformMakeScale(2.0f, 2.0f);

}];

}

— (void) collectionView:(UICollectionView *)collectionView

didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath{

UICollectionViewCell *selectedCell =

[collectionView cellForItemAtIndexPath: indexPath];

[UIView animateWithDuration: kAnimationDuration animations: ^{

selectedCell.transform = CGAffineTransformMakeScale(1.0f, 1.0f);

}];

}

Как видите, мы используем функцию CGAffineTransformMakeScale из фреймворка Core Graphics для создания аффинного преобразования, а потом присваиваем это преобразование самой ячейке. Достигается нужный эффект: сначала ячейка увеличивается вдвое, а потом уменьшается до исходного размера. Эта функция подробнее описана в разделе 17.12.

 

См. также

Разделы 5.2, 5.3, 5.5, 17.12.

 

5.7. Создание верхних и нижних колонтитулов в макете с последовательной компоновкой

 

Постановка задачи

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

 

Решение

Выполните следующие шаги.

1. Создайте по файлу. xib для верхнего и для нижнего колонтитулов.

2. Найдите в библиотеке объектов конструктора интерфейса по одному объекту Collection Reusable View и перетащите их в ваши. xib-файлы. Убедитесь, что эти многоразовые сборные виды являются единственными видами в своих. xib-файлах. Таким образом, многоразовый сборный вид становится корневым видом вашего. xib-файла. Именно так создаются колонтитулы в сборных видах.

3. Если вы хотите более полно контролировать поведение. xib-файлов, создайте класс Objective-C и ассоциируйте с ним корневой вид вашего. xib-файла. Таким образом, всякий раз, когда iOS будет загружать с диска содержимое. xib-файла, ассоциированный с ним класс также будет загружаться в память и вы будете получать доступ к иерархии видов в. xib-файле.

4. Инстанцируйте метод экземпляра registerNib: forSupplementaryViewOfKind: withReuseIdentifier: сборного вида и зарегистрируйте ваши nib-файлы для разновидностей видов UICollectionElementKindSectionHeader и UICollectionElementKindSectionFooter.

5. Чтобы правильно оформить виды верхних и нижних колонтитулов перед тем, как они будут отображены, реализуйте метод collectionView: viewForSupplementaryElementOfKind: atIndexPath: источника данных сборного вида, а в этом методе запустите другой метод сборного вида, dequeueReusableSupplementaryViewOfKind: withReuseIdentifier: forIndexPath:, чтобы извлечь из очереди многоразовый вид верхнего или нижнего колонтитула.

6. Наконец, необходимо убедиться, что размер видов для верхних и нижних колонтитулов задается путем присваивания соответствующих значений свойствам headerReferenceSize и footerReferenceSize макетного объекта, отвечающего за последовательную компоновку.

 

Обсуждение

Итак, теперь нам требуется создать. xib-файлы для специальных верхних и нижних колонтитулов. Назовем их Header.xib и Footer.xib. Мы создаем их по тому же принципу, который описан в разделе 5.5, поэтому я не буду вновь повторять здесь этот материал. Убедитесь в том, что и для верхнего и для нижнего колонтитула у вас есть по одному классу Objective-C. Назовите их соответственно Header и Footer. Необходимо гарантировать, что оба этих класса наследуют от UICollectionReusableView. Закончив с этим, сконфигурируйте в конструкторе интерфейса подпись и кнопку, затем перетащите подпись в файл Header, а кнопку — в файл Footer. Свяжите их с вашими классами так, как показано на рис. 5.10 и 5.11.

Рис. 5.10. Конфигурирование ячейки верхнего колонтитула для сборного вида в конструкторе интерфейса

Рис. 5.11. Конфигурирование ячейки нижнего колонтитула для сборного вида в конструкторе интерфейса

Я связал подпись из верхнего колонтитула с классом Header с помощью свойства аутлета в файле Header.h. Назовем аутлет просто label:

#import 

@interface Header: UICollectionReusableView

@property (weak, nonatomic) IBOutlet UILabel *label;

@end

То же самое я делаю и в нижнем колонтитуле, связав кнопку из файла Footer.xib с аутлетом из файла Footer.h и назвав аутлет button:

#import 

@interface Footer: UICollectionReusableView

@property (weak, nonatomic) IBOutlet UIButton *button;

@end

Теперь в контроллере сборного вида определим идентификаторы для ячеек верхнего и нижнего колонтитулов:

#import «ViewController.h»

#import «MyCollectionViewCell.h»

#import «Header.h»

#import «Footer.h»

static NSString *kCollectionViewCellIdentifier = @"Cells";

static NSString *kCollectionViewHeaderIdentifier = @"Headers";

static NSString *kCollectionViewFooterIdentifier = @"Footers";

@implementation ViewController

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

— (instancetype) initWithCollectionViewLayout:(UICollectionViewLayout *)layout{

self = [super initWithCollectionViewLayout: layout];

if (self!= nil){

/* Регистрируем nib-файл со сборным видом для удобного получения */

UINib *nib = [UINib nibWithNibName:

NSStringFromClass([MyCollectionViewCell class])

bundle: [NSBundle mainBundle]];

[self.collectionView registerNib: nib

forCellWithReuseIdentifier: kCollectionViewCellIdentifier];

/* Регистрируем nib-файл верхнего колонтитула */

UINib *headerNib = [UINib

nibWithNibName: NSStringFromClass([Header class])

bundle: [NSBundle mainBundle]];

[self.collectionView registerNib: headerNib

forSupplementaryViewOfKind: UICollectionElementKindSectionHeader

withReuseIdentifier: kCollectionViewHeaderIdentifier];

/* Регистрируем nib-файл нижнего колонтитула */

UINib *footerNib = [UINib

nibWithNibName: NSStringFromClass([Footer class])

bundle: [NSBundle mainBundle]];

[self.collectionView registerNib: footerNib

forSupplementaryViewOfKind: UICollectionElementKindSectionFooter

withReuseIdentifier: kCollectionViewFooterIdentifier];

}

return self;

}

Переходим к реализации метода collectionView: viewForSupplemen taryElementOfKind: atIndexPath: сборного вида. Этот метод нужен нам для конфигурирования верхних и нижних колонтитулов и предоставления их обратно сборному виду:

— (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView

viewForSupplementaryElementOfKind:(NSString *)kind

atIndexPath:(NSIndexPath *)indexPath{

NSString *reuseIdentifier = kCollectionViewHeaderIdentifier;

if ([kind isEqualToString: UICollectionElementKindSectionFooter]){

reuseIdentifier = kCollectionViewFooterIdentifier;

}

UICollectionReusableView *view =

[collectionView dequeueReusableSupplementaryViewOfKind: kind

withReuseIdentifier: reuseIdentifier

forIndexPath: indexPath];

if ([kind isEqualToString: UICollectionElementKindSectionHeader]){

Header *header = (Header *)view;

header.label.text = [NSString stringWithFormat:@"Section Header %lu",

(unsigned long)indexPath.section + 1];

}

else if ([kind isEqualToString: UICollectionElementKindSectionFooter]){

Footer *footer = (Footer *)view;

NSString *title = [NSString stringWithFormat:@"Section Footer %lu",

(unsigned long)indexPath.section + 1];

[footer.button setTitle: title forState: UIControlStateNormal];

}

return view;

}

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

— (UICollectionViewFlowLayout *) flowLayout{

UICollectionViewFlowLayout *flowLayout =

[[UICollectionViewFlowLayout alloc] init];

flowLayout.minimumLineSpacing = 20.0f;

flowLayout.minimumInteritemSpacing = 10.0f;

flowLayout.itemSize = CGSizeMake(80.0f, 120.0f);

flowLayout.scrollDirection = UICollectionViewScrollDirectionVertical;

flowLayout.sectionInset = UIEdgeInsetsMake(10.0f, 20.0f, 10.0f, 20.0f);

/* Задаем базовый размер для видов с верхними и нижними колонтитулами */

flowLayout.headerReferenceSize = CGSizeMake(300.0f, 50.0f);

flowLayout.footerReferenceSize = CGSizeMake(300.0f, 50.0f);

return flowLayout;

}

Итак, все готово! Если вы теперь запустите приложение в эмуляторе iPad, то увидите примерно такой результат, как на рис. 5.12.

Рис. 5.12. Верхние и нижние колонтитулы в сборном виде

 

См. также

Разделы 5.2, 5.3, 5.5.

 

5.8. Добавление собственных вариантов взаимодействий к сборным видам

 

Постановка задачи

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

 

Решение

Инстанцируйте механизм распознавания жестов, а потом просмотрите распознаватели жестов, уже имеющиеся в сборном виде, и проверьте, нет ли среди них такого, который похож на нужный вам. Если найдете такой механизм, вызовите в нем метод requireGestureRecognizerToFail: и передайте этому методу в качестве параметра ваш собственный распознаватель жестов. Так вы гарантируете, что распознаватель жестов, имеющийся в сборном виде и напоминающий тот, что нужен вам, будет в случае необходимости подхватывать обработку жестов. Это станет происходить, если вашему распознавателю жестов не удастся обработать те или иные данные либо данные не будут соответствовать его требованиям/критериям. Таким образом, если ваш распознаватель способен обработать жест, он это сделает, в противном случае жест будет передан механизму распознавания, уже имеющемуся в сборном виде. Этот механизм обработает его сам.

Как только выполните описанную работу, добавьте ваш распознаватель жестов к сборному виду. Как вы помните, в экземпляре UICollectionViewController объект вашего сборного вида будет доступен через свойство контроллера collectionView, а не через свойство view.

 

Обсуждение

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

Рассмотрим пример. В этом примере мы собираемся добавить к сборному виду возможность уменьшения и увеличения изображения (то есть его масштабирования). Этот пример будет выстроен на основе того, который мы подготовили в разделе 5.5. Итак, первым делом мы должны добавить распознаватель щипков в коллекцию распознавателей жестов, имеющихся в сборном виде. Мы сделаем это в методе viewDidLoad контроллера сборного вида:

— (void) viewDidLoad{

[super viewDidLoad];

self.collectionView.backgroundColor = [UIColor whiteColor];

UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc]

initWithTarget: self

action:@selector(handlePinches:)];

for (UIGestureRecognizer *recognizer in

self.collectionView.gestureRecognizers){

if ([recognizer isKindOfClass: [pinch class]]){

[recognizer requireGestureRecognizerToFail: pinch];

}

}

[self.collectionView addGestureRecognizer: pinch];

}

Настраиваем распознаватель жестов щипка для вызова метода handlePinches: контроллера вида. Сейчас мы напишем этот метод:

— (void) handlePinches:(UIPinchGestureRecognizer *)paramSender{

CGSize DefaultLayoutItemSize = CGSizeMake(80.0f, 120.0f);

UICollectionViewFlowLayout *layout =

(UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout;

layout.itemSize =

CGSizeMake(DefaultLayoutItemSize.width * paramSender.scale,

DefaultLayoutItemSize.height * paramSender.scale);

[layout invalidateLayout];

}

В этом коде есть две очень важные детали.

1. Предполагается, что по умолчанию размер элемента в макете последовательной компоновки сборного вида имеет ширину 80 точек и высоту 120 точек. Именно так мы создали макет с последовательной компоновкой для сборного вида в разделе 5.3. Затем мы берем коэффициент масштабирования, полученный от распознавателя жестов щипка, и умножаем на него размер элементов из сборного вида. В результате эти экранные элементы могут уменьшиться или увеличиться в зависимости от того, как именно пользователь масштабирует экран.

2. После того как был изменен размер элемента, применяемый по умолчанию в макете с последовательной компоновкой, макет необходимо обновить. В табличных видах мы обновляли либо нужные секции таблицы, либо всю таблицу, но в данном случае обновляем или упраздняем макет, прикрепленный к сборному виду. Это делается, чтобы сборный вид полностью «перерисовал» себя после изменения макета. Поскольку сборный вид в каждый момент времени может содержать всего один макетный объект, при упразднении такого макетного объекта потребуется перезагрузить весь сборный вид. Если бы мы могли иметь отдельный макет для каждой секции, то могли бы перезагружать только те секции, которые связаны с данным макетом. Но, имея такой код, как сейчас, при упразднении макетного объекта придется перерисовывать весь сборный вид.

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

 

См. также

Разделы 5.3 и 5.5.

 

5.9. Представление контекстных меню в ячейках сборных видов

 

Постановка задачи

Если пользователь нажимает на один из экранных элементов в вашем сборном виде и удерживает на нем палец, требуется вывести контекстное меню. С помощью команд из этого меню элемент можно будет скопировать, переместить и т. д.

 

Решение

Контекстные меню по умолчанию встроены в сборные виды. Чтобы активизировать их, потребуется всего лишь реализовать следующие методы из протокола UICollectionViewDelegate.

• collectionView: shouldShowMenuForItemAtIndexPath: — среда времени исполнения передает этому методу индексный путь к элементу. Метод возвращает логическое значение, указывающее дальнейшее действие: должен этот элемент открывать контекстное меню или нет.

• collectionView: canPerformAction: forItemAtIndexPath: withSender: — среда времени исполнения передает этому методу селектор типа SEL. Можно проверить селектор (обычно для этого он преобразуется в строку, которая затем сравнивается со строкой, представляющей действие) и определить, хотите ли вы, чтобы указанное действие произошло. Возвратите YES, чтобы разрешить такое действие, либо NO, чтобы подавить его. Не забывайте, что вы всегда можете преобразовать селектор в строку, воспользовавшись методом NSStringFromSelector. Типичные примеры селекторов — copy: или paste: для команд контекстного меню Копировать или Вставить соответственно.

• collectionView: performAction: forItemAtIndexPath: withSender: — здесь выполняется действие, которое было с вашего разрешения отображено в сборном виде с помощью вышеупомянутых делегатных методов.

 

Обсуждение

Не откладывая в долгий ящик, расширим код, написанный в разделе 5.5. Мы будем выводить в ячейках контекстное меню Copy (Копировать), если пользователь нажмет на ячейку и на некоторое время задержит на ней палец. Когда пользователь выбирает элемент в меню копирования, мы скопируем изображение из ячейки в буфер обмена. После этого пользователь сможет вставить это изображение в файлы из других программ, например из почтового приложения Mail.

Первым делом реализуем в делегате сборного вида метод collectionView: shouldShowMenuForItemAtIndexPath:. В данном примере мы работаем с контроллером сборного вида, который сам является и делегатом и источником данных. Поэтому фактически нам придется всего лишь реализовать вышеупомянутый метод в контроллере сборного вида, вот так:

— (BOOL) collectionView:(UICollectionView *)collectionView

shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath{

return YES;

}

Теперь мы хотим обеспечить, чтобы в ячейках нашего сборного вида отображалось лишь контекстное меню для копирования. В рамках этого примера рассмотрим, как можно отфильтровать доступные элементы меню и отобразить только нужные:

— (BOOL) collectionView:(UICollectionView *)collectionView

canPerformAction:(SEL)action

forItemAtIndexPath:(NSIndexPath *)indexPath

withSender:(id)sender{

if (action == @selector(copy:)){

return YES;

}

return NO;

}

Как видите, нам даже не требуется преобразовывать селектор в строку, чтобы сравнить его с такими строками, как copy:. Мы всего лишь используем оператор равенства, чтобы проверить, отвечает ли запрошенный селектор нашим ожиданиям. Если это так, возвращаем YES, в противном случае — NO.

Наконец, нам потребуется реализовать в делегате метод collectionView: performAction: forItemAtIndexPath: withSender:. С помощью этого метода мы узнаем, было ли вызвано действие копирования, а потом копируем изображение, взятое из ячейки, в буфер обмена. Пользователь сможет вставить из буфера изображение в файл из совершенно другого приложения:

— (void) collectionView:(UICollectionView *)collectionView

performAction:(SEL)action

forItemAtIndexPath:(NSIndexPath *)indexPath

withSender:(id)sender{

if (action == @selector(copy:)){

MyCollectionViewCell *cell = (MyCollectionViewCell *)[collectionView

cellForItemAtIndexPath: indexPath];

[[UIPasteboard generalPasteboard]

setImage: cell.imageViewBackgroundImage.image];

}

}

Теперь, если вы запустите приложение и нажмете один из элементов в сборном виде, а потом будете удерживать на нем палец, то получите примерно такой результат, как на рис. 5.13.

Рис. 5.13. Элемент контекстного меню, отображаемый в ячейке сборного вида

 

См. также

Раздел 5.5.