Из этого туториала вы узнаете, как использовать класс iOS 6 UICollectionView
для создания макета «Springboard», который выглядит и действует как домашний экран iOS! Попутно вы получите полное представление об основах представления коллекции, чтобы создать свои собственные макеты на основе сетки.
Предварительный просмотр проекта
Обзор Представления Коллекции
Представления коллекций — это замечательное новое дополнение к API UIKit в iOS 6. Некоторые знатоки платформы считают, что представления коллекций должны стать таким же фундаментальным и распространяющимся компонентом в дизайне приложений iOS, как представления таблиц (и ваш автор скромен склонен согласиться!). Представления коллекций и представления таблиц имеют много общих принципов проектирования (не говоря уже о похожем API), поскольку они служат общей цели — представить коллекцию связанных данных. Лучше не рассматривать представления коллекций как замену представлениям таблиц, а скорее как новое предложение более общей и гибкой архитектуры для взаимодействия с не табличными наборами данных.
Поскольку представления коллекций должны быть гибкими, расширяемыми и настраиваемыми, архитектура и, следовательно, API на первый взгляд могут показаться немного сложными. Есть ряд взаимодействующих классов, о которых нужно знать. В зависимости от того, насколько просты или сложны ваши требования, вам может потребоваться создать подкласс некоторых из этих классов, чтобы достичь желаемых результатов. К счастью, API разработан с нуля, чтобы помочь с этим. Мы увидим несколько конкретных примеров этого позже в этом уроке.
Давайте начнем с краткого обзора основных классов в структуре представления коллекции. В мире представления коллекций конкретная единица данных визуально инкапсулируется экземпляром UICollectionViewCell
. Класс UICollectionView
предоставляет прокручиваемый контейнер, содержащий коллекцию ячеек. Хотя представление коллекции знает о положении ячеек, их видимость (или ее отсутствие) фактически не является обязанностью класса. Ответственность за обработку макета ложится на плечи класса UICollectionViewLayout
. Переключив класс макета для одного и того же представления коллекции, мы могли бы заставить одни и те же данные отображаться совершенно по-разному.
Говоря о визуальных элементах, кроме ячеек есть еще два: дополнительные виды и виды оформления. Отображаемые данные могут быть логически организованы в разделы, а дополнительные представления могут использоваться для предоставления «метаданных» по разделам (например, верхние и нижние колонтитулы в табличном представлении). С другой стороны, декоративные представления являются скорее «декоративными», чем управляемыми данными: вспомните фон книжной полки в приложении iBooks. Конечно, я только что упомянул это для полноты картины. Пользовательский интерфейс для этого проекта будет полностью состоять из элементов ячеек, и в нем не будет никаких разделов.
UICollectionViewLayout
не ограничивается конкретным типом макета, но поставляется с подклассом UICollectionViewFlowLayout
, который был разработан для построения как линейных, так и сеточных макетов.
Подобно табличным представлениям, представление коллекции взаимодействует с базовыми данными через источник данных и отвечает на события пользовательского интерфейса с помощью делегата.
Класс макета может управлять макетом несколькими способами — путем установки глобальных свойств для ячеек или посредством делегирования. Последний наделяет класс макета более тонким контролем внешнего вида интерфейса.
Конечно, есть еще несколько классов, которые я хочу упомянуть, и я приду к ним позже.
Учебный подход
Давайте сначала поговорим о внешности: в этом уроке мы собираемся создать пользовательский интерфейс, который выглядит как домашний экран iPhone / iPad (также называемый SpringBoard). Он будет состоять из прокручиваемой сетки значков. Проведите влево или вправо, чтобы открыть новую страницу значков. Как и у SpringBoard, интерфейс будет иметь статический фон («обои») за ним. Мало того, мы также осуществим удаление значков: длительное нажатие на любой из значков вызовет режим удаления. Значки начнут дрожать (из-за страха быть удаленными!), И кнопка закрытия появится на каждом из них. Нажатие на которое приведет к исчезновению значка из сетки. Нажатие на иконку само по себе модально представит новый вид, который можно отклонить с помощью кнопки, чтобы вернуть наш весенний вид доски.
За кулисами пользовательский интерфейс поддерживается очень простой моделью данных (которую мы вскоре представим). Это сделано для того, чтобы мы могли сосредоточиться на реализации интерфейса с минимальной добавленной сложностью, добавляемой «модельным» концом архитектуры MVC. Значки представят часть данных (как ячейки в табличных представлениях), а представление, представленное модально после нажатия на любую иконку, представит остальные наши данные.
Реализация Barebones
Поскольку это (предположительно) первый раз, когда мы разрабатываем макет с коллекционными представлениями, легко получить множество интерактивных объектов и потерять большую картину. Чтобы избежать этого, я хочу начать с минималистичной реализации, которая по-прежнему соответствует нашему основному требованию макета прокрутки на основе сетки, а затем исследовать, как мы можем использовать всю мощь инфраструктуры представления коллекций для совершенствования нашего пользовательского интерфейса и введения новых функций. ,
Запустите Xcode, создайте новый проект с «Пустым шаблоном» (мы сделаем все в коде для этого урока) и назовите его «SpringBoardLayoutTut». Убедитесь, что это проект для iPad, и что ARC включен.
Отмените выбор всех режимов, кроме «Портрет», в разделе «Поддерживаемые ориентации интерфейса».
Замените весь код в AppDelegate.m следующим:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
#import «AppDelegate.h»
@interface ViewController : UICollectionViewController
@end
@implementation ViewController
— (void)viewDidLoad
{
[super viewDidLoad];
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@»ID»];
}
// collection view data source methods ////////////////////////////////////
— (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 100;
}
— (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@»ID» forIndexPath:indexPath];
UILabel *label = [[UILabel alloc] initWithFrame:cell.bounds];
label.textAlignment = NSTextAlignmentCenter;
label.text = [NSString stringWithFormat:@»%d», indexPath.row];
[cell.contentView addSubview:label];
return cell;
}
/////////////////////////////////////////////////////////////////////////////////
// collection view delegate methods ////////////////////////////////////////
— (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(@»cell #%d was selected», indexPath.row);
}
/////////////////////////////////////////////////////////////////////////////////
@end
@implementation AppDelegate
{
ViewController *vc;
}
— (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
vc = [[ViewController alloc] initWithCollectionViewLayout:layout];
// setting cell attributes globally via layout properties ///////////////
layout.itemSize = CGSizeMake(128, 128);
layout.minimumInteritemSpacing = 64;
layout.minimumLineSpacing = 64;
layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
layout.sectionInset = UIEdgeInsetsMake(32, 32, 32, 32);
/////////////////////////////////////////////////////////////////////////////
self.window.rootViewController = vc;
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
@end
|
Создайте приложение. Вы должны получить (очень) базовый интерфейс сетки.
Приведенный выше код предназначен только для того, чтобы проиллюстрировать, что базовая компоновка сетки доступна практически из коробки с классом UICollectionViewFlowLayout
. Он не предназначен для иллюстрации того, как вы на самом деле структурируете свой код; это то, для чего предназначена остальная часть урока!
Если вы знаете свои табличные представления, большая часть приведенного выше кода должна выглядеть знакомо.
Наш корневой контроллер представления является экземпляром UICollectionViewController
, который поставляется с экземпляром UICollectionView
качестве своего представления и по умолчанию установлен как источник данных и делегат представления представления. Мы регистрируем класс UICollectionViewCell
в контроллере представления коллекции и присваиваем ему идентификатор. UICollectionViewCell
предназначен для использования в подклассах, как мы скоро увидим, поэтому необходим шаг регистрации / идентификации. Мы также создаем экземпляр UICollectionViewFlowLayout
, устанавливаем его как макет представления нашей коллекции и устанавливаем для него некоторые свойства, определяющие, как наши ячейки будут измеряться и располагаться в сетке. Наши «данные» просто состоят из ста целых чисел. Обратите внимание на знакомый шаг удаления ячеек из табличных представлений, который перерабатывает ячейки, которые были перемещены за пределы экрана, для экономии памяти и создания ячейки. В отличие от iOS 5, -dequeueReusableCellWithReuseIdentifier: метод автоматически возвращает новую ячейку, если в очереди повторного использования ее нет. Когда на экране должна появиться ячейка, к ней прикрепляется метка, которая отображает одно из чисел из нашего целочисленного списка. Нажатие на ячейку просто дает подтверждение в виде сообщения журнала.
Повторим еще раз, смысл вышеприведенного кода состоял в том, чтобы показать вам, что базовую компоновку сетки очень легко достичь с помощью коллекционных представлений и класса макета потока.
Остальная часть руководства посвящена тому, как сделать интерфейс более привлекательным, а также более интерактивным и функциональным.
Создание простой модели данных
Давайте приступим к делу. Мы начнем с создания очень простой модели данных, которая состоит из массива, содержащего имена всех семейств шрифтов, установленных на iOS, и словаря, содержащего сами шрифты, с ключами их соответствующих семейств шрифтов.
Создайте подкласс SimpleModel
именем SimpleModel
и замените содержимое SimpleModel.h и SimpleModel.m следующим:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
// SimpleModel.h
#import <Foundation/Foundation.h>
@interface SimpleModel : NSObject
@property (nonatomic, strong) NSMutableArray *fontFamilies;
@property (nonatomic, strong) NSMutableDictionary *fontFaces;
@end
// SimpleModel.m
#import «SimpleModel.h»
@implementation SimpleModel
— (id)init
{
if (self = [super init])
{
self.fontFamilies = [NSMutableArray arrayWithArray:[UIFont familyNames]];
self.fontFaces = [NSMutableDictionary dictionaryWithCapacity:self.fontFamilies.count];
for ( NSString *familyName in self.fontFamilies)
{
NSArray *fontsList = [UIFont fontNamesForFamilyName:familyName];
[self.fontFaces setObject:fontsList forKey:familyName];
}
}
return self;
}
@end
|
Настройка иконки
Нашим вторым делом является UICollectionViewCell
подкласса UICollectionViewCell
, чтобы значки выглядели как иконки!
Добавьте платформу QuartzCore в свой проект. Нам это понадобится, потому что мы будем CALayer
свойствами CALayer
и анимировать их.
Создайте новый файл Objective-C, сделайте его подклассом UICollectionViewCell
и назовите его Icon
. Замените код в файлах Icon.h и Icon.m следующим:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
// Icon.h
#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
@interface Icon : UICollectionViewCell
@property (nonatomic, strong) UILabel *label;
//@property (nonatomic, strong) UIButton *deleteButton;
@end
// Icon.m
#define MARGIN 2
#import «Icon.h»
#import <QuartzCore/QuartzCore.h>
//#import «SpringboardLayoutAttributes.h» // TO UNCOMMENT LATER
//static UIImage *deleteButtonImg;
@implementation Icon
— (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
UIView *insetView = [[UIView alloc] initWithFrame:CGRectInset(self.bounds, self.bounds.size.width/8, self.bounds.size.height/8)];
[self.contentView addSubview:insetView];
self.layer.shouldRasterize = YES;
self.label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, insetView.frame.size.width, insetView.frame.size.height)];
self.label.autoresizingMask = UIViewAutoresizingFlexibleHeight |
self.label.textAlignment = NSTextAlignmentCenter;
self.label.numberOfLines = 3;
self.label.lineBreakMode = NSLineBreakByWordWrapping;
float dim = MIN(self.label.bounds.size.width, self.label.bounds.size.height);
self.label.clipsToBounds = YES;
self.label.layer.cornerRadius = dim/8;
self.label.layer.opacity = 0.7;
self.label.layer.borderColor = [UIColor darkGrayColor].CGColor;
self.label.layer.borderWidth = 1.0;
self.label.font = [UIFont systemFontOfSize:dim/6];
self.label.backgroundColor = [UIColor lightGrayColor];
[insetView addSubview:self.label];
// INSERT ICON DELETE BUTTON SNIPPET HERE
}
return self;
}
// INSERT LAYOUT ATTRIBUTE APPLICATION SNIPPET HERE
// INSERT CELL ANIMATION SNIPPET HERE
@end
|
UILabel
содержимого нашей ячейки значка вставлено с немного меньшим представлением (добавленным как подпредставление), которое служит для хранения нашего экземпляра UILabel
который является основной видимой особенностью нашего значка. Причиной использования вложенного подпредставления является то, что мы можем позже добавить кнопку удаления, которая визуально выходит за пределы метки (так, чтобы она выглядела круто), но лежала в границах представления невидимого содержимого (потому что создание управляющего регистра касается его прямоугольник границ лежит вне родительского представления, это немного сложно, и мы бы хотели этого избежать).
Я прокомментировал некоторые вещи, которые не являются частью нашего приложения в его нынешнем виде. Я попрошу вас раскомментировать их позже. В качестве альтернативы мы будем заполнять код везде, где написано // INSERT ... SNIPPET HERE.
Я сделал это таким образом, чтобы мы могли постепенно работать над нашим приложением, а не увязать в деталях сразу.
Настройка макета
Создайте новый файл класса Objective C, сделайте его подклассом UICollectionViewFlowLayout
и назовите его SpringboardLayout
. Замените код в SpringboardLayout.h и SpringboardLayout.m следующим:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
// SpringboardLayout.h
#import <UIKit/UIKit.h>
// INSERT DELEGATE PROTOCOL SNIPPET HERE
@interface SpringboardLayout : UICollectionViewFlowLayout
@end
// SpringboardLayout.m
#import «SpringboardLayout.h»
// #import «SpringboardLayoutAttributes.h» // UNCOMMENT LATER
@implementation SpringboardLayout
— (id)init
{
if (self = [super init])
{
self.itemSize = CGSizeMake(144, 144);
self.minimumInteritemSpacing = 48;
self.minimumLineSpacing = 48;
self.scrollDirection = UICollectionViewScrollDirectionHorizontal;
self.sectionInset = UIEdgeInsetsMake(32, 32, 32, 32);
}
return self;
}
// INSERT DELETION MODE SNIPPET HERE
// INSERT ATTRIBUTES SNIPPET HERE
@end
|
Создание View Controller
Создайте новый файл класса Objective-C. Вызовите класс ViewController
и сделайте его подклассом UICollectionViewController
. Замените код в ViewController.m следующим:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
|
// ViewController.m
#import «ViewController.h»
#import «SimpleModel.h»
#import «Icon.h»
#import «SpringboardLayout.h»
@implementation ViewController
{
SimpleModel *model;
// BOOL isDeletionModeActive;
}
— (void)viewDidLoad
{
[super viewDidLoad];
model = [[SimpleModel alloc] init];
[self.collectionView registerClass:[Icon class] forCellWithReuseIdentifier:@»ICON»];
//INSERT GESTURE RECOGNIZER CREATION SNIPPET HERE:
}
#pragma mark — data source methods
— (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 1;
}
— (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return model.fontFamilies.count;
}
— (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
Icon *icon = [collectionView dequeueReusableCellWithReuseIdentifier:@»ICON» forIndexPath:indexPath];
icon.label.text = [model.fontFamilies objectAtIndex:indexPath.row];
// INSERT DELETE BUTTON CONFIG SNIPPET HERE
// INSERT DELETE BUTTON ACTION SNIPPET HERE
}
#pragma mark — delegate methods
— (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
UIViewController *vc = [[UIViewController alloc] init];
vc.view.frame = [UIScreen mainScreen].bounds;
NSMutableAttributedString *attribString = [[NSMutableAttributedString alloc] init];
NSArray *faces = [model.fontFaces objectForKey:[model.fontFamilies objectAtIndex:indexPath.row]];
for ( NSString *face in faces)
{
[attribString appendAttributedString:[[NSAttributedString alloc]
initWithString:
[NSString stringWithFormat:@»%@\n», face]
attributes:[NSDictionary dictionaryWithObject:[UIFont fontWithName:face size:30.0] forKey:NSFontAttributeName]]];
}
UITextView *content = [[UITextView alloc] initWithFrame:vc.view.bounds];
content.attributedText = attribString;
content.textAlignment = NSTextAlignmentCenter;
content.editable = NO;
[vc.view addSubview:content];
UIButton *b = [UIButton buttonWithType:UIButtonTypeRoundedRect];
b.frame = CGRectMake(vc.view.bounds.size.width/2 — 40, vc.view.bounds.size.height — 100, 80, 60);
[b setTitle:@»Close» forState:UIControlStateNormal];
[b addTarget:self action:@selector(dismiss) forControlEvents:UIControlEventTouchUpInside];
[vc.view addSubview:b];
vc.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[self presentViewController:vc animated:YES completion:nil];
}
// INSERT SHOULD SELECT SNIPPET HERE
#pragma mark — dismiss modally presented view controller
— (void)dismiss
{
[self dismissViewControllerAnimated:YES completion:nil];
}
// INSERT GESTURE RECOGNIZER ACTIONS SNIPPET HERE
// INSERT LAYOUT DELEGATE SNIPPET HERE
@end
|
Настройка приложения-делегата
Мы почти готовы создать новую и улучшенную версию нашего интерфейса.
Добавьте следующие два изображения в ваш проект: Image One , Image Two . Они будут служить фоном для нашего интерфейса, похожего на трамплин (спасибо Фабио с сайта www.999wallpapers.com за щедрое разрешение использовать их!).
Замените содержимое AppDelegate.m
следующим кодом, который создает и настраивает контроллер представления и назначает его макету:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
// AppDelegate.m
#import «AppDelegate.h»
#import «ViewController.h»
#import «SpringboardLayout.h»
@implementation AppDelegate
{
SpringboardLayout *springboardLayout;
ViewController *viewController;
}
— (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
springboardLayout = [[SpringboardLayout alloc] init];
viewController = [[ViewController alloc] initWithCollectionViewLayout:springboardLayout];
self.window.rootViewController = viewController;
viewController.collectionView.pagingEnabled = YES;
viewController.collectionView.backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@»coolrobot.png»]];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
@end
|
Создайте приложение. Пользовательский интерфейс должен иметь более сложный вид и поведение.
Краткий обзор — и взгляд в будущее
Давайте сделаем шаг назад и рассмотрим то, что мы уже сделали, и то, как написанный код переводит наши требования с точки зрения фреймворка.
Мы создали класс Icon
который умеет рисовать сам, и имеет метку, которую мы можем заполнить нашими данными. Детали нашего макета управляются классом SpringboardLayout
, который мы UICollectionViewLayout
в класс UICollectionViewLayout
, который предназначен для построения линейных или сеточных макетов. В этот момент, если вы сделаете замечание, что нам на самом деле не нужно UICollectionViewFlowLayout
подкласс класса UICollectionViewFlowLayout
как мы на самом деле не расширили его поведение, установите только несколько его свойств, ну, вы бы чмок! Поскольку все наши ячейки визуально идентичны, мы смогли уйти, просто установив некоторые глобальные свойства в классе макета, чтобы получить сетчатое расположение, которое мы хотели, и — если бы это было все, что мы хотели — мы могли бы сделать это на UICollectionViewFlowLayout
сам экземпляр (как мы делали в первый раз ‘раунд). Мы UICollectionViewFlowLayout
подкласс UICollectionViewFlowLayout
в ожидании функциональности, которую мы вскоре добавим в наш пользовательский интерфейс.
На этом этапе я упомяну, что другой способ передачи информации в макет — это делегирование. Это было бы полезно, если бы мы хотели, чтобы наши ячейки имели разные размеры, например, или если у нас было несколько разделов в нашем пользовательском интерфейсе, и мы хотели немного различаться для каждого раздела.
Фактическое представление данных использует знакомые источники данных и механизмы делегирования, которые мы знаем и любим из табличных представлений. Экземпляр UICollectionViewController
действует как оба по умолчанию. Представление коллекции определяет количество ячеек, которые оно содержит, и содержимое каждой ячейки, запрашивая источник данных, и информирует делегата, когда пользователь нажимает на ячейку, чтобы он мог ответить соответствующим образом. В нашем примере мы отвечаем созданием и модальным представлением контроллера представления, который отображает список граней шрифта, соответствующих семейству шрифтов, в ячейке, по которой щелкнули.
Теперь давайте рассмотрим нашу следующую цель и продолжим организовывать наш код таким образом, чтобы каждый класс в среде выполнял обязанности, для которых он предназначен, не больше и не меньше.
Реализация режима удаления
В Icon.h раскомментируйте строку:
1
|
@property (nonatomic, strong) UIButton *deleteButton;
|
В Icon.m раскомментируйте:
1
|
static UIImage *deleteButtonImg;
|
Затем вставьте следующий блок кода в соответствующем месте:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// INSERT ICON DELETE BUTTON SNIPPET HERE
self.deleteButton = [[UIButton alloc] initWithFrame:CGRectMake(frame.size.width/16, frame.size.width/16, frame.size.width/4, frame.size.width/4)];
if (!deleteButtonImg)
{
CGRect buttonFrame = self.deleteButton.frame;
UIGraphicsBeginImageContext(buttonFrame.size);
CGFloat sz = MIN(buttonFrame.size.width, buttonFrame.size.height);
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(buttonFrame.size.width/2, buttonFrame.size.height/2) radius:sz/2-MARGIN startAngle:0 endAngle:M_PI * 2 clockwise:YES];
[path moveToPoint:CGPointMake(MARGIN, MARGIN)];
[path addLineToPoint:CGPointMake(sz-MARGIN, sz-MARGIN)];
[path moveToPoint:CGPointMake(MARGIN, sz-MARGIN)];
[path addLineToPoint:CGPointMake(sz-MARGIN, MARGIN)];
[[UIColor redColor] setFill];
[[UIColor whiteColor] setStroke];
[path setLineWidth:3.0];
[path fill];
[path stroke];
deleteButtonImg = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
[self.deleteButton setImage:deleteButtonImg forState:UIControlStateNormal];
[self.contentView addSubview:self.deleteButton];
|
Мы могли бы включить нашу кнопку закрытия в качестве растрового изображения в наше приложение, но поскольку его довольно просто создать в виде векторной графики с помощью UIBezierPath
(который мы затем преобразуем в растровое изображение), мы сделали это вместо этого. Одним из преимуществ этого подхода (т.е. создания векторного пути, а затем растеризации) является то, что нам не нужно беспокоиться о предоставлении нескольких изображений для устройств с различным разрешением.
Давайте создадим метод target-action для нашей кнопки закрытия.
В ViewController.m
вставьте следующие фрагменты кода:
1
2
3
|
// INSERT DELETE BUTTON CONFIG SNIPPET HERE
[icon.deleteButton addTarget:self action:@selector(delete:) forControlEvents:UIControlEventTouchUpInside];
|
и
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
// INSERT DELETE BUTTON ACTION SNIPPET HERE
#pragma mark — delete for button
— (void)delete:(UIButton *)sender
{
NSIndexPath *indexPath = [self.collectionView indexPathForCell:(Icon *)sender.superview.superview];
[model.fontFaces removeObjectForKey:[model.fontFamilies objectAtIndex:indexPath.row]];
[model.fontFamilies removeObjectAtIndex:indexPath.row];
[self.collectionView deleteItemsAtIndexPaths:[NSArray arrayWithObject:indexPath]];
}
[/obj]
If you were to build and run the app at this point, you’d see a close button with a slightly oversized cross on the upper-left corner of each icon.
While we’re at it, let’s also write the wriggly animation code for the icons.
[objc]
// INSERT CELL ANIMATION SNIPPET HERE
— (void)startQuivering
{
CABasicAnimation *quiverAnim = [CABasicAnimation animationWithKeyPath:@»transform.rotation»];
float startAngle = (-2) * M_PI/180.0;
float stopAngle = -startAngle;
quiverAnim.fromValue = [NSNumber numberWithFloat:startAngle];
quiverAnim.toValue = [NSNumber numberWithFloat:3 * stopAngle];
quiverAnim.autoreverses = YES;
quiverAnim.duration = 0.2;
quiverAnim.repeatCount = HUGE_VALF;
float timeOffset = (float)(arc4random() % 100)/100 — 0.50;
quiverAnim.timeOffset = timeOffset;
CALayer *layer = self.layer;
[layer addAnimation:quiverAnim forKey:@»quivering»];
}
— (void)stopQuivering
{
CALayer *layer = self.layer;
[layer removeAnimationForKey:@»quivering»];
}
|
По сути, наша анимация заставляет нашу иконку качаться (беспокойно) из стороны в сторону. Добавляя меньшее случайное смещение времени к каждому значку, мы предотвращаем синхронизацию анимации значков, что ослабило бы иллюзию, к которой мы стремимся.
Если вы хотите взглянуть на то, как выглядит анимация, [self startQuivering];
оператор [self startQuivering];
в конце блока if (self) {…} в методе Icon
init. Просто обязательно удалите его позже, хотя! На приведенном ниже снимке экрана показана кнопка удаления и значки в разных фазах анимации вращения.
Теперь мы хотим ввести в наше приложение режим удаления, который активируется и деактивируется соответствующими жестами, вместо того, чтобы постоянно удалять кнопку удаления.
Кажется разумным сделать контроллер представления коллекции ответственным за активацию и деактивацию режима удаления. Давайте введем логическую переменную для отслеживания режима и переопределим метод делегата, который указывает, разрешено ли для ячейки, на которую пользователь нажимает, быть выбранным или нет. Мы заставим этот метод возвращать YES или NO в зависимости от значения логической переменной.
В ViewController.m раскомментируйте объявление:
1
|
BOOL isDeletionModeActive;
|
Затем добавьте следующий метод в указанное место в файле:
1
2
3
4
5
6
7
|
// INSERT SHOULD SELECT SNIPPET HERE
— (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
if (isDeletionModeActive) return NO;
else return YES;
}
|
Чуть дальше мы реализуем распознаватель жестов, который может изменить значение этой переменной.
Теперь, как мы передаем эту информацию в класс SpringboardLayout
? Мы могли бы рассмотреть определение соответствующего логического свойства в классе макета, которое может изменить контроллер представления. Но это создает ненужную связь между контроллером представления и классом макета. Кроме того, нам нужно убедиться, что мы синхронизируем изменения в обоих значениях свойств (т. Е. В контроллере представления и макете). С точки зрения дизайна, гораздо лучшим решением является использование делегирования: метод, требуемый протоколом, позволяет макету запрашивать своего делегата о состоянии приложения. Фактически, определяя протокол здесь, мы имитируем или, скорее, расширяем собственный подход Apple, в котором они определили UIViewControllerDelegateFlowLayout
который может предоставлять информацию классу UIViewControllerFlowLayout
. Все методы в этом протоколе являются необязательными. Если не реализовано, компоновка возвращается к глобальным свойствам (которые мы фактически установили), как я упоминал ранее. Технический момент здесь заключается в том, что этот протокол уже расширяет протокол UICollectionViewDelegate
, и мы собираемся расширять его дальше.
Наш протокол будет содержать метод, который макет может запросить, чтобы определить, активен ли режим удаления или нет.
В SpringboardLayout.h добавьте следующее объявление протокола:
1
2
3
4
5
6
7
8
9
|
// INSERT DELEGATE PROTOCOL SNIPPET HERE
@protocol SpringboardLayoutDelegate
@required
— (BOOL)isDeletionModeActiveForCollectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout;
@end
|
Я добавил параметры для представления коллекции и макета, чтобы соответствовать их определению Apple. Идея состоит в том, что один и тот же объект может выступать в качестве делегата для нескольких представлений коллекции или макетов, и нам может потребоваться разрешить их.
В ViewController.h добавьте #import "SpringboardLayout.h"
и объявите, что контроллер принимает наш протокол, добавив <SpringboardLayoutDelegate>
в конце оператора @interface, чтобы он читал:
1
|
@interface ViewController : UICollectionViewController<code><SpringboardLayoutDelegate></code>
|
В ViewController.m реализуйте необходимый метод:
1
2
3
4
5
6
7
8
|
// INSERT LAYOUT DELEGATE SNIPPET HERE:
#pragma mark — spring board layout delegate
— (BOOL) isDeletionModeActiveForCollectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout
{
return isDeletionModeActive;
}
|
Следует признать, что все, что мы делаем, это передаем значение переменной логического режима в макет, но в более крупном проекте это может иметь большое значение с точки зрения повторного использования кода и возможности сопровождения.
В SpringboardLayout вставьте этот удобный метод:
01
02
03
04
05
06
07
08
09
10
11
12
|
// INSERT DELETION MODE SNIPPET HERE
— (BOOL)isDeletionModeOn
{
if ([[self.collectionView.delegate class] conformsToProtocol:@protocol(SpringboardLayoutDelegate)])
{
return [(id)self.collectionView.delegate isDeletionModeActiveForCollectionView:self.collectionView layout:self];
}
return NO;
}
|
Обратите внимание, что мы хотим повторно использовать один и тот же объект делегата (который является свойством класса представления коллекции), а не создавать и устанавливать новый. Поэтому мы сначала проверяем, соответствует ли делегат нашему протоколу, и если да, то отправляем ему соответствующее сообщение. По умолчанию класс UICollectionViewController
(и, следовательно, наш подкласс ViewController
) является делегатом представления своей коллекции и источником данных. И так как мы объявили, что ViewController
соответствует SpringboardLayoutDelegate
, поэтому условие if в вышеуказанном методе будет истинным.
Теперь, как мы можем передать эту информацию в ячейки, чтобы они могли показать или скрыть кнопку закрытия и запустить / остановить анимацию?
Поскольку наш экземпляр SpringboardLayout
отвечает за настройку свойств ячеек, связанных с визуальным представлением и расположением ячеек, он должен отвечать за вызов соответствующих изменений ячеек, когда наш режим удаления активирован или деактивирован. Как именно? Это часть архитектуры представления коллекции, о которой мы отложили обсуждение ранее.
Архитектура представления коллекции Apple представляет еще один класс, называемый UICollectionViewLayoutAttributes
, который UICollectionViewLayoutAttributes
информацию из макета в ячейку. Экземпляры атрибутов создаются для каждой ячейки, и их вполне можно настроить, чтобы они зависели от некоторых свойств состояния нашего приложения. В нашем конкретном случае представьте, что нормальное состояние или состояние удаления. Ячейки получают эти атрибуты и соответственно настраиваются.
Хотя на первый взгляд может показаться, что эта архитектура добавляет ненужный уровень косвенности, на самом деле она довольно умная. Позволяя классу атрибутов опосредовать связь между макетом и ячейками, мы могли бы, скажем, создавать разные подклассы ячеек для одного и того же интерфейса, которые по-разному интерпретируют атрибуты, что приводит к различным визуальным поведениям.
Класс UICollectionViewLayoutAttribute
содержит свойства, которые напрямую соответствуют многим визуальным свойствам ячейки (например, непрозрачность, преобразование и т. Д.), Но нам нужно расширить его, чтобы мы могли определить атрибут, который передает, находится ли приложение в режиме удаления или не. Давайте сделаем это сейчас.
Создайте новый подкласс UICollectionViewLayoutAttributes
именем SpringboardLayoutAttributes
. Замените содержимое файлов * .h и * .m следующим текстом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// SpringboardLayoutAttributes.h
#import <UIKit/UIKit.h>
@interface SpringboardLayoutAttributes : UICollectionViewLayoutAttributes
@property (nonatomic, getter = isDeleteButtonHidden) BOOL deleteButtonHidden;
@end
// SpringboardLayoutAttributes.m
#import «SpringboardLayoutAttributes.h»
@implementation SpringboardLayoutAttributes
— (id)copyWithZone:(NSZone *)zone
{
SpringboardLayoutAttributes *attributes = [super copyWithZone:zone];
attributes.deleteButtonHidden = _deleteButtonHidden;
return attributes;
}
@end
|
Мы ввели логическое свойство, которое определяет, хотим ли мы, чтобы кнопка удаления была скрытой или нет. Метод -copyWithZone:
(метод NSObject
) необходимо переопределить, чтобы вновь добавленное свойство в подклассе учитывалось при копировании объекта атрибутов.
Раскомментируйте оператор импорта #import "SpringboardLayoutAttributes.h"
и вставьте следующие методы, которые информируют макет точного класса атрибутов (наш собственный класс SpringboardLayoutAttributes
) и методы, которые помогают ему создавать атрибуты ячейки, учитывая, активен ли режим удаления или не:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
+ (Class)layoutAttributesClass
{
return [SpringboardLayoutAttributes class];
}
- ( SpringboardLayoutAttributes *)layoutAttributesForItemAtIndexPath:( NSIndexPath *)indexPath {
SpringboardLayoutAttributes *attributes = ( SpringboardLayoutAttributes *)[ super layoutAttributesForItemAtIndexPath :indexPath]; if ([ self isDeletionModeOn ]) attributes .deleteButtonHidden = NO ; else
attributes .deleteButtonHidden = YES ; return attributes;
}
- ( NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *attributesArrayInRect = [ super layoutAttributesForElementsInRect :rect]; for ( SpringboardLayoutAttributes *attribs in attributesArrayInRect) {
if ([ self isDeletionModeOn ]) attribs .deleteButtonHidden = NO ; else attribs .deleteButtonHidden = YES ; }
return attributesArrayInRect; }
|
Последний шаг — переопределить метод, унаследованный нашим Icon
классом, который позволяет ему настраивать свой экземпляр в соответствии с переданными ему атрибутами.
В Cell.m вставьте следующий метод в указанное место:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
// INSERT LAYOUT ATTRIBUTE APPLICATION SNIPPET HERE - ( void )applyLayoutAttributes:( SpringboardLayoutAttributes *)layoutAttributes {
if (layoutAttributes .isDeleteButtonHidden ) {
self .deleteButton .layer .opacity = 0 .0 ; [ self stopQuivering ]; }
else
{
self .deleteButton .layer .opacity = 1 .0 ; [ self startQuivering ]; }
}
|
Теперь осталось только настроить распознаватели жестов для включения и отключения режима удаления.
В ViewController.h измените оператор @interface, чтобы объявить, что класс контроллера представления соответствует классу UIGestureRecognizerDelegate:
1
|
@interface ViewController : UICollectionViewController<UIGestureRecognizerDelegate> |
В классе ViewController.m вставьте следующий фрагмент в метод -viewDidLoad: в указанном месте:
1
2
3
4
5
6
7
8
|
//INSERT GESTURE RECOGNIZER SNIPPET CREATION HERE: UILongPressGestureRecognizer *longPress = [[ UILongPressGestureRecognizer alloc ] initWithTarget : self action : @selector (activateDeletionMode:)]; longPress .delegate = self ; [ self .collectionView addGestureRecognizer :longPress]; UITapGestureRecognizer *tap = [[ UITapGestureRecognizer alloc ] initWithTarget : self action : @selector (endDeletionMode:)]; tap .delegate = self ; [ self .collectionView addGestureRecognizer :tap]; |
Вставьте методы действий для распознавателей жестов:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
// INSERT GESTURE RECOGNIZER ACTIONS HERE #pragma mark - gesture-recognition action methods - ( BOOL )gestureRecognizer:( UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch 🙁 UITouch *)touch {
CGPoint touchPoint = [touch locationInView : self .collectionView ]; NSIndexPath *indexPath = [ self .collectionView indexPathForItemAtPoint :touchPoint]; if (indexPath && [gestureRecognizer isKindOfClass :[ UITapGestureRecognizer class ]]) {
return NO;
}
return YES;
}
- ( void )activateDeletionMode:( UILongPressGestureRecognizer *)gr {
if (gr .state == UIGestureRecognizerStateBegan) {
NSIndexPath *indexPath = [ self .collectionView indexPathForItemAtPoint :[gr locationInView : self .collectionView ]]; if (indexPath) {
isDeletionModeActive = YES ; SpringboardLayout *layout = ( SpringboardLayout *) self .collectionView .collectionViewLayout ; [layout invalidateLayout ]; }
}
}
- ( void )endDeletionMode:( UITapGestureRecognizer *)gr {
if (isDeletionModeActive) {
NSIndexPath *indexPath = [ self .collectionView indexPathForItemAtPoint :[gr locationInView : self .collectionView ]]; if (!indexPath) {
isDeletionModeActive = NO ; SpringboardLayout *layout = ( SpringboardLayout *) self .collectionView .collectionViewLayout ; [layout invalidateLayout ]; }
}
}
|
Обратите внимание на использование нами очень удобного метода -indexPathForItemAtPoint:,UICollectionView
который позволяет нам проверять, находится ли наше долгое нажатие или касание в пределах элемента ячейки или нет. Также обратите внимание на самое ключевое сообщение * [layout invalidateLayout] *. Это сигнализирует о возникновении некоторого события, которое требует перестройки интерфейса. В этом случае это активация или деактивация режима удаления. Новые атрибуты создаются SpringboardLayout
классом для всех видимых ячеек (с переключенным свойством режима удаления), и ячейки применяют эти атрибуты к себе.
Создайте проект и посмотрите на интересный анимированный режим удаления, который вы только что создали! Длительное нажатие на любую иконку активирует его, а нажатие в любом месте на заднем плане отключает его, как на домашнем экране iOS.
Куда пойти отсюда?
Я бы порекомендовал вам прочитать «Руководство по программированию представлений коллекции» для iOS и посмотреть сеансы 205 и 219 (соответственно, вводные и расширенные представления коллекции) из WWDC 2012. Код некоторых примеров, представленных в этих докладах, также доступен в коде сеанса. так что вы можете поиграть с ними.
Возможно, вы захотите расширить пример, который мы создали до сих пор, чтобы наделить его более похожим на Springboard поведением, таким как перемещение значков в исходное положение при первой загрузке приложения или включение перетаскивания значков и возможность замены их позиции. Возможно, вы захотите просмотреть API для различных классов в структуре представления коллекции, так как есть много методов, которые мы не затронули в этом руководстве, которые вам могут потребоваться или которые вы сможете эффективно использовать.
Я надеюсь, что этот урок дал вам прочную основу для начала работы с вашим собственным приложением, основанным на представлении коллекции. Спасибо за прочтение!