Статьи

Анимированные компоненты с UIKit Dynamics: часть 2

В первом уроке этого короткого цикла по UIKit Dynamics мы изучили основы API, создав анимированный компонент меню. В этом уроке мы продолжим работу над нашим проектом и внедрим еще один анимированный компонент — настраиваемое представление предупреждений.

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

Представлением оповещения будет экземпляр UIView к которому мы добавим следующие подпредставления:

  • объект UILabel для отображения заголовка представления предупреждения
  • объект UILabel для отображения сообщения представления предупреждения
  • один или несколько экземпляров UIButton позволяющих пользователю взаимодействовать с представлением предупреждений

Мы будем использовать класс UISnapBehavior для представления представления предупреждений. Как видно из названия, этот подкласс UIDynamicBehavior заставляет динамический элемент привязываться к точке, как если бы он был притянут к ней магнитным UIDynamicBehavior .

Класс UISnapBehavior определяет одно дополнительное свойство — damping , которое определяет величину колебаний, когда динамический элемент достигает точки, к которой он притягивается.

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

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

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

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

Нажмите Command-N на клавиатуре, чтобы создать новый файл, и выберите класс Objective-C из списка шаблонов iOS . Сделайте его подклассом NSObject и назовите его AlertComponent .

Следующим шагом является объявление нескольких частных свойств. Откройте AlertComponent.m , добавьте расширение класса вверху и объявите следующие свойства:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
@interface AlertComponent ()
 
@property (nonatomic, strong) UIView *alertView;
@property (nonatomic, strong) UIView *backgroundView;
@property (nonatomic, strong) UIView *targetView;
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UILabel *messageLabel;
@property (nonatomic, strong) UIDynamicAnimator *animator;
@property (nonatomic, strong) NSString *title;
@property (nonatomic, strong) NSString *message;
@property (nonatomic, strong) NSArray *buttonTitles;
@property (nonatomic) CGRect initialAlertViewFrame;
 
@end

Функция каждого свойства станет понятной по мере реализации компонента оповещения. Пришло время создать пользовательский инициализатор компонента.

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

1
2
3
4
5
@interface AlertComponent : NSObject
 
— (id)initAlertWithTitle:(NSString *)title andMessage:(NSString *)message andButtonTitles:(NSArray *)buttonTitles andTargetView:(UIView *)targetView;
 
@end

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

Откройте AlertComponent.m и объявите следующие частные методы в расширении частного класса:

1
2
3
4
5
6
7
8
@interface AlertComponent()
 
 
— (void)setupBackgroundView;
— (void)setupAlertView;
 
@end

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

В setupAlertView мы делаем три вещи:

  • инициализировать и настроить представление предупреждений
  • инициализировать и настроить метки представления предупреждений
  • инициализировать и настроить кнопки просмотра предупреждений

Давайте начнем с вычисления размера и положения представления оповещения, как показано в фрагменте кода ниже.

1
2
3
4
5
6
7
— (void)setupAlertView {
    // Set the size of the alert view.
    CGSize alertViewSize = CGSizeMake(250.0, 130.0 + 50.0 * self.buttonTitles.count);
     
    // Set the initial origin point depending on the direction of the alert view.
    CGPoint initialOriginPoint = CGPointMake(self.targetView.center.x, self.targetView.frame.origin.y — alertViewSize.height);
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
self.alertView = [[UIView alloc] initWithFrame:CGRectMake(initialOriginPoint.x, initialOriginPoint.y, alertViewSize.width, alertViewSize.height)];
 
// Background color.
[self.alertView setBackgroundColor:[UIColor colorWithRed:0.94 green:0.94 blue:0.94 alpha:1.0]];
 
// Make the alert view with rounded corners.
[self.alertView.layer setCornerRadius:10.0];
 
// Set a border to the alert view.
[self.alertView.layer setBorderWidth:1.0];
[self.alertView.layer setBorderColor:[UIColor blackColor].CGColor];
 
    // Assign the initial alert view frame to the respective property.
    self.initialAlertViewFrame = self.alertView.frame;

Используя alertViewSize и initialOriginPoint , мы инициализируем объект alertView и устанавливаем его цвет фона. Мы cornerRadius углы представления предупреждений, устанавливая значение угла layer его layer cornerRadius , его borderWidth 1.0 , а borderColor — черного цвета. Мы также сохраняем начальный кадр представления оповещения в его initialAlertViewFrame как оно понадобится нам позже.

Если XCode сообщает, что не знает о alertView layer alertView , то добавьте следующий оператор import вверху файла реализации:

1
#import <QuartzCore/QuartzCore.h>

Пришло время добавить ярлыки. Давайте начнем с заголовка.

1
2
3
4
5
6
7
8
// Setup the title label.
self.titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(0.0, 10.0, self.alertView.frame.size.width, 40.0)];
[self.titleLabel setText:self.title];
[self.titleLabel setTextAlignment:NSTextAlignmentCenter];
[self.titleLabel setFont:[UIFont fontWithName:@»Avenir-Heavy» size:14.0]];
 
// Add the title label to the alert view.
[self.alertView addSubview:self.titleLabel];

Настройка метки сообщения очень похожа.

01
02
03
04
05
06
07
08
09
10
// Setup the message label.
self.messageLabel = [[UILabel alloc] initWithFrame:CGRectMake(0.0, self.titleLabel.frame.origin.y + self.titleLabel.frame.size.height, self.alertView.frame.size.width, 80.0)];
[self.messageLabel setText:self.message];
[self.messageLabel setTextAlignment:NSTextAlignmentCenter];
[self.messageLabel setFont:[UIFont fontWithName:@»Avenir» size:14.0]];
[self.messageLabel setNumberOfLines:3];
[self.messageLabel setLineBreakMode:NSLineBreakByWordWrapping];
 
// Add the message label to the alert view.
[self.alertView addSubview:self.messageLabel];

Обратите внимание, что для свойства numberOfLines установлено значение 3 а для lineBreakMode установлено значение NSLineBreakByWordWrapping .

Последнее, что нам нужно настроить, это кнопки просмотра предупреждений. Несмотря на то, что количество кнопок может варьироваться, их настройка и расположение довольно просты. Мы разделяем кнопки на 5 пунктов и используем цикл for для их инициализации.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
CGFloat lastSubviewBottomY = self.messageLabel.frame.origin.y + self.messageLabel.frame.size.height;
 
for (int i=0; i<[self.buttonTitles count]; i++) {
    UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(10.0, lastSubviewBottomY + 5.0, self.alertView.frame.size.width — 20.0, 40.0)];
    [button setTitle:[self.buttonTitles objectAtIndex:i] forState:UIControlStateNormal];
    [button.titleLabel setFont:[UIFont fontWithName:@»Avenir» size:13.0]];
    [button setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
    [button setTitleColor:[UIColor yellowColor] forState:UIControlStateHighlighted];
    [button setBackgroundColor:[UIColor colorWithRed:0.0 green:0.47 blue:0.39 alpha:1.0]];
    [button addTarget:self action:@selector(handleButtonTap:) forControlEvents:UIControlEventTouchUpInside];
    [button setTag:i + 1];
     
    [self.alertView addSubview:button];
     
    lastSubviewBottomY = button.frame.origin.y + button.frame.size.height;
}

Обратите внимание, что каждая кнопка вызывает метод handleButtonTap: при нажатии. Мы можем определить, какую кнопку пользователь нажал, tag свойство tag кнопки.

Наконец, добавьте представление оповещения в целевое или родительское представление, добавив следующую строку в нижней части метода setupAlertView:

1
2
// Add the alert view to the parent view.
[self.targetView addSubview:self.alertView];

Второй метод, который нам нужно реализовать, это setupBackgroundView . Фоновое представление не позволит пользователю взаимодействовать с родительским представлением оповещения, пока отображается оповещение. Мы изначально установили его alpha свойство на 0.0 , что означает, что оно прозрачно.

1
2
3
4
5
6
— (void)setupBackgroundView {
    self.backgroundView = [[UIView alloc] initWithFrame:self.targetView.frame];
    [self.backgroundView setBackgroundColor:[UIColor grayColor]];
    [self.backgroundView setAlpha:0.0];
    [self.targetView addSubview:self.backgroundView];
}

Когда setupAlertView и setupBackgroundView готовы к использованию, давайте реализуем пользовательский инициализатор, который мы объявили ранее.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
— (id)initAlertWithTitle:(NSString *)title andMessage:(NSString *)message andButtonTitles:(NSArray *)buttonTitles andTargetView:(UIView *)targetView {
    if (self = [super init]) {
        // Assign the parameter values to local properties.
        self.title = title;
        self.message = message;
        self.targetView = targetView;
        self.buttonTitles = buttonTitles;
         
        // Setup the background view.
        [self setupBackgroundView];
         
        // Setup the alert view.
        [self setupAlertView];
         
        // Setup the animator.
        self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.targetView];
    }
     
    return self;
}

Мы устанавливаем свойства title , message , targetView и buttonTitles , вызываем setupBackgroundView и setupAlertView и инициализируем динамический аниматор, передавая self.targetView качестве своего ссылочного представления.

Чтобы показать представление оповещения после его инициализации, нам нужно объявить и реализовать открытый метод, который может быть вызван, например, контроллером представления, в котором размещено представление оповещения. Откройте AlertComponent.h и добавьте следующее объявление метода:

1
— (void)showAlertView;

Вернитесь к AlertComponent.m, чтобы реализовать showAlertView . Как я упоминал ранее в этом руководстве, мы будем использовать новый подкласс UIDynamicBehavior для отображения представления предупреждений, UISnapBehavior . Давайте посмотрим, как мы используем этот класс в showAlertView .

01
02
03
04
05
06
07
08
09
10
11
12
— (void)showAlertView {
    [self.animator removeAllBehaviors];
     
    UISnapBehavior *snapBehavior = [[UISnapBehavior alloc] initWithItem:self.alertView snapToPoint:self.targetView.center];
    snapBehavior.damping = 0.8;
    [self.animator addBehavior:snapBehavior];
     
     
    [UIView animateWithDuration:0.75 animations:^{
        [self.backgroundView setAlpha:0.5];
    }];
}

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

Как видите, использование привязки не сложно. Мы указываем, к какому динамическому элементу следует применить поведение, и задаем точку, к которой должен привязываться динамический элемент. Мы также устанавливаем свойство damping поведения, как мы обсуждали ранее. Также обратите внимание, что мы анимируем свойство alpha фонового представления.

Чтобы проверить представление предупреждений, нам нужно внести некоторые изменения в класс ViewController . Давайте начнем с добавления экземпляра UIButton в представление контроллера представления, чтобы показать представление предупреждения. Откройте Main.storyboard и перетащите экземпляр UIButton из библиотеки объектов в представление контроллера представления. Расположите кнопку в нижней части представления и присвойте ей заголовок « Показать представление предупреждений» . Добавьте действие в ViewController.h, как показано ниже.

1
2
3
4
5
@interface ViewController : UIViewController
 
— (IBAction)showAlertView:(id)sender;
 
@end

Вернитесь к раскадровке и подключите действие контроллера представления к кнопке. Откройте ViewController.m и импортируйте файл AlertComponent класса AlertComponent .

1
#import «AlertComponent.h»

Затем объявите свойство в расширении частного класса типа AlertComponent и назовите его alertComponent .

1
2
3
4
5
6
7
8
@interface ViewController ()
 
@property (nonatomic, strong) MenuComponent *menuComponent;
@property (nonatomic, strong) AlertComponent *alertComponent;
 
— (void)showMenu:(UIGestureRecognizer *)gestureRecognizer;
 
@end

Затем мы инициализируем компонент оповещения в методе viewDidLoad контроллера представления.

1
2
3
4
5
6
7
8
9
— (void)viewDidLoad {
    …
     
    // Initialize Alert Component
    self.alertComponent = [[AlertComponent alloc] initAlertWithTitle:@»Custom Alert»
                                                          andMessage:@»You have a new e-mail message, but I don’t know from whom.»
                                                     andButtonTitles:@[@»Show me», @»I don’t care», @»For me, really?»]
                                                       andTargetView:self.view];
}

Чтобы показать компонент оповещения, вызовите showAlertView: в только что созданном showAlertView:

1
2
3
— (IBAction)showAlertView:(id)sender {
    [self.alertComponent showAlertView];
}

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

Как мы видели ранее, handleButtonTap: метод вызывается, когда пользователь нажимает кнопку представления предупреждений. Окно оповещения должно скрываться при нажатии одной из кнопок. Посмотрим, как это работает.

Пересмотрите AlertComponent.m и, в расширении частного класса, объявите handleButtonTap: метод.

1
2
3
4
5
6
7
@interface AlertComponent()
 
 
— (void)handleButtonTap:(UIButton *)sender;
 
@end

В этом методе мы создаем ряд динамических поведений и добавляем их в объект динамического аниматора. Нам нужно динамическое поведение:

  • гравитационное поведение, которое притягивает внимание к верхней части экрана
  • поведение при столкновении с границей за пределами экрана, которая останавливает просмотр предупреждений
  • поведение толчка, при котором вид предупреждения немного подталкивает к нижней части экрана

После удаления существующего поведения из динамического аниматора и инициализации поведения push, как показано ниже.

1
2
3
4
5
6
7
8
— (void)handleButtonTap:(UIButton *)sender {
    // Remove all behaviors from animator.
    [self.animator removeAllBehaviors];
     
    UIPushBehavior *pushBehavior = [[UIPushBehavior alloc] initWithItems:@[self.alertView] mode:UIPushBehaviorModeInstantaneous];
    [pushBehavior setAngle:M_PI_2 magnitude:20.0];
    [self.animator addBehavior:pushBehavior];
}

Свойство angle поведения толчка определяет направление толчка. Устанавливая угол на M_PI_2 , сила поведения толчка направлена ​​к нижней части экрана.

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

1
2
3
UIGravityBehavior *gravityBehavior = [[UIGravityBehavior alloc] initWithItems:@[self.alertView]];
[gravityBehavior setGravityDirection:CGVectorMake(0.0, -1.0)];
[self.animator addBehavior:gravityBehavior];

Что интересно в поведении столкновений, так это то, что мы определяем границу за пределами экрана.

1
2
3
4
5
6
UICollisionBehavior *collisionBehavior = [[UICollisionBehavior alloc] initWithItems:@[self.alertView]];
[collisionBehavior addBoundaryWithIdentifier:@»alertCollisionBoundary»
                                   fromPoint:CGPointMake(self.initialAlertViewFrame.origin.x, self.initialAlertViewFrame.origin.y — 10.0)
                                     toPoint:CGPointMake(self.initialAlertViewFrame.origin.x + self.initialAlertViewFrame.size.width, self.initialAlertViewFrame.origin.y — 10.0)];
 
[self.animator addBehavior:collisionBehavior];

Нам также нужно динамическое поведение элемента для настройки эластичности столкновения. В результате представление предупреждений немного отскочит при столкновении с границей за пределами экрана.

1
2
3
UIDynamicItemBehavior *itemBehavior = [[UIDynamicItemBehavior alloc] initWithItems:@[self.alertView]];
itemBehavior.elasticity = 0.4;
[self.animator addBehavior:itemBehavior];

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

1
2
3
[UIView animateWithDuration:2.0 animations:^{
    [self.backgroundView setAlpha:0.0];
}];

Запустите ваше приложение еще раз, чтобы увидеть результат.

Хотя представление предупреждений реагирует на взаимодействие с пользователем, в настоящее время мы не знаем, какую кнопку нажал пользователь. Это то, на чем мы сосредоточимся в этом разделе.

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

Начнем с обновления публичного метода showAlertView . Метод должен принять обработчик завершения, который вызывает представление оповещения, когда пользователь нажал одну из кнопок. В AlertComponent.h обновите объявление метода showAlertView из:

1
— (void)showAlertView;

чтобы:

1
— (void)showAlertViewWithSelectionHandler:(void(^)(NSInteger buttonIndex, NSString *buttonTitle))handler;

Обработчик завершения принимает два параметра: индекс типа NSInteger и заголовок типа NSString кнопки, которая была нажата пользователем. Если мы хотим вызвать обработчик завершения, когда пользователь нажимает кнопку представления предупреждений, нам нужно сохранить ссылку на обработчик завершения. Это означает, что нам нужно объявить свойство для обработчика завершения. Мы делаем это в расширении частного класса в AlertComponent.m .

1
2
3
4
5
6
7
8
9
@interface AlertComponent ()
 
 
@property (nonatomic, strong) void(^selectionHandler)(NSInteger, NSString *);
 
 
@end

По-прежнему в AlertComponent.m обновите описание метода, как мы это делали в заголовочном файле минуту назад, и сохраните обработчик завершения в свойстве selectionHandler , которое мы только что объявили.

1
2
3
4
5
— (void)showAlertViewWithSelectionHandler:(void (^)(NSInteger, NSString *))handler {
    self.selectionHandler = handler;
     
    …
}

Последний фрагмент головоломки вызывает обработчик завершения в handleButtonTap: передавая тег и заголовок кнопки.

1
2
3
4
5
6
— (void)handleButtonTap:(UIButton *)sender {
    // Call the selection handler.
    self.selectionHandler(sender.tag, sender.titleLabel.text);
     
    …
}

Компонент AlertComponent завершен. Пришло время проверить все. Вернитесь к ViewController.m и обновите действие showAlertView: как показано ниже. Как вы можете видеть, мы вызываем новый showAlertViewWithSelectionHandler: метод и передаем блок, который будет вызываться, когда пользователь нажимает кнопку в представлении предупреждений.

1
2
3
4
5
— (IBAction)showAlertView:(id)sender {
    [self.alertComponent showAlertViewWithSelectionHandler:^(NSInteger buttonIndex, NSString *buttonTitle) {
        NSLog(@»%ld, %@», (long)buttonIndex, buttonTitle);
    }];
}

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

UIKit Dynamics впервые появился в iOS 7 и может помочь вам быстро создавать реалистичные анимации. Эта небольшая серия иллюстрирует, что использование UIKit Dynamics в ваших проектах не сложно, и вам не нужно быть экспертом по математике или физике.

Обратите внимание, что UIKit Dynamics в первую очередь предназначен для использования в приложениях, основанных на представлении. Если вы ищете подобное решение для игр, то я рекомендую взглянуть на Apple Sprite Kit , который предназначен для разработки игр.